@edgeandnode/eds-react 1.0.2 → 1.1.1

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.
Files changed (103) hide show
  1. package/README.md +32 -7
  2. package/dist/EDSContext.d.ts +2 -2
  3. package/dist/EDSContext.d.ts.map +1 -1
  4. package/dist/EDSContext.js +1 -1
  5. package/dist/EDSContext.js.map +1 -1
  6. package/dist/EDSProvider.d.ts +13 -6
  7. package/dist/EDSProvider.d.ts.map +1 -1
  8. package/dist/EDSProvider.js +48 -63
  9. package/dist/EDSProvider.js.map +1 -1
  10. package/dist/components/Avatar.d.ts +3 -3
  11. package/dist/components/Avatar.d.ts.map +1 -1
  12. package/dist/components/Avatar.js.map +1 -1
  13. package/dist/components/Button.d.ts.map +1 -1
  14. package/dist/components/Button.js +1 -1
  15. package/dist/components/Button.js.map +1 -1
  16. package/dist/components/Card.d.ts +8 -4
  17. package/dist/components/Card.d.ts.map +1 -1
  18. package/dist/components/Card.js +2 -1
  19. package/dist/components/Card.js.map +1 -1
  20. package/dist/components/Keyboard.js +1 -1
  21. package/dist/components/LocalTheme.d.ts +21 -8
  22. package/dist/components/LocalTheme.d.ts.map +1 -1
  23. package/dist/components/LocalTheme.js +10 -4
  24. package/dist/components/LocalTheme.js.map +1 -1
  25. package/dist/components/Menu.parts.d.ts +43 -9
  26. package/dist/components/Menu.parts.d.ts.map +1 -1
  27. package/dist/components/Menu.parts.js +118 -114
  28. package/dist/components/Menu.parts.js.map +1 -1
  29. package/dist/components/Modal.parts.d.ts +1 -1
  30. package/dist/components/Modal.parts.d.ts.map +1 -1
  31. package/dist/components/Modal.parts.js +2 -2
  32. package/dist/components/Modal.parts.js.map +1 -1
  33. package/dist/components/Pane.parts.d.ts +23 -22
  34. package/dist/components/Pane.parts.d.ts.map +1 -1
  35. package/dist/components/Pane.parts.js +3 -3
  36. package/dist/components/Pane.parts.js.map +1 -1
  37. package/dist/components/Prose.d.ts +1 -1
  38. package/dist/components/Prose.d.ts.map +1 -1
  39. package/dist/components/Prose.js +3 -3
  40. package/dist/components/Prose.js.map +1 -1
  41. package/dist/components/Prose.meta.d.ts +5 -0
  42. package/dist/components/Prose.meta.d.ts.map +1 -1
  43. package/dist/components/Prose.meta.js +10 -0
  44. package/dist/components/Prose.meta.js.map +1 -1
  45. package/dist/components/Select.parts.d.ts +8 -3
  46. package/dist/components/Select.parts.d.ts.map +1 -1
  47. package/dist/components/Select.parts.js +26 -17
  48. package/dist/components/Select.parts.js.map +1 -1
  49. package/dist/components/ThemeSwitcher.d.ts +5 -5
  50. package/dist/components/ThemeSwitcher.d.ts.map +1 -1
  51. package/dist/components/ThemeSwitcher.js +5 -4
  52. package/dist/components/ThemeSwitcher.js.map +1 -1
  53. package/dist/components/base/MaybeButtonOrLink.d.ts +7 -5
  54. package/dist/components/base/MaybeButtonOrLink.d.ts.map +1 -1
  55. package/dist/components/base/MaybeButtonOrLink.js +7 -2
  56. package/dist/components/base/MaybeButtonOrLink.js.map +1 -1
  57. package/dist/components/base/Render.d.ts +25 -6
  58. package/dist/components/base/Render.d.ts.map +1 -1
  59. package/dist/components/base/Render.js +4 -3
  60. package/dist/components/base/Render.js.map +1 -1
  61. package/dist/components/base/index.d.ts +1 -1
  62. package/dist/components/base/index.d.ts.map +1 -1
  63. package/dist/components/base/index.js.map +1 -1
  64. package/dist/components/index.d.ts +1 -1
  65. package/dist/components/index.d.ts.map +1 -1
  66. package/dist/components/index.js +1 -1
  67. package/dist/components/index.js.map +1 -1
  68. package/dist/hooks/useEDS.d.ts +16 -8
  69. package/dist/hooks/useEDS.d.ts.map +1 -1
  70. package/dist/hooks/useEDS.js +16 -8
  71. package/dist/hooks/useEDS.js.map +1 -1
  72. package/dist/index.d.ts +2 -1
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +1 -0
  75. package/dist/index.js.map +1 -1
  76. package/dist/theme-persistence.d.ts +18 -0
  77. package/dist/theme-persistence.d.ts.map +1 -0
  78. package/dist/theme-persistence.js +25 -0
  79. package/dist/theme-persistence.js.map +1 -0
  80. package/package.json +20 -20
  81. package/skill/SKILL.md +91 -57
  82. package/skill/references/tokens.md +1 -1
  83. package/src/EDSContext.ts +2 -2
  84. package/src/EDSProvider.tsx +80 -74
  85. package/src/components/Avatar.tsx +5 -3
  86. package/src/components/Button.tsx +4 -0
  87. package/src/components/Card.tsx +14 -8
  88. package/src/components/Keyboard.tsx +1 -1
  89. package/src/components/LocalTheme.tsx +22 -14
  90. package/src/components/Menu.parts.tsx +365 -244
  91. package/src/components/Modal.parts.tsx +8 -1
  92. package/src/components/Pane.parts.tsx +31 -26
  93. package/src/components/Prose.meta.ts +10 -0
  94. package/src/components/Prose.tsx +4 -3
  95. package/src/components/Select.parts.tsx +37 -29
  96. package/src/components/ThemeSwitcher.tsx +11 -13
  97. package/src/components/base/MaybeButtonOrLink.tsx +11 -6
  98. package/src/components/base/Render.tsx +28 -4
  99. package/src/components/base/index.ts +2 -0
  100. package/src/components/index.ts +8 -1
  101. package/src/hooks/useEDS.ts +16 -8
  102. package/src/index.ts +2 -1
  103. package/src/theme-persistence.ts +31 -0
