---
title: "Theming and light/dark"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Theming and light/dark}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  eval = FALSE
)
```

# Basics

```{r}
library(shiny)
library(shinyglass)

ui <- glass_page(
  title = "Liquid Glass",
  persist = TRUE,
  scene = "tahoe",
  plotOutput("p")
)
```

`glass_page()` is `fluidPage()` plus [glass_theme()], the Light / Dark / Auto
toggle, intensity slider, and accent wells. Pair it with [observe_glass()] in
the server. For a bare page, still pass `theme = glass_theme()` to
`fluidPage()` / `navbarPage()` / `bslib::page_sidebar()`.

# Presets

| `preset` | Behavior |
|----------|----------|
| `"light"` | Light glass pack (default) |
| `"dark"` | Dark glass pack |
| `"auto"` | Follows `prefers-color-scheme`; updates when the OS theme changes |

Light and dark surface tokens ship as dual CSS custom-property packs on
`[data-glass-preset]`. Switching preset updates
`document.documentElement.dataset.glassPreset`: no Sass recompile and no
full page reload.

# Runtime updates from the server

```{r}
ui <- fluidPage(
  theme = glass_theme(preset = "auto"),
  glass_theme_toggle(selected = "auto"),
  selectInput("accent", "Accent", c(
    Blue = "#007AFF", Purple = "#AF52DE", Orange = "#FF9500"
  ))
)

server <- function(input, output, session) {
  observe_glass_theme_toggle(input, session)
  observeEvent(input$accent, {
    update_glass_theme(session, primary = input$accent)
  }, ignoreInit = TRUE)
}
```

`update_glass_theme()` accepts:

| Argument | Effect |
|----------|--------|
| `preset` | `"light"` / `"dark"` / `"auto"` |
| `tint` | Content-aware ambient color from plots/images |
| `primary` | Live accent via CSS variables (`--bs-primary`, …) |
| `intensity` | Ultra Clear (`0`) to Tinted (`1`). Default `0.5`. |
| `material` | `"regular"`. `"clear"` is deprecated (maps to intensity `0.12`) |
| `plot_surface` | `"clear"` / `"opaque"` |
| `scene` | Named wallpaper pack (`glass_scenes()`) |
| `flatten` | Print / capture mode (see [glass_flatten()]) |
| `tokens` | Named `--glass-*` overrides ([glass_css_tokens()]) |

`primary` updates Bootstrap accent CSS variables so buttons, checked
controls, and other accent surfaces follow without a reload. A few
Sass-baked one-offs may still need a full reload to pick up a new color.

# Theme toggle helper

```{r}
ui <- fluidPage(
  theme = glass_theme(preset = "auto"),
  glass_theme_toggle(),           # Light / Dark / Auto buttons
  # ...
)

