@hemerson/design-system 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,14 @@
1
1
  # Hemerson Design System
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@hemerson/design-system?color=7ea8a7)](https://www.npmjs.com/package/@hemerson/design-system)
4
+ [![CI](https://github.com/hemerson-git/hemerson-design-system/actions/workflows/ci.yml/badge.svg)](https://github.com/hemerson-git/hemerson-design-system/actions/workflows/ci.yml)
5
+ [![Storybook](https://img.shields.io/badge/Storybook-live-ff4785?logo=storybook&logoColor=white)](https://hemerson-git.github.io/hemerson-design-system/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-78718e)](LICENSE)
7
+
3
8
  A dark-first design system with a sage-teal accent and liquid-glass navigation, built for React. It ships design tokens for a dark and a light theme, a stylesheet, and React components with a Storybook to browse them.
4
9
 
10
+ **[Browse the components in Storybook →](https://hemerson-git.github.io/hemerson-design-system/)**
11
+
5
12
  ## Install
6
13
 
7
14
  ```bash
@@ -19,8 +26,37 @@ export function Toolbar() {
19
26
  }
20
27
  ```
21
28
 
22
- - **Themes:** set `data-theme="dark"` (the default) or `data-theme="light"` on `<html>`.
23
- - **Reduced transparency:** set `data-transparency="reduced"` on `<html>` to turn every glass surface solid. Offer it as a setting in your app.
29
+ - **Themes and transparency:** apps open dark. Call `applyStoredAppearance()` before the first render to restore what the visitor chose last time, and put `ThemeToggle` and `TransparencyToggle` in your toolbar. Without React, `setTheme` and `setTransparency` do the same job. Under the hood these set `data-theme` and `data-transparency` on `<html>`.
30
+ - **Toasts:** wrap the app in `ToastProvider` and call `useToast()` to stack up to three at a time.
31
+ - **Edge lens:** call `enableGlassRefraction()` from `@hemerson/design-system/glass` once at startup to bend the content at the glass edges in Chromium. Other browsers keep frosted glass.
32
+
33
+ ```ts
34
+ // main.tsx, before the first render, so the page never flashes the wrong theme
35
+ import { applyStoredAppearance } from "@hemerson/design-system/appearance";
36
+
37
+ applyStoredAppearance();
38
+ ```
39
+
40
+ ```tsx
41
+ import { ThemeToggle, TransparencyToggle } from "@hemerson/design-system";
42
+
43
+ <ThemeToggle /> // dark ↔ light, remembered
44
+ <TransparencyToggle /> // solid glass, remembered
45
+ ```
46
+
47
+ ```tsx
48
+ import { ToastProvider, useToast } from "@hemerson/design-system";
49
+
50
+ <ToastProvider>
51
+ <App />
52
+ </ToastProvider>;
53
+
54
+ // anywhere inside
55
+ const toast = useToast();
56
+ toast.success("Boleto salvo", "Fulano de Tal · R$ 1.234,56");
57
+ toast.error("Não foi possível salvar");
58
+ ```
59
+
24
60
  - **Fonts:** `styles.css` loads Raleway and JetBrains Mono from Google Fonts. To self-host them, import `tokens.css` and your own font files instead.
25
61
 
26
62
  ## What's inside
@@ -28,6 +64,8 @@ export function Toolbar() {
28
64
  | Export | Contents |
29
65
  | --- | --- |
30
66
  | `@hemerson/design-system` | React components |
67
+ | `@hemerson/design-system/appearance` | `applyStoredAppearance()`, `setTheme()`, `setTransparency()`, without React |
68
+ | `@hemerson/design-system/glass` | `enableGlassRefraction()`, the Chromium edge lens, without React |
31
69
  | `@hemerson/design-system/styles.css` | Fonts, tokens and component styles |
32
70
  | `@hemerson/design-system/tokens.css` | CSS custom properties for both themes |
33
71
  | `@hemerson/design-system/tokens.json` | The tokens as data |
@@ -46,6 +84,10 @@ npm run build-storybook
46
84
 
47
85
  Tokens live in [`tokens/tokens.json`](tokens/tokens.json); `npm run tokens` turns them into `src/styles/tokens.css`. Each component lives in `src/components/<Name>/` with its stories and tests.
48
86
 
87
+ ## Releases
88
+
89
+ Versions follow [semver](https://semver.org) and are managed with [Changesets](https://github.com/changesets/changesets). Every change to the package comes with a changeset (`npx changeset`). Merging to `master` opens a "version packages" pull request that bumps the version and updates the [changelog](CHANGELOG.md); merging that pull request publishes to npm from GitHub Actions. Storybook redeploys on every push to `master`.
90
+
49
91
  ## Contributing
50
92
 
51
93
  Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for how to set up the project and how contributors are credited.
@@ -0,0 +1,23 @@
1
+ export type Theme = "dark" | "light";
2
+ export type Transparency = "full" | "reduced";
3
+ export interface Appearance {
4
+ theme: Theme;
5
+ transparency: Transparency;
6
+ }
7
+ export declare const APPEARANCE_STORAGE_KEYS: {
8
+ readonly theme: "hds-theme";
9
+ readonly transparency: "hds-transparency";
10
+ };
11
+ /** The saved choices, or dark with full transparency when nothing is saved. */
12
+ export declare function readStoredAppearance(): Appearance;
13
+ /** What <html> currently shows. */
14
+ export declare function getAppearance(): Appearance;
15
+ /**
16
+ * Puts the saved theme and transparency on <html>, dark when nothing is saved.
17
+ * Call it as early as possible, before the first paint, so the page never flashes the other theme.
18
+ */
19
+ export declare function applyStoredAppearance(): Appearance;
20
+ export declare function setTheme(theme: Theme): void;
21
+ export declare function setTransparency(transparency: Transparency): void;
22
+ /** Runs `listener` whenever the theme or transparency changes through these helpers. */
23
+ export declare function subscribeToAppearance(listener: () => void): () => void;
@@ -0,0 +1,2 @@
1
+ export { APPEARANCE_STORAGE_KEYS, applyStoredAppearance, getAppearance, readStoredAppearance, setTheme, setTransparency, subscribeToAppearance, } from "./appearance";
2
+ export type { Appearance, Theme, Transparency } from "./appearance";
@@ -0,0 +1,13 @@
1
+ import { type Appearance, type Theme, type Transparency } from "./appearance";
2
+ export interface UseAppearance extends Appearance {
3
+ setTheme: (theme: Theme) => void;
4
+ setTransparency: (transparency: Transparency) => void;
5
+ toggleTheme: () => void;
6
+ toggleTransparency: () => void;
7
+ }
8
+ /**
9
+ * The page's theme and transparency, kept in sync across every component that uses it.
10
+ * The first use applies the saved choice (dark when nothing is saved), unless the app already
11
+ * called applyStoredAppearance() at startup.
12
+ */
13
+ export declare function useAppearance(): UseAppearance;
@@ -0,0 +1,57 @@
1
+ //#region src/appearance/appearance.ts
2
+ var e = {
3
+ theme: "hds-theme",
4
+ transparency: "hds-transparency"
5
+ }, t = /* @__PURE__ */ new Set();
6
+ function n(e) {
7
+ try {
8
+ return localStorage.getItem(e);
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+ function r(e, t) {
14
+ try {
15
+ localStorage.setItem(e, t);
16
+ } catch {}
17
+ }
18
+ function i() {
19
+ return {
20
+ theme: n(e.theme) === "light" ? "light" : "dark",
21
+ transparency: n(e.transparency) === "reduced" ? "reduced" : "full"
22
+ };
23
+ }
24
+ function a() {
25
+ let e = document.documentElement;
26
+ return {
27
+ theme: e.getAttribute("data-theme") === "light" ? "light" : "dark",
28
+ transparency: e.getAttribute("data-transparency") === "reduced" ? "reduced" : "full"
29
+ };
30
+ }
31
+ function o({ theme: e, transparency: n }) {
32
+ let r = document.documentElement;
33
+ r.setAttribute("data-theme", e), n === "reduced" ? r.setAttribute("data-transparency", "reduced") : r.removeAttribute("data-transparency"), t.forEach((e) => e());
34
+ }
35
+ function s() {
36
+ let e = i();
37
+ return o(e), e;
38
+ }
39
+ function c(t) {
40
+ r(e.theme, t), o({
41
+ ...a(),
42
+ theme: t
43
+ });
44
+ }
45
+ function l(t) {
46
+ r(e.transparency, t), o({
47
+ ...a(),
48
+ transparency: t
49
+ });
50
+ }
51
+ function u(e) {
52
+ return t.add(e), () => t.delete(e);
53
+ }
54
+ //#endregion
55
+ export { c as a, i, s as n, l as o, a as r, u as s, e as t };
56
+
57
+ //# sourceMappingURL=appearance-J15aUjQp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appearance-J15aUjQp.js","names":["APPEARANCE_STORAGE_KEYS","listeners","read","key","write","value","readStoredAppearance","getAppearance","root","apply","theme","transparency","listener","applyStoredAppearance","appearance","setTheme","setTransparency","subscribeToAppearance"],"sources":["../src/appearance/appearance.ts"],"sourcesContent":["/*\n * Theme and transparency for the whole page, kept on <html> and remembered in localStorage.\n * Projects open in the dark theme whatever the system setting: light, and reduced\n * transparency, are the visitor's own choices. No React here, so plain HTML pages can use it.\n */\n\nexport type Theme = \"dark\" | \"light\";\nexport type Transparency = \"full\" | \"reduced\";\n\nexport interface Appearance {\n theme: Theme;\n transparency: Transparency;\n}\n\nexport const APPEARANCE_STORAGE_KEYS = {\n theme: \"hds-theme\",\n transparency: \"hds-transparency\",\n} as const;\n\nconst listeners = new Set<() => void>();\n\nfunction read(key: string): string | null {\n try {\n return localStorage.getItem(key);\n } catch {\n // Private windows and blocked storage: fall back to the defaults\n return null;\n }\n}\n\nfunction write(key: string, value: string) {\n try {\n localStorage.setItem(key, value);\n } catch {\n // The choice still applies for this visit\n }\n}\n\n/** The saved choices, or dark with full transparency when nothing is saved. */\nexport function readStoredAppearance(): Appearance {\n return {\n theme: read(APPEARANCE_STORAGE_KEYS.theme) === \"light\" ? \"light\" : \"dark\",\n transparency: read(APPEARANCE_STORAGE_KEYS.transparency) === \"reduced\" ? \"reduced\" : \"full\",\n };\n}\n\n/** What <html> currently shows. */\nexport function getAppearance(): Appearance {\n const root = document.documentElement;\n return {\n theme: root.getAttribute(\"data-theme\") === \"light\" ? \"light\" : \"dark\",\n transparency: root.getAttribute(\"data-transparency\") === \"reduced\" ? \"reduced\" : \"full\",\n };\n}\n\nfunction apply({ theme, transparency }: Appearance) {\n const root = document.documentElement;\n root.setAttribute(\"data-theme\", theme);\n if (transparency === \"reduced\") {\n root.setAttribute(\"data-transparency\", \"reduced\");\n } else {\n root.removeAttribute(\"data-transparency\");\n }\n listeners.forEach((listener) => listener());\n}\n\n/**\n * Puts the saved theme and transparency on <html>, dark when nothing is saved.\n * Call it as early as possible, before the first paint, so the page never flashes the other theme.\n */\nexport function applyStoredAppearance(): Appearance {\n const appearance = readStoredAppearance();\n apply(appearance);\n return appearance;\n}\n\nexport function setTheme(theme: Theme) {\n write(APPEARANCE_STORAGE_KEYS.theme, theme);\n apply({ ...getAppearance(), theme });\n}\n\nexport function setTransparency(transparency: Transparency) {\n write(APPEARANCE_STORAGE_KEYS.transparency, transparency);\n apply({ ...getAppearance(), transparency });\n}\n\n/** Runs `listener` whenever the theme or transparency changes through these helpers. */\nexport function subscribeToAppearance(listener: () => void): () => void {\n listeners.add(listener);\n return () => listeners.delete(listener);\n}\n"],"mappings":";AAcA,IAAaA,IAA0B;CACrC,OAAO;CACP,cAAc;AAChB,GAEMC,oBAAY,IAAI,IAAgB;AAEtC,SAASC,EAAK,GAA4B;CACxC,IAAI;EACF,OAAO,aAAa,QAAQC,CAAG;CACjC,QAAQ;EAEN,OAAO;CACT;AACF;AAEA,SAASC,EAAM,GAAa,GAAe;CACzC,IAAI;EACF,aAAa,QAAQD,GAAKE,CAAK;CACjC,QAAQ,CAER;AACF;AAGA,SAAgBC,IAAmC;CACjD,OAAO;EACL,OAAOJ,EAAKF,EAAwB,KAAK,MAAM,UAAU,UAAU;EACnE,cAAcE,EAAKF,EAAwB,YAAY,MAAM,YAAY,YAAY;CACvF;AACF;AAGA,SAAgBO,IAA4B;CAC1C,IAAMC,IAAO,SAAS;CACtB,OAAO;EACL,OAAOA,EAAK,aAAa,YAAY,MAAM,UAAU,UAAU;EAC/D,cAAcA,EAAK,aAAa,mBAAmB,MAAM,YAAY,YAAY;CACnF;AACF;AAEA,SAASC,EAAM,EAAE,UAAO,mBAA4B;CAClD,IAAMD,IAAO,SAAS;CAOtB,AANAA,EAAK,aAAa,cAAcE,CAAK,GACjCC,MAAiB,YACnBH,EAAK,aAAa,qBAAqB,SAAS,IAEhDA,EAAK,gBAAgB,mBAAmB,GAE1CP,EAAU,SAAS,MAAaW,EAAS,CAAC;AAC5C;AAMA,SAAgBC,IAAoC;CAClD,IAAMC,IAAaR,EAAqB;CAExC,OADAG,EAAMK,CAAU,GACTA;AACT;AAEA,SAAgBC,EAAS,GAAc;CAErC,AADAX,EAAMJ,EAAwB,OAAOU,CAAK,GAC1CD,EAAM;EAAE,GAAGF,EAAc;EAAG;CAAM,CAAC;AACrC;AAEA,SAAgBS,EAAgB,GAA4B;CAE1D,AADAZ,EAAMJ,EAAwB,cAAcW,CAAY,GACxDF,EAAM;EAAE,GAAGF,EAAc;EAAG;CAAa,CAAC;AAC5C;AAGA,SAAgBU,EAAsB,GAAkC;CAEtE,OADAhB,EAAU,IAAIW,CAAQ,SACTX,EAAU,OAAOW,CAAQ;AACxC"}
@@ -0,0 +1 @@
1
+ export { t as APPEARANCE_STORAGE_KEYS, n as applyStoredAppearance, r as getAppearance, i as readStoredAppearance, a as setTheme, o as setTransparency, s as subscribeToAppearance } from "./appearance-J15aUjQp.js";
@@ -0,0 +1,17 @@
1
+ import { type ButtonProps } from "../Button/Button";
2
+ type ToggleProps = Omit<ButtonProps, "onClick" | "children" | "icon" | "iconOnly" | "aria-label" | "aria-pressed">;
3
+ export interface ThemeToggleProps extends ToggleProps {
4
+ /** Accessible name while dark. Default "Mudar para o tema claro". */
5
+ lightLabel?: string;
6
+ /** Accessible name while light. Default "Mudar para o tema escuro". */
7
+ darkLabel?: string;
8
+ }
9
+ /** Switches between the dark theme (the default) and the light one, and remembers the choice. */
10
+ export declare function ThemeToggle({ lightLabel, darkLabel, variant, ...rest }: ThemeToggleProps): import("react").JSX.Element;
11
+ export interface TransparencyToggleProps extends ToggleProps {
12
+ /** Accessible name. Default "Reduzir transparência"; the pressed state says whether it is on. */
13
+ label?: string;
14
+ }
15
+ /** Turns every glass surface solid, or back to glass, and remembers the choice. */
16
+ export declare function TransparencyToggle({ label, variant, ...rest }: TransparencyToggleProps): import("react").JSX.Element;
17
+ export {};
@@ -12,7 +12,9 @@ export interface DialogProps {
12
12
  closeLabel?: string;
13
13
  /** `panel` focuses the panel itself instead of its first control. */
14
14
  initialFocus?: "first" | "panel";
15
+ /** md (default, 560px) for forms and confirmations · lg (880px) for wide content such as a print preview. */
16
+ size?: "md" | "lg";
15
17
  className?: string;
16
18
  }
17
19
  /** A modal panel of thick glass for a focused task or a confirmation. */
18
- export declare function Dialog({ open, onClose, title, description, children, footer, closeLabel, initialFocus, className, }: DialogProps): import("react").ReactPortal | null;
20
+ export declare function Dialog({ open, onClose, title, description, children, footer, closeLabel, initialFocus, size, className, }: DialogProps): import("react").ReactPortal | null;
@@ -1,5 +1,6 @@
1
- import { type ReactNode } from "react";
2
- export type ToastTone = "neutral" | "success" | "warning" | "danger";
1
+ import type { ReactNode } from "react";
2
+ import { type ToastTone } from "./ToastView";
3
+ export type { ToastTone } from "./ToastView";
3
4
  export interface ToastProps {
4
5
  open: boolean;
5
6
  onClose?: () => void;
@@ -12,5 +13,8 @@ export interface ToastProps {
12
13
  closeLabel?: string;
13
14
  className?: string;
14
15
  }
15
- /** A short glass notice at the bottom of the screen that confirms an action or reports a problem. */
16
+ /**
17
+ * A short glass notice at the bottom of the screen that confirms an action or reports a problem.
18
+ * To raise toasts from anywhere in an app, wrap it in ToastProvider and use `useToast()` instead.
19
+ */
16
20
  export declare function Toast({ open, onClose, tone, title, description, duration, closeLabel, className, }: ToastProps): import("react").ReactPortal | null;
@@ -0,0 +1,29 @@
1
+ import { type ReactNode } from "react";
2
+ import { type ToastTone } from "./ToastView";
3
+ export interface ToastOptions {
4
+ title: ReactNode;
5
+ description?: ReactNode;
6
+ tone?: ToastTone;
7
+ /** Milliseconds before it closes itself; 0 keeps it open until dismissed. Default 5000. */
8
+ duration?: number;
9
+ }
10
+ export interface ToastApi {
11
+ /** Shows a toast and returns its id, for `dismiss`. */
12
+ show: (options: ToastOptions) => string;
13
+ success: (title: ReactNode, description?: ReactNode) => string;
14
+ error: (title: ReactNode, description?: ReactNode) => string;
15
+ warning: (title: ReactNode, description?: ReactNode) => string;
16
+ info: (title: ReactNode, description?: ReactNode) => string;
17
+ dismiss: (id: string) => void;
18
+ }
19
+ export interface ToastProviderProps {
20
+ children?: ReactNode;
21
+ /** Most toasts shown at once; the oldest goes first. Default 3. */
22
+ max?: number;
23
+ /** Accessible name of each close button. Default "Fechar aviso". */
24
+ closeLabel?: string;
25
+ }
26
+ /** Shows the toasts raised with `useToast()` anywhere inside it, stacked at the bottom of the screen. */
27
+ export declare function ToastProvider({ children, max, closeLabel }: ToastProviderProps): import("react").JSX.Element;
28
+ /** Raises toasts from anywhere inside a ToastProvider: `toast.success("Boleto salvo")`. */
29
+ export declare function useToast(): ToastApi;
@@ -0,0 +1,14 @@
1
+ import { type ReactNode } from "react";
2
+ export type ToastTone = "neutral" | "success" | "warning" | "danger";
3
+ export interface ToastViewProps {
4
+ tone: ToastTone;
5
+ title: ReactNode;
6
+ description?: ReactNode;
7
+ /** Milliseconds before it closes itself; 0 keeps it open. */
8
+ duration: number;
9
+ onClose?: () => void;
10
+ closeLabel: string;
11
+ className?: string;
12
+ }
13
+ /** One toast: the glass card, its icon and close button, and a timer that pauses on hover or focus. */
14
+ export declare function ToastView({ tone, title, description, duration, onClose, closeLabel, className }: ToastViewProps): import("react").JSX.Element;
@@ -0,0 +1,88 @@
1
+ //#region src/glass/lens.ts
2
+ function e(e, t, n, r) {
3
+ let i = new Uint8ClampedArray(e * t * 4), a = Math.max(0, Math.min(n, e / 2, t / 2));
4
+ for (let n = 0; n < t; n++) for (let o = 0; o < e; o++) {
5
+ let s = o + .5 - e / 2, c = n + .5 - t / 2, l = Math.abs(s) - (e / 2 - a), u = Math.abs(c) - (t / 2 - a), d, f = 0, p = 0;
6
+ if (l > 0 && u > 0) {
7
+ let e = Math.hypot(l, u);
8
+ d = a - e, f = l / e * Math.sign(s), p = u / e * Math.sign(c);
9
+ } else l > u ? (d = e / 2 - Math.abs(s), f = Math.sign(s)) : (d = t / 2 - Math.abs(c), p = Math.sign(c));
10
+ let m = Math.max(0, 1 - d / r), h = m * m, g = (n * e + o) * 4;
11
+ i[g] = 128 - f * h * 127, i[g + 1] = 128 - p * h * 127, i[g + 2] = 128, i[g + 3] = 255;
12
+ }
13
+ return i;
14
+ }
15
+ function t(e) {
16
+ let t = Math.max(4, Math.min(16, e / 3)), n = t * .45;
17
+ return {
18
+ rim: t,
19
+ maxShift: n,
20
+ scale: n / (127 / 255)
21
+ };
22
+ }
23
+ //#endregion
24
+ //#region src/glass/enableGlassRefraction.ts
25
+ var n = "http://www.w3.org/2000/svg", r = ".hds-glass:not(.hds-glass--thick)", i = null, a = /* @__PURE__ */ new Map();
26
+ function o() {
27
+ return (navigator.userAgentData?.brands ?? []).some(({ brand: e }) => e.includes("Chromium"));
28
+ }
29
+ function s() {
30
+ return i?.isConnected || (i = document.createElementNS(n, "svg"), i.setAttribute("id", "hds-glass-defs"), i.setAttribute("width", "0"), i.setAttribute("height", "0"), i.setAttribute("aria-hidden", "true"), i.style.position = "absolute", document.body.appendChild(i)), i;
31
+ }
32
+ function c(r, i, o) {
33
+ let c = `${r}x${i}r${o}`, l = a.get(c);
34
+ if (l) return l;
35
+ let u = document.createElement("canvas");
36
+ u.width = r, u.height = i;
37
+ let d = u.getContext("2d");
38
+ if (!d) return null;
39
+ let { rim: f, scale: p } = t(i), m = d.createImageData(r, i);
40
+ m.data.set(e(r, i, o, f)), d.putImageData(m, 0, 0);
41
+ let h = `hds-glass-lens-${a.size}`, g = document.createElementNS(n, "filter");
42
+ Object.entries({
43
+ id: h,
44
+ x: "0",
45
+ y: "0",
46
+ width: String(r),
47
+ height: String(i),
48
+ filterUnits: "userSpaceOnUse",
49
+ "color-interpolation-filters": "sRGB"
50
+ }).forEach(([e, t]) => g.setAttribute(e, t));
51
+ let _ = document.createElementNS(n, "feImage");
52
+ _.setAttribute("href", u.toDataURL()), _.setAttribute("x", "0"), _.setAttribute("y", "0"), _.setAttribute("width", String(r)), _.setAttribute("height", String(i)), _.setAttribute("result", "map");
53
+ let v = document.createElementNS(n, "feDisplacementMap");
54
+ return v.setAttribute("in", "SourceGraphic"), v.setAttribute("in2", "map"), v.setAttribute("scale", p.toFixed(2)), v.setAttribute("xChannelSelector", "R"), v.setAttribute("yChannelSelector", "G"), g.append(_, v), s().appendChild(g), a.set(c, h), h;
55
+ }
56
+ function l(e) {
57
+ let t = Math.round(e.offsetWidth), n = Math.round(e.offsetHeight);
58
+ if (t < 8 || n < 8) return;
59
+ let r = parseFloat(getComputedStyle(e).borderTopLeftRadius) || 0, i = c(t, n, Math.min(r, n / 2));
60
+ i && e.style.setProperty("--hds-glass-lens", `url(#${i})`);
61
+ }
62
+ var u = !1;
63
+ function d() {
64
+ if (typeof document > "u") return !1;
65
+ let e = window.matchMedia?.("(prefers-reduced-transparency: reduce)").matches ?? !1;
66
+ if (!o() || e) return !1;
67
+ if (u) return !0;
68
+ u = !0;
69
+ let t = new ResizeObserver((e) => {
70
+ for (let t of e) l(t.target);
71
+ }), n = (e) => {
72
+ e.querySelectorAll(r).forEach((e) => {
73
+ l(e), t.observe(e);
74
+ });
75
+ };
76
+ return n(document), new MutationObserver((e) => {
77
+ for (let i of e) i.addedNodes.forEach((e) => {
78
+ e instanceof HTMLElement && (e.matches(r) && (l(e), t.observe(e)), n(e));
79
+ });
80
+ }).observe(document.body, {
81
+ childList: !0,
82
+ subtree: !0
83
+ }), document.documentElement.setAttribute("data-glass-refraction", ""), !0;
84
+ }
85
+ //#endregion
86
+ export { d as t };
87
+
88
+ //# sourceMappingURL=enableGlassRefraction-CgDeg-2E.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enableGlassRefraction-CgDeg-2E.js","names":["lensDisplacementMap","data","width","height","r","radius","y","x","px","py","qx","qy","distance","nx","ny","length","t","rim","strength","i","lensSettings","maxShift","SVG","GLASS","defs","filters","isChromium","brand","ensureDefs","filterFor","key","existing","canvas","context","image","id","filter","name","value","map","displace","scale","applyLens","element","active","enableGlassRefraction","reducesTransparency","resizeObserver","entry","entries","watch","mutation","mutations","node"],"sources":["../src/glass/lens.ts","../src/glass/enableGlassRefraction.ts"],"sourcesContent":["/**\n * A displacement map for the edge lens: near the rim the backdrop is sampled from further\n * inside the shape, so content appears to bend towards the edge; the middle is untouched.\n *\n * Red holds the horizontal shift and green the vertical one, with 128 meaning \"no shift\".\n * The shift never exceeds `maxShift`, and `maxShift` stays below the rim width, so content\n * is compressed towards the edge but never flipped.\n */\nexport function lensDisplacementMap(width: number, height: number, radius: number, rim: number): Uint8ClampedArray {\n const data = new Uint8ClampedArray(width * height * 4);\n const r = Math.max(0, Math.min(radius, width / 2, height / 2));\n\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n // Distance to the edge of the rounded rectangle and the outward normal there\n const px = x + 0.5 - width / 2;\n const py = y + 0.5 - height / 2;\n const qx = Math.abs(px) - (width / 2 - r);\n const qy = Math.abs(py) - (height / 2 - r);\n let distance: number;\n let nx = 0;\n let ny = 0;\n\n if (qx > 0 && qy > 0) {\n const length = Math.hypot(qx, qy);\n distance = r - length;\n nx = (qx / length) * Math.sign(px);\n ny = (qy / length) * Math.sign(py);\n } else if (qx > qy) {\n distance = width / 2 - Math.abs(px);\n nx = Math.sign(px);\n } else {\n distance = height / 2 - Math.abs(py);\n ny = Math.sign(py);\n }\n\n const t = Math.max(0, 1 - distance / rim);\n const strength = t * t;\n const i = (y * width + x) * 4;\n data[i] = 128 - nx * strength * 127;\n data[i + 1] = 128 - ny * strength * 127;\n data[i + 2] = 128;\n data[i + 3] = 255;\n }\n }\n\n return data;\n}\n\n/** Rim width and the filter scale that keep the largest shift at 45% of the rim. */\nexport function lensSettings(height: number) {\n const rim = Math.max(4, Math.min(16, height / 3));\n const maxShift = rim * 0.45;\n // feDisplacementMap moves by scale × (channel / 255 − 0.5); the map uses at most ±127\n const scale = maxShift / (127 / 255);\n return { rim, maxShift, scale };\n}\n","import { lensDisplacementMap, lensSettings } from \"./lens\";\n\nconst SVG = \"http://www.w3.org/2000/svg\";\nconst GLASS = \".hds-glass:not(.hds-glass--thick)\";\n\nlet defs: SVGSVGElement | null = null;\nconst filters = new Map<string, string>();\n\nfunction isChromium() {\n const brands = (navigator as Navigator & { userAgentData?: { brands: { brand: string }[] } }).userAgentData?.brands ?? [];\n return brands.some(({ brand }) => brand.includes(\"Chromium\"));\n}\n\nfunction ensureDefs() {\n if (defs?.isConnected) return defs;\n defs = document.createElementNS(SVG, \"svg\");\n defs.setAttribute(\"id\", \"hds-glass-defs\");\n defs.setAttribute(\"width\", \"0\");\n defs.setAttribute(\"height\", \"0\");\n defs.setAttribute(\"aria-hidden\", \"true\");\n defs.style.position = \"absolute\";\n document.body.appendChild(defs);\n return defs;\n}\n\n/** One SVG filter per element size and corner radius, shared by elements that match. */\nfunction filterFor(width: number, height: number, radius: number): string | null {\n const key = `${width}x${height}r${radius}`;\n const existing = filters.get(key);\n if (existing) return existing;\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = width;\n canvas.height = height;\n const context = canvas.getContext(\"2d\");\n if (!context) return null;\n\n const { rim, scale } = lensSettings(height);\n const image = context.createImageData(width, height);\n image.data.set(lensDisplacementMap(width, height, radius, rim));\n context.putImageData(image, 0, 0);\n\n const id = `hds-glass-lens-${filters.size}`;\n const filter = document.createElementNS(SVG, \"filter\");\n const attributes: Record<string, string> = {\n id,\n x: \"0\",\n y: \"0\",\n width: String(width),\n height: String(height),\n filterUnits: \"userSpaceOnUse\",\n \"color-interpolation-filters\": \"sRGB\",\n };\n Object.entries(attributes).forEach(([name, value]) => filter.setAttribute(name, value));\n\n const map = document.createElementNS(SVG, \"feImage\");\n map.setAttribute(\"href\", canvas.toDataURL());\n map.setAttribute(\"x\", \"0\");\n map.setAttribute(\"y\", \"0\");\n map.setAttribute(\"width\", String(width));\n map.setAttribute(\"height\", String(height));\n map.setAttribute(\"result\", \"map\");\n\n const displace = document.createElementNS(SVG, \"feDisplacementMap\");\n displace.setAttribute(\"in\", \"SourceGraphic\");\n displace.setAttribute(\"in2\", \"map\");\n displace.setAttribute(\"scale\", scale.toFixed(2));\n displace.setAttribute(\"xChannelSelector\", \"R\");\n displace.setAttribute(\"yChannelSelector\", \"G\");\n\n filter.append(map, displace);\n ensureDefs().appendChild(filter);\n filters.set(key, id);\n return id;\n}\n\nfunction applyLens(element: HTMLElement) {\n const width = Math.round(element.offsetWidth);\n const height = Math.round(element.offsetHeight);\n if (width < 8 || height < 8) return;\n\n const radius = parseFloat(getComputedStyle(element).borderTopLeftRadius) || 0;\n const id = filterFor(width, height, Math.min(radius, height / 2));\n if (id) element.style.setProperty(\"--hds-glass-lens\", `url(#${id})`);\n}\n\nlet active = false;\n\n/**\n * Turns on the edge lens: glass bends the backdrop near its rim, like Apple's Liquid Glass.\n * Every glass element present now or added later gets a lens sized to it.\n *\n * Only Chromium renders SVG filters in backdrop-filter. Firefox accepts them and then\n * drops the whole backdrop-filter, blur included, so other browsers keep plain frosted\n * glass. Nothing happens when the user reduces transparency.\n * Returns whether the lens was turned on.\n */\nexport function enableGlassRefraction(): boolean {\n if (typeof document === \"undefined\") return false;\n\n const reducesTransparency = window.matchMedia?.(\"(prefers-reduced-transparency: reduce)\").matches ?? false;\n if (!isChromium() || reducesTransparency) return false;\n if (active) return true;\n active = true;\n\n const resizeObserver = new ResizeObserver((entries) => {\n for (const entry of entries) applyLens(entry.target as HTMLElement);\n });\n const watch = (root: ParentNode) => {\n root.querySelectorAll<HTMLElement>(GLASS).forEach((element) => {\n applyLens(element);\n resizeObserver.observe(element);\n });\n };\n\n watch(document);\n new MutationObserver((mutations) => {\n for (const mutation of mutations) {\n mutation.addedNodes.forEach((node) => {\n if (!(node instanceof HTMLElement)) return;\n if (node.matches(GLASS)) {\n applyLens(node);\n resizeObserver.observe(node);\n }\n watch(node);\n });\n }\n }).observe(document.body, { childList: true, subtree: true });\n\n document.documentElement.setAttribute(\"data-glass-refraction\", \"\");\n return true;\n}\n"],"mappings":"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"}
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Adds the SVG filter that makes glass refract its backdrop, and marks
3
- * `<html data-glass-refraction>` so the styles use it.
2
+ * Turns on the edge lens: glass bends the backdrop near its rim, like Apple's Liquid Glass.
3
+ * Every glass element present now or added later gets a lens sized to it.
4
4
  *
5
- * Only Chromium renders the effect. Firefox accepts it in `@supports` and then
6
- * drops the whole backdrop-filter, blur included, so other browsers are left alone.
7
- * Returns whether refraction was turned on.
5
+ * Only Chromium renders SVG filters in backdrop-filter. Firefox accepts them and then
6
+ * drops the whole backdrop-filter, blur included, so other browsers keep plain frosted
7
+ * glass. Nothing happens when the user reduces transparency.
8
+ * Returns whether the lens was turned on.
8
9
  */
9
10
  export declare function enableGlassRefraction(): boolean;
@@ -0,0 +1 @@
1
+ export { enableGlassRefraction } from "./enableGlassRefraction";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A displacement map for the edge lens: near the rim the backdrop is sampled from further
3
+ * inside the shape, so content appears to bend towards the edge; the middle is untouched.
4
+ *
5
+ * Red holds the horizontal shift and green the vertical one, with 128 meaning "no shift".
6
+ * The shift never exceeds `maxShift`, and `maxShift` stays below the rim width, so content
7
+ * is compressed towards the edge but never flipped.
8
+ */
9
+ export declare function lensDisplacementMap(width: number, height: number, radius: number, rim: number): Uint8ClampedArray;
10
+ /** Rim width and the filter scale that keep the largest shift at 45% of the rim. */
11
+ export declare function lensSettings(height: number): {
12
+ rim: number;
13
+ maxShift: number;
14
+ scale: number;
15
+ };
package/dist/glass.js ADDED
@@ -0,0 +1 @@
1
+ export { t as enableGlassRefraction } from "./enableGlassRefraction-CgDeg-2E.js";
package/dist/index.d.ts CHANGED
@@ -16,6 +16,8 @@ export { Dialog } from "./components/Dialog/Dialog";
16
16
  export type { DialogProps } from "./components/Dialog/Dialog";
17
17
  export { Toast } from "./components/Toast/Toast";
18
18
  export type { ToastProps, ToastTone } from "./components/Toast/Toast";
19
+ export { ToastProvider, useToast } from "./components/Toast/ToastProvider";
20
+ export type { ToastApi, ToastOptions, ToastProviderProps } from "./components/Toast/ToastProvider";
19
21
  export { Input } from "./components/Input/Input";
20
22
  export type { InputProps } from "./components/Input/Input";
21
23
  export { Select } from "./components/Select/Select";
@@ -28,6 +30,11 @@ export { Badge } from "./components/Badge/Badge";
28
30
  export type { BadgeProps, BadgeTone } from "./components/Badge/Badge";
29
31
  export { Table } from "./components/Table/Table";
30
32
  export type { TableColumn, TableProps } from "./components/Table/Table";
33
+ export { ThemeToggle, TransparencyToggle } from "./components/AppearanceToggles/AppearanceToggles";
34
+ export type { ThemeToggleProps, TransparencyToggleProps } from "./components/AppearanceToggles/AppearanceToggles";
35
+ export { useAppearance } from "./appearance/useAppearance";
36
+ export type { UseAppearance } from "./appearance/useAppearance";
37
+ export * from "./appearance";
31
38
  export { Container } from "./components/Container/Container";
32
39
  export type { ContainerProps } from "./components/Container/Container";
33
40
  export { SectionTitle } from "./components/SectionTitle/SectionTitle";