@@ -0,0 +1,18 @@
1
+ import type { Theme } from './EDSContext.ts';
2
+ export declare const PERSISTENCE_KEY = "eds-active-theme";
3
+ export declare const COOKIE_REGEX: RegExp;
4
+ export declare const VALID_THEMES: readonly Theme[];
5
+ export declare function isValidTheme(value: unknown): value is Theme;
6
+ export declare function parseThemeCookie(cookies: string | null): Theme | undefined;
7
+ /**
8
+ * Returns the theme saved by `EDSProvider`'s `persistTheme="cookie"` from the request's headers, or
9
+ * `undefined` if there's none. Pass the result to `defaultTheme`, so the server renders the saved
10
+ * theme too.
11
+ *
12
+ * @example
13
+ * ````tsx
14
+ * <EDSProvider persistTheme="cookie" defaultTheme={getThemeFromCookies(await headers())}>
15
+ * ````
16
+ */
17
+ export declare function getThemeFromCookies(headers: Pick<Headers, 'get'>): Theme | undefined;
18
+ //# sourceMappingURL=theme-persistence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-persistence.d.ts","sourceRoot":"","sources":["../src/theme-persistence.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AAE5C,eAAO,MAAM,eAAe,qBAAqB,CAAA;AACjD,eAAO,MAAM,YAAY,QAAsD,CAAA;AAE/E,eAAO,MAAM,YAAY,EAAE,SAAS,KAAK,EAAgC,CAAA;AAEzE,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,CAE3D;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,qBAGtD;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,qBAEhE"}
@@ -0,0 +1,25 @@
1
+ // Not a client module, so server components can call `getThemeFromCookies`
2
+ export const PERSISTENCE_KEY = 'eds-active-theme';
3
+ export const COOKIE_REGEX = new RegExp(`(?:^|;\\s*)${PERSISTENCE_KEY}=([^;]+)`);
4
+ export const VALID_THEMES = ['dark', 'light', 'system'];
5
+ export function isValidTheme(value) {
6
+ return typeof value === 'string' && VALID_THEMES.includes(value);
7
+ }
8
+ export function parseThemeCookie(cookies) {
9
+ const value = cookies?.match(COOKIE_REGEX)?.[1];
10
+ return isValidTheme(value) ? value : undefined;
11
+ }
12
+ /**
13
+ * Returns the theme saved by `EDSProvider`'s `persistTheme="cookie"` from the request's headers, or
14
+ * `undefined` if there's none. Pass the result to `defaultTheme`, so the server renders the saved
15
+ * theme too.
16
+ *
17
+ * @example
18
+ * ````tsx
19
+ * <EDSProvider persistTheme="cookie" defaultTheme={getThemeFromCookies(await headers())}>
20
+ * ````
21
+ */
22
+ export function getThemeFromCookies(headers) {
23
+ return parseThemeCookie(headers.get('cookie'));
24
+ }
25
+ //# sourceMappingURL=theme-persistence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-persistence.js","sourceRoot":"","sources":["../src/theme-persistence.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAI3E,MAAM,CAAC,MAAM,eAAe,GAAG,kBAAkB,CAAA;AACjD,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,MAAM,CAAC,cAAc,eAAe,UAAU,CAAC,CAAA;AAE/E,MAAM,CAAC,MAAM,YAAY,GAAqB,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAA;AAEzE,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,YAAkC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AACzF,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAsB;IACrD,MAAM,KAAK,GAAG,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;IAC/C,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;AAChD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAA6B;IAC/D,OAAO,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAA;AAChD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edgeandnode/eds-react",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
4
4
  "description": "React components for the Edge Design System",
5
5
  "author": "Edge & Node and contributors",
6
6
  "license": "MIT",
@@ -47,48 +47,48 @@
47
47
  "tailwindcss": "^4.3.2"
48
48
  },
49
49
  "dependencies": {
50
- "@base-ui/react": "^1.8.0",
51
- "@base-ui/utils": "^0.4.0",
52
- "@edgeandnode/eds-css": "1.0.2",
50
+ "@base-ui/react": "~1.9.0",
51
+ "@base-ui/utils": "^0.5.0",
52
+ "@edgeandnode/eds-css": "1.1.0",
53
53
  "@edgeandnode/eds-icons": "1.0.2",
54
54
  "@edgeandnode/eds-utils": "1.0.2",
55
55
  "@react-aria/utils": "^3.34.1",
56
56
  "@react-hookz/web": "^26.1.0",
57
57
  "@ver0/deep-equal": "^2.0.1",
58
58
  "escape-string-regexp": "^5.0.0",
59
- "motion": "^13.4.5",
60
- "react-aria": "^3.52.1",
61
- "react-keyed-flatten-children": "^5.1.3",
59
+ "motion": "^14.3.0",
60
+ "react-aria": "^3.53.1",
61
+ "react-keyed-flatten-children": "^5.1.4",
62
62
  "react-markdown": "^10.1.0",
63
63
  "remark-gfm": "^4.0.1",
64
- "shiki": "^4.4.3",
64
+ "shiki": "^4.5.0",
65
65
  "style-observer": "^0.1.2",
66
66
  "ts-extras": "^1.3.0",
67
67
  "type-fest": "^5.10.0"
68
68
  },
