@edgeandnode/eds-react 1.0.1 → 1.1.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.
Files changed (219) hide show
  1. package/README.md +50 -8
  2. package/dist/EDSContext.d.ts +4 -1
  3. package/dist/EDSContext.d.ts.map +1 -1
  4. package/dist/EDSContext.js +7 -0
  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 +60 -68
  9. package/dist/EDSProvider.js.map +1 -1
  10. package/dist/components/Address.d.ts +3 -3
  11. package/dist/components/Address.d.ts.map +1 -1
  12. package/dist/components/Address.js.map +1 -1
  13. package/dist/components/Avatar.d.ts +3 -3
  14. package/dist/components/Avatar.d.ts.map +1 -1
  15. package/dist/components/Avatar.js +3 -2
  16. package/dist/components/Avatar.js.map +1 -1
  17. package/dist/components/Breadcrumbs.parts.d.ts +7 -6
  18. package/dist/components/Breadcrumbs.parts.d.ts.map +1 -1
  19. package/dist/components/Breadcrumbs.parts.js +15 -14
  20. package/dist/components/Breadcrumbs.parts.js.map +1 -1
  21. package/dist/components/Button.d.ts.map +1 -1
  22. package/dist/components/Button.js +3 -2
  23. package/dist/components/Button.js.map +1 -1
  24. package/dist/components/Card.d.ts +8 -4
  25. package/dist/components/Card.d.ts.map +1 -1
  26. package/dist/components/Card.js +2 -1
  27. package/dist/components/Card.js.map +1 -1
  28. package/dist/components/Cluster.d.ts +3 -3
  29. package/dist/components/Cluster.d.ts.map +1 -1
  30. package/dist/components/Cluster.js.map +1 -1
  31. package/dist/components/CodeBlock.parts.d.ts.map +1 -1
  32. package/dist/components/CodeBlock.parts.js +16 -20
  33. package/dist/components/CodeBlock.parts.js.map +1 -1
  34. package/dist/components/CopyButton.d.ts.map +1 -1
  35. package/dist/components/CopyButton.js +3 -2
  36. package/dist/components/CopyButton.js.map +1 -1
  37. package/dist/components/Icon.d.ts.map +1 -1
  38. package/dist/components/Icon.js +3 -3
  39. package/dist/components/Icon.js.map +1 -1
  40. package/dist/components/Keyboard.js +1 -1
  41. package/dist/components/LocalTheme.d.ts +29 -0
  42. package/dist/components/LocalTheme.d.ts.map +1 -0
  43. package/dist/components/LocalTheme.js +28 -0
  44. package/dist/components/LocalTheme.js.map +1 -0
  45. package/dist/components/Menu.parts.d.ts +43 -9
  46. package/dist/components/Menu.parts.d.ts.map +1 -1
  47. package/dist/components/Menu.parts.js +121 -115
  48. package/dist/components/Menu.parts.js.map +1 -1
  49. package/dist/components/Modal.parts.d.ts +1 -1
  50. package/dist/components/Modal.parts.d.ts.map +1 -1
  51. package/dist/components/Modal.parts.js +22 -11
  52. package/dist/components/Modal.parts.js.map +1 -1
  53. package/dist/components/Pane.parts.d.ts +23 -22
  54. package/dist/components/Pane.parts.d.ts.map +1 -1
  55. package/dist/components/Pane.parts.js +3 -3
  56. package/dist/components/Pane.parts.js.map +1 -1
  57. package/dist/components/Prose.d.ts +1 -1
  58. package/dist/components/Prose.d.ts.map +1 -1
  59. package/dist/components/Prose.js +3 -3
  60. package/dist/components/Prose.js.map +1 -1
  61. package/dist/components/Prose.meta.d.ts +5 -0
  62. package/dist/components/Prose.meta.d.ts.map +1 -1
  63. package/dist/components/Prose.meta.js +10 -0
  64. package/dist/components/Prose.meta.js.map +1 -1
  65. package/dist/components/SegmentedControl.parts.d.ts.map +1 -1
  66. package/dist/components/SegmentedControl.parts.js +3 -2
  67. package/dist/components/SegmentedControl.parts.js.map +1 -1
  68. package/dist/components/Select.parts.d.ts +12 -7
  69. package/dist/components/Select.parts.d.ts.map +1 -1
  70. package/dist/components/Select.parts.js +28 -18
  71. package/dist/components/Select.parts.js.map +1 -1
  72. package/dist/components/TabSet.parts.d.ts.map +1 -1
  73. package/dist/components/TabSet.parts.js +3 -2
  74. package/dist/components/TabSet.parts.js.map +1 -1
  75. package/dist/components/Table.parts.d.ts.map +1 -1
  76. package/dist/components/Table.parts.js +3 -4
  77. package/dist/components/Table.parts.js.map +1 -1
  78. package/dist/components/ThemeSwitcher.d.ts +5 -5
  79. package/dist/components/ThemeSwitcher.d.ts.map +1 -1
  80. package/dist/components/ThemeSwitcher.js +6 -5
  81. package/dist/components/ThemeSwitcher.js.map +1 -1
  82. package/dist/components/Tooltip.parts.js +2 -2
  83. package/dist/components/Tooltip.parts.js.map +1 -1
  84. package/dist/components/base/Addon.d.ts.map +1 -1
  85. package/dist/components/base/Addon.js +2 -4
  86. package/dist/components/base/Addon.js.map +1 -1
  87. package/dist/components/base/ButtonOrLink.parts.d.ts.map +1 -1
  88. package/dist/components/base/ButtonOrLink.parts.js +2 -1
  89. package/dist/components/base/ButtonOrLink.parts.js.map +1 -1
  90. package/dist/components/base/MaybeButtonOrLink.d.ts +7 -5
  91. package/dist/components/base/MaybeButtonOrLink.d.ts.map +1 -1
  92. package/dist/components/base/MaybeButtonOrLink.js +7 -2
  93. package/dist/components/base/MaybeButtonOrLink.js.map +1 -1
  94. package/dist/components/base/Render.d.ts +25 -6
  95. package/dist/components/base/Render.d.ts.map +1 -1
  96. package/dist/components/base/Render.js +4 -3
  97. package/dist/components/base/Render.js.map +1 -1
  98. package/dist/components/base/index.d.ts +1 -1
  99. package/dist/components/base/index.d.ts.map +1 -1
  100. package/dist/components/base/index.js.map +1 -1
  101. package/dist/components/index.d.ts +1 -0
  102. package/dist/components/index.d.ts.map +1 -1
  103. package/dist/components/index.js +1 -0
  104. package/dist/components/index.js.map +1 -1
  105. package/dist/hooks/useCSSProp.js +1 -1
  106. package/dist/hooks/useCSSProp.js.map +1 -1
  107. package/dist/hooks/useEDS.d.ts +24 -1
  108. package/dist/hooks/useEDS.d.ts.map +1 -1
  109. package/dist/hooks/useEDS.js +26 -2
  110. package/dist/hooks/useEDS.js.map +1 -1
  111. package/dist/hooks/useStyleObserver.d.ts.map +1 -1
  112. package/dist/hooks/useStyleObserver.js +2 -1
  113. package/dist/hooks/useStyleObserver.js.map +1 -1
  114. package/dist/icons/CalendarDynamicIcon.d.ts.map +1 -1
  115. package/dist/icons/CalendarDynamicIcon.js +3 -1
  116. package/dist/icons/CalendarDynamicIcon.js.map +1 -1
  117. package/dist/index.d.ts +2 -1
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +1 -0
  120. package/dist/index.js.map +1 -1
  121. package/dist/theme-persistence.d.ts +18 -0
  122. package/dist/theme-persistence.d.ts.map +1 -0
  123. package/dist/theme-persistence.js +25 -0
  124. package/dist/theme-persistence.js.map +1 -0
  125. package/dist/utils/getReactNodeKey.d.ts.map +1 -1
  126. package/dist/utils/getReactNodeKey.js +31 -13
  127. package/dist/utils/getReactNodeKey.js.map +1 -1
  128. package/dist/utils/index.d.ts +1 -1
  129. package/dist/utils/index.d.ts.map +1 -1
  130. package/dist/utils/index.js +1 -1
  131. package/dist/utils/index.js.map +1 -1
  132. package/dist/utils/internal/getTooltipProps.d.ts +9 -0
  133. package/dist/utils/internal/getTooltipProps.d.ts.map +1 -0
  134. package/dist/utils/internal/getTooltipProps.js +18 -0
  135. package/dist/utils/internal/getTooltipProps.js.map +1 -0
  136. package/dist/utils/internal/index.d.ts +4 -0
  137. package/dist/utils/internal/index.d.ts.map +1 -0
  138. package/dist/utils/internal/index.js +4 -0
  139. package/dist/utils/internal/index.js.map +1 -0
  140. package/dist/utils/internal/resolveLazy.d.ts +29 -0
  141. package/dist/utils/internal/resolveLazy.d.ts.map +1 -0
  142. package/dist/utils/internal/resolveLazy.js +40 -0
  143. package/dist/utils/internal/resolveLazy.js.map +1 -0
  144. package/dist/utils/internal/styleScheduler.d.ts.map +1 -0
  145. package/dist/utils/internal/styleScheduler.js.map +1 -0
  146. package/dist/utils/isComponentType.d.ts +9 -0
  147. package/dist/utils/isComponentType.d.ts.map +1 -0
  148. package/dist/utils/isComponentType.js +22 -0
  149. package/dist/utils/isComponentType.js.map +1 -0
  150. package/dist/utils/isReactNode.d.ts +7 -0
  151. package/dist/utils/isReactNode.d.ts.map +1 -1
  152. package/dist/utils/isReactNode.js +11 -0
  153. package/dist/utils/isReactNode.js.map +1 -1
  154. package/dist/utils/reactNodeToString.d.ts +3 -0
  155. package/dist/utils/reactNodeToString.d.ts.map +1 -1
  156. package/dist/utils/reactNodeToString.js +6 -0
  157. package/dist/utils/reactNodeToString.js.map +1 -1
  158. package/dist/utils/styleObserver.js +1 -1
  159. package/dist/utils/trimReactNode.d.ts +3 -0
  160. package/dist/utils/trimReactNode.d.ts.map +1 -1
  161. package/dist/utils/trimReactNode.js +6 -0
  162. package/dist/utils/trimReactNode.js.map +1 -1
  163. package/package.json +24 -23
  164. package/scripts/update-skill.cjs +18 -5
  165. package/skill/SKILL.md +95 -57
  166. package/skill/references/tokens.md +2 -8
  167. package/src/EDSContext.ts +10 -1
  168. package/src/EDSProvider.tsx +111 -80
  169. package/src/components/Address.tsx +4 -3
  170. package/src/components/Avatar.tsx +10 -5
  171. package/src/components/Breadcrumbs.parts.tsx +35 -18
  172. package/src/components/Button.tsx +7 -2
  173. package/src/components/Card.tsx +14 -8
  174. package/src/components/Cluster.tsx +5 -3
  175. package/src/components/CodeBlock.parts.tsx +20 -24
  176. package/src/components/CopyButton.tsx +3 -2
  177. package/src/components/Icon.tsx +3 -9
  178. package/src/components/Keyboard.tsx +1 -1
  179. package/src/components/LocalTheme.tsx +47 -0
  180. package/src/components/Menu.parts.tsx +368 -245
  181. package/src/components/Modal.parts.tsx +28 -11
  182. package/src/components/Pane.parts.tsx +31 -26
  183. package/src/components/Prose.meta.ts +10 -0
  184. package/src/components/Prose.tsx +4 -3
  185. package/src/components/SegmentedControl.parts.tsx +3 -8
  186. package/src/components/Select.parts.tsx +43 -34
  187. package/src/components/TabSet.parts.tsx +3 -5
  188. package/src/components/Table.parts.tsx +3 -3
  189. package/src/components/ThemeSwitcher.tsx +13 -15
  190. package/src/components/Tooltip.parts.tsx +2 -2
  191. package/src/components/base/Addon.tsx +3 -4
  192. package/src/components/base/ButtonOrLink.parts.tsx +2 -1
  193. package/src/components/base/MaybeButtonOrLink.tsx +11 -6
  194. package/src/components/base/Render.tsx +28 -4
  195. package/src/components/base/index.ts +2 -0
  196. package/src/components/index.ts +8 -0
  197. package/src/hooks/useCSSProp.ts +1 -1
  198. package/src/hooks/useEDS.ts +26 -2
  199. package/src/hooks/useStyleObserver.ts +2 -1
  200. package/src/icons/CalendarDynamicIcon.tsx +1 -0
  201. package/src/index.ts +2 -1
  202. package/src/theme-persistence.ts +31 -0
  203. package/src/utils/getReactNodeKey.ts +30 -14
  204. package/src/utils/index.ts +1 -1
  205. package/src/utils/internal/getTooltipProps.ts +25 -0
  206. package/src/utils/internal/index.ts +3 -0
  207. package/src/utils/internal/resolveLazy.ts +44 -0
  208. package/src/utils/isComponentType.ts +27 -0
  209. package/src/utils/isReactNode.ts +11 -0
  210. package/src/utils/reactNodeToString.ts +7 -0
  211. package/src/utils/styleObserver.ts +1 -1
  212. package/src/utils/trimReactNode.tsx +7 -0
  213. package/dist/utils/styleScheduler.d.ts.map +0 -1
  214. package/dist/utils/styleScheduler.js.map +0 -1
  215. package/src/__snapshots__/compiled.css +0 -22499
  216. package/styles/tailwind.storybook.css +0 -19
  217. /package/dist/utils/{styleScheduler.d.ts → internal/styleScheduler.d.ts} +0 -0
  218. /package/dist/utils/{styleScheduler.js → internal/styleScheduler.js} +0 -0
  219. /package/src/utils/{styleScheduler.ts → internal/styleScheduler.ts} +0 -0
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: eds
3
- version: 1.0.1
3
+ version: 1.1.0
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://f87aca8--6ab2abb8934bb76c7e8e086c.chromatic.com/mcp"
31
+ "url": "https://3ebe140--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.1",
46
+ "@edgeandnode/eds-react": "^1.1.0",
47
47
  "react": "^19.0.0",
