---
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`) → Tinted (`1`) |
| `material` | `"regular"` / `"clear"` |
| `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", # or "clear" over media-rich content
  plot_surface = "clear", # or "opaque" for dense charts/tables
  intensity = 0.45,     # 0 Ultra Clear → 1 Tinted (iOS 27)
  tint = TRUE,      # sample plot/image colors into glass surfaces
  specular = TRUE,  # pointer specular highlight
  nav_morph = TRUE  # compact navbar while scrolling down
)
```

`material = "regular"` is the adaptive Tahoe-style fill (default). Use
`"clear"` when chrome sits over rich media and labels stay bold.

`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. 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.45)
```

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.
