@edgeandnode/eds-react 1.0.2 → 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.
- package/README.md +32 -7
- package/dist/EDSContext.d.ts +2 -2
- package/dist/EDSContext.d.ts.map +1 -1
- package/dist/EDSContext.js +1 -1
- package/dist/EDSContext.js.map +1 -1
- package/dist/EDSProvider.d.ts +13 -6
- package/dist/EDSProvider.d.ts.map +1 -1
- package/dist/EDSProvider.js +48 -63
- package/dist/EDSProvider.js.map +1 -1
- package/dist/components/Address.d.ts +3 -3
- package/dist/components/Address.d.ts.map +1 -1
- package/dist/components/Address.js.map +1 -1
- package/dist/components/Avatar.d.ts +3 -3
- package/dist/components/Avatar.d.ts.map +1 -1
- package/dist/components/Avatar.js.map +1 -1
- package/dist/components/Breadcrumbs.parts.d.ts +3 -3
- package/dist/components/Breadcrumbs.parts.d.ts.map +1 -1
- package/dist/components/Breadcrumbs.parts.js.map +1 -1
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Button.js +1 -1
- package/dist/components/Button.js.map +1 -1
- package/dist/components/Card.d.ts +8 -4
- package/dist/components/Card.d.ts.map +1 -1
- package/dist/components/Card.js +2 -1
- package/dist/components/Card.js.map +1 -1
- package/dist/components/Cluster.d.ts +3 -3
- package/dist/components/Cluster.d.ts.map +1 -1
- package/dist/components/Cluster.js.map +1 -1
- package/dist/components/Keyboard.js +1 -1
- package/dist/components/LocalTheme.d.ts +21 -8
- package/dist/components/LocalTheme.d.ts.map +1 -1
- package/dist/components/LocalTheme.js +10 -4
- package/dist/components/LocalTheme.js.map +1 -1
- package/dist/components/Menu.parts.d.ts +43 -9
- package/dist/components/Menu.parts.d.ts.map +1 -1
- package/dist/components/Menu.parts.js +118 -114
- package/dist/components/Menu.parts.js.map +1 -1
- package/dist/components/Modal.parts.d.ts +1 -1
- package/dist/components/Modal.parts.d.ts.map +1 -1
- package/dist/components/Modal.parts.js +2 -2
- package/dist/components/Modal.parts.js.map +1 -1
- package/dist/components/Pane.parts.d.ts +23 -22
- package/dist/components/Pane.parts.d.ts.map +1 -1
- package/dist/components/Pane.parts.js +3 -3
- package/dist/components/Pane.parts.js.map +1 -1
- package/dist/components/Prose.d.ts +1 -1
- package/dist/components/Prose.d.ts.map +1 -1
- package/dist/components/Prose.js +3 -3
- package/dist/components/Prose.js.map +1 -1
- package/dist/components/Prose.meta.d.ts +5 -0
- package/dist/components/Prose.meta.d.ts.map +1 -1
- package/dist/components/Prose.meta.js +10 -0
- package/dist/components/Prose.meta.js.map +1 -1
- package/dist/components/Select.parts.d.ts +8 -3
- package/dist/components/Select.parts.d.ts.map +1 -1
- package/dist/components/Select.parts.js +26 -17
- package/dist/components/Select.parts.js.map +1 -1
- package/dist/components/ThemeSwitcher.d.ts +5 -5
- package/dist/components/ThemeSwitcher.d.ts.map +1 -1
- package/dist/components/ThemeSwitcher.js +5 -4
- package/dist/components/ThemeSwitcher.js.map +1 -1
- package/dist/components/base/MaybeButtonOrLink.d.ts +7 -5
- package/dist/components/base/MaybeButtonOrLink.d.ts.map +1 -1
- package/dist/components/base/MaybeButtonOrLink.js +7 -2
- package/dist/components/base/MaybeButtonOrLink.js.map +1 -1
- package/dist/components/base/Render.d.ts +25 -6
- package/dist/components/base/Render.d.ts.map +1 -1
- package/dist/components/base/Render.js +4 -3
- package/dist/components/base/Render.js.map +1 -1
- package/dist/components/base/index.d.ts +1 -1
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/base/index.js.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/hooks/useEDS.d.ts +16 -8
- package/dist/hooks/useEDS.d.ts.map +1 -1
- package/dist/hooks/useEDS.js +16 -8
- package/dist/hooks/useEDS.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/theme-persistence.d.ts +18 -0
- package/dist/theme-persistence.d.ts.map +1 -0
- package/dist/theme-persistence.js +25 -0
- package/dist/theme-persistence.js.map +1 -0
- package/package.json +20 -20
- package/skill/SKILL.md +90 -56
- package/skill/references/tokens.md +1 -1
- package/src/EDSContext.ts +2 -2
- package/src/EDSProvider.tsx +80 -74
- package/src/components/Address.tsx +4 -3
- package/src/components/Avatar.tsx +7 -3
- package/src/components/Breadcrumbs.parts.tsx +4 -3
- package/src/components/Button.tsx +4 -0
- package/src/components/Card.tsx +14 -8
- package/src/components/Cluster.tsx +5 -3
- package/src/components/Keyboard.tsx +1 -1
- package/src/components/LocalTheme.tsx +22 -14
- package/src/components/Menu.parts.tsx +365 -244
- package/src/components/Modal.parts.tsx +8 -1
- package/src/components/Pane.parts.tsx +31 -26
- package/src/components/Prose.meta.ts +10 -0
- package/src/components/Prose.tsx +4 -3
- package/src/components/Select.parts.tsx +37 -29
- package/src/components/ThemeSwitcher.tsx +11 -13
- package/src/components/base/MaybeButtonOrLink.tsx +11 -6
- package/src/components/base/Render.tsx +28 -4
- package/src/components/base/index.ts +2 -0
- package/src/components/index.ts +8 -1
- package/src/hooks/useEDS.ts +16 -8
- package/src/index.ts +2 -1
- package/src/theme-persistence.ts +31 -0
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: eds
|
|
3
|
-
version: 1.0
|
|
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://
|
|
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
|
|
46
|
+
"@edgeandnode/eds-react": "^1.1.0",
|
|
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,
|
|
107
|
+
const { theme, setTheme, activeTheme } = useEDS()
|
|
97
108
|
```
|
|
98
109
|
|
|
99
|
-
`theme` is `'dark' | 'light' | 'system'
|
|
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
|
|
515
|
-
|
|
|
516
|
-
| `Address`
|
|
517
|
-
| `Avatar` / `AvatarGroup`
|
|
518
|
-
| `Breadcrumbs`
|
|
519
|
-
| `Button` / `ButtonGroup`
|
|
520
|
-
| `Card`
|
|
521
|
-
| `Checkbox` / `Checkbox.Group`
|
|
522
|
-
| `Chip` / `Chip.Group`
|
|
523
|
-
| `Cluster`
|
|
524
|
-
| `CodeBlock` / `CodeBlock.Tabs`
|
|
525
|
-
| `CodeInline`
|
|
526
|
-
| `CopyButton`
|
|
527
|
-
| `CurrencyInput`
|
|
528
|
-
| `Dark` / `Light`
|
|
529
|
-
| `DescriptionList`
|
|
530
|
-
| `Divider`
|
|
531
|
-
| `Icon`
|
|
532
|
-
| `Input`
|
|
533
|
-
| `Keyboard`
|
|
534
|
-
| `Label`
|
|
535
|
-
| `Link`
|
|
536
|
-
| `Menu`
|
|
537
|
-
| `Modal`
|
|
538
|
-
| `OTCInput`
|
|
539
|
-
| `Pane`
|
|
540
|
-
| `Prose`
|
|
541
|
-
| `Radio` / `Radio.Group`
|
|
542
|
-
| `Search`
|
|
543
|
-
| `SegmentedControl`
|
|
544
|
-
| `Select`
|
|
545
|
-
| `Status`
|
|
546
|
-
| `Stepper`
|
|
547
|
-
| `Switch` / `Switch.Area`
|
|
548
|
-
| `Table`
|
|
549
|
-
| `TabSet`
|
|
550
|
-
| `Tag`
|
|
551
|
-
| `TextArea`
|
|
552
|
-
| `ThemeSwitcher`
|
|
553
|
-
| `ToggleButton`
|
|
554
|
-
| `Tooltip`
|
|
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
|
-
|
|
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
|
-
|
|
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,11 +700,11 @@ 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).
|
|
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.
|
|
@@ -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 (
|
|
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
|
+
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
|
+
|
|
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` —
|
|
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,
|
|
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().
|
|
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 `
|
|
33
|
+
/** The theme of the nearest `Dark`, `Light`, or `LocalTheme` ancestor, if any. */
|
|
34
34
|
export const LocalThemeContext = createContext<ResolvedTheme | undefined>(undefined)
|
package/src/EDSProvider.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
useLayoutEffect,
|
|
7
7
|
useMemo,
|
|
8
8
|
useRef,
|
|
9
|
-
|
|
9
|
+
useSyncExternalStore,
|
|
10
10
|
type ComponentProps,
|
|
11
11
|
} from 'react'
|
|
12
12
|
import { mergeProps } from '@base-ui/react'
|
|
@@ -24,17 +24,15 @@ import {
|
|
|
24
24
|
} from './EDSContext.ts'
|
|
25
25
|
import { useControlled } from './hooks/useControlled.ts'
|
|
26
26
|
import { useIsHydrated } from './hooks/useIsHydrated.ts'
|
|
27
|
+
import {
|
|
28
|
+
COOKIE_REGEX,
|
|
29
|
+
isValidTheme,
|
|
30
|
+
parseThemeCookie,
|
|
31
|
+
PERSISTENCE_KEY,
|
|
32
|
+
VALID_THEMES,
|
|
33
|
+
} from './theme-persistence.ts'
|
|
27
34
|
import { cn } from './utils/index.ts'
|
|
28
35
|
|
|
29
|
-
const PERSISTENCE_KEY = 'eds-active-theme'
|
|
30
|
-
const COOKIE_REGEX = new RegExp(`(?:^|; )${PERSISTENCE_KEY}=([^;]+)`)
|
|
31
|
-
|
|
32
|
-
const VALID_THEMES: readonly Theme[] = ['dark', 'light', 'system']
|
|
33
|
-
|
|
34
|
-
function isValidTheme(value: unknown): value is Theme {
|
|
35
|
-
return typeof value === 'string' && (VALID_THEMES as readonly string[]).includes(value)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
36
|
interface PersistenceAdapter {
|
|
39
37
|
get: () => Theme | null
|
|
40
38
|
set: (value: Theme) => void
|
|
@@ -67,17 +65,14 @@ function resolvePersistence(persistTheme: ThemePersistence | undefined): Persist
|
|
|
67
65
|
return {
|
|
68
66
|
get: () => {
|
|
69
67
|
try {
|
|
70
|
-
|
|
71
|
-
if (match === null) return null
|
|
72
|
-
const value = decodeURIComponent(match[1] as string)
|
|
73
|
-
return isValidTheme(value) ? value : null
|
|
68
|
+
return parseThemeCookie(document.cookie) ?? null
|
|
74
69
|
} catch {
|
|
75
70
|
return null
|
|
76
71
|
}
|
|
77
72
|
},
|
|
78
73
|
set: (value) => {
|
|
79
74
|
try {
|
|
80
|
-
document.cookie = `${PERSISTENCE_KEY}=${
|
|
75
|
+
document.cookie = `${PERSISTENCE_KEY}=${value}; path=/; max-age=31536000; samesite=lax`
|
|
81
76
|
} catch {
|
|
82
77
|
// ignore restrictive environments
|
|
83
78
|
}
|
|
@@ -107,12 +102,13 @@ function resolvePersistence(persistTheme: ThemePersistence | undefined): Persist
|
|
|
107
102
|
const prefersDarkMediaQuery =
|
|
108
103
|
typeof window !== 'undefined' ? window.matchMedia('(prefers-color-scheme: dark)') : null
|
|
109
104
|
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
105
|
+
function subscribeToSystemTheme(onChange: () => void) {
|
|
106
|
+
prefersDarkMediaQuery?.addEventListener('change', onChange)
|
|
107
|
+
return () => prefersDarkMediaQuery?.removeEventListener('change', onChange)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getSystemTheme(): ResolvedTheme {
|
|
111
|
+
return prefersDarkMediaQuery?.matches === false ? 'light' : 'dark'
|
|
116
112
|
}
|
|
117
113
|
|
|
118
114
|
export interface EDSProviderProps extends Omit<ComponentProps<'div'>, 'dir'> {
|
|
@@ -138,16 +134,22 @@ export interface EDSProviderProps extends Omit<ComponentProps<'div'>, 'dir'> {
|
|
|
138
134
|
* - `'localStorage'`: reads/writes `localStorage['eds-active-theme']`. Inline pre-hydration script
|
|
139
135
|
* reads the value before first paint — no SSR flash.
|
|
140
136
|
* - `'cookie'`: reads/writes a `eds-active-theme` cookie (`path=/; max-age=31536000;
|
|
141
|
-
* samesite=lax`). Same no-flash SSR;
|
|
137
|
+
* samesite=lax`). Same no-flash SSR; pass `getThemeFromCookies()` to `defaultTheme` on the
|
|
138
|
+
* server so it renders the saved theme too.
|
|
142
139
|
* - `{ get, set }`: custom adapter (different key, encryption, remote storage, etc.). Invalid
|
|
143
|
-
* values from `get()` fall back to `defaultTheme`.
|
|
144
|
-
* zero-flash SSR, inject your own inline script that adds the matching
|
|
145
|
-
* `<html>` before paint.
|
|
140
|
+
* values from `get()` fall back to `defaultTheme`. In uncontrolled mode, no pre-hydration
|
|
141
|
+
* script is emitted; for zero-flash SSR, inject your own inline script that adds the matching
|
|
142
|
+
* `eds-*` class to `<html>` before paint.
|
|
146
143
|
*
|
|
147
144
|
* The two string presets share the key `'eds-active-theme'` — use a custom adapter for per-app
|
|
148
145
|
* isolation on the same origin.
|
|
149
146
|
*/
|
|
150
147
|
persistTheme?: ThemePersistence | undefined
|
|
148
|
+
/**
|
|
149
|
+
* Nonce for the inline scripts and styles `EDSProvider` adds, if your Content Security Policy
|
|
150
|
+
* requires one.
|
|
151
|
+
*/
|
|
152
|
+
nonce?: string | undefined
|
|
151
153
|
/** @default 'ltr' */
|
|
152
154
|
direction?: ComponentProps<typeof DirectionProvider>['direction'] | undefined
|
|
153
155
|
/** @default 'en' // or for `direction="rtl"`, 'ar' */
|
|
@@ -161,7 +163,8 @@ export type { EDSContextValue, ResolvedTheme, Theme, ThemePersistence } from './
|
|
|
161
163
|
* Provides global configuration and enhancements for EDS components.
|
|
162
164
|
*
|
|
163
165
|
* Note: Only one EDSProvider should be used per app, typically at the root. If you need different
|
|
164
|
-
* themes for different parts of the app, use the `Dark` and `
|
|
166
|
+
* themes for different parts of the app, use the `Dark`, `Light`, and `LocalTheme` components
|
|
167
|
+
* instead.
|
|
165
168
|
*
|
|
166
169
|
* **Next.js App Router:** add `suppressHydrationWarning` to your `<html>` tag in `app/layout.tsx`.
|
|
167
170
|
* The pre-hydration script applies the theme class to `<html>` before React hydrates (preventing a
|
|
@@ -200,6 +203,7 @@ export function EDSProvider({
|
|
|
200
203
|
defaultTheme = 'dark',
|
|
201
204
|
onThemeChange,
|
|
202
205
|
persistTheme,
|
|
206
|
+
nonce,
|
|
203
207
|
direction = 'ltr',
|
|
204
208
|
language = direction === 'rtl' ? 'ar' : 'en',
|
|
205
209
|
buttonOrLinkConfig,
|
|
@@ -288,6 +292,7 @@ export function EDSProvider({
|
|
|
288
292
|
}
|
|
289
293
|
|
|
290
294
|
const style = document.createElement('style')
|
|
295
|
+
if (nonce !== undefined) style.nonce = nonce
|
|
291
296
|
style.textContent = '*:not(.eds-segmented-control *) { transition: none !important; }'
|
|
292
297
|
document.head.appendChild(style)
|
|
293
298
|
removeClasses()
|
|
@@ -301,30 +306,15 @@ export function EDSProvider({
|
|
|
301
306
|
style.remove()
|
|
302
307
|
removeClasses()
|
|
303
308
|
}
|
|
304
|
-
}, [theme])
|
|
305
|
-
|
|
306
|
-
// `activeTheme` resolves `'system'` to `'dark'` or `'light'` via `prefers-color-scheme`. Initial
|
|
307
|
-
// state skips `matchMedia` to keep SSR and first client render in sync; the effect catches up
|
|
308
|
-
// pre-paint.
|
|
309
|
-
const systemFallback: ResolvedTheme = defaultTheme === 'system' ? 'dark' : defaultTheme
|
|
309
|
+
}, [theme, nonce])
|
|
310
310
|
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
// The server can't know the OS preference, so it renders `'dark'`, `:root`'s default
|
|
312
|
+
const systemTheme = useSyncExternalStore<ResolvedTheme>(
|
|
313
|
+
subscribeToSystemTheme,
|
|
314
|
+
getSystemTheme,
|
|
315
|
+
() => 'dark',
|
|
313
316
|
)
|
|
314
317
|
|
|
315
|
-
useLayoutEffect(() => {
|
|
316
|
-
setActiveTheme(resolveTheme(theme, systemFallback))
|
|
317
|
-
|
|
318
|
-
if (theme !== 'system') return
|
|
319
|
-
|
|
320
|
-
const handleChange = (event: MediaQueryListEvent) => {
|
|
321
|
-
setActiveTheme(event.matches ? 'dark' : 'light')
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
prefersDarkMediaQuery?.addEventListener('change', handleChange)
|
|
325
|
-
return () => prefersDarkMediaQuery?.removeEventListener('change', handleChange)
|
|
326
|
-
}, [theme, systemFallback])
|
|
327
|
-
|
|
328
318
|
/**
|
|
329
319
|
* Add the `data-eds-active` attribute to the `html` element when the Space key is pressed to
|
|
330
320
|
* improve Tailwind's `active` state.
|
|
@@ -390,20 +380,22 @@ export function EDSProvider({
|
|
|
390
380
|
if (lightDarkTest !== '' && !lightDarkTest.includes('light-dark')) {
|
|
391
381
|
// oxlint-disable-next-line no-console
|
|
392
382
|
console.warn(
|
|
393
|
-
"[EDSProvider] `light-dark()` was transpiled by your build tools, so components inside `Dark` and `
|
|
383
|
+
"[EDSProvider] `light-dark()` was transpiled by your build tools, so components inside `Dark`, `Light`, and `LocalTheme` will use the page's colors. Target Chrome 123, Safari 17.5, and Firefox 120 or newer (see the `@edgeandnode/eds-react` README).",
|
|
394
384
|
)
|
|
395
385
|
}
|
|
396
386
|
}, [])
|
|
397
387
|
|
|
388
|
+
const isHydrated = useIsHydrated()
|
|
389
|
+
|
|
398
390
|
const edsContextValue = useMemo(
|
|
399
391
|
() => ({
|
|
400
392
|
theme,
|
|
401
|
-
activeTheme,
|
|
402
393
|
setTheme,
|
|
394
|
+
systemTheme,
|
|
403
395
|
direction,
|
|
404
396
|
language,
|
|
405
397
|
}),
|
|
406
|
-
[theme,
|
|
398
|
+
[theme, setTheme, systemTheme, direction, language],
|
|
407
399
|
)
|
|
408
400
|
|
|
409
401
|
return (
|
|
@@ -412,10 +404,19 @@ export function EDSProvider({
|
|
|
412
404
|
<Pane.Provider>
|
|
413
405
|
<Tooltip.Provider timeout={300}>
|
|
414
406
|
<ButtonOrLink.Config {...buttonOrLinkConfig}>
|
|
415
|
-
{
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
407
|
+
{/**
|
|
408
|
+
* Apply the theme before first paint to prevent a flash, as the browser runs this script and
|
|
409
|
+
* applies this style while parsing the server-rendered HTML. The style applies a light or system
|
|
410
|
+
* theme until a theme class is set on `<html>`, in case there's no script (with a custom
|
|
411
|
+
* `persistTheme` adapter) or it can't run (e.g. blocked by a Content Security Policy). Both are
|
|
412
|
+
* removed after hydration.
|
|
413
|
+
*/}
|
|
414
|
+
{isHydrated ? null : (
|
|
415
|
+
<>
|
|
416
|
+
{renderThemeScript(controlledTheme, defaultTheme, persistTheme, nonce)}
|
|
417
|
+
{renderThemeStyle(controlledTheme ?? defaultTheme, nonce)}
|
|
418
|
+
</>
|
|
419
|
+
)}
|
|
419
420
|
<div
|
|
420
421
|
dir={direction}
|
|
421
422
|
lang={language}
|
|
@@ -431,38 +432,43 @@ export function EDSProvider({
|
|
|
431
432
|
)
|
|
432
433
|
}
|
|
433
434
|
|
|
434
|
-
function
|
|
435
|
+
function renderThemeScript(
|
|
435
436
|
controlledTheme: Theme | undefined,
|
|
436
437
|
defaultTheme: Theme,
|
|
437
438
|
persistTheme: ThemePersistence | undefined,
|
|
439
|
+
nonce: string | undefined,
|
|
438
440
|
) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
return `document.documentElement.classList.add('eds-${controlledTheme}')`
|
|
443
|
-
}
|
|
444
|
-
if (persistTheme !== 'localStorage' && persistTheme !== 'cookie') {
|
|
445
|
-
return `document.documentElement.classList.add('eds-${defaultTheme}')`
|
|
441
|
+
if (controlledTheme !== undefined || persistTheme === undefined) {
|
|
442
|
+
const script = `document.documentElement.classList.add('eds-${controlledTheme ?? defaultTheme}')`
|
|
443
|
+
return <script nonce={nonce}>{script}</script>
|
|
446
444
|
}
|
|
445
|
+
// Custom adapters need the app's own script (see `persistTheme`), which this would conflict with
|
|
446
|
+
if (persistTheme !== 'localStorage' && persistTheme !== 'cookie') return null
|
|
447
447
|
|
|
448
|
-
const invalidCheck = VALID_THEMES.map((
|
|
448
|
+
const invalidCheck = VALID_THEMES.map((theme) => `t!=='${theme}'`).join('&&')
|
|
449
449
|
const readExpression =
|
|
450
450
|
persistTheme === 'localStorage'
|
|
451
451
|
? `localStorage.getItem('${PERSISTENCE_KEY}')`
|
|
452
|
-
: `
|
|
453
|
-
|
|
452
|
+
: `(document.cookie.match(${COOKIE_REGEX})||[])[1]`
|
|
453
|
+
const script = `(function(){try{var t=${readExpression};if(${invalidCheck})t='${defaultTheme}';document.documentElement.classList.add('eds-'+t)}catch(e){document.documentElement.classList.add('eds-${defaultTheme}')}})()`
|
|
454
|
+
return <script nonce={nonce}>{script}</script>
|
|
454
455
|
}
|
|
455
456
|
|
|
456
|
-
|
|
457
|
+
const ROOT_WITHOUT_THEME_CLASS = `:root:not(${Object.values(THEME_CLASSES)
|
|
458
|
+
.map((themeClass) => `.${themeClass}`)
|
|
459
|
+
.join(', ')})`
|
|
457
460
|
|
|
458
461
|
/**
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
* Outputs its tag during SSR and during the client's initial render (ensuring the markup matches),
|
|
462
|
-
* and removes itself after hydration to keep the DOM clean. The browser executes the inline script
|
|
463
|
-
* as it parses the HTML, before React hydrates and before any styled body content paints.
|
|
462
|
+
* `:root` is dark by default, so other themes need a style until the theme class is set on
|
|
463
|
+
* `<html>`.
|
|
464
464
|
*/
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
465
|
+
const THEME_STYLES: Record<Theme, string | null> = {
|
|
466
|
+
dark: null,
|
|
467
|
+
light: `${ROOT_WITHOUT_THEME_CLASS}{color-scheme:only light;--eds-color-scheme:light}`,
|
|
468
|
+
system: `${ROOT_WITHOUT_THEME_CLASS}{color-scheme:light dark}@media (prefers-color-scheme:light){${ROOT_WITHOUT_THEME_CLASS}{--eds-color-scheme:light}}`,
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function renderThemeStyle(theme: Theme, nonce: string | undefined) {
|
|
472
|
+
const style = THEME_STYLES[theme]
|
|
473
|
+
return style !== null ? <style nonce={nonce}>{style}</style> : null
|
|
468
474
|
}
|
|
@@ -26,14 +26,15 @@ export declare namespace AddressProps {
|
|
|
26
26
|
interface ButtonProps
|
|
27
27
|
extends
|
|
28
28
|
CommonProps,
|
|
29
|
-
MaybeButtonOrLinkProps.ButtonProps,
|
|
29
|
+
Omit<MaybeButtonOrLinkProps.ButtonProps, 'as'>,
|
|
30
30
|
MaybeButtonOrLinkProps.DisableableProps {}
|
|
31
31
|
interface LinkProps
|
|
32
32
|
extends
|
|
33
33
|
CommonProps,
|
|
34
|
-
MaybeButtonOrLinkProps.LinkProps,
|
|
34
|
+
Omit<MaybeButtonOrLinkProps.LinkProps, 'as'>,
|
|
35
35
|
MaybeButtonOrLinkProps.DisableableProps {}
|
|
36
|
-
interface OtherElementProps
|
|
36
|
+
interface OtherElementProps
|
|
37
|
+
extends CommonProps, Omit<MaybeButtonOrLinkProps.OtherElementProps, 'as'> {}
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export type AddressProps =
|
|
@@ -36,10 +36,14 @@ export declare namespace AvatarProps {
|
|
|
36
36
|
tooltip?: TooltipProps['content'] | Omit<TooltipProps, 'children'> | undefined
|
|
37
37
|
}
|
|
38
38
|
interface CommonProps extends OwnProps, EDSComponentProps<typeof AvatarMeta> {}
|
|
39
|
-
interface ButtonProps
|
|
40
|
-
|
|
39
|
+
interface ButtonProps
|
|
40
|
+
extends CommonProps, Omit<MaybeButtonOrLinkProps.ButtonProps, 'as' | 'children' | 'render'> {}
|
|
41
|
+
interface LinkProps
|
|
42
|
+
extends CommonProps, Omit<MaybeButtonOrLinkProps.LinkProps, 'as' | 'children' | 'render'> {}
|
|
41
43
|
interface OtherElementProps
|
|
42
|
-
extends
|
|
44
|
+
extends
|
|
45
|
+
CommonProps,
|
|
46
|
+
Omit<MaybeButtonOrLinkProps.OtherElementProps, 'as' | 'children' | 'render'> {}
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
export type AvatarProps =
|
|
@@ -269,9 +269,10 @@ export declare namespace BreadcrumbsItemProps {
|
|
|
269
269
|
current?: boolean | undefined
|
|
270
270
|
}
|
|
271
271
|
interface CommonProps extends OwnProps, EDSComponentProps<typeof BreadcrumbsItemMeta> {}
|
|
272
|
-
interface ButtonProps extends CommonProps, MaybeButtonOrLinkProps.ButtonProps {}
|
|
273
|
-
interface LinkProps extends CommonProps, MaybeButtonOrLinkProps.LinkProps {}
|
|
274
|
-
interface OtherElementProps
|
|
272
|
+
interface ButtonProps extends CommonProps, Omit<MaybeButtonOrLinkProps.ButtonProps, 'as'> {}
|
|
273
|
+
interface LinkProps extends CommonProps, Omit<MaybeButtonOrLinkProps.LinkProps, 'as'> {}
|
|
274
|
+
interface OtherElementProps
|
|
275
|
+
extends CommonProps, Omit<MaybeButtonOrLinkProps.OtherElementProps, 'as'> {}
|
|
275
276
|
}
|
|
276
277
|
|
|
277
278
|
export type BreadcrumbsItemProps =
|