48
48
  "react-dom": "^19.0.0"
49
49
  },
@@ -67,6 +67,8 @@ Add this to the project's `.mcp.json` file (create it if it doesn't exist). For
67
67
 
68
68
  **Important:** `@edgeandnode/eds-css`, which `@edgeandnode/eds-react` depends on, disables Tailwind's default content scanning as an optimization, so you must explicitly [register your source paths with `@source` directives](https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-registering-sources). Make sure to include all paths where Tailwind classes are used.
69
69
 
70
+ **Browser targets:** EDS's color tokens rely on native `light-dark()`, so the app must target Chrome 123, Safari 17.5, and Firefox 120 or newer. Otherwise, build tools transpile it (e.g. Lightning CSS, which Vite and Next.js use) and components inside `<Dark>`/`<Light>` get the page's colors (`EDSProvider` warns in the console; with Vite, only in production builds). In Next.js, add `"browserslist": ["chrome >= 123", "edge >= 123", "firefox >= 120", "safari >= 17.5", "ios_saf >= 17.5"]` to `package.json`; in Vite, set `build.target: ['chrome123', 'edge123', 'firefox120', 'safari17.5', 'ios17.5']` (it also covers CSS, and unlike `build.cssTarget`, a Vite-based Storybook picks it up).
71
+
70
72
  ### App Root