server <- function(input, output, session) {
  observe_glass_theme_toggle(input, session)
}
```

Buttons call `window.shinyglass.setPreset()` immediately and also fire
Shiny inputs (`glass_toggle_light`, …) so the server can stay in sync.

# Behavior knobs

```{r}
glass_theme(
  preset = "auto",
  primary = "#AF52DE",
  blur = 36,
  saturation = 200,
  radius = "1.5rem",
  material = "regular", # "clear" is deprecated; use a low intensity
  plot_surface = "clear", # or "opaque" for dense charts/tables
  intensity = 0.5,      # 0 Ultra Clear to 1 Tinted (iOS 27 midpoint)
  tint = TRUE,      # sample plot/image colors into glass surfaces
  specular = TRUE,  # static top and bottom highlights (no pointer tilt)
  nav_morph = TRUE, # compact navbar while scrolling down
  refraction = TRUE,
  morph = TRUE,
  press = TRUE,
  scroll_edge = TRUE,
  minimize_tabs = TRUE,
  icon_style = "clear", # or "color" / "all"
  floating_sidebar = FALSE
)
```

`material = "regular"` is the adaptive fill (default). `material = "clear"`
is deprecated. Use a low `intensity` (Ultra Clear is `0`) when chrome sits
over rich media and labels stay bold. Omitting `intensity` with
`material = "clear"` still maps to `0.12` and warns. What changed in
0.5.0, and how to sit closer to the previous look, is
`vignette("migrating")`.

Reduce Transparency (the OS setting) paints opaque fills and turns backdrop
blur off, including before JavaScript runs. Increase Contrast and forced
colors override the inline glass variables. The in-app slider does not have
to move for those overrides to apply. Reduce Transparency, Increase
Contrast, and forced colors turn refraction off. Reduced motion turns
press springs and tab shrinking off. `morph` still shows the active pill,
without the spring, when motion is reduced.

`refraction = TRUE` is the Chromium squircle lens (index of refraction about
1.5). Safari and Firefox ignore the displacement map and keep blur plus tint.
`icon_style = "clear"` is the default for bar and button icons. Light mode
uses near-black ink, not pure white, so the glyph stays above the contrast
floor. `floating_sidebar = TRUE` is the opt-in iPadOS 26 detached sidebar.

`plot_surface = "clear"` is the default: plot and table hosts stay
translucent so the wallpaper shows through. Use `"opaque"` when dense
charts or tables need a regular ~94% panel (HIG: glass for nav/controls,
stronger materials for content). That is also the CSS class
`.glass-plot-surface-opaque` on a host or ancestor, and
`update_glass_theme(session, plot_surface = "opaque")` / 
`window.shinyglass.setPlotSurface("opaque")` switch it live.

# Plot and table surfaces

Apple HIG keeps Liquid Glass on chrome (nav, sidebars, controls) and uses a
denser material for content that must stay readable. shinyglass follows that
split:

| Host | Clear (default) | Opaque |
|------|-----------------|--------|
| ggplot (`theme_glass()`) | Transparent paper | ~94% panel fill |
| plotly (`plotly_glass()`) | Transparent paper/plot | Same paper token |
| gt (`gt_theme_glass()`) | Transparent table chrome | Panel + header wash |
| DT | CSS host is translucent | `--glass-plot-panel` fill |

`glass_plot_surface_input()` is the drop-in control. Optional
`dt_options_glass()` keeps DataTables inside the card (`scrollX`, wrapping
headers). The visible skin is still package CSS, not a DT theme dialect.

```{r}
ui <- fluidPage(
  theme = glass_theme(plot_surface = "opaque"),
  glass_plot_surface_input(),
  plotOutput("p"),
  DT::DTOutput("tbl")
)
```

# Public CSS tokens

`glass_css_tokens()` lists stable `--glass-*` variables, including the
refraction cues `--glass-refract-a`, `--glass-refract-b`,
`--glass-refract-brightness`, and `--glass-refract-contrast`. Those scale
with intensity. They are color and backdrop boosts, not a lens
displacement. Override them without forking SCSS:

```{r}
th <- glass_theme(tokens = list(blur = "28px", radius = "1.25rem"))
# or later:
# th <- glass_add_tokens(th, list(`--glass-bg` = "rgba(255,255,255,0.3)"))
```

`glass_token_pack("light")` / `glass_token_pack("dark")` returns the default
values for a pack. Live updates: `update_glass_theme(session, tokens = list(blur = "40px"))`
or `window.shinyglass.setTokens({ "--glass-blur": "40px" })`.

Use [glass_intensity_slider()] for a live Ultra Clear → Tinted control. It
mirrors iOS 27 **Settings → Appearance → Liquid Glass**. The web cannot read
that OS slider, so the in-app control is intentional. It is the supported way
to match the system look from a Shiny app:

```{r}
glass_intensity_slider("glass_intensity", value = 0.5)
```

All three JS behaviors default to `TRUE` (same as 0.1.x). They respect
`prefers-reduced-motion: reduce` where relevant. Advanced escape hatch:
`window.__shinyglassDisableTint = true` still disables tint.

# CSS variables (migration from 0.1.x)

**Breaking in 0.2.0:** light/dark surfaces are dual CSS packs, not a
Bootswatch `darkly` rebuild. Prefer CSS custom properties over Sass
`$glass-*` tokens when customizing:

```css
:root {
  /* shared knobs also set by glass_theme(blur=..., radius=...) */
  --glass-blur: 36px;
  --glass-radius: 1.25rem;
  --bs-primary: #007AFF;       /* also updated by update_glass_theme(primary=) */
  --glass-primary: #007AFF;
}

