@refraction-ui/react 0.12.1 → 0.12.3
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/dist/{applicationinsights-web-62HUCH3W.js → applicationinsights-web-H7OBLIVJ.js} +199 -68
- package/dist/applicationinsights-web-H7OBLIVJ.js.map +1 -0
- package/dist/index.cjs +236 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +39 -9
- package/dist/index.js.map +1 -1
- package/dist/internal/button/index.d.cts +12 -3
- package/dist/internal/button/index.d.ts +12 -3
- package/dist/internal/react-status-indicator/index.d.cts +5 -18
- package/dist/internal/react-status-indicator/index.d.ts +5 -18
- package/dist/internal/react-theme/index.d.cts +19 -1
- package/dist/internal/react-theme/index.d.ts +19 -1
- package/dist/internal/theme/index.d.cts +35 -1
- package/dist/internal/theme/index.d.ts +35 -1
- package/dist/theme.cjs +27 -4
- package/dist/theme.cjs.map +1 -1
- package/dist/theme.js +27 -4
- package/dist/theme.js.map +1 -1
- package/package.json +1 -1
- package/dist/applicationinsights-web-62HUCH3W.js.map +0 -1
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { AccessibilityProps, KeyboardHandlerMap, TokenContract } from '../shared/index.cjs';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* `primary` is an alias of `default` — included so the universal React-ecosystem
|
|
5
|
+
* muscle-memory (`<Button variant="primary">`) renders as the most-emphasized
|
|
6
|
+
* button instead of falling through to the unstyled base. See issue #201.
|
|
7
|
+
*/
|
|
8
|
+
type ButtonVariant = 'default' | 'primary' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
|
9
|
+
/** Variants that map to the same visual style as `default`. */
|
|
10
|
+
declare const BUTTON_VARIANT_ALIASES: Partial<Record<ButtonVariant, ButtonVariant>>;
|
|
11
|
+
/** Resolve an alias to its canonical variant. Pass-through for unknown/undefined. */
|
|
12
|
+
declare function resolveButtonVariant(variant: ButtonVariant | undefined): ButtonVariant | undefined;
|
|
4
13
|
type ButtonSize = 'xs' | 'sm' | 'default' | 'lg' | 'icon';
|
|
5
14
|
interface ButtonProps {
|
|
6
15
|
variant?: ButtonVariant;
|
|
@@ -25,10 +34,10 @@ declare function getButtonType(props: ButtonProps): string;
|
|
|
25
34
|
|
|
26
35
|
declare const buttonTokens: TokenContract;
|
|
27
36
|
declare const buttonVariants: (props?: ({
|
|
28
|
-
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | undefined;
|
|
37
|
+
variant?: "default" | "primary" | "destructive" | "outline" | "secondary" | "ghost" | "link" | undefined;
|
|
29
38
|
size?: "default" | "xs" | "sm" | "lg" | "icon" | undefined;
|
|
30
39
|
} & {
|
|
31
40
|
className?: string;
|
|
32
41
|
}) | undefined) => string;
|
|
33
42
|
|
|
34
|
-
export { type ButtonAPI, type ButtonProps, type ButtonSize, type ButtonVariant, buttonTokens, buttonVariants, createButton, getButtonType };
|
|
43
|
+
export { BUTTON_VARIANT_ALIASES, type ButtonAPI, type ButtonProps, type ButtonSize, type ButtonVariant, buttonTokens, buttonVariants, createButton, getButtonType, resolveButtonVariant };
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { AccessibilityProps, KeyboardHandlerMap, TokenContract } from '../shared/index.js';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* `primary` is an alias of `default` — included so the universal React-ecosystem
|
|
5
|
+
* muscle-memory (`<Button variant="primary">`) renders as the most-emphasized
|
|
6
|
+
* button instead of falling through to the unstyled base. See issue #201.
|
|
7
|
+
*/
|
|
8
|
+
type ButtonVariant = 'default' | 'primary' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
|
9
|
+
/** Variants that map to the same visual style as `default`. */
|
|
10
|
+
declare const BUTTON_VARIANT_ALIASES: Partial<Record<ButtonVariant, ButtonVariant>>;
|
|
11
|
+
/** Resolve an alias to its canonical variant. Pass-through for unknown/undefined. */
|
|
12
|
+
declare function resolveButtonVariant(variant: ButtonVariant | undefined): ButtonVariant | undefined;
|
|
4
13
|
type ButtonSize = 'xs' | 'sm' | 'default' | 'lg' | 'icon';
|
|
5
14
|
interface ButtonProps {
|
|
6
15
|
variant?: ButtonVariant;
|
|
@@ -25,10 +34,10 @@ declare function getButtonType(props: ButtonProps): string;
|
|
|
25
34
|
|
|
26
35
|
declare const buttonTokens: TokenContract;
|
|
27
36
|
declare const buttonVariants: (props?: ({
|
|
28
|
-
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | undefined;
|
|
37
|
+
variant?: "default" | "primary" | "destructive" | "outline" | "secondary" | "ghost" | "link" | undefined;
|
|
29
38
|
size?: "default" | "xs" | "sm" | "lg" | "icon" | undefined;
|
|
30
39
|
} & {
|
|
31
40
|
className?: string;
|
|
32
41
|
}) | undefined) => string;
|
|
33
42
|
|
|
34
|
-
export { type ButtonAPI, type ButtonProps, type ButtonSize, type ButtonVariant, buttonTokens, buttonVariants, createButton, getButtonType };
|
|
43
|
+
export { BUTTON_VARIANT_ALIASES, type ButtonAPI, type ButtonProps, type ButtonSize, type ButtonVariant, buttonTokens, buttonVariants, createButton, getButtonType, resolveButtonVariant };
|
|
@@ -1,32 +1,19 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
1
2
|
import * as React from 'react';
|
|
2
3
|
import { StatusType } from '../status-indicator/index.cjs';
|
|
3
4
|
export { StatusProps as CoreStatusProps, STATUS_COLORS, STATUS_LABELS, StatusAPI, StatusType, statusContainerStyles, statusDotVariants, statusLabelStyles, statusPulseVariants } from '../status-indicator/index.cjs';
|
|
4
5
|
|
|
5
6
|
interface StatusIndicatorProps {
|
|
6
7
|
type: StatusType;
|
|
8
|
+
/** Explicit text label. Takes precedence over `children`. */
|
|
7
9
|
label?: string;
|
|
10
|
+
/** Composable label content. Used when `label` is omitted. */
|
|
11
|
+
children?: React.ReactNode;
|
|
8
12
|
pulse?: boolean;
|
|
9
13
|
showLabel?: boolean;
|
|
10
14
|
className?: string;
|
|
11
15
|
}
|
|
12
|
-
declare function StatusIndicator({ type, label, pulse, showLabel, className, }: StatusIndicatorProps):
|
|
13
|
-
className: string;
|
|
14
|
-
role?: string | undefined;
|
|
15
|
-
tabIndex?: number | undefined;
|
|
16
|
-
'aria-label'?: string | undefined;
|
|
17
|
-
'aria-labelledby'?: string | undefined;
|
|
18
|
-
'aria-describedby'?: string | undefined;
|
|
19
|
-
'aria-controls'?: string | undefined;
|
|
20
|
-
'aria-expanded'?: boolean | undefined;
|
|
21
|
-
'aria-selected'?: boolean | undefined;
|
|
22
|
-
'aria-hidden'?: boolean | undefined;
|
|
23
|
-
'aria-disabled'?: boolean | undefined;
|
|
24
|
-
'aria-pressed'?: boolean | "mixed" | undefined;
|
|
25
|
-
'aria-checked'?: boolean | "mixed" | undefined;
|
|
26
|
-
'aria-current'?: boolean | "page" | "step" | "location" | "date" | "time" | undefined;
|
|
27
|
-
'aria-live'?: "off" | "assertive" | "polite" | undefined;
|
|
28
|
-
'aria-atomic'?: boolean | undefined;
|
|
29
|
-
}, HTMLElement>;
|
|
16
|
+
declare function StatusIndicator({ type, label, children, pulse, showLabel, className, }: StatusIndicatorProps): react_jsx_runtime.JSX.Element;
|
|
30
17
|
declare namespace StatusIndicator {
|
|
31
18
|
var displayName: string;
|
|
32
19
|
}
|
|
@@ -1,32 +1,19 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
1
2
|
import * as React from 'react';
|
|
2
3
|
import { StatusType } from '../status-indicator/index.js';
|
|
3
4
|
export { StatusProps as CoreStatusProps, STATUS_COLORS, STATUS_LABELS, StatusAPI, StatusType, statusContainerStyles, statusDotVariants, statusLabelStyles, statusPulseVariants } from '../status-indicator/index.js';
|
|
4
5
|
|
|
5
6
|
interface StatusIndicatorProps {
|
|
6
7
|
type: StatusType;
|
|
8
|
+
/** Explicit text label. Takes precedence over `children`. */
|
|
7
9
|
label?: string;
|
|
10
|
+
/** Composable label content. Used when `label` is omitted. */
|
|
11
|
+
children?: React.ReactNode;
|
|
8
12
|
pulse?: boolean;
|
|
9
13
|
showLabel?: boolean;
|
|
10
14
|
className?: string;
|
|
11
15
|
}
|
|
12
|
-
declare function StatusIndicator({ type, label, pulse, showLabel, className, }: StatusIndicatorProps):
|
|
13
|
-
className: string;
|
|
14
|
-
role?: string | undefined;
|
|
15
|
-
tabIndex?: number | undefined;
|
|
16
|
-
'aria-label'?: string | undefined;
|
|
17
|
-
'aria-labelledby'?: string | undefined;
|
|
18
|
-
'aria-describedby'?: string | undefined;
|
|
19
|
-
'aria-controls'?: string | undefined;
|
|
20
|
-
'aria-expanded'?: boolean | undefined;
|
|
21
|
-
'aria-selected'?: boolean | undefined;
|
|
22
|
-
'aria-hidden'?: boolean | undefined;
|
|
23
|
-
'aria-disabled'?: boolean | undefined;
|
|
24
|
-
'aria-pressed'?: boolean | "mixed" | undefined;
|
|
25
|
-
'aria-checked'?: boolean | "mixed" | undefined;
|
|
26
|
-
'aria-current'?: boolean | "page" | "step" | "location" | "date" | "time" | undefined;
|
|
27
|
-
'aria-live'?: "off" | "assertive" | "polite" | undefined;
|
|
28
|
-
'aria-atomic'?: boolean | undefined;
|
|
29
|
-
}, HTMLElement>;
|
|
16
|
+
declare function StatusIndicator({ type, label, children, pulse, showLabel, className, }: StatusIndicatorProps): react_jsx_runtime.JSX.Element;
|
|
30
17
|
declare namespace StatusIndicator {
|
|
31
18
|
var displayName: string;
|
|
32
19
|
}
|
|
@@ -28,14 +28,32 @@ declare function ThemeToggle({ className, variant }: ThemeToggleProps): React.De
|
|
|
28
28
|
}, HTMLDivElement>;
|
|
29
29
|
|
|
30
30
|
interface ThemeScriptProps {
|
|
31
|
+
/** localStorage key the ThemeProvider reads. */
|
|
31
32
|
storageKey?: string;
|
|
33
|
+
/** Attribute strategy. `class` toggles `light`/`dark` on <html>; otherwise an HTML attribute. */
|
|
32
34
|
attribute?: 'class' | 'data-theme';
|
|
35
|
+
/**
|
|
36
|
+
* Theme to apply when nothing is stored. Match this to `<ThemeProvider defaultMode>`
|
|
37
|
+
* so the pre-paint script and provider agree on the no-storage path. Defaults
|
|
38
|
+
* to `'system'` (current behaviour — OS preference wins). Issue #317.
|
|
39
|
+
*/
|
|
40
|
+
defaultMode?: ThemeMode;
|
|
41
|
+
/**
|
|
42
|
+
* When `false` the inline script never reads `prefers-color-scheme`; the
|
|
43
|
+
* `defaultMode` is applied as-is for first-time visitors. Default `true`.
|
|
44
|
+
*/
|
|
45
|
+
enableSystem?: boolean;
|
|
33
46
|
}
|
|
34
47
|
/**
|
|
35
48
|
* Renders an inline <script> that prevents theme flash on SSR pages.
|
|
36
49
|
* Place this in the <head> of your document (in Next.js layout.tsx, Remix root, etc.)
|
|
50
|
+
*
|
|
51
|
+
* Mirror the props your <ThemeProvider> uses so first-paint and React mount stay in sync:
|
|
52
|
+
*
|
|
53
|
+
* <ThemeScript defaultMode="light" storageKey="app_theme" />
|
|
54
|
+
* <ThemeProvider defaultMode="light" storageKey="app_theme">…</ThemeProvider>
|
|
37
55
|
*/
|
|
38
|
-
declare function ThemeScript({ storageKey, attribute, }: ThemeScriptProps): React.DetailedReactHTMLElement<{
|
|
56
|
+
declare function ThemeScript({ storageKey, attribute, defaultMode, enableSystem, }: ThemeScriptProps): React.DetailedReactHTMLElement<{
|
|
39
57
|
dangerouslySetInnerHTML: {
|
|
40
58
|
__html: string;
|
|
41
59
|
};
|
|
@@ -28,14 +28,32 @@ declare function ThemeToggle({ className, variant }: ThemeToggleProps): React.De
|
|
|
28
28
|
}, HTMLDivElement>;
|
|
29
29
|
|
|
30
30
|
interface ThemeScriptProps {
|
|
31
|
+
/** localStorage key the ThemeProvider reads. */
|
|
31
32
|
storageKey?: string;
|
|
33
|
+
/** Attribute strategy. `class` toggles `light`/`dark` on <html>; otherwise an HTML attribute. */
|
|
32
34
|
attribute?: 'class' | 'data-theme';
|
|
35
|
+
/**
|
|
36
|
+
* Theme to apply when nothing is stored. Match this to `<ThemeProvider defaultMode>`
|
|
37
|
+
* so the pre-paint script and provider agree on the no-storage path. Defaults
|
|
38
|
+
* to `'system'` (current behaviour — OS preference wins). Issue #317.
|
|
39
|
+
*/
|
|
40
|
+
defaultMode?: ThemeMode;
|
|
41
|
+
/**
|
|
42
|
+
* When `false` the inline script never reads `prefers-color-scheme`; the
|
|
43
|
+
* `defaultMode` is applied as-is for first-time visitors. Default `true`.
|
|
44
|
+
*/
|
|
45
|
+
enableSystem?: boolean;
|
|
33
46
|
}
|
|
34
47
|
/**
|
|
35
48
|
* Renders an inline <script> that prevents theme flash on SSR pages.
|
|
36
49
|
* Place this in the <head> of your document (in Next.js layout.tsx, Remix root, etc.)
|
|
50
|
+
*
|
|
51
|
+
* Mirror the props your <ThemeProvider> uses so first-paint and React mount stay in sync:
|
|
52
|
+
*
|
|
53
|
+
* <ThemeScript defaultMode="light" storageKey="app_theme" />
|
|
54
|
+
* <ThemeProvider defaultMode="light" storageKey="app_theme">…</ThemeProvider>
|
|
37
55
|
*/
|
|
38
|
-
declare function ThemeScript({ storageKey, attribute, }: ThemeScriptProps): React.DetailedReactHTMLElement<{
|
|
56
|
+
declare function ThemeScript({ storageKey, attribute, defaultMode, enableSystem, }: ThemeScriptProps): React.DetailedReactHTMLElement<{
|
|
39
57
|
dangerouslySetInnerHTML: {
|
|
40
58
|
__html: string;
|
|
41
59
|
};
|
|
@@ -43,7 +43,41 @@ declare function createTheme(config?: ThemeConfig, storage?: StorageAdapter, med
|
|
|
43
43
|
* Inject this as a <script> tag in the <head> before any CSS.
|
|
44
44
|
* Works with any framework (React, Astro, plain HTML).
|
|
45
45
|
*/
|
|
46
|
-
|
|
46
|
+
|
|
47
|
+
interface ThemeScriptOptions {
|
|
48
|
+
/** localStorage key the corresponding ThemeProvider reads. */
|
|
49
|
+
storageKey?: string;
|
|
50
|
+
/** Attribute strategy: 'class' toggles `light`/`dark` classes; otherwise an HTML attribute. */
|
|
51
|
+
attribute?: 'class' | 'data-theme';
|
|
52
|
+
/**
|
|
53
|
+
* Theme to apply when nothing is stored AND `enableSystem` is false
|
|
54
|
+
* (or `defaultMode` is explicitly 'light'/'dark'). Match this to your
|
|
55
|
+
* ThemeProvider's `defaultMode` so the pre-paint script and provider agree.
|
|
56
|
+
* Defaults to `'system'` — current behavior, OS preference wins.
|
|
57
|
+
*
|
|
58
|
+
* Issue #317.
|
|
59
|
+
*/
|
|
60
|
+
defaultMode?: ThemeMode;
|
|
61
|
+
/**
|
|
62
|
+
* When `true` (default) and `defaultMode === 'system'`, the script falls
|
|
63
|
+
* through to `prefers-color-scheme`. When `false`, the script applies
|
|
64
|
+
* `defaultMode` directly and never reads the OS preference.
|
|
65
|
+
*/
|
|
66
|
+
enableSystem?: boolean;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build the pre-paint inline script. Mirrors the resolution order used by
|
|
70
|
+
* the React/Astro `ThemeProvider`:
|
|
71
|
+
*
|
|
72
|
+
* 1. stored value (light|dark|system)
|
|
73
|
+
* 2. if no stored value: `defaultMode` (light|dark applied as-is)
|
|
74
|
+
* 3. if stored or default is 'system' AND `enableSystem`: matchMedia
|
|
75
|
+
* 4. else: 'light'
|
|
76
|
+
*
|
|
77
|
+
* Back-compat: with no options the script behaves exactly as before —
|
|
78
|
+
* stored value, then matchMedia, then 'light'.
|
|
79
|
+
*/
|
|
80
|
+
declare function getThemeScript(storageKeyOrOptions?: string | ThemeScriptOptions, attribute?: 'class' | 'data-theme'): string;
|
|
47
81
|
|
|
48
82
|
/**
|
|
49
83
|
* Browser DOM adapters for the theme machine.
|
|
@@ -43,7 +43,41 @@ declare function createTheme(config?: ThemeConfig, storage?: StorageAdapter, med
|
|
|
43
43
|
* Inject this as a <script> tag in the <head> before any CSS.
|
|
44
44
|
* Works with any framework (React, Astro, plain HTML).
|
|
45
45
|
*/
|
|
46
|
-
|
|
46
|
+
|
|
47
|
+
interface ThemeScriptOptions {
|
|
48
|
+
/** localStorage key the corresponding ThemeProvider reads. */
|
|
49
|
+
storageKey?: string;
|
|
50
|
+
/** Attribute strategy: 'class' toggles `light`/`dark` classes; otherwise an HTML attribute. */
|
|
51
|
+
attribute?: 'class' | 'data-theme';
|
|
52
|
+
/**
|
|
53
|
+
* Theme to apply when nothing is stored AND `enableSystem` is false
|
|
54
|
+
* (or `defaultMode` is explicitly 'light'/'dark'). Match this to your
|
|
55
|
+
* ThemeProvider's `defaultMode` so the pre-paint script and provider agree.
|
|
56
|
+
* Defaults to `'system'` — current behavior, OS preference wins.
|
|
57
|
+
*
|
|
58
|
+
* Issue #317.
|
|
59
|
+
*/
|
|
60
|
+
defaultMode?: ThemeMode;
|
|
61
|
+
/**
|
|
62
|
+
* When `true` (default) and `defaultMode === 'system'`, the script falls
|
|
63
|
+
* through to `prefers-color-scheme`. When `false`, the script applies
|
|
64
|
+
* `defaultMode` directly and never reads the OS preference.
|
|
65
|
+
*/
|
|
66
|
+
enableSystem?: boolean;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build the pre-paint inline script. Mirrors the resolution order used by
|
|
70
|
+
* the React/Astro `ThemeProvider`:
|
|
71
|
+
*
|
|
72
|
+
* 1. stored value (light|dark|system)
|
|
73
|
+
* 2. if no stored value: `defaultMode` (light|dark applied as-is)
|
|
74
|
+
* 3. if stored or default is 'system' AND `enableSystem`: matchMedia
|
|
75
|
+
* 4. else: 'light'
|
|
76
|
+
*
|
|
77
|
+
* Back-compat: with no options the script behaves exactly as before —
|
|
78
|
+
* stored value, then matchMedia, then 'light'.
|
|
79
|
+
*/
|
|
80
|
+
declare function getThemeScript(storageKeyOrOptions?: string | ThemeScriptOptions, attribute?: 'class' | 'data-theme'): string;
|
|
47
81
|
|
|
48
82
|
/**
|
|
49
83
|
* Browser DOM adapters for the theme machine.
|
package/dist/theme.cjs
CHANGED
|
@@ -90,8 +90,29 @@ function createTheme(config = {}, storage, mediaQuery) {
|
|
|
90
90
|
}
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
|
-
function getThemeScript(
|
|
94
|
-
|
|
93
|
+
function getThemeScript(storageKeyOrOptions = "rfr-theme", attribute = "class") {
|
|
94
|
+
const opts = typeof storageKeyOrOptions === "string" ? {
|
|
95
|
+
storageKey: storageKeyOrOptions,
|
|
96
|
+
attribute,
|
|
97
|
+
defaultMode: "system",
|
|
98
|
+
enableSystem: true
|
|
99
|
+
} : {
|
|
100
|
+
storageKey: storageKeyOrOptions.storageKey ?? "rfr-theme",
|
|
101
|
+
attribute: storageKeyOrOptions.attribute ?? "class",
|
|
102
|
+
defaultMode: storageKeyOrOptions.defaultMode ?? "system",
|
|
103
|
+
enableSystem: storageKeyOrOptions.enableSystem ?? true
|
|
104
|
+
};
|
|
105
|
+
const noSystemFallback = opts.defaultMode === "light" || opts.defaultMode === "dark" || !opts.enableSystem;
|
|
106
|
+
const safeDefault = opts.defaultMode === "dark" ? "dark" : "light";
|
|
107
|
+
const resolveExpr = noSystemFallback ? (
|
|
108
|
+
// Stored wins → else hard default. No matchMedia call at all.
|
|
109
|
+
`var t=m==='dark'?'dark':(m==='light'?'light':'${safeDefault}');`
|
|
110
|
+
) : (
|
|
111
|
+
// Original behaviour: stored → else matchMedia → else light.
|
|
112
|
+
`var s=window.matchMedia('(prefers-color-scheme:dark)').matches;var t=m==='dark'||(m!=='light'&&s)?'dark':'light';`
|
|
113
|
+
);
|
|
114
|
+
const applyExpr = opts.attribute === "class" ? "d.classList.remove('light','dark');d.classList.add(t);" : `d.setAttribute('${opts.attribute}',t);`;
|
|
115
|
+
return `(function(){try{var m=localStorage.getItem('${opts.storageKey}');${resolveExpr}var d=document.documentElement;${applyExpr}d.style.colorScheme=t;}catch(e){}})()`;
|
|
95
116
|
}
|
|
96
117
|
function createLocalStorageAdapter() {
|
|
97
118
|
return {
|
|
@@ -330,11 +351,13 @@ function ThemeToggle({ className, variant = "segmented" }) {
|
|
|
330
351
|
}
|
|
331
352
|
function ThemeScript({
|
|
332
353
|
storageKey = "rfr-theme",
|
|
333
|
-
attribute = "class"
|
|
354
|
+
attribute = "class",
|
|
355
|
+
defaultMode = "system",
|
|
356
|
+
enableSystem = true
|
|
334
357
|
}) {
|
|
335
358
|
return React2__namespace.createElement("script", {
|
|
336
359
|
dangerouslySetInnerHTML: {
|
|
337
|
-
__html: getThemeScript(storageKey, attribute)
|
|
360
|
+
__html: getThemeScript({ storageKey, attribute, defaultMode, enableSystem })
|
|
338
361
|
}
|
|
339
362
|
});
|
|
340
363
|
}
|
package/dist/theme.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../theme/src/theme-machine.ts","../../theme/src/theme-script.ts","../../theme/src/dom-adapters.ts","../../shared/src/dev-feedback.ts","../../react-theme/src/theme-provider.tsx","../../react-theme/src/theme-toggle.tsx","../../react-theme/src/theme-script-component.tsx"],"names":["React","React2","React3"],"mappings":"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","file":"theme.cjs","sourcesContent":["/**\n * Headless theme state machine — pure TypeScript, zero DOM dependencies.\n * Manages light/dark/system mode with system preference tracking.\n */\n\nexport type ThemeMode = 'light' | 'dark' | 'system'\nexport type ResolvedTheme = 'light' | 'dark'\n\nexport interface ThemeState {\n /** User's chosen mode */\n mode: ThemeMode\n /** Resolved theme after system preference detection */\n resolved: ResolvedTheme\n}\n\nexport interface ThemeConfig {\n /** Initial mode. Default: 'system' */\n defaultMode?: ThemeMode\n /** localStorage key. Default: 'rfr-theme' */\n storageKey?: string\n /** HTML attribute to set. Default: 'class' */\n attribute?: 'class' | 'data-theme'\n}\n\nexport interface StorageAdapter {\n get(key: string): string | null\n set(key: string, value: string): void\n}\n\nexport interface MediaQueryAdapter {\n matches(query: string): boolean\n subscribe(query: string, callback: (matches: boolean) => void): () => void\n}\n\nexport interface ThemeAPI {\n /** Get current state */\n getState(): ThemeState\n /** Set mode (light/dark/system) */\n setMode(mode: ThemeMode): void\n /** Subscribe to state changes */\n subscribe(fn: (state: ThemeState) => void): () => void\n /** Clean up subscriptions */\n destroy(): void\n}\n\nfunction resolveTheme(mode: ThemeMode, systemPrefersDark: boolean): ResolvedTheme {\n if (mode === 'system') {\n return systemPrefersDark ? 'dark' : 'light'\n }\n return mode\n}\n\nexport function createTheme(\n config: ThemeConfig = {},\n storage?: StorageAdapter,\n mediaQuery?: MediaQueryAdapter,\n): ThemeAPI {\n const {\n defaultMode = 'system',\n storageKey = 'rfr-theme',\n } = config\n\n const listeners = new Set<(state: ThemeState) => void>()\n let cleanupMediaQuery: (() => void) | null = null\n\n // Read persisted mode or use default\n const persisted = storage?.get(storageKey) as ThemeMode | null\n let mode: ThemeMode = persisted && ['light', 'dark', 'system'].includes(persisted)\n ? persisted\n : defaultMode\n\n // Detect system preference\n let systemPrefersDark = mediaQuery?.matches('(prefers-color-scheme: dark)') ?? false\n\n let state: ThemeState = {\n mode,\n resolved: resolveTheme(mode, systemPrefersDark),\n }\n\n function notify() {\n for (const fn of listeners) {\n fn(state)\n }\n }\n\n function updateState(newMode: ThemeMode) {\n mode = newMode\n state = { mode, resolved: resolveTheme(mode, systemPrefersDark) }\n storage?.set(storageKey, mode)\n notify()\n }\n\n // Listen for system preference changes\n if (mediaQuery) {\n cleanupMediaQuery = mediaQuery.subscribe(\n '(prefers-color-scheme: dark)',\n (matches) => {\n systemPrefersDark = matches\n if (mode === 'system') {\n state = { mode, resolved: resolveTheme(mode, systemPrefersDark) }\n notify()\n }\n },\n )\n }\n\n return {\n getState() {\n return state\n },\n\n setMode(newMode: ThemeMode) {\n if (newMode !== mode) {\n updateState(newMode)\n }\n },\n\n subscribe(fn: (state: ThemeState) => void) {\n listeners.add(fn)\n return () => {\n listeners.delete(fn)\n }\n },\n\n destroy() {\n listeners.clear()\n cleanupMediaQuery?.()\n },\n }\n}\n","/**\n * Inline script for preventing theme flash on page load.\n * Inject this as a <script> tag in the <head> before any CSS.\n * Works with any framework (React, Astro, plain HTML).\n */\n\nexport function getThemeScript(\n storageKey = 'rfr-theme',\n attribute: 'class' | 'data-theme' = 'class',\n): string {\n // This string is injected as innerHTML of a <script> tag.\n // It runs before any CSS/JS loads, preventing flash of wrong theme.\n return `(function(){try{var m=localStorage.getItem('${storageKey}');var s=window.matchMedia('(prefers-color-scheme:dark)').matches;var t=m==='dark'||(m!=='light'&&s)?'dark':'light';var d=document.documentElement;${\n attribute === 'class'\n ? \"d.classList.remove('light','dark');d.classList.add(t);\"\n : `d.setAttribute('${attribute}',t);`\n }d.style.colorScheme=t;}catch(e){}})()`\n}\n","/**\n * Browser DOM adapters for the theme machine.\n * These bridge the headless core to browser APIs.\n */\n\nimport type { StorageAdapter, MediaQueryAdapter, ResolvedTheme } from './theme-machine.js'\n\n/** localStorage adapter */\nexport function createLocalStorageAdapter(): StorageAdapter {\n return {\n get(key) {\n try {\n return localStorage.getItem(key)\n } catch {\n return null\n }\n },\n set(key, value) {\n try {\n localStorage.setItem(key, value)\n } catch {\n // localStorage unavailable (SSR, incognito quota exceeded, etc.)\n }\n },\n }\n}\n\n/** matchMedia adapter */\nexport function createMediaQueryAdapter(): MediaQueryAdapter {\n return {\n matches(query) {\n if (typeof window === 'undefined') return false\n return window.matchMedia(query).matches\n },\n subscribe(query, callback) {\n if (typeof window === 'undefined') return () => {}\n const mql = window.matchMedia(query)\n const handler = (e: MediaQueryListEvent) => callback(e.matches)\n mql.addEventListener('change', handler)\n return () => mql.removeEventListener('change', handler)\n },\n }\n}\n\n/** Apply resolved theme to the document */\nexport function applyThemeToDOM(\n resolved: ResolvedTheme,\n attribute: 'class' | 'data-theme' = 'class',\n): void {\n if (typeof document === 'undefined') return\n\n const root = document.documentElement\n if (attribute === 'class') {\n root.classList.remove('light', 'dark')\n root.classList.add(resolved)\n } else {\n root.setAttribute(attribute, resolved)\n }\n root.style.colorScheme = resolved\n}\n","/**\n * dev-feedback — zero-dependency `devWarn` / `devError` primitives.\n *\n * Design constraints (epic #247, issue #248):\n * - Guarded by `process.env.NODE_ENV !== 'production'` so production bundlers\n * dead-code-strip every call (the guard is a static string compare that\n * minifiers fold to `false` in prod builds).\n * - Warn-once dedupe per `code` — a footgun is reported once, not on every\n * render.\n * - NO import of `@refraction-ui/logger` (no hard dependency on the telemetry\n * lib). Forwarding to a telemetry sink happens ONLY if the consumer\n * explicitly injects one (dependency inversion, never an import).\n */\n\n/**\n * Minimal structural shape of the record a telemetry sink consumes. This is a\n * deliberate structural mirror of `@refraction-ui/logger`'s `LogRecord` — it is\n * NOT imported, so `@refraction-ui/shared` keeps zero dependency on the\n * telemetry lib. A consumer that wires the real logger sink satisfies this\n * shape structurally.\n */\nexport interface DevFeedbackRecord {\n level: 'warn' | 'error'\n message: string\n timestamp: number\n /** Structured detail — the library-origin envelope lives here. */\n context: Record<string, unknown>\n}\n\n/**\n * The narrow contract a consumer-injected telemetry sink must satisfy. Kept\n * intentionally minimal and structural so the real `TelemetrySink` from\n * `@refraction-ui/logger` is assignable WITHOUT shared importing the logger.\n */\nexport interface DevFeedbackSink {\n /** Receive a single dev-feedback record. Must never throw to the caller. */\n log(record: DevFeedbackRecord): void\n}\n\n/**\n * Minimal ambient view of `process.env` so we can read `NODE_ENV` WITHOUT\n * pulling `@types/node` into this zero-dependency package. Accessed defensively\n * (the `typeof process === 'undefined'` guard) so this is safe in browsers too.\n */\ndeclare const process:\n | { env?: { NODE_ENV?: string } }\n | undefined\n\n/** Per-code dedupe set — module-scoped so it survives across calls. */\nconst seen = new Set<string>()\n\n/**\n * Optional, consumer-injected sink. `null` until a consumer explicitly wires\n * one via {@link setDevFeedbackSink}. Nothing phones home implicitly.\n */\nlet injectedSink: DevFeedbackSink | null = null\n\n/**\n * Wire an optional telemetry sink that {@link devWarn} / {@link devError}\n * forward to (in addition to the console). Inversion of control: the consumer\n * owns the sink; this package never imports it. Pass `null` to unwire.\n *\n * Forwarding still only happens in non-production (the calls themselves are\n * stripped in prod), and still respects warn-once dedupe.\n */\nexport function setDevFeedbackSink(sink: DevFeedbackSink | null): void {\n injectedSink = sink\n}\n\n/** Test-only / consumer-only escape hatch to reset warn-once dedupe state. */\nexport function resetDevFeedback(): void {\n seen.clear()\n}\n\nfunction isDev(): boolean {\n // String compare (not a negated truthiness) so bundlers can statically fold\n // `process.env.NODE_ENV` and strip the whole branch in production builds.\n return (\n typeof process === 'undefined' ||\n process.env?.NODE_ENV !== 'production'\n )\n}\n\nfunction emit(\n level: 'warn' | 'error',\n code: string,\n message: string,\n detail?: Record<string, unknown>,\n): void {\n if (!isDev()) return\n\n // Warn-once dedupe, keyed by (level, code) so an error and a warning sharing\n // a code are not collapsed into one.\n const key = `${level}:${code}`\n if (seen.has(key)) return\n seen.add(key)\n\n const text = `[refraction-ui] ${code}: ${message}`\n\n if (level === 'error') {\n console.error(text, detail ?? '')\n } else {\n console.warn(text, detail ?? '')\n }\n\n // Forward to the consumer-injected sink ONLY if one was explicitly wired.\n if (injectedSink) {\n const record: DevFeedbackRecord = {\n level,\n message: `${code}: ${message}`,\n timestamp: Date.now(),\n context: { code, ...(detail ?? {}) },\n }\n try {\n injectedSink.log(record)\n } catch {\n // A broken sink must never break the consumer app.\n }\n }\n}\n\n/**\n * Emit a development-only warning for a refraction-ui footgun.\n *\n * @param code Stable, greppable identifier (e.g. `'react/no-controlled-prop'`).\n * @param message Human-readable explanation.\n * @param detail Optional structured detail (forwarded to an injected sink).\n *\n * Stripped entirely in production. Warned at most once per `code`.\n */\nexport function devWarn(\n code: string,\n message: string,\n detail?: Record<string, unknown>,\n): void {\n emit('warn', code, message, detail)\n}\n\n/**\n * Emit a development-only error for a refraction-ui misuse / invariant break.\n *\n * @param code Stable, greppable identifier.\n * @param message Human-readable explanation.\n * @param detail Optional structured detail (forwarded to an injected sink).\n *\n * Stripped entirely in production. Reported at most once per `code`.\n */\nexport function devError(\n code: string,\n message: string,\n detail?: Record<string, unknown>,\n): void {\n emit('error', code, message, detail)\n}\n","import * as React from 'react'\nimport {\n createTheme,\n createLocalStorageAdapter,\n createMediaQueryAdapter,\n applyThemeToDOM,\n type ThemeMode,\n type ResolvedTheme,\n type ThemeConfig,\n type ThemeAPI,\n} from '@refraction-ui/theme'\nimport { devWarn } from '@refraction-ui/shared'\n\nexport interface ThemeContextValue {\n mode: ThemeMode\n resolved: ResolvedTheme\n setMode: (mode: ThemeMode) => void\n}\n\nconst ThemeContext = React.createContext<ThemeContextValue | null>(null)\n\nexport interface ThemeProviderProps extends ThemeConfig {\n children: React.ReactNode\n}\n\nexport function ThemeProvider({\n children,\n defaultMode = 'system',\n storageKey = 'rfr-theme',\n attribute = 'class',\n}: ThemeProviderProps) {\n const themeRef = React.useRef<ThemeAPI | null>(null)\n\n // Initialize theme machine once (client-side only for adapters)\n if (!themeRef.current) {\n const isBrowser = typeof window !== 'undefined'\n themeRef.current = createTheme(\n { defaultMode, storageKey, attribute },\n isBrowser ? createLocalStorageAdapter() : undefined,\n isBrowser ? createMediaQueryAdapter() : undefined,\n )\n }\n\n const [state, setState] = React.useState(() => themeRef.current!.getState())\n\n React.useEffect(() => {\n const theme = themeRef.current!\n // Apply initial theme to DOM\n applyThemeToDOM(theme.getState().resolved, attribute)\n\n // Subscribe to changes\n const unsub = theme.subscribe((newState) => {\n setState(newState)\n applyThemeToDOM(newState.resolved, attribute)\n })\n\n return () => {\n unsub()\n theme.destroy()\n }\n }, [attribute])\n\n const contextValue = React.useMemo<ThemeContextValue>(\n () => ({\n mode: state.mode,\n resolved: state.resolved,\n setMode: (mode: ThemeMode) => themeRef.current?.setMode(mode),\n }),\n [state.mode, state.resolved],\n )\n\n return React.createElement(ThemeContext.Provider, { value: contextValue }, children)\n}\n\nexport function useTheme(): ThemeContextValue {\n const context = React.useContext(ThemeContext)\n if (!context) {\n devWarn(\n 'react-theme/use-theme-outside-provider',\n 'useTheme() was called outside a <ThemeProvider>. Wrap your app (or the consuming subtree) in <ThemeProvider> so the theme context is available.',\n )\n throw new Error('useTheme must be used within a <ThemeProvider>')\n }\n return context\n}\n","import * as React from 'react'\nimport { useTheme } from './theme-provider.js'\nimport type { ThemeMode } from '@refraction-ui/theme'\n\nconst modes: { value: ThemeMode; label: string; icon: string }[] = [\n { value: 'light', label: 'Light', icon: 'sun' },\n { value: 'dark', label: 'Dark', icon: 'moon' },\n { value: 'system', label: 'System', icon: 'monitor' },\n]\n\n// Inline SVG icons — no external icon library dependency\nconst icons: Record<string, React.ReactNode> = {\n sun: React.createElement('svg', {\n xmlns: 'http://www.w3.org/2000/svg', width: 16, height: 16, viewBox: '0 0 24 24',\n fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',\n },\n React.createElement('circle', { cx: 12, cy: 12, r: 5 }),\n React.createElement('path', { d: 'M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42' }),\n ),\n moon: React.createElement('svg', {\n xmlns: 'http://www.w3.org/2000/svg', width: 16, height: 16, viewBox: '0 0 24 24',\n fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',\n },\n React.createElement('path', { d: 'M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z' }),\n ),\n monitor: React.createElement('svg', {\n xmlns: 'http://www.w3.org/2000/svg', width: 16, height: 16, viewBox: '0 0 24 24',\n fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',\n },\n React.createElement('rect', { x: 2, y: 3, width: 20, height: 14, rx: 2, ry: 2 }),\n React.createElement('line', { x1: 8, y1: 21, x2: 16, y2: 21 }),\n React.createElement('line', { x1: 12, y1: 17, x2: 12, y2: 21 }),\n ),\n}\n\nexport interface ThemeToggleProps {\n className?: string\n /** 'dropdown' shows a menu, 'segmented' shows inline buttons */\n variant?: 'dropdown' | 'segmented'\n}\n\nexport function ThemeToggle({ className, variant = 'segmented' }: ThemeToggleProps) {\n const { mode, setMode } = useTheme()\n\n if (variant === 'segmented') {\n return React.createElement('div', {\n className: `inline-flex items-center gap-1 rounded-lg border p-1 ${className ?? ''}`,\n role: 'radiogroup',\n 'aria-label': 'Theme',\n },\n modes.map(({ value, label, icon }) =>\n React.createElement('button', {\n key: value,\n type: 'button',\n role: 'radio',\n 'aria-checked': mode === value,\n 'aria-label': label,\n className: `inline-flex items-center justify-center rounded-md p-1.5 text-sm transition-colors ${\n mode === value\n ? 'bg-accent text-accent-foreground'\n : 'text-muted-foreground hover:bg-muted'\n }`,\n onClick: () => setMode(value),\n }, icons[icon]),\n ),\n )\n }\n\n // Dropdown variant — simplified, no external dropdown dependency\n const [open, setOpen] = React.useState(false)\n const ref = React.useRef<HTMLDivElement>(null)\n\n React.useEffect(() => {\n if (!open) return\n const handler = (e: MouseEvent) => {\n if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)\n }\n document.addEventListener('mousedown', handler)\n return () => document.removeEventListener('mousedown', handler)\n }, [open])\n\n const currentIcon = modes.find((m) => m.value === mode)?.icon ?? 'monitor'\n\n return React.createElement('div', { ref, className: `relative ${className ?? ''}` },\n React.createElement('button', {\n type: 'button',\n 'aria-label': 'Toggle theme',\n 'aria-expanded': open,\n className: 'inline-flex items-center justify-center rounded-md p-2 text-sm transition-colors hover:bg-muted',\n onClick: () => setOpen(!open),\n }, icons[currentIcon]),\n open && React.createElement('div', {\n className: 'absolute right-0 top-full mt-1 z-50 min-w-[8rem] rounded-md border bg-popover p-1 shadow-md',\n role: 'menu',\n },\n modes.map(({ value, label, icon }) =>\n React.createElement('button', {\n key: value,\n type: 'button',\n role: 'menuitem',\n className: `flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors hover:bg-accent ${\n mode === value ? 'bg-accent' : ''\n }`,\n onClick: () => { setMode(value); setOpen(false) },\n }, icons[icon], label),\n ),\n ),\n )\n}\n","import * as React from 'react'\nimport { getThemeScript } from '@refraction-ui/theme'\n\nexport interface ThemeScriptProps {\n storageKey?: string\n attribute?: 'class' | 'data-theme'\n}\n\n/**\n * Renders an inline <script> that prevents theme flash on SSR pages.\n * Place this in the <head> of your document (in Next.js layout.tsx, Remix root, etc.)\n */\nexport function ThemeScript({\n storageKey = 'rfr-theme',\n attribute = 'class',\n}: ThemeScriptProps) {\n return React.createElement('script', {\n dangerouslySetInnerHTML: {\n __html: getThemeScript(storageKey, attribute),\n },\n })\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../theme/src/theme-machine.ts","../../theme/src/theme-script.ts","../../theme/src/dom-adapters.ts","../../shared/src/dev-feedback.ts","../../react-theme/src/theme-provider.tsx","../../react-theme/src/theme-toggle.tsx","../../react-theme/src/theme-script-component.tsx"],"names":["React","React2","React3"],"mappings":"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","file":"theme.cjs","sourcesContent":["/**\n * Headless theme state machine — pure TypeScript, zero DOM dependencies.\n * Manages light/dark/system mode with system preference tracking.\n */\n\nexport type ThemeMode = 'light' | 'dark' | 'system'\nexport type ResolvedTheme = 'light' | 'dark'\n\nexport interface ThemeState {\n /** User's chosen mode */\n mode: ThemeMode\n /** Resolved theme after system preference detection */\n resolved: ResolvedTheme\n}\n\nexport interface ThemeConfig {\n /** Initial mode. Default: 'system' */\n defaultMode?: ThemeMode\n /** localStorage key. Default: 'rfr-theme' */\n storageKey?: string\n /** HTML attribute to set. Default: 'class' */\n attribute?: 'class' | 'data-theme'\n}\n\nexport interface StorageAdapter {\n get(key: string): string | null\n set(key: string, value: string): void\n}\n\nexport interface MediaQueryAdapter {\n matches(query: string): boolean\n subscribe(query: string, callback: (matches: boolean) => void): () => void\n}\n\nexport interface ThemeAPI {\n /** Get current state */\n getState(): ThemeState\n /** Set mode (light/dark/system) */\n setMode(mode: ThemeMode): void\n /** Subscribe to state changes */\n subscribe(fn: (state: ThemeState) => void): () => void\n /** Clean up subscriptions */\n destroy(): void\n}\n\nfunction resolveTheme(mode: ThemeMode, systemPrefersDark: boolean): ResolvedTheme {\n if (mode === 'system') {\n return systemPrefersDark ? 'dark' : 'light'\n }\n return mode\n}\n\nexport function createTheme(\n config: ThemeConfig = {},\n storage?: StorageAdapter,\n mediaQuery?: MediaQueryAdapter,\n): ThemeAPI {\n const {\n defaultMode = 'system',\n storageKey = 'rfr-theme',\n } = config\n\n const listeners = new Set<(state: ThemeState) => void>()\n let cleanupMediaQuery: (() => void) | null = null\n\n // Read persisted mode or use default\n const persisted = storage?.get(storageKey) as ThemeMode | null\n let mode: ThemeMode = persisted && ['light', 'dark', 'system'].includes(persisted)\n ? persisted\n : defaultMode\n\n // Detect system preference\n let systemPrefersDark = mediaQuery?.matches('(prefers-color-scheme: dark)') ?? false\n\n let state: ThemeState = {\n mode,\n resolved: resolveTheme(mode, systemPrefersDark),\n }\n\n function notify() {\n for (const fn of listeners) {\n fn(state)\n }\n }\n\n function updateState(newMode: ThemeMode) {\n mode = newMode\n state = { mode, resolved: resolveTheme(mode, systemPrefersDark) }\n storage?.set(storageKey, mode)\n notify()\n }\n\n // Listen for system preference changes\n if (mediaQuery) {\n cleanupMediaQuery = mediaQuery.subscribe(\n '(prefers-color-scheme: dark)',\n (matches) => {\n systemPrefersDark = matches\n if (mode === 'system') {\n state = { mode, resolved: resolveTheme(mode, systemPrefersDark) }\n notify()\n }\n },\n )\n }\n\n return {\n getState() {\n return state\n },\n\n setMode(newMode: ThemeMode) {\n if (newMode !== mode) {\n updateState(newMode)\n }\n },\n\n subscribe(fn: (state: ThemeState) => void) {\n listeners.add(fn)\n return () => {\n listeners.delete(fn)\n }\n },\n\n destroy() {\n listeners.clear()\n cleanupMediaQuery?.()\n },\n }\n}\n","/**\n * Inline script for preventing theme flash on page load.\n * Inject this as a <script> tag in the <head> before any CSS.\n * Works with any framework (React, Astro, plain HTML).\n */\n\nimport type { ThemeMode } from './theme-machine.js'\n\nexport interface ThemeScriptOptions {\n /** localStorage key the corresponding ThemeProvider reads. */\n storageKey?: string\n /** Attribute strategy: 'class' toggles `light`/`dark` classes; otherwise an HTML attribute. */\n attribute?: 'class' | 'data-theme'\n /**\n * Theme to apply when nothing is stored AND `enableSystem` is false\n * (or `defaultMode` is explicitly 'light'/'dark'). Match this to your\n * ThemeProvider's `defaultMode` so the pre-paint script and provider agree.\n * Defaults to `'system'` — current behavior, OS preference wins.\n *\n * Issue #317.\n */\n defaultMode?: ThemeMode\n /**\n * When `true` (default) and `defaultMode === 'system'`, the script falls\n * through to `prefers-color-scheme`. When `false`, the script applies\n * `defaultMode` directly and never reads the OS preference.\n */\n enableSystem?: boolean\n}\n\n/**\n * Build the pre-paint inline script. Mirrors the resolution order used by\n * the React/Astro `ThemeProvider`:\n *\n * 1. stored value (light|dark|system)\n * 2. if no stored value: `defaultMode` (light|dark applied as-is)\n * 3. if stored or default is 'system' AND `enableSystem`: matchMedia\n * 4. else: 'light'\n *\n * Back-compat: with no options the script behaves exactly as before —\n * stored value, then matchMedia, then 'light'.\n */\nexport function getThemeScript(\n storageKeyOrOptions: string | ThemeScriptOptions = 'rfr-theme',\n attribute: 'class' | 'data-theme' = 'class',\n): string {\n const opts: Required<ThemeScriptOptions> =\n typeof storageKeyOrOptions === 'string'\n ? {\n storageKey: storageKeyOrOptions,\n attribute,\n defaultMode: 'system',\n enableSystem: true,\n }\n : {\n storageKey: storageKeyOrOptions.storageKey ?? 'rfr-theme',\n attribute: storageKeyOrOptions.attribute ?? 'class',\n defaultMode: storageKeyOrOptions.defaultMode ?? 'system',\n enableSystem: storageKeyOrOptions.enableSystem ?? true,\n }\n\n // `noSystemFallback` short-circuits matchMedia entirely when the consumer\n // wants brand consistency over OS preference (defaultMode='light'/'dark'\n // OR enableSystem=false).\n const noSystemFallback =\n opts.defaultMode === 'light' || opts.defaultMode === 'dark' || !opts.enableSystem\n const safeDefault: 'light' | 'dark' =\n opts.defaultMode === 'dark' ? 'dark' : 'light'\n\n const resolveExpr = noSystemFallback\n ? // Stored wins → else hard default. No matchMedia call at all.\n `var t=m==='dark'?'dark':(m==='light'?'light':'${safeDefault}');`\n : // Original behaviour: stored → else matchMedia → else light.\n `var s=window.matchMedia('(prefers-color-scheme:dark)').matches;var t=m==='dark'||(m!=='light'&&s)?'dark':'light';`\n\n const applyExpr =\n opts.attribute === 'class'\n ? \"d.classList.remove('light','dark');d.classList.add(t);\"\n : `d.setAttribute('${opts.attribute}',t);`\n\n return `(function(){try{var m=localStorage.getItem('${opts.storageKey}');${resolveExpr}var d=document.documentElement;${applyExpr}d.style.colorScheme=t;}catch(e){}})()`\n}\n","/**\n * Browser DOM adapters for the theme machine.\n * These bridge the headless core to browser APIs.\n */\n\nimport type { StorageAdapter, MediaQueryAdapter, ResolvedTheme } from './theme-machine.js'\n\n/** localStorage adapter */\nexport function createLocalStorageAdapter(): StorageAdapter {\n return {\n get(key) {\n try {\n return localStorage.getItem(key)\n } catch {\n return null\n }\n },\n set(key, value) {\n try {\n localStorage.setItem(key, value)\n } catch {\n // localStorage unavailable (SSR, incognito quota exceeded, etc.)\n }\n },\n }\n}\n\n/** matchMedia adapter */\nexport function createMediaQueryAdapter(): MediaQueryAdapter {\n return {\n matches(query) {\n if (typeof window === 'undefined') return false\n return window.matchMedia(query).matches\n },\n subscribe(query, callback) {\n if (typeof window === 'undefined') return () => {}\n const mql = window.matchMedia(query)\n const handler = (e: MediaQueryListEvent) => callback(e.matches)\n mql.addEventListener('change', handler)\n return () => mql.removeEventListener('change', handler)\n },\n }\n}\n\n/** Apply resolved theme to the document */\nexport function applyThemeToDOM(\n resolved: ResolvedTheme,\n attribute: 'class' | 'data-theme' = 'class',\n): void {\n if (typeof document === 'undefined') return\n\n const root = document.documentElement\n if (attribute === 'class') {\n root.classList.remove('light', 'dark')\n root.classList.add(resolved)\n } else {\n root.setAttribute(attribute, resolved)\n }\n root.style.colorScheme = resolved\n}\n","/**\n * dev-feedback — zero-dependency `devWarn` / `devError` primitives.\n *\n * Design constraints (epic #247, issue #248):\n * - Guarded by `process.env.NODE_ENV !== 'production'` so production bundlers\n * dead-code-strip every call (the guard is a static string compare that\n * minifiers fold to `false` in prod builds).\n * - Warn-once dedupe per `code` — a footgun is reported once, not on every\n * render.\n * - NO import of `@refraction-ui/logger` (no hard dependency on the telemetry\n * lib). Forwarding to a telemetry sink happens ONLY if the consumer\n * explicitly injects one (dependency inversion, never an import).\n */\n\n/**\n * Minimal structural shape of the record a telemetry sink consumes. This is a\n * deliberate structural mirror of `@refraction-ui/logger`'s `LogRecord` — it is\n * NOT imported, so `@refraction-ui/shared` keeps zero dependency on the\n * telemetry lib. A consumer that wires the real logger sink satisfies this\n * shape structurally.\n */\nexport interface DevFeedbackRecord {\n level: 'warn' | 'error'\n message: string\n timestamp: number\n /** Structured detail — the library-origin envelope lives here. */\n context: Record<string, unknown>\n}\n\n/**\n * The narrow contract a consumer-injected telemetry sink must satisfy. Kept\n * intentionally minimal and structural so the real `TelemetrySink` from\n * `@refraction-ui/logger` is assignable WITHOUT shared importing the logger.\n */\nexport interface DevFeedbackSink {\n /** Receive a single dev-feedback record. Must never throw to the caller. */\n log(record: DevFeedbackRecord): void\n}\n\n/**\n * Minimal ambient view of `process.env` so we can read `NODE_ENV` WITHOUT\n * pulling `@types/node` into this zero-dependency package. Accessed defensively\n * (the `typeof process === 'undefined'` guard) so this is safe in browsers too.\n */\ndeclare const process:\n | { env?: { NODE_ENV?: string } }\n | undefined\n\n/** Per-code dedupe set — module-scoped so it survives across calls. */\nconst seen = new Set<string>()\n\n/**\n * Optional, consumer-injected sink. `null` until a consumer explicitly wires\n * one via {@link setDevFeedbackSink}. Nothing phones home implicitly.\n */\nlet injectedSink: DevFeedbackSink | null = null\n\n/**\n * Wire an optional telemetry sink that {@link devWarn} / {@link devError}\n * forward to (in addition to the console). Inversion of control: the consumer\n * owns the sink; this package never imports it. Pass `null` to unwire.\n *\n * Forwarding still only happens in non-production (the calls themselves are\n * stripped in prod), and still respects warn-once dedupe.\n */\nexport function setDevFeedbackSink(sink: DevFeedbackSink | null): void {\n injectedSink = sink\n}\n\n/** Test-only / consumer-only escape hatch to reset warn-once dedupe state. */\nexport function resetDevFeedback(): void {\n seen.clear()\n}\n\nfunction isDev(): boolean {\n // String compare (not a negated truthiness) so bundlers can statically fold\n // `process.env.NODE_ENV` and strip the whole branch in production builds.\n return (\n typeof process === 'undefined' ||\n process.env?.NODE_ENV !== 'production'\n )\n}\n\nfunction emit(\n level: 'warn' | 'error',\n code: string,\n message: string,\n detail?: Record<string, unknown>,\n): void {\n if (!isDev()) return\n\n // Warn-once dedupe, keyed by (level, code) so an error and a warning sharing\n // a code are not collapsed into one.\n const key = `${level}:${code}`\n if (seen.has(key)) return\n seen.add(key)\n\n const text = `[refraction-ui] ${code}: ${message}`\n\n if (level === 'error') {\n console.error(text, detail ?? '')\n } else {\n console.warn(text, detail ?? '')\n }\n\n // Forward to the consumer-injected sink ONLY if one was explicitly wired.\n if (injectedSink) {\n const record: DevFeedbackRecord = {\n level,\n message: `${code}: ${message}`,\n timestamp: Date.now(),\n context: { code, ...(detail ?? {}) },\n }\n try {\n injectedSink.log(record)\n } catch {\n // A broken sink must never break the consumer app.\n }\n }\n}\n\n/**\n * Emit a development-only warning for a refraction-ui footgun.\n *\n * @param code Stable, greppable identifier (e.g. `'react/no-controlled-prop'`).\n * @param message Human-readable explanation.\n * @param detail Optional structured detail (forwarded to an injected sink).\n *\n * Stripped entirely in production. Warned at most once per `code`.\n */\nexport function devWarn(\n code: string,\n message: string,\n detail?: Record<string, unknown>,\n): void {\n emit('warn', code, message, detail)\n}\n\n/**\n * Emit a development-only error for a refraction-ui misuse / invariant break.\n *\n * @param code Stable, greppable identifier.\n * @param message Human-readable explanation.\n * @param detail Optional structured detail (forwarded to an injected sink).\n *\n * Stripped entirely in production. Reported at most once per `code`.\n */\nexport function devError(\n code: string,\n message: string,\n detail?: Record<string, unknown>,\n): void {\n emit('error', code, message, detail)\n}\n","import * as React from 'react'\nimport {\n createTheme,\n createLocalStorageAdapter,\n createMediaQueryAdapter,\n applyThemeToDOM,\n type ThemeMode,\n type ResolvedTheme,\n type ThemeConfig,\n type ThemeAPI,\n} from '@refraction-ui/theme'\nimport { devWarn } from '@refraction-ui/shared'\n\nexport interface ThemeContextValue {\n mode: ThemeMode\n resolved: ResolvedTheme\n setMode: (mode: ThemeMode) => void\n}\n\nconst ThemeContext = React.createContext<ThemeContextValue | null>(null)\n\nexport interface ThemeProviderProps extends ThemeConfig {\n children: React.ReactNode\n}\n\nexport function ThemeProvider({\n children,\n defaultMode = 'system',\n storageKey = 'rfr-theme',\n attribute = 'class',\n}: ThemeProviderProps) {\n const themeRef = React.useRef<ThemeAPI | null>(null)\n\n // Initialize theme machine once (client-side only for adapters)\n if (!themeRef.current) {\n const isBrowser = typeof window !== 'undefined'\n themeRef.current = createTheme(\n { defaultMode, storageKey, attribute },\n isBrowser ? createLocalStorageAdapter() : undefined,\n isBrowser ? createMediaQueryAdapter() : undefined,\n )\n }\n\n const [state, setState] = React.useState(() => themeRef.current!.getState())\n\n React.useEffect(() => {\n const theme = themeRef.current!\n // Apply initial theme to DOM\n applyThemeToDOM(theme.getState().resolved, attribute)\n\n // Subscribe to changes\n const unsub = theme.subscribe((newState) => {\n setState(newState)\n applyThemeToDOM(newState.resolved, attribute)\n })\n\n return () => {\n unsub()\n theme.destroy()\n }\n }, [attribute])\n\n const contextValue = React.useMemo<ThemeContextValue>(\n () => ({\n mode: state.mode,\n resolved: state.resolved,\n setMode: (mode: ThemeMode) => themeRef.current?.setMode(mode),\n }),\n [state.mode, state.resolved],\n )\n\n return React.createElement(ThemeContext.Provider, { value: contextValue }, children)\n}\n\nexport function useTheme(): ThemeContextValue {\n const context = React.useContext(ThemeContext)\n if (!context) {\n devWarn(\n 'react-theme/use-theme-outside-provider',\n 'useTheme() was called outside a <ThemeProvider>. Wrap your app (or the consuming subtree) in <ThemeProvider> so the theme context is available.',\n )\n throw new Error('useTheme must be used within a <ThemeProvider>')\n }\n return context\n}\n","import * as React from 'react'\nimport { useTheme } from './theme-provider.js'\nimport type { ThemeMode } from '@refraction-ui/theme'\n\nconst modes: { value: ThemeMode; label: string; icon: string }[] = [\n { value: 'light', label: 'Light', icon: 'sun' },\n { value: 'dark', label: 'Dark', icon: 'moon' },\n { value: 'system', label: 'System', icon: 'monitor' },\n]\n\n// Inline SVG icons — no external icon library dependency\nconst icons: Record<string, React.ReactNode> = {\n sun: React.createElement('svg', {\n xmlns: 'http://www.w3.org/2000/svg', width: 16, height: 16, viewBox: '0 0 24 24',\n fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',\n },\n React.createElement('circle', { cx: 12, cy: 12, r: 5 }),\n React.createElement('path', { d: 'M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42' }),\n ),\n moon: React.createElement('svg', {\n xmlns: 'http://www.w3.org/2000/svg', width: 16, height: 16, viewBox: '0 0 24 24',\n fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',\n },\n React.createElement('path', { d: 'M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z' }),\n ),\n monitor: React.createElement('svg', {\n xmlns: 'http://www.w3.org/2000/svg', width: 16, height: 16, viewBox: '0 0 24 24',\n fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',\n },\n React.createElement('rect', { x: 2, y: 3, width: 20, height: 14, rx: 2, ry: 2 }),\n React.createElement('line', { x1: 8, y1: 21, x2: 16, y2: 21 }),\n React.createElement('line', { x1: 12, y1: 17, x2: 12, y2: 21 }),\n ),\n}\n\nexport interface ThemeToggleProps {\n className?: string\n /** 'dropdown' shows a menu, 'segmented' shows inline buttons */\n variant?: 'dropdown' | 'segmented'\n}\n\nexport function ThemeToggle({ className, variant = 'segmented' }: ThemeToggleProps) {\n const { mode, setMode } = useTheme()\n\n if (variant === 'segmented') {\n return React.createElement('div', {\n className: `inline-flex items-center gap-1 rounded-lg border p-1 ${className ?? ''}`,\n role: 'radiogroup',\n 'aria-label': 'Theme',\n },\n modes.map(({ value, label, icon }) =>\n React.createElement('button', {\n key: value,\n type: 'button',\n role: 'radio',\n 'aria-checked': mode === value,\n 'aria-label': label,\n className: `inline-flex items-center justify-center rounded-md p-1.5 text-sm transition-colors ${\n mode === value\n ? 'bg-accent text-accent-foreground'\n : 'text-muted-foreground hover:bg-muted'\n }`,\n onClick: () => setMode(value),\n }, icons[icon]),\n ),\n )\n }\n\n // Dropdown variant — simplified, no external dropdown dependency\n const [open, setOpen] = React.useState(false)\n const ref = React.useRef<HTMLDivElement>(null)\n\n React.useEffect(() => {\n if (!open) return\n const handler = (e: MouseEvent) => {\n if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)\n }\n document.addEventListener('mousedown', handler)\n return () => document.removeEventListener('mousedown', handler)\n }, [open])\n\n const currentIcon = modes.find((m) => m.value === mode)?.icon ?? 'monitor'\n\n return React.createElement('div', { ref, className: `relative ${className ?? ''}` },\n React.createElement('button', {\n type: 'button',\n 'aria-label': 'Toggle theme',\n 'aria-expanded': open,\n className: 'inline-flex items-center justify-center rounded-md p-2 text-sm transition-colors hover:bg-muted',\n onClick: () => setOpen(!open),\n }, icons[currentIcon]),\n open && React.createElement('div', {\n className: 'absolute right-0 top-full mt-1 z-50 min-w-[8rem] rounded-md border bg-popover p-1 shadow-md',\n role: 'menu',\n },\n modes.map(({ value, label, icon }) =>\n React.createElement('button', {\n key: value,\n type: 'button',\n role: 'menuitem',\n className: `flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors hover:bg-accent ${\n mode === value ? 'bg-accent' : ''\n }`,\n onClick: () => { setMode(value); setOpen(false) },\n }, icons[icon], label),\n ),\n ),\n )\n}\n","import * as React from 'react'\nimport { getThemeScript, type ThemeMode } from '@refraction-ui/theme'\n\nexport interface ThemeScriptProps {\n /** localStorage key the ThemeProvider reads. */\n storageKey?: string\n /** Attribute strategy. `class` toggles `light`/`dark` on <html>; otherwise an HTML attribute. */\n attribute?: 'class' | 'data-theme'\n /**\n * Theme to apply when nothing is stored. Match this to `<ThemeProvider defaultMode>`\n * so the pre-paint script and provider agree on the no-storage path. Defaults\n * to `'system'` (current behaviour — OS preference wins). Issue #317.\n */\n defaultMode?: ThemeMode\n /**\n * When `false` the inline script never reads `prefers-color-scheme`; the\n * `defaultMode` is applied as-is for first-time visitors. Default `true`.\n */\n enableSystem?: boolean\n}\n\n/**\n * Renders an inline <script> that prevents theme flash on SSR pages.\n * Place this in the <head> of your document (in Next.js layout.tsx, Remix root, etc.)\n *\n * Mirror the props your <ThemeProvider> uses so first-paint and React mount stay in sync:\n *\n * <ThemeScript defaultMode=\"light\" storageKey=\"app_theme\" />\n * <ThemeProvider defaultMode=\"light\" storageKey=\"app_theme\">…</ThemeProvider>\n */\nexport function ThemeScript({\n storageKey = 'rfr-theme',\n attribute = 'class',\n defaultMode = 'system',\n enableSystem = true,\n}: ThemeScriptProps) {\n return React.createElement('script', {\n dangerouslySetInnerHTML: {\n __html: getThemeScript({ storageKey, attribute, defaultMode, enableSystem }),\n },\n })\n}\n"]}
|
package/dist/theme.js
CHANGED
|
@@ -67,8 +67,29 @@ function createTheme(config = {}, storage, mediaQuery) {
|
|
|
67
67
|
}
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
|
-
function getThemeScript(
|
|
71
|
-
|
|
70
|
+
function getThemeScript(storageKeyOrOptions = "rfr-theme", attribute = "class") {
|
|
71
|
+
const opts = typeof storageKeyOrOptions === "string" ? {
|
|
72
|
+
storageKey: storageKeyOrOptions,
|
|
73
|
+
attribute,
|
|
74
|
+
defaultMode: "system",
|
|
75
|
+
enableSystem: true
|
|
76
|
+
} : {
|
|
77
|
+
storageKey: storageKeyOrOptions.storageKey ?? "rfr-theme",
|
|
78
|
+
attribute: storageKeyOrOptions.attribute ?? "class",
|
|
79
|
+
defaultMode: storageKeyOrOptions.defaultMode ?? "system",
|
|
80
|
+
enableSystem: storageKeyOrOptions.enableSystem ?? true
|
|
81
|
+
};
|
|
82
|
+
const noSystemFallback = opts.defaultMode === "light" || opts.defaultMode === "dark" || !opts.enableSystem;
|
|
83
|
+
const safeDefault = opts.defaultMode === "dark" ? "dark" : "light";
|
|
84
|
+
const resolveExpr = noSystemFallback ? (
|
|
85
|
+
// Stored wins → else hard default. No matchMedia call at all.
|
|
86
|
+
`var t=m==='dark'?'dark':(m==='light'?'light':'${safeDefault}');`
|
|
87
|
+
) : (
|
|
88
|
+
// Original behaviour: stored → else matchMedia → else light.
|
|
89
|
+
`var s=window.matchMedia('(prefers-color-scheme:dark)').matches;var t=m==='dark'||(m!=='light'&&s)?'dark':'light';`
|
|
90
|
+
);
|
|
91
|
+
const applyExpr = opts.attribute === "class" ? "d.classList.remove('light','dark');d.classList.add(t);" : `d.setAttribute('${opts.attribute}',t);`;
|
|
92
|
+
return `(function(){try{var m=localStorage.getItem('${opts.storageKey}');${resolveExpr}var d=document.documentElement;${applyExpr}d.style.colorScheme=t;}catch(e){}})()`;
|
|
72
93
|
}
|
|
73
94
|
function createLocalStorageAdapter() {
|
|
74
95
|
return {
|
|
@@ -288,11 +309,13 @@ function ThemeToggle({ className, variant = "segmented" }) {
|
|
|
288
309
|
}
|
|
289
310
|
function ThemeScript({
|
|
290
311
|
storageKey = "rfr-theme",
|
|
291
|
-
attribute = "class"
|
|
312
|
+
attribute = "class",
|
|
313
|
+
defaultMode = "system",
|
|
314
|
+
enableSystem = true
|
|
292
315
|
}) {
|
|
293
316
|
return React2.createElement("script", {
|
|
294
317
|
dangerouslySetInnerHTML: {
|
|
295
|
-
__html: getThemeScript(storageKey, attribute)
|
|
318
|
+
__html: getThemeScript({ storageKey, attribute, defaultMode, enableSystem })
|
|
296
319
|
}
|
|
297
320
|
});
|
|
298
321
|
}
|