71
73
 
72
74
  ```tsx
@@ -80,21 +82,32 @@ function App() {
80
82
  }
81
83
  ```
82
84
 
83
- **Only one `EDSProvider` per app** — rendering more than one will throw an error. To apply different themes to parts of the UI, use the `eds-dark` / `eds-light` / `eds-system` classes instead (see "Theme Classes" in [tokens.md](references/tokens.md)).
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.
84
86
 
85
87
  ### Theming
86
88
 
87
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.
88
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
+
89
102
  To read or change the theme from anywhere in the app, use the `useEDS()` hook:
90
103
 
91
104
  ```tsx
92
- import { useEDS } from '@edgeandnode/eds-react'
105
+ import { useEDS } from '@edgeandnode/eds-react/hooks'
93
106
 
94
- const { theme, activeTheme, setTheme } = useEDS()
107
+ const { theme, setTheme, activeTheme } = useEDS()
95
108
  ```
96
109
 
97
- `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).
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.
98
111
 
99
112
  ### Router Integration (Next.js)
100
113
 
@@ -159,6 +172,23 @@ import { EDSProvider } from '@edgeandnode/eds-react'
159
172
  >
160
173
  ```
161
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
+
162
192
  ## Constraints
163
193
 
164
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`.
@@ -212,7 +242,7 @@ import { EDSProvider } from '@edgeandnode/eds-react'
212
242
  </Button>
213
243
  ```