:root[data-glass-preset="dark"] {
  /* override a dark-pack value if needed */
  --glass-bg: rgba(255, 255, 255, 0.1);
}
```

Apps that overrode Sass-only `$glass-*` tokens should target `--glass-*`
or `[data-glass-preset="dark"]` instead.

# Client helpers

```js
window.shinyglass.setPreset("dark"); // or "light" / "auto"
window.shinyglass.getPreset();       // resolved "light" | "dark"
window.shinyglass.getMode();         // requested mode incl. "auto"
window.shinyglass.setTint(false);
window.shinyglass.setPrimary("#AF52DE");
window.shinyglass.getPrimary();
window.shinyglass.setPlotSurface("opaque"); // or "clear"
window.shinyglass.getPlotSurface();
window.shinyglass.setScene("aurora");
window.shinyglass.enterFlatten();
window.shinyglass.setTokens({ "--glass-blur": "28px" });
```

# Teal

```{r}
options(teal.bs_theme = glass_theme(preset = "auto"))
# then teal::init(...) as usual
```

# Persistence

`glass_theme(persist = TRUE)` (and `glass_page()`, which defaults to on)
writes preset, intensity, accent, material, plot surface, and scene to
`localStorage` for this app path so a refresh restores the last look.

# Wallpaper scenes

`glass_theme(scene = "tahoe")` (or `"dusk"` / `"mesh"` / `"aurora"` /
`"harbor"` / `"grove"`) swaps the page gradient and orbs. See
[glass_scenes()] for ids and labels. `wallpaper = "https://..."` paints a
frosted photo behind the glass: the photo is blurred and washed
(`--glass-wallpaper-wash`) so body ink (`#1d1d1f` light / `#f5f5f7` dark)
stays above a 4.5:1 floor on glass surfaces and a 3:1 floor on large chrome.
Do not place raw body text on an unwashed photo. Change scene live with
`update_glass_theme(session, scene = "aurora")` or [glass_scene_input()].

# Print / export flatten

Backdrop blur and translucent fills break chromote screenshots, print-to-PDF,
and static HTML. Enter flatten to swap glass fills to the opaque menu pack and
drop `backdrop-filter`:

```{r}
glass_flatten(session, TRUE)   # or update_glass_theme(session, flatten = TRUE)
# window.shinyglass.enterFlatten()
# ?glass_flatten=1
```

`@media print` applies the same rules automatically. Call
`glass_flatten(session, FALSE)` or `window.shinyglass.exitFlatten()` to restore
live glass.

# Plots that match the chrome

```{r}
output$p <- renderPlot({
  ggplot2::ggplot(mtcars, ggplot2::aes(wt, mpg)) +
    ggplot2::geom_point() +
    theme_glass(input = input)
}, bg = "transparent")
```

[theme_glass()] is a ggplot2 theme with transparent panels and light/dark ink.
[plotly_glass()] and [gt_theme_glass()] do the same for those packages.
[glass_plot_colors()] returns `ink`, `grid`, `fill`, and `paper`. Pass
`surface = "opaque"` (or `glass_theme(plot_surface = "opaque")`) when the
panel itself should be a regular fill instead of show-through glass.

# Reduced motion

When the user prefers reduced motion (`prefers-reduced-motion: reduce`):

- Navbar morph transforms are disabled (JS + CSS)
- Pointer specular tracking is skipped
- Content tint sampling is skipped
- Decorative transitions on cards/inputs/nav are removed

The scroll-edge toolbar densify (`body.glass-scroll-edge`) stays on: it is a
contrast treatment for content under floating bars, not a motion effect.
Reduced transparency still forces the Tinted intensity endpoint.

This is automatic and responds to OS preference changes while the app is open.
Reduced-transparency changes also update the surfaces without changing the
requested slider value.

To disable only the idle decorative animation, use
`glass_theme(ambient_motion = FALSE)`. To disable all optional effects, use:

```{r eval=FALSE}
glass_theme(tint = FALSE, specular = FALSE, nav_morph = FALSE,
            ambient_motion = FALSE)
```

This retains glass surfaces and blur; it is not a zero-paint-cost mode.

# Multiple intensity controls

Intensity is page-wide. The first explicit slider `value` in each newly inserted
group sets that intensity; peer sliders synchronize visually. A restricted-range
slider displays the nearest endpoint when the global value is outside its range.
Bounds must satisfy `0 <= min < max <= 1`, `step` must be finite and positive,
and an explicit `value` must be within the slider's range.

# Active-on-accent contrast

Bootstrap’s `color-contrast()` often picks **black** ink for system blue
`#007AFF`. `glass_theme()` forces light ink on primary fills so checked
checkboxes, radios, switches, and active pagination retain light ink.
White on `#007AFF` is approximately 4.02:1: it meets the 3:1 large-text threshold,
but not the 4.5:1 ordinary-text threshold. Choose and test an appropriate darker
accent when ordinary white labels must meet that threshold. The QA guide's
`--text-aa` audit checks size-aware text thresholds; translucent backgrounds,
gradients, and focus visibility still need visual review.