69
69
  "devDependencies": {
70
- "@storybook/addon-mcp": "^10.6.0",
71
- "@storybook/addon-themes": "^10.6.0",
72
- "@storybook/addon-vitest": "^10.6.0",
73
- "@storybook/react": "^10.6.0",
74
- "@storybook/react-vite": "^10.6.0",
70
+ "@storybook/addon-mcp": "^10.6.2",
71
+ "@storybook/addon-themes": "^10.6.2",
72
+ "@storybook/addon-vitest": "^10.6.2",
73
+ "@storybook/react": "^10.6.2",
74
+ "@storybook/react-vite": "^10.6.2",
75
75
  "@svgr/core": "^8.1.0",
76
76
  "@svgr/plugin-jsx": "^8.1.0",
77
77
  "@svgr/plugin-svgo": "^8.1.0",
78
78
  "@tailwindcss/cli": "^4.3.3",
79
79
  "@tailwindcss/postcss": "^4.3.3",
80
80
  "@tailwindcss/vite": "^4.3.3",
81
- "@types/node": "^26.6.3",
81
+ "@types/node": "^26.6.5",
82
82
  "@types/react": "^19.3.0",
83
83
  "@types/react-dom": "^19.3.0",
84
84
  "@types/react-is": "^19.3.0",
85
- "@web3icons/react": "^4.1.22",
85
+ "@web3icons/react": "^4.1.24",
86
86
  "lightningcss": "^1.33.0",
87
- "postcss": "^8.5.28",
88
- "react": "^19.3.0",
89
- "react-dom": "^19.3.0",
90
- "react-is": "^19.3.0",
91
- "storybook": "^10.6.0",
87
+ "postcss": "^8.5.30",
88
+ "react": "^19.3.1",
89
+ "react-dom": "^19.3.1",
90
+ "react-is": "^19.3.1",
91
+ "storybook": "^10.6.2",
92
92
  "storybook-addon-rtl": "^3.0.2",
93
93
  "tailwindcss": "^4.3.3"
94
94
  },
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: eds
3
- version: 1.0.2
3
+ version: 1.1.1
4
4
  description: The Edge Design System (EDS) is for building user interfaces for Edge & Node products. Use this skill when designing, implementing, reviewing, or polishing Edge & Node product UIs, when using `@edgeandnode/eds-react`, or when EDS is mentioned. Covers component selection, design patterns, tokens, and best practices – not just APIs. APIs are fully typed in the npm package.
5
5
  allowed-tools:
6
6
  - mcp__storybook__get-changed-stories
@@ -28,7 +28,7 @@ EDS is the design system for Edge & Node products. This skill teaches you how to
28
28
  "mcpServers": {
29
29
  "storybook": {
30
30
  "type": "http",
31
- "url": "https://e4b81a8--6ab2abb8934bb76c7e8e086c.chromatic.com/mcp"
31
+ "url": "https://e3c7a4b--6ab2abb8934bb76c7e8e086c.chromatic.com/mcp"
32
32
  }
33
33
  }
34
34
  }
@@ -43,7 +43,7 @@ Add this to the project's `.mcp.json` file (create it if it doesn't exist). For
43
43
  ```json
44
44
  {
45
45
  "dependencies": {
46
- "@edgeandnode/eds-react": "^1.0.2",
46
+ "@edgeandnode/eds-react": "^1.1.1",
47
47
  "react": "^19.0.0",
48
48
  "react-dom": "^19.0.0"
49
49
  },
@@ -82,21 +82,32 @@ function App() {
82
82
  }
83
83
  ```
84
84
 
85
- **Only one `EDSProvider` per app** — rendering more than one will throw an error. To render part of the UI in a different theme, wrap it in `<Dark>` or `<Light>` instead (e.g. `<Dark as="section">`).
85
+ **Only one `EDSProvider` per app** — rendering more than one will throw an error. To render part of the UI in a different theme, wrap it in `<Dark>` or `<Light>` instead (e.g. `<Dark as="section">`), or in `<LocalTheme theme={…}>` if the theme changes.
86
86
 
87
87
  ### Theming
88
88
 
89
89
  `EDSProvider`'s theme handling supports **controlled** mode (pass `theme`, optionally with `onThemeChange`) and **uncontrolled** mode (omit `theme`; optionally pass `defaultTheme`, which defaults to `'dark'`). To make the user's theme selection survive reloads, opt into persistence with `persistTheme="localStorage"`, `persistTheme="cookie"`, or a custom `{ get, set }` adapter — omit for no persistence.
90
90
 