214
244
 
215
- 15. **Pass icons as components, not elements, in addon props.** Use `addonBefore={PlayIcon}` not `addonBefore={<PlayIcon alt="" />}`. The element form (`<PlayIcon alt="..." />`) is only needed when passing a non-empty `alt` or a `variant`, or when the component consuming the addon is a React Server Component (RSCs can't receive functions as props — use `addonBefore={<PlayIcon alt="" />}`). Addons have a default size tied to the parent component — don't set `size` on them.
245
+ 15. **Pass icons as components, not elements, in addon props.** Use `addonBefore={PlayIcon}` not `addonBefore={<PlayIcon alt="" />}`. The element form (`<PlayIcon alt="..." />`) is only needed when passing a non-empty `alt` or a `variant`, or when passing it from a server component (server components can't pass functions to client components — use `addonBefore={<PlayIcon alt="" />}`). Addons have a default size tied to the parent component — don't set `size` on them.
216
246
 
217
247
  16. **EDS icons don't shrink.** Icons from `@edgeandnode/eds-react/icons` have `shrink-0` built in. Don't add it manually — it's redundant. However, if you wrap an icon in another element (e.g. a `div`), that wrapper may still need `shrink-0` if it's a child of a `flex` container.
218
248
 
@@ -402,7 +432,7 @@ Login buttons are typically `secondary`, not `primary`. Sign-up or critical CTAs
402
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`.
403
433
  - **`text-10` is very small.** Use it sparingly, mostly paired with `text-caption`. For labels and small text, prefer `text-12`.
404
434
  - **`font-sans` (Inter) is the default and inherits.** Never set it explicitly.
405
- - **`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.
406
436
  - **`font-mono` is the system monospace font on purpose.** Don't replace it with a web font. `CodeBlock` and `CodeInline` already apply it.
407
437
 
408
438
  ### Anti-Patterns
@@ -509,46 +539,47 @@ EDS components handle basic accessibility. Your job:
509
539
 
510
540
  Memorize this list before any UI work to avoid reimplementing existing components.
511
541
 
512
- | Component | Description |
513
- | ------------------------------ | ------------------------------------------------------------------ |
514
- | `Address` | Crypto addresses. Auto-truncates. Supports copy, friendly name. |
515
- | `Avatar` / `AvatarGroup` | User/wallet avatars from hashes. Can be clickable. |
516
- | `Breadcrumbs` | Navigation hierarchy. Auto-collapses. Underused. |
517
- | `Button` / `ButtonGroup` | Actions/navigation. `<button>` or `<a>` based on `href`. |
518
- | `Card` | Content grouping. Can be clickable with interactive content. |
519
- | `Checkbox` / `Checkbox.Group` | Multi-select. `Checkbox.Area` for card-style. |
520
- | `Chip` / `Chip.Group` | Filters, tags. `type="checkbox"` for multi-select. |
521
- | `Cluster` | "+X more" patterns. Works with `AvatarGroup`, `Tag`. Underused. |
522
- | `CodeBlock` / `CodeBlock.Tabs` | Code snippets with syntax highlighting. Underused. |
523
- | `CodeInline` | Inline code for variables, function names, etc. |
524
- | `CopyButton` | Copy to clipboard. Extends `Button`. |
525
- | `CurrencyInput` | Crypto amounts. Use with `useCurrencyInput` hook. |
526
- | `DescriptionList` | Key-value pairs, metadata. Underused. |
527
- | `Divider` | Visual separation. Horizontal or vertical. |
528
- | `Icon` | Wraps icons. `src` and `alt` required. |
529
- | `Input` | Text/number input. Use `useNumberInput` for numbers. |
530
- | `Keyboard` | Keyboard shortcut display. Use instead of `<kbd>`. Underused. |
531
- | `Label` | Form labels only, not general styling. |
532
- | `Link` | Navigation. Inherits font size/weight. `href` required. |
533
- | `Menu` | Dropdowns, selects, context menus. `trigger` for uncontrolled. |
534
- | `Modal` | Dialogs. `trigger` for uncontrolled. |
535
- | `OTCInput` | One-time codes (2FA, OTP). Extends `Input`. |
536
- | `Pane` | Responsive sidebars. Overlay on mobile via CSS props. |
537
- | `Prose` | Renders markdown as styled HTML with EDS components. |
538
- | `Radio` / `Radio.Group` | Single-select. `Radio.Area` for card-style. |
539
- | `Search` | Search input. `focusKey` defaults to `'/'`. |
540
- | `SegmentedControl` | View options. Requires `aria-label`. |
541
- | `Select` | Single or `multiple`, `searchable`, declarative or data-driven. |
542
- | `Status` | Status indicators. Colored dot + optional text. |
543
- | `Stepper` | Multi-step flow indicators. |
544
- | `Switch` / `Switch.Area` | Toggle on/off. `Switch.Area` for card-style. |
545
- | `Table` | Data tables. Declarative or data-driven. |
546
- | `TabSet` | View switching. `fullWidth` for constrained space. Underused. |
547
- | `Tag` | Tags/categories. Not clickable; use `Chip` if clickable. |
548
- | `TextArea` | Multi-line input. |
549
- | `ThemeSwitcher` | Light/dark theme toggle. Right-click to toggle system sync. |
550
- | `ToggleButton` | Toggle actions. Extends `Button` with `checked`/`onCheckedChange`. |
551
- | `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. |
552
583
 
553
584
  ### Icons
554
585
 
@@ -626,7 +657,7 @@ import type { DistributedOmit } from 'type-fest'
626
657
  import { Card, type CardProps } from '@edgeandnode/eds-react'
627
658
  import { cn } from '@edgeandnode/eds-react/utils'
628
659
 
629
- interface EntityCardProps extends DistributedOmit<CardProps, 'variant'> {
660
+ type EntityCardProps = DistributedOmit<CardProps, 'variant'> & {
630
661
  name: string
631
662
  imageUrl: string
632
663
  }
@@ -641,7 +672,7 @@ function EntityCard({ name, imageUrl, className, ...props }: EntityCardProps) {
641
672
  }
642
673
  ```
643
674
 
644
- 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`.
645
676
 
646
677
  ### Controlling components
647
678
 
@@ -664,16 +695,16 @@ Some components expand to fill available width by default: `Card`, `CodeBlock`,
664
695
 
665
696
  - **Address:** Handles truncation, ENS/friendly names (via `children`), copy (`'auto'` by default — shows on hover/focus). **Note:** `Address` renders its own `Avatar` using `createIdenticon(address)` — don't add a separate `Avatar` next to it or you'll get duplicate avatars.
666
697
  - **Button & ButtonGroup:** Set `size` on the group, not nested buttons. Can mix `tertiary` and `secondary`. `orientation="vertical"` for modals/panes. `fullWidth` makes buttons equal width; `className="w-full"` makes them grow proportionally. Danger takes over primary's visual role. **Sizing:** Product UIs should default to `size="small"` for most buttons; use medium (default) only for important actions. `size="large"` is for marketing pages only.
667
- - **Breadcrumbs:** Structure: `Product Name > Section > Current Page`. **Quirk:** `current` defaults to `true` when no `href` — always pass `current` explicitly.
698
+ - **Breadcrumbs:** Structure: `Product Name > Section > Current Page`. The last item defaults to `current`, so only set `current` to override that.
668
699
  - **Chip.Group:** Requires `aria-label`.
669
700
  - **DescriptionList:** Long values truncate with gradient fade, scroll on mobile.
670
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>`).
671
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.
672
- - **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"`).
673
- - **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).
674
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.
675
706
  - **Pane:** Use `prop-layout-overlay` at the appropriate breakpoint (e.g. `max-lg:`) for mobile overlay.
