@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.
- 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/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/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/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 +91 -57
- package/skill/references/tokens.md +1 -1
- package/src/EDSContext.ts +2 -2
- package/src/EDSProvider.tsx +80 -74
- package/src/components/Avatar.tsx +5 -3
- package/src/components/Button.tsx +4 -0
- package/src/components/Card.tsx +14 -8
- 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/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
|
}
|
|
@@ -36,10 +36,12 @@ 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, 'children' | 'render'> {}
|
|
41
|
+
interface LinkProps
|
|
42
|
+
extends CommonProps, Omit<MaybeButtonOrLinkProps.LinkProps, 'children' | 'render'> {}
|
|
41
43
|
interface OtherElementProps
|
|
42
|
-
extends CommonProps, Omit<MaybeButtonOrLinkProps.OtherElementProps, 'children'> {}
|
|
44
|
+
extends CommonProps, Omit<MaybeButtonOrLinkProps.OtherElementProps, 'children' | 'render'> {}
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export type AvatarProps =
|
|
@@ -168,6 +168,10 @@ export function Button({
|
|
|
168
168
|
u:*:transition-opacity
|
|
169
169
|
u:*:@state-disabled/button:opacity-disabled
|
|
170
170
|
u:*:@state-disabled/button:grayscale
|
|
171
|
+
u:*:@prop-variant-inverse/button:scheme-only-dark
|
|
172
|
+
u:*:@prop-variant-inverse/button:dark:scheme-only-light
|
|
173
|
+
u:*:@prop-variant-primary/button:scheme-only-dark
|
|
174
|
+
u:*:@prop-variant-danger/button:scheme-only-dark
|
|
171
175
|
u:@prop-size-xsmall/button:addon-small
|
|
172
176
|
u:@prop-size-small/button:addon-small
|
|
173
177
|
u:@prop-size-medium/button:addon-medium
|
package/src/components/Card.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useId, type ReactNode } from 'react'
|
|
3
|
+
import { useId, type ElementType, type ReactNode, type Ref } from 'react'
|
|
4
4
|
import { mergeProps } from '@base-ui/react'
|
|
5
|
+
import type { DistributedOmit } from 'type-fest'
|
|
5
6
|
|
|
6
7
|
import type { EDSComponentProps } from '@edgeandnode/eds-css'
|
|
7
8
|
import { ArrowUpRightInteractiveIcon } from '@edgeandnode/eds-react/icons'
|
|
@@ -32,20 +33,24 @@ export declare namespace CardProps {
|
|
|
32
33
|
*/
|
|
33
34
|
autoIcon?: boolean | undefined
|
|
34
35
|
}
|
|
36
|
+
interface CommonProps extends EDSComponentProps<typeof CardMeta> {
|
|
37
|
+
/** Attached to the card's outer element, even when the card is a button or link. */
|
|
38
|
+
ref?: Ref<HTMLElement> | undefined
|
|
39
|
+
}
|
|
35
40
|
interface ButtonProps
|
|
36
41
|
extends
|
|
37
|
-
|
|
42
|
+
CommonProps,
|
|
38
43
|
ClickableProps,
|
|
39
|
-
MaybeButtonOrLinkProps.ButtonProps,
|
|
44
|
+
Omit<MaybeButtonOrLinkProps.ButtonProps, 'ref' | 'render'>,
|
|
40
45
|
MaybeButtonOrLinkProps.DisableableProps {}
|
|
41
46
|
interface LinkProps
|
|
42
47
|
extends
|
|
43
|
-
|
|
48
|
+
CommonProps,
|
|
44
49
|
ClickableProps,
|
|
45
|
-
MaybeButtonOrLinkProps.LinkProps,
|
|
50
|
+
Omit<MaybeButtonOrLinkProps.LinkProps, 'ref' | 'render'>,
|
|
46
51
|
MaybeButtonOrLinkProps.DisableableProps {}
|
|
47
52
|
interface OtherElementProps
|
|
48
|
-
extends
|
|
53
|
+
extends CommonProps, Omit<MaybeButtonOrLinkProps.OtherElementProps, 'ref' | 'render'> {}
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
export type CardProps = CardProps.ButtonProps | CardProps.LinkProps | CardProps.OtherElementProps
|
|
@@ -57,7 +62,7 @@ export function Card({ ref: passedRef, as, variant, disabled, children, ...props
|
|
|
57
62
|
interactiveContent = false,
|
|
58
63
|
autoIcon = true,
|
|
59
64
|
...remainingProps
|
|
60
|
-
} = props as CardProps.ButtonProps | CardProps.LinkProps
|
|
65
|
+
} = props as DistributedOmit<CardProps.ButtonProps | CardProps.LinkProps, 'ref'>
|
|
61
66
|
const { rootProps, nestedProps } = splitProps(remainingProps)
|
|
62
67
|
|
|
63
68
|
const [statePassedRef, state] = useCSSState(
|
|
@@ -75,7 +80,8 @@ export function Card({ ref: passedRef, as, variant, disabled, children, ...props
|
|
|
75
80
|
{ ref: statePassedRef },
|
|
76
81
|
)
|
|
77
82
|
|
|
78
|
-
|
|
83
|
+
// The union of every HTML tag name is too complex a type for JSX
|
|
84
|
+
const Element = (as ?? 'div') as ElementType
|
|
79
85
|
const uniqueId = useId()
|
|
80
86
|
|
|
81
87
|
return (
|
|
@@ -54,7 +54,7 @@ export function Keyboard({ ref: passedRef, size, children, ...props }: KeyboardP
|
|
|
54
54
|
>
|
|
55
55
|
<span
|
|
56
56
|
className={cn(
|
|
57
|
-
`inline-flex h-(--height) min-w-(--height) items-center justify-center rounded-4 border border-
|
|
57
|
+
`inline-flex h-(--height) min-w-(--height) items-center justify-center rounded-4 border border-current/15
|
|
58
58
|
@prop-size-small/keyboard:gap-0.5
|
|
59
59
|
@prop-size-small/keyboard:px-0.75
|
|
60
60
|
@prop-size-small/keyboard:text-10
|
|
@@ -1,39 +1,47 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import type { ComponentProps, ElementType } from 'react'
|
|
4
3
|
import { mergeProps } from '@base-ui/react'
|
|
4
|
+
import type { DistributedOmit } from 'type-fest'
|
|
5
5
|
|
|
6
6
|
import { LocalThemeContext, THEME_CLASSES, type ResolvedTheme } from '../EDSContext.ts'
|
|
7
|
+
import { Render, type AsOrRenderProps, type HTMLElementProps } from './base/Render.tsx'
|
|
7
8
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
export declare namespace LocalThemeProps {
|
|
10
|
+
interface CommonProps extends HTMLElementProps {
|
|
11
|
+
/** The theme to render the content in. */
|
|
12
|
+
theme: ResolvedTheme
|
|
13
|
+
}
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
export type LocalThemeProps = LocalThemeProps.CommonProps & AsOrRenderProps
|
|
17
|
+
export type DarkProps = DistributedOmit<LocalThemeProps, 'theme'>
|
|
18
|
+
export type LightProps = DistributedOmit<LocalThemeProps, 'theme'>
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Renders its content in the given theme, whatever the page's theme is. Unlike switching between
|
|
22
|
+
* `Dark` and `Light`, changing `theme` doesn't remount the content. Components that need to know
|
|
23
|
+
* the theme in JavaScript can read it with `useEDS().activeTheme`.
|
|
24
|
+
*/
|
|
25
|
+
export function LocalTheme({ theme, ...props }: LocalThemeProps) {
|
|
18
26
|
return (
|
|
19
27
|
<LocalThemeContext.Provider value={theme}>
|
|
20
|
-
<
|
|
28
|
+
<Render {...mergeProps({ className: THEME_CLASSES[theme] }, props)} />
|
|
21
29
|
</LocalThemeContext.Provider>
|
|
22
30
|
)
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
/**
|
|
26
34
|
* Renders its content in the dark theme, whatever the page's theme is. Components that need to know
|
|
27
|
-
* the theme in JavaScript can read it with `useEDS().
|
|
35
|
+
* the theme in JavaScript can read it with `useEDS().activeTheme`.
|
|
28
36
|
*/
|
|
29
|
-
export function Dark(props:
|
|
37
|
+
export function Dark(props: DarkProps) {
|
|
30
38
|
return <LocalTheme theme="dark" {...props} />
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
/**
|
|
34
42
|
* Renders its content in the light theme, whatever the page's theme is. Components that need to
|
|
35
|
-
* know the theme in JavaScript can read it with `useEDS().
|
|
43
|
+
* know the theme in JavaScript can read it with `useEDS().activeTheme`.
|
|
36
44
|
*/
|
|
37
|
-
export function Light(props:
|
|
45
|
+
export function Light(props: LightProps) {
|
|
38
46
|
return <LocalTheme theme="light" {...props} />
|
|
39
47
|
}
|