91
+ With `persistTheme="cookie"` in a server-rendered app, also pass the saved theme to `defaultTheme`, so the server renders it instead of switching to it after hydration (e.g. `ThemeSwitcher`'s selected option), except for a saved `'system'` theme, which the server can't resolve. `getThemeFromCookies()` reads it from the request's headers, and returns `undefined` if there's none (add `?? 'system'` for a default other than `'dark'`). In Next.js, do it in the root layout (which makes routes render on each request), and if `EDSProvider` is in a client component, pass the saved theme to it through a prop (the `Theme` type is exported):
92
+
93
+ ```tsx
94
+ import { headers } from 'next/headers'
95
+ import { EDSProvider, getThemeFromCookies } from '@edgeandnode/eds-react'
96
+
97
+ <EDSProvider persistTheme="cookie" defaultTheme={getThemeFromCookies(await headers())}>
98
+ ```
99
+
100
+ If the app's Content Security Policy requires nonces on inline scripts and styles, pass the nonce to `EDSProvider`'s `nonce` prop.
101
+
91
102
  To read or change the theme from anywhere in the app, use the `useEDS()` hook:
92
103
 
93
104
  ```tsx
94
105
  import { useEDS } from '@edgeandnode/eds-react/hooks'
95
106
 
96
- const { theme, activeTheme, setTheme } = useEDS()
107
+ const { theme, setTheme, activeTheme } = useEDS()
97
108
  ```
98
109
 
99
- `theme` is `'dark' | 'light' | 'system'`. `activeTheme` resolves `'system'` to `'dark'` or `'light'` based on the user's OS preference. `setTheme(theme)` changes it (use `<ThemeSwitcher>` for the UI). `localTheme` is the theme of the nearest `<Dark>` or `<Light>` ancestor, or `undefined` if there's none (including inside a `Modal`, which always uses the page's theme) — use `localTheme ?? activeTheme` when rendering something that depends on the theme in JavaScript. `localThemeClass` is that ancestor's class (or `undefined` if there's none) — add it to content you render outside of that ancestor in the DOM (e.g. with `createPortal`) so it keeps its theme.
110
+ `theme` is the page's theme, `'dark' | 'light' | 'system'`, and `setTheme(theme)` changes it (use `<ThemeSwitcher>` for the UI). `activeTheme` is the theme content is rendered in, `'dark'` or `'light'` — use it when rendering something that depends on the theme in JavaScript. It's `localTheme` if there is one, or else the page's theme with `'system'` resolved to `systemTheme` (the OS preference). `localTheme` is the theme of the nearest `<Dark>`, `<Light>`, or `<LocalTheme>` ancestor, or `undefined` if there's none (including inside a `Modal`, which always uses the page's theme). `localThemeClass` is that ancestor's class (or `undefined` if there's none) — add it to content you render outside of that ancestor in the DOM (e.g. with `createPortal`) so it keeps its theme.
100
111
 
101
112
  ### Router Integration (Next.js)
102
113
 
@@ -161,6 +172,23 @@ import { EDSProvider } from '@edgeandnode/eds-react'
161
172
  >
162
173
  ```
163
174
 
175
+ ### Plain CSS (Without EDS's Tailwind Setup)
176
+
177
+ If the project can't use `@edgeandnode/eds-react` and EDS's Tailwind setup (e.g. it doesn't use Tailwind, or it's built with a framework that ships its own Tailwind build), it can still use EDS's design tokens and fonts as plain CSS custom properties from `@edgeandnode/eds-css`. **This setup has no components and no utilities**, so every UI element has to be built and styled by hand. Only use it when the regular setup isn't possible, and tell the user about this trade-off when suggesting it.
178
+
179
+ Import the tokens and fonts, and set the page's color scheme:
180
+
181
+ ```css
182
+ @import '@edgeandnode/eds-css/fonts.css';
183
+ @import '@edgeandnode/eds-css/variables.css';
184
+
185
+ :root {
186
+ color-scheme: dark;
187
+ }
188
+ ```
189
+
190
+ The tokens in [tokens.md](references/tokens.md) map to variables named after their utilities' namespaces, with an `eds-` prefix: `text-default` is `var(--eds-text-color-default)`, `bg-muted` is `var(--eds-background-color-muted)`, `border-subtle` is `var(--eds-border-color-subtle)`, `text-14` is `var(--eds-text-14)` (and `var(--eds-text-14--line-height)`), `font-medium` is `var(--eds-font-weight-medium)`, `rounded-8` is `var(--eds-radius-8)`, and `p-4` is `calc(var(--eds-spacing) * 4)`. Color tokens use `light-dark()`, so set `color-scheme` (`dark`, `light`, or `light dark`) on the page and on any part of it that should use the other theme. The constraints below about components and utilities don't apply, but the design guidance does.
191
+
164
192
  ## Constraints
165
193
 
166
194
  1. **Check component APIs before using them.** If the Storybook MCP is available, call `get-documentation` first — it shows prop types, valid values, and real usage examples. Otherwise, check the TypeScript types and JSDoc comments in `@edgeandnode/eds-react`. Never guess prop names or values. Fix all TypeScript errors. Do not use `as any` or `@ts-ignore`.
@@ -404,7 +432,7 @@ Login buttons are typically `secondary`, not `primary`. Sign-up or critical CTAs
404
432
  - **`text-caption` is for small annotations, not section headings.** Use it for labels like "STATUS", "CATEGORY", "OVERVIEW" in small text. For section headings, use regular `text-20 font-medium` or `text-18 font-medium`.
405
433
  - **`text-10` is very small.** Use it sparingly, mostly paired with `text-caption`. For labels and small text, prefer `text-12`.
406
434
  - **`font-sans` (Inter) is the default and inherits.** Never set it explicitly.
407
- - **`font-serif` (Newsreader) is opt-in.** Use it for editorial and marketing headings. Don't use it in product UI unless the user asks for it.
435
+ - **`font-serif` (Newsreader) is opt-in.** Use it for editorial and marketing headings. Don't use it in product UI unless the user asks for it. For serif headings in `prose` content, add the `prose-serif` class (or the `serif` prop on `<Prose>`) instead of setting it on each heading.
408
436
  - **`font-mono` is the system monospace font on purpose.** Don't replace it with a web font. `CodeBlock` and `CodeInline` already apply it.
409
437
 
410
438
  ### Anti-Patterns
@@ -511,47 +539,47 @@ EDS components handle basic accessibility. Your job:
511
539
 
512
540
  Memorize this list before any UI work to avoid reimplementing existing components.
513
541
 
514
- | Component | Description |
515
- | ------------------------------ | ------------------------------------------------------------------ |
516
- | `Address` | Crypto addresses. Auto-truncates. Supports copy, friendly name. |
517
- | `Avatar` / `AvatarGroup` | User/wallet avatars from hashes. Can be clickable. |
518
- | `Breadcrumbs` | Navigation hierarchy. Auto-collapses. Underused. |
519
- | `Button` / `ButtonGroup` | Actions/navigation. `<button>` or `<a>` based on `href`. |
520
- | `Card` | Content grouping. Can be clickable with interactive content. |
521
- | `Checkbox` / `Checkbox.Group` | Multi-select. `Checkbox.Area` for card-style. |
522
- | `Chip` / `Chip.Group` | Filters, tags. `type="checkbox"` for multi-select. |
523
- | `Cluster` | "+X more" patterns. Works with `AvatarGroup`, `Tag`. Underused. |
524
- | `CodeBlock` / `CodeBlock.Tabs` | Code snippets with syntax highlighting. Underused. |
525
- | `CodeInline` | Inline code for variables, function names, etc. |
526
- | `CopyButton` | Copy to clipboard. Extends `Button`. |
527
- | `CurrencyInput` | Crypto amounts. Use with `useCurrencyInput` hook. |
528
- | `Dark` / `Light` | Renders part of the page in the dark or light theme. |
529
- | `DescriptionList` | Key-value pairs, metadata. Underused. |
530
- | `Divider` | Visual separation. Horizontal or vertical. |
531
- | `Icon` | Wraps icons. `src` and `alt` required. |
532
- | `Input` | Text/number input. Use `useNumberInput` for numbers. |
533
- | `Keyboard` | Keyboard shortcut display. Use instead of `<kbd>`. Underused. |
534
- | `Label` | Form labels only, not general styling. |
535
- | `Link` | Navigation. Inherits font size/weight. `href` required. |
536
- | `Menu` | Dropdowns, selects, context menus. `trigger` for uncontrolled. |
537
- | `Modal` | Dialogs. `trigger` for uncontrolled. |
538
- | `OTCInput` | One-time codes (2FA, OTP). Extends `Input`. |
539
- | `Pane` | Responsive sidebars. Overlay on mobile via CSS props. |
540
- | `Prose` | Renders markdown as styled HTML with EDS components. |
541
- | `Radio` / `Radio.Group` | Single-select. `Radio.Area` for card-style. |
542
- | `Search` | Search input. `focusKey` defaults to `'/'`. |
543
- | `SegmentedControl` | View options. Requires `aria-label`. |
544
- | `Select` | Single or `multiple`, `searchable`, declarative or data-driven. |
545
- | `Status` | Status indicators. Colored dot + optional text. |
546
- | `Stepper` | Multi-step flow indicators. |
547
- | `Switch` / `Switch.Area` | Toggle on/off. `Switch.Area` for card-style. |
548
- | `Table` | Data tables. Declarative or data-driven. |
549
- | `TabSet` | View switching. `fullWidth` for constrained space. Underused. |
550
- | `Tag` | Tags/categories. Not clickable; use `Chip` if clickable. |
551
- | `TextArea` | Multi-line input. |
552
- | `ThemeSwitcher` | Light/dark theme toggle. Right-click to toggle system sync. |
553
- | `ToggleButton` | Toggle actions. Extends `Button` with `checked`/`onCheckedChange`. |
554
- | `Tooltip` | Hover hints. Only wrap interactive elements. |
542
+ | Component | Description |
543
+ | ------------------------------- | ------------------------------------------------------------------ |
544
+ | `Address` | Crypto addresses. Auto-truncates. Supports copy, friendly name. |
545
+ | `Avatar` / `AvatarGroup` | User/wallet avatars from hashes. Can be clickable. |
546
+ | `Breadcrumbs` | Navigation hierarchy. Auto-collapses. Underused. |
547
+ | `Button` / `ButtonGroup` | Actions/navigation. `<button>` or `<a>` based on `href`. |
548
+ | `Card` | Content grouping. Can be clickable with interactive content. |
549
+ | `Checkbox` / `Checkbox.Group` | Multi-select. `Checkbox.Area` for card-style. |
550
+ | `Chip` / `Chip.Group` | Filters, tags. `type="checkbox"` for multi-select. |
551
+ | `Cluster` | "+X more" patterns. Works with `AvatarGroup`, `Tag`. Underused. |
552
+ | `CodeBlock` / `CodeBlock.Tabs` | Code snippets with syntax highlighting. Underused. |
553
+ | `CodeInline` | Inline code for variables, function names, etc. |
554
+ | `CopyButton` | Copy to clipboard. Extends `Button`. |
555
+ | `CurrencyInput` | Crypto amounts. Use with `useCurrencyInput` hook. |
556
+ | `Dark` / `Light` / `LocalTheme` | Renders part of the page in the dark or light theme. |
557
+ | `DescriptionList` | Key-value pairs, metadata. Underused. |
558
+ | `Divider` | Visual separation. Horizontal or vertical. |
559
+ | `Icon` | Wraps icons. `src` and `alt` required. |
560
+ | `Input` | Text/number input. Use `useNumberInput` for numbers. |
561
+ | `Keyboard` | Keyboard shortcut display. Use instead of `<kbd>`. Underused. |
562
+ | `Label` | Form labels only, not general styling. |
563
+ | `Link` | Navigation. Inherits font size/weight. `href` required. |
564
+ | `Menu` | Dropdowns, selects, context menus. `trigger` for uncontrolled. |
565
+ | `Modal` | Dialogs. `trigger` for uncontrolled. |
566
+ | `OTCInput` | One-time codes (2FA, OTP). Extends `Input`. |
567
+ | `Pane` | Responsive sidebars. Overlay on mobile via CSS props. |
568
+ | `Prose` | Renders markdown as styled HTML with EDS components. |
569
+ | `Radio` / `Radio.Group` | Single-select. `Radio.Area` for card-style. |
570
+ | `Search` | Search input. `focusKey` defaults to `'/'`. |
571
+ | `SegmentedControl` | View options. Requires `aria-label`. |
572
+ | `Select` | Single or `multiple`, `searchable`, declarative or data-driven. |
573
+ | `Status` | Status indicators. Colored dot + optional text. |
574
+ | `Stepper` | Multi-step flow indicators. |
575
+ | `Switch` / `Switch.Area` | Toggle on/off. `Switch.Area` for card-style. |
576
+ | `Table` | Data tables. Declarative or data-driven. |
577
+ | `TabSet` | View switching. `fullWidth` for constrained space. Underused. |
578
+ | `Tag` | Tags/categories. Not clickable; use `Chip` if clickable. |
579
+ | `TextArea` | Multi-line input. |
580
+ | `ThemeSwitcher` | Light/dark theme toggle. Right-click to toggle system sync. |
581
+ | `ToggleButton` | Toggle actions. Extends `Button` with `checked`/`onCheckedChange`. |
582
+ | `Tooltip` | Hover hints. Only wrap interactive elements. |
555
583
 
556
584
  ### Icons
557
585
 
@@ -629,7 +657,7 @@ import type { DistributedOmit } from 'type-fest'
629
657
  import { Card, type CardProps } from '@edgeandnode/eds-react'
630
658
  import { cn } from '@edgeandnode/eds-react/utils'
631
659
 
632
- interface EntityCardProps extends DistributedOmit<CardProps, 'variant'> {
660
+ type EntityCardProps = DistributedOmit<CardProps, 'variant'> & {
633
661
  name: string
634
662
  imageUrl: string
635
663
  }
@@ -644,7 +672,7 @@ function EntityCard({ name, imageUrl, className, ...props }: EntityCardProps) {
644
672
  }
645
673
  ```
646
674
 
647
- Use `DistributedOmit` from `type-fest` (not `Omit`) to preserve discriminated unions in prop types. Spread `{...props}` to forward all remaining props including `ref`.
675
+ Many EDS prop types (e.g. `CardProps`, `PaneProps`, `DarkProps`) are unions, which interfaces can't extend, so combine them with `&` in a type alias, and use `DistributedOmit` from `type-fest` (not `Omit`) to preserve the union. Spread `{...props}` to forward all remaining props including `ref`.
648
676
 
649
677
  ### Controlling components
650
678
 
@@ -672,18 +700,18 @@ Some components expand to fill available width by default: `Card`, `CodeBlock`,
672
700
  - **DescriptionList:** Long values truncate with gradient fade, scroll on mobile.
673
701
  - **Input:** For number inputs, use the `useNumberInput` hook (or `useCurrencyInput` for currency amounts) — it handles parsing, formatting, and min/max/required validation, and returns `props` to spread on `<Input>` (or `<CurrencyInput>`).
674
702
  - **Link:** Inherits font properties (except color when `variant="primary"`). External links auto-get `ArrowUpRightInteractiveIcon` via `addonAfter`; prevent with `addonAfter={null}`. Default variant is `primary` (emphasized) — use `variant="secondary"` for de-emphasized links like footers, fine print, etc.
675
- - **Menu:** Prefer the uncontrolled `trigger` prop (element form when possible, function form for advanced customization) over the controlled `open` + `onOpenChange`. Set `triggerMode="right-click"` and `anchor="pointer"` to turn any `Menu` into a context menu anchored at the right-click location (or long-press on mobile). Use `<Menu.Search>` in `header` for a search input that integrates with the menu's keyboard nav, and `<Menu.Empty>` for the empty state (defaults to `"No results"`).
676
- - **Modal:** Prefer the uncontrolled `trigger` prop (element form when possible, function form for advanced customization) over the controlled `open` + `onOpenChange`. Body text inside a `Modal` is muted by default; add `text-default` explicitly on headings or emphasized text (or use the `prose` class / `<Prose>`, which handle it automatically).
703
+ - **Menu:** Prefer the uncontrolled `trigger` prop (element form when possible, function form for advanced customization) over the controlled `open` + `onOpenChange`. Set `triggerMode="right-click"` and `anchor="pointer"` to turn any `Menu` into a context menu anchored at the right-click location (or long-press on mobile). Set `searchable` to add a search input that filters items by their rendered text, addons included (e.g. a `Tag`); set `label` on a `Menu.Item` to match against something else instead (e.g. `label={address}` on an item showing `formatAddress(address)`). Pass a function to `searchable` for custom matching, or `"manual"` to filter items yourself (e.g. results from an API) with `searchValue` and `onSearchValueChange`, and set `loading` while fetching them. `empty` replaces the default `"No results"` state. A custom `header` must include `<Menu.Search>`, which handles the keyboard navigation.
704
+ - **Modal:** Prefer the uncontrolled `trigger` prop (element form when possible, function form for advanced customization) over the controlled `open` + `onOpenChange`. Body text inside a `Modal` is muted by default; add `text-default` explicitly on headings or emphasized text (or use the `prose` class / `<Prose>`, which handle it automatically). A `Modal.Header` title names the dialog; without one, pass `aria-label` (`aria-label`, `aria-labelledby`, and `aria-describedby` go on the dialog itself).
677
705
  - **Multi-step Modals:** Always render the exact same set of `<Modal.Header>` / `<Modal.Body>` / `<Modal.Footer>` in every step — leave one empty if a step doesn't need it, don't omit it. If every step is inlined in the same file, you can render them directly (see the `WithSteps` story). If you extract any step into its own component, wrap the conditional in an outer `<Modal.Step>` (provider) AND have each extracted step component wrap its Header/Body/Footer in an inner `<Modal.Step>` (consumer). Together they preserve stable element identity for the slots so React doesn't remount them across steps, which is what lets the transition animate. See the "Multi-step Modals" section in [patterns.md](references/patterns.md) for a complete example.
678
706
  - **Pane:** Use `prop-layout-overlay` at the appropriate breakpoint (e.g. `max-lg:`) for mobile overlay.
679
- - **Prose:** Apply the `prose` class to any container of text that may wrap or contain formatting, even a single paragraph. It styles headings, lists (`<ul>`, `<ol>`), and inline formatting (`<strong>`, `<em>`, etc.), and applies `text-muted` to body text with `text-default` on headings and emphasized (bolded) elements. The `<Prose>` component is a superset: it renders a container with the `prose` class AND converts markdown links, inline code, code blocks, and tables into proper EDS components (`Link`, `CodeInline`, `CodeBlock`, `Table`). Use it specifically for rendering markdown content; for hand-written JSX, use the `prose` class and reach for EDS components directly.
707
+ - **Prose:** Apply the `prose` class to any container of text that may wrap or contain formatting, even a single paragraph. It styles headings, lists (`<ul>`, `<ol>`), and inline formatting (`<strong>`, `<em>`, etc.), and applies `text-muted` to body text with `text-default` on headings and emphasized (bolded) elements. Regular links, inline code, and code blocks look like `Link`, `CodeInline`, and `CodeBlock` (without syntax highlighting), but prefer the components in JSX. The `<Prose>` component is a superset: it renders a container with the `prose` class AND converts markdown links, inline code, code blocks, and tables into proper EDS components (`Link`, `CodeInline`, `CodeBlock`, `Table`). Use it specifically for rendering markdown content; for hand-written JSX, use the `prose` class and reach for EDS components directly.
680
708
  - **Search:** `focusKey` defaults to `'/'`; set to `null` if multiple on same page.
681
709
  - **SegmentedControl:** Requires `aria-label`. Options can be icon-only.
682
710
  - **Select:** Renders its own trigger by default. Only use the `trigger` prop for a custom trigger — prefer its element form over its function form.
683
711
  - **Table:** Use `align="end"` on `Table.HeaderCell` for right-aligned columns (e.g. action buttons) — all cells in that column are automatically styled. Don't use `text-end` or `justify-end` on cell contents. Similarly, use `width` and `shrink` props on `Table.HeaderCell` to control column sizing.
684
712
  - **TabSet:** `TabSet.Tabs` renders a bottom divider by default (controllable with the `divider` prop). Add `gap-*` between `TabSet.Tabs` and `TabSet.Panels` for visual breathing room. Put borders on `TabSet.Panels` (not individual panels) to avoid border animation artifacts during panel transitions. Check `TabSet.stories.tsx` for good examples.
685
713
  - **Tag:** Not clickable; use `Chip` if you need a clickable tag.
686
- - **Tooltip:** Hard to discover on mobile. Use inline text for essential info.
714
+ - **Tooltip:** Hard to discover on mobile. Use inline text for essential info. It gives its trigger an `onClick`, which turns an `Avatar`, `Address`, `Cluster`, or `Breadcrumbs.Item` into a button, so pass them `as="span"` inside a link or another button (e.g. `<Cluster as="span">` in a clickable `Card`).
687
715
 
688
716
  ### Unstyled components
689
717
 
@@ -692,16 +720,22 @@ Some components carry no built-in visual styles — only behavior, semantics, or
692
720
  - **`Checkbox.Area` / `Radio.Area` / `Switch.Area`** — clickable label wrappers for card-style checkable patterns; style them as `Card`s, rows, tiles, whatever the design calls for. See the "Checkable Cards" section in [patterns.md](references/patterns.md).
693
721
  - **`Radio.Group`** — semantic wrapper around a group of `<Radio>`s. Apply your own layout (`flex`, `gap`, etc.).
694
722
  - **`Pane`** — responsive sidebar container. Provides positioning behavior but no visual styling.
695
- - **`Dark` / `Light`** — render their element (`as`, `div` by default) in the dark or light theme. They only set the color scheme and default text color, so style them as the section needs (e.g. `bg-default p-8`). `dark:`/`light:` variants on the element itself still follow the outer theme (style queries only look at ancestors), so use them on its descendants.
723
+ - **`Dark` / `Light` / `LocalTheme`** — render their element (a `div` by default, see below) in the dark or light theme. `LocalTheme` takes it as a `theme` prop, so changing it doesn't remount the content like switching between `Dark` and `Light` would. They only set the color scheme and default text color, so style them as the section needs (e.g. `bg-default p-8`). `dark:`/`light:` variants on the element itself still follow the outer theme (style queries only look at ancestors), so use them on its descendants.
696
724
  - **`ButtonOrLink`** (from `@edgeandnode/eds-react/base`) — semantic `<button>`/`<a>` wrapper with no visual styling. The other base components (`Transition`, `ExperimentalTransition`, `Presence`, `Render`, `Portal`) have transition or structural styles you shouldn't override.
697
725
 
726
+ ### Changing the rendered element
727
+
728
+ `Dark`, `Light`, `LocalTheme`, and `Pane` render a `div` by default, and their props are typed for any HTML element (e.g. `ref` takes an `HTMLElement`). Pass a tag name in `as` (e.g. `<Pane as="aside">`), or an element in `render` to render a component or pass props specific to the element (e.g. `<Dark render={<NextLink href="/" />}>`). `as` and `render` can't be combined.
729
+
730
+ `Card`, `Avatar`, `Address`, `Cluster`, and `Breadcrumbs.Item` render a button or link when given an `onClick` or `href`. Otherwise, their `as` also only takes a tag name, and it keeps them from becoming a button when something else gives them an `onClick` (see `Tooltip` above). When `Card` isn't a button or link, its props are typed for any HTML element too.
731
+
698
732
  ### Base components
699
733
 
700
734
  Available from `@edgeandnode/eds-react/base`. These are mostly unstyled lower-level building blocks — most consumers won't use them directly, but they're useful for advanced patterns:
701
735
 
702
736
  - `ButtonOrLink` — the foundation of `Button`, `Link`, `Address`, `Avatar`, `Card`, etc. Renders `<button>` or `<a>` based on `href`. Supports `linkComponent` for router integration.
703
737
  - `Transition` / `ExperimentalTransition` — Enter/exit animations with height transition. Used internally by `Modal`, `TabSet`, etc.
704
- - `Render` — polymorphic rendering via the `render` prop (e.g. `render={<Card as="label" />}`).
738
+ - `Render` — renders a `div`, the tag name passed in `as`, or the element or render function passed in `render` (e.g. `render={<Card as="label" />}`). The `AsOrRenderProps` and `HTMLElementProps` types add the same `as` and `render` props to your own components.
705
739
  - `Portal` — renders children in a portal (used by `Modal`, `Tooltip`, `Menu`).
706
740
  - `Presence` — conditionally renders children with enter/exit transitions (used by `ExperimentalTransition`, `Pane`).
707
741
 
@@ -728,7 +762,7 @@ import { formatBigInt, parseBigInt, numberToBigInt, bigIntToNumber } from '@edge
728
762
  ```
729
763
 
730
764
  - `cn` — concatenate class names, filtering out falsy values (like `clsx`)
731
- - `useEDS` — read and change the current theme (`{ theme, setTheme, activeTheme, localTheme, localThemeClass, dirProps }`); see "Theming"
765
+ - `useEDS` — read and change the current theme (`{ theme, setTheme, systemTheme, localTheme, activeTheme, localThemeClass, dirProps }`); see "Theming"
732
766
  - `useControlled` — manage controlled/uncontrolled state
733
767
  - `useNumberInput` — numeric input validation and formatting (use with `Input`)
734
768
  - `useCurrencyInput` — currency input validation (use with `CurrencyInput`)
@@ -168,7 +168,7 @@ When in doubt, add more space. Dense UIs look cheap.
168
168
 
169
169
  ## Theme Sections
170
170
 
171
- The `EDSProvider` `theme` prop sets the root theme. To render part of the UI in another theme, wrap it in `<Dark>` or `<Light>` (e.g. `<Dark as="section">`). They can be nested, and components inside them can read the section's theme with `useEDS().localTheme`. Menus, selects, and tooltips use the theme of the section they're in, while modals use the page's theme.
171
+ The `EDSProvider` `theme` prop sets the root theme. To render part of the UI in another theme, wrap it in `<Dark>` or `<Light>` (e.g. `<Dark as="section">`), or in `<LocalTheme theme={…}>` if the theme changes (switching between `Dark` and `Light` would remount their content). They can be nested, and components inside them can read the section's theme with `useEDS().activeTheme`. Menus, selects, and tooltips use the theme of the section they're in, while modals use the page's theme.
172
172
 
173
173
  ## Breakpoints
174
174
 
package/src/EDSContext.ts CHANGED
@@ -22,13 +22,13 @@ export type ThemePersistence =
22
22
 
23
23
  export interface EDSContextValue {
24
24
  theme: Theme
25
- activeTheme: ResolvedTheme
26
25
  setTheme: (theme: Theme) => void
26
+ systemTheme: ResolvedTheme
27
27
  direction: NonNullable<ComponentProps<typeof DirectionProvider>['direction']>
28
28
  language: string
29
29
  }
30
30
 
31
31
  export const EDSContext = createContext<EDSContextValue | null>(null)
32
32
 
33
- /** The theme of the nearest `Dark` or `Light` ancestor, if any. */
33
+ /** The theme of the nearest `Dark`, `Light`, or `LocalTheme` ancestor, if any. */
34
34
  export const LocalThemeContext = createContext<ResolvedTheme | undefined>(undefined)