676
- - **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.
677
708
  - **Search:** `focusKey` defaults to `'/'`; set to `null` if multiple on same page.
678
709
  - **SegmentedControl:** Requires `aria-label`. Options can be icon-only.
679
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.
@@ -689,15 +720,22 @@ Some components carry no built-in visual styles — only behavior, semantics, or
689
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).
690
721
  - **`Radio.Group`** — semantic wrapper around a group of `<Radio>`s. Apply your own layout (`flex`, `gap`, etc.).
691
722
  - **`Pane`** — responsive sidebar container. Provides positioning behavior but no visual styling.
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.
692
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.
693
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
+ When `Card` isn't a button or link, its `as` also only takes a tag name, and its props are typed for any HTML element too.
731
+
694
732
  ### Base components
695
733
 
696
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:
697
735
 
698
736
  - `ButtonOrLink` — the foundation of `Button`, `Link`, `Address`, `Avatar`, `Card`, etc. Renders `<button>` or `<a>` based on `href`. Supports `linkComponent` for router integration.
699
737
  - `Transition` / `ExperimentalTransition` — Enter/exit animations with height transition. Used internally by `Modal`, `TabSet`, etc.
700
- - `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.
701
739
  - `Portal` — renders children in a portal (used by `Modal`, `Tooltip`, `Menu`).
702
740
  - `Presence` — conditionally renders children with enter/exit transitions (used by `ExperimentalTransition`, `Pane`).
703
741
 
@@ -724,7 +762,7 @@ import { formatBigInt, parseBigInt, numberToBigInt, bigIntToNumber } from '@edge
724
762
  ```
725
763
 
726
764
  - `cn` — concatenate class names, filtering out falsy values (like `clsx`)
727
- - `useEDS` — read and change the current theme (`{ theme, activeTheme, setTheme, dirProps }`); see "Theming"
765
+ - `useEDS` — read and change the current theme (`{ theme, setTheme, systemTheme, localTheme, activeTheme, localThemeClass, dirProps }`); see "Theming"
728
766
  - `useControlled` — manage controlled/uncontrolled state
729
767
  - `useNumberInput` — numeric input validation and formatting (use with `Input`)
730
768
  - `useCurrencyInput` — currency input validation (use with `CurrencyInput`)
@@ -166,15 +166,9 @@ Standard Tailwind spacing works (4px grid). EDS does **not** override spacing to
166
166
 
167
167
  When in doubt, add more space. Dense UIs look cheap.
168
168
 
169
- ## Theme Classes
169
+ ## Theme Sections
170
170
 
171
- | Class | Effect |
172
- | ------------ | ---------------------------------------------------------- |
173
- | `eds-dark` | Forces dark color scheme for this element and descendants |
174
- | `eds-light` | Forces light color scheme for this element and descendants |
175
- | `eds-system` | Follows the OS color scheme preference |
176
-
177
- These can be applied to any element to create nested theme regions. The `EDSProvider` `theme` prop sets the root theme; these classes override it locally.
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.
178
172
 
179
173
  ## Breakpoints
180
174
 
package/src/EDSContext.ts CHANGED
@@ -6,6 +6,12 @@ import type { DirectionProvider } from '@base-ui/react/direction-provider'
6
6
  export type Theme = 'dark' | 'light' | 'system'
7
7
  export type ResolvedTheme = Exclude<Theme, 'system'>
8
8
 
9
+ export const THEME_CLASSES: Record<Theme, string> = {
10
+ dark: 'eds-dark',
11
+ light: 'eds-light',
12
+ system: 'eds-system',
13
+ }
14
+
9
15
  export type ThemePersistence =
10
16
  | 'localStorage'
11
17
  | 'cookie'
@@ -16,10 +22,13 @@ export type ThemePersistence =
16
22
 
17
23
  export interface EDSContextValue {
18
24
  theme: Theme
19
- activeTheme: ResolvedTheme
20
25
  setTheme: (theme: Theme) => void
26
+ systemTheme: ResolvedTheme
21
27
  direction: NonNullable<ComponentProps<typeof DirectionProvider>['direction']>
22
28
  language: string
23
29
  }
24
30
 
25
31
  export const EDSContext = createContext<EDSContextValue | null>(null)
32
+
33
+ /** The theme of the nearest `Dark`, `Light`, or `LocalTheme` ancestor, if any. */
34
+ export const LocalThemeContext = createContext<ResolvedTheme | undefined>(undefined)