@shadow-js/ui 1.0.1 → 1.0.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/CHANGELOG.md +26 -0
- package/README.md +3 -3
- package/dist/cli.mjs +453 -262
- package/dist/dev/theme/theme-provider.js +1 -1
- package/dist/dev/theme/theme-provider.js.map +1 -1
- package/dist/prod/theme/theme-provider.js +1 -1
- package/dist/prod/theme/theme-provider.js.map +1 -1
- package/package.json +11 -11
- package/registry/carousel/carousel.tsx +79 -15
- package/registry/chart/chart.tsx +5 -1
- package/registry/input-group/input-group.tsx +2 -1
- package/registry/resizable/resizable.tsx +175 -34
- package/registry.json +7 -3
|
@@ -82,7 +82,7 @@ function ThemeProvider(props) {
|
|
|
82
82
|
setSystemDark(event.matches);
|
|
83
83
|
};
|
|
84
84
|
const onStorage = (event) => {
|
|
85
|
-
if (event.key !== storageKey()) return;
|
|
85
|
+
if (event.key !== null && event.key !== storageKey()) return;
|
|
86
86
|
setThemeState(isTheme(event.newValue) ? event.newValue : defaultTheme());
|
|
87
87
|
};
|
|
88
88
|
query.addEventListener("change", onSystemChange);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme-provider.js","names":[],"sources":["../../../src/theme/theme-provider.ts"],"sourcesContent":["import {\n type Accessor,\n batch,\n createContext,\n onCleanup,\n onMount,\n untrack,\n useContext,\n useEffect,\n useMemo,\n useSignal,\n} from \"@shadow-js/core\";\nimport type { JsxChild } from \"@shadow-js/web/jsx-runtime\";\nimport {\n applyTheme,\n DEFAULT_STORAGE_KEY,\n isTheme,\n type ResolvedTheme,\n readStoredTheme,\n resolveTheme,\n type Theme,\n type ThemeOptions,\n writeStoredTheme,\n} from \"./theme-logic\";\n\n/**\n * What {@link useTheme} returns. Every value is an accessor: call it to read.\n * @since 1.0.0\n */\nexport interface ThemeContextValue {\n /** The user's choice: `\"light\"`, `\"dark\"` or `\"system\"`. Call it to read the value. */\n readonly theme: Accessor<Theme>;\n /**\n * The theme that is shown: `\"light\"` or `\"dark\"`. `undefined` on the server and until the provider\n * has mounted, so markup that depends on it hydrates without a mismatch.\n */\n readonly resolvedTheme: Accessor<ResolvedTheme | undefined>;\n /**\n * The operating system preference: `\"light\"` or `\"dark\"`. `undefined` on the server and until the\n * provider has mounted.\n */\n readonly systemTheme: Accessor<ResolvedTheme | undefined>;\n /**\n * Chooses a theme, or derives one from the previous choice, and stores it under the provider's\n * `storageKey`. Throws a `TypeError` for a value that is not `\"light\"`, `\"dark\"` or `\"system\"`.\n */\n readonly setTheme: (theme: Theme | ((previous: Theme) => Theme)) => void;\n}\n\n/**\n * Props of {@link ThemeProvider}: the {@link ThemeOptions} plus a transition switch and the children.\n * @since 1.0.0\n */\nexport interface ThemeProviderProps extends ThemeOptions {\n /**\n * Stops CSS transitions for one frame while the theme changes, so colors flip at once instead\n * of fading piecemeal.\n * @default false\n */\n readonly disableTransitionOnChange?: boolean | undefined;\n /** The app to render inside the theme context. */\n readonly children?: JsxChild | undefined;\n}\n\nconst MISSING: ThemeContextValue = {\n theme: () => \"system\",\n resolvedTheme: () => undefined,\n systemTheme: () => undefined,\n setTheme: () => undefined,\n};\n\nconst ThemeContext = createContext<ThemeContextValue>(MISSING, { name: \"ShadowUITheme\" });\n\nconst DARK_QUERY = \"(prefers-color-scheme: dark)\";\n\n/** Disables transitions until the next frame; returns nothing, cleans itself up. */\nfunction suppressTransitions(): void {\n const style = document.createElement(\"style\");\n style.textContent =\n \"*,*::before,*::after{transition:none!important;animation-duration:0s!important}\";\n document.head.append(style);\n // Reading a computed style forces the browser to apply the change with transitions off.\n window.getComputedStyle(document.body).getPropertyValue(\"opacity\");\n window.setTimeout(() => style.remove(), 1);\n}\n\n/**\n * The page's `localStorage`, or `undefined` when the browser blocks it: reading the property\n * itself throws a `DOMException` in some sandboxed and locked-down contexts.\n */\nfunction localStorageOrUndefined(): Storage | undefined {\n try {\n return window.localStorage;\n } catch (error) {\n if (error instanceof DOMException) return undefined;\n throw error;\n }\n}\n\n/**\n * Provides the color theme to the app: `\"light\"`, `\"dark\"` or `\"system\"`. The choice is persisted in\n * `localStorage`, applied to `<html>` as a `light` or `dark` class (or a `data-*` attribute), and kept\n * in sync across tabs and with the operating system. The provider renders no element of its own, and\n * nothing touches the DOM until it has mounted, so it is safe to render on the server. Pair it with\n * {@link themeScript} in the document head so the first paint already has the right theme.\n *\n * @param props The {@link ThemeProviderProps}: theme options (`storageKey`, `defaultTheme`,\n * `attribute`, `enableSystem`), `disableTransitionOnChange` and `children`.\n * @returns The children inside the theme context.\n * @example\n * import { ThemeProvider } from \"@shadow-js/ui\";\n *\n * function App() {\n * return <p>Hello</p>;\n * }\n *\n * export const root = (\n * <ThemeProvider defaultTheme=\"system\">\n * <App />\n * </ThemeProvider>\n * );\n * @see {@link useTheme}\n * @since 1.0.0\n */\nexport function ThemeProvider(props: ThemeProviderProps): JsxChild {\n const storageKey = (): string => props.storageKey ?? DEFAULT_STORAGE_KEY;\n const defaultTheme = (): Theme => props.defaultTheme ?? \"system\";\n const attribute = (): \"class\" | `data-${string}` => props.attribute ?? \"class\";\n const enableSystem = (): boolean => props.enableSystem ?? true;\n\n const [theme, setThemeState] = useSignal<Theme>(untrack(defaultTheme));\n const [systemDark, setSystemDark] = useSignal<boolean | undefined>(undefined);\n const resolved = useMemo((): ResolvedTheme | undefined => {\n const dark = systemDark();\n if (dark === undefined) return undefined;\n return resolveTheme(theme(), dark, enableSystem());\n });\n const system = useMemo((): ResolvedTheme | undefined => {\n const dark = systemDark();\n return dark === undefined ? undefined : dark ? \"dark\" : \"light\";\n });\n\n onMount(() => {\n const query = window.matchMedia(DARK_QUERY);\n batch(() => {\n const storage = localStorageOrUndefined();\n const stored = storage === undefined ? undefined : readStoredTheme(storage, storageKey());\n if (stored !== undefined) setThemeState(stored);\n setSystemDark(query.matches);\n });\n const onSystemChange = (event: MediaQueryListEvent): void => {\n setSystemDark(event.matches);\n };\n const onStorage = (event: StorageEvent): void => {\n if (event.key !== storageKey()) return;\n setThemeState(isTheme(event.newValue) ? event.newValue : defaultTheme());\n };\n query.addEventListener(\"change\", onSystemChange);\n window.addEventListener(\"storage\", onStorage);\n onCleanup(() => {\n query.removeEventListener(\"change\", onSystemChange);\n window.removeEventListener(\"storage\", onStorage);\n });\n });\n\n useEffect(() => {\n const value = resolved();\n if (value === undefined) return;\n const attributeName = attribute();\n const current = document.documentElement;\n if (props.disableTransitionOnChange === true) suppressTransitions();\n applyTheme(current, value, attributeName);\n });\n\n const value: ThemeContextValue = {\n theme,\n resolvedTheme: resolved,\n systemTheme: system,\n setTheme(next) {\n const chosen = typeof next === \"function\" ? next(untrack(theme)) : next;\n if (!isTheme(chosen)) {\n throw new TypeError(\n `setTheme: expected \"light\", \"dark\" or \"system\", received ${String(chosen)}`,\n );\n }\n setThemeState(chosen);\n const storage = localStorageOrUndefined();\n if (storage !== undefined) writeStoredTheme(storage, untrack(storageKey), chosen);\n },\n };\n\n return ThemeContext.Provider({\n value,\n get children() {\n return props.children;\n },\n }) as JsxChild;\n}\n\n/**\n * Reads the theme and its setter. Call it inside a component rendered under {@link ThemeProvider}.\n *\n * @returns The {@link ThemeContextValue}: accessors for the chosen, the resolved and the system\n * theme, and `setTheme`.\n * @throws Error when no `ThemeProvider` is above the caller.\n * @example\n * import { useTheme } from \"@shadow-js/ui\";\n *\n * export function ThemeToggle() {\n * const { resolvedTheme, setTheme } = useTheme();\n * return (\n * <button onClick={() => setTheme(resolvedTheme() === \"dark\" ? \"light\" : \"dark\")}>\n * Toggle theme\n * </button>\n * );\n * }\n * @see {@link ThemeProvider}\n * @since 1.0.0\n */\nexport function useTheme(): ThemeContextValue {\n const value = useContext(ThemeContext);\n if (value === MISSING) throw new Error(\"useTheme() must be used within <ThemeProvider>\");\n return value;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"theme-provider.js","names":[],"sources":["../../../src/theme/theme-provider.ts"],"sourcesContent":["import {\n type Accessor,\n batch,\n createContext,\n onCleanup,\n onMount,\n untrack,\n useContext,\n useEffect,\n useMemo,\n useSignal,\n} from \"@shadow-js/core\";\nimport type { JsxChild } from \"@shadow-js/web/jsx-runtime\";\nimport {\n applyTheme,\n DEFAULT_STORAGE_KEY,\n isTheme,\n type ResolvedTheme,\n readStoredTheme,\n resolveTheme,\n type Theme,\n type ThemeOptions,\n writeStoredTheme,\n} from \"./theme-logic\";\n\n/**\n * What {@link useTheme} returns. Every value is an accessor: call it to read.\n * @since 1.0.0\n */\nexport interface ThemeContextValue {\n /** The user's choice: `\"light\"`, `\"dark\"` or `\"system\"`. Call it to read the value. */\n readonly theme: Accessor<Theme>;\n /**\n * The theme that is shown: `\"light\"` or `\"dark\"`. `undefined` on the server and until the provider\n * has mounted, so markup that depends on it hydrates without a mismatch.\n */\n readonly resolvedTheme: Accessor<ResolvedTheme | undefined>;\n /**\n * The operating system preference: `\"light\"` or `\"dark\"`. `undefined` on the server and until the\n * provider has mounted.\n */\n readonly systemTheme: Accessor<ResolvedTheme | undefined>;\n /**\n * Chooses a theme, or derives one from the previous choice, and stores it under the provider's\n * `storageKey`. Throws a `TypeError` for a value that is not `\"light\"`, `\"dark\"` or `\"system\"`.\n */\n readonly setTheme: (theme: Theme | ((previous: Theme) => Theme)) => void;\n}\n\n/**\n * Props of {@link ThemeProvider}: the {@link ThemeOptions} plus a transition switch and the children.\n * @since 1.0.0\n */\nexport interface ThemeProviderProps extends ThemeOptions {\n /**\n * Stops CSS transitions for one frame while the theme changes, so colors flip at once instead\n * of fading piecemeal.\n * @default false\n */\n readonly disableTransitionOnChange?: boolean | undefined;\n /** The app to render inside the theme context. */\n readonly children?: JsxChild | undefined;\n}\n\nconst MISSING: ThemeContextValue = {\n theme: () => \"system\",\n resolvedTheme: () => undefined,\n systemTheme: () => undefined,\n setTheme: () => undefined,\n};\n\nconst ThemeContext = createContext<ThemeContextValue>(MISSING, { name: \"ShadowUITheme\" });\n\nconst DARK_QUERY = \"(prefers-color-scheme: dark)\";\n\n/** Disables transitions until the next frame; returns nothing, cleans itself up. */\nfunction suppressTransitions(): void {\n const style = document.createElement(\"style\");\n style.textContent =\n \"*,*::before,*::after{transition:none!important;animation-duration:0s!important}\";\n document.head.append(style);\n // Reading a computed style forces the browser to apply the change with transitions off.\n window.getComputedStyle(document.body).getPropertyValue(\"opacity\");\n window.setTimeout(() => style.remove(), 1);\n}\n\n/**\n * The page's `localStorage`, or `undefined` when the browser blocks it: reading the property\n * itself throws a `DOMException` in some sandboxed and locked-down contexts.\n */\nfunction localStorageOrUndefined(): Storage | undefined {\n try {\n return window.localStorage;\n } catch (error) {\n if (error instanceof DOMException) return undefined;\n throw error;\n }\n}\n\n/**\n * Provides the color theme to the app: `\"light\"`, `\"dark\"` or `\"system\"`. The choice is persisted in\n * `localStorage`, applied to `<html>` as a `light` or `dark` class (or a `data-*` attribute), and kept\n * in sync across tabs and with the operating system. The provider renders no element of its own, and\n * nothing touches the DOM until it has mounted, so it is safe to render on the server. Pair it with\n * {@link themeScript} in the document head so the first paint already has the right theme.\n *\n * @param props The {@link ThemeProviderProps}: theme options (`storageKey`, `defaultTheme`,\n * `attribute`, `enableSystem`), `disableTransitionOnChange` and `children`.\n * @returns The children inside the theme context.\n * @example\n * import { ThemeProvider } from \"@shadow-js/ui\";\n *\n * function App() {\n * return <p>Hello</p>;\n * }\n *\n * export const root = (\n * <ThemeProvider defaultTheme=\"system\">\n * <App />\n * </ThemeProvider>\n * );\n * @see {@link useTheme}\n * @since 1.0.0\n */\nexport function ThemeProvider(props: ThemeProviderProps): JsxChild {\n const storageKey = (): string => props.storageKey ?? DEFAULT_STORAGE_KEY;\n const defaultTheme = (): Theme => props.defaultTheme ?? \"system\";\n const attribute = (): \"class\" | `data-${string}` => props.attribute ?? \"class\";\n const enableSystem = (): boolean => props.enableSystem ?? true;\n\n const [theme, setThemeState] = useSignal<Theme>(untrack(defaultTheme));\n const [systemDark, setSystemDark] = useSignal<boolean | undefined>(undefined);\n const resolved = useMemo((): ResolvedTheme | undefined => {\n const dark = systemDark();\n if (dark === undefined) return undefined;\n return resolveTheme(theme(), dark, enableSystem());\n });\n const system = useMemo((): ResolvedTheme | undefined => {\n const dark = systemDark();\n return dark === undefined ? undefined : dark ? \"dark\" : \"light\";\n });\n\n onMount(() => {\n const query = window.matchMedia(DARK_QUERY);\n batch(() => {\n const storage = localStorageOrUndefined();\n const stored = storage === undefined ? undefined : readStoredTheme(storage, storageKey());\n if (stored !== undefined) setThemeState(stored);\n setSystemDark(query.matches);\n });\n const onSystemChange = (event: MediaQueryListEvent): void => {\n setSystemDark(event.matches);\n };\n const onStorage = (event: StorageEvent): void => {\n // A null key is `storage.clear()`: the stored theme is gone, like a removed key.\n if (event.key !== null && event.key !== storageKey()) return;\n setThemeState(isTheme(event.newValue) ? event.newValue : defaultTheme());\n };\n query.addEventListener(\"change\", onSystemChange);\n window.addEventListener(\"storage\", onStorage);\n onCleanup(() => {\n query.removeEventListener(\"change\", onSystemChange);\n window.removeEventListener(\"storage\", onStorage);\n });\n });\n\n useEffect(() => {\n const value = resolved();\n if (value === undefined) return;\n const attributeName = attribute();\n const current = document.documentElement;\n if (props.disableTransitionOnChange === true) suppressTransitions();\n applyTheme(current, value, attributeName);\n });\n\n const value: ThemeContextValue = {\n theme,\n resolvedTheme: resolved,\n systemTheme: system,\n setTheme(next) {\n const chosen = typeof next === \"function\" ? next(untrack(theme)) : next;\n if (!isTheme(chosen)) {\n throw new TypeError(\n `setTheme: expected \"light\", \"dark\" or \"system\", received ${String(chosen)}`,\n );\n }\n setThemeState(chosen);\n const storage = localStorageOrUndefined();\n if (storage !== undefined) writeStoredTheme(storage, untrack(storageKey), chosen);\n },\n };\n\n return ThemeContext.Provider({\n value,\n get children() {\n return props.children;\n },\n }) as JsxChild;\n}\n\n/**\n * Reads the theme and its setter. Call it inside a component rendered under {@link ThemeProvider}.\n *\n * @returns The {@link ThemeContextValue}: accessors for the chosen, the resolved and the system\n * theme, and `setTheme`.\n * @throws Error when no `ThemeProvider` is above the caller.\n * @example\n * import { useTheme } from \"@shadow-js/ui\";\n *\n * export function ThemeToggle() {\n * const { resolvedTheme, setTheme } = useTheme();\n * return (\n * <button onClick={() => setTheme(resolvedTheme() === \"dark\" ? \"light\" : \"dark\")}>\n * Toggle theme\n * </button>\n * );\n * }\n * @see {@link ThemeProvider}\n * @since 1.0.0\n */\nexport function useTheme(): ThemeContextValue {\n const value = useContext(ThemeContext);\n if (value === MISSING) throw new Error(\"useTheme() must be used within <ThemeProvider>\");\n return value;\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{applyTheme as e,isTheme as t,readStoredTheme as n,resolveTheme as r,writeStoredTheme as i}from"./theme-logic.js";import{batch as a,createContext as o,onCleanup as s,onMount as c,untrack as l,useContext as u,useEffect as d,useMemo as f,useSignal as p}from"@shadow-js/core";const m={theme:()=>`system`,resolvedTheme:()=>void 0,systemTheme:()=>void 0,setTheme:()=>void 0},h=o(m,{name:`ShadowUITheme`});function g(){let e=document.createElement(`style`);e.textContent=`*,*::before,*::after{transition:none!important;animation-duration:0s!important}`,document.head.append(e),window.getComputedStyle(document.body).getPropertyValue(`opacity`),window.setTimeout(()=>e.remove(),1)}function _(){try{return window.localStorage}catch(e){if(e instanceof DOMException)return;throw e}}function v(o){let u=()=>o.storageKey??`shadow-ui-theme`,m=()=>o.defaultTheme??`system`,v=()=>o.attribute??`class`,y=()=>o.enableSystem??!0,[b,x]=p(l(m)),[S,C]=p(void 0),w=f(()=>{let e=S();if(e!==void 0)return r(b(),e,y())}),T=f(()=>{let e=S();return e===void 0?void 0:e?`dark`:`light`});c(()=>{let e=window.matchMedia(`(prefers-color-scheme: dark)`);a(()=>{let t=_(),r=t===void 0?void 0:n(t,u());r!==void 0&&x(r),C(e.matches)});let r=e=>{C(e.matches)},i=e=>{e.key===u()&&x(t(e.newValue)?e.newValue:m())};e.addEventListener(`change`,r),window.addEventListener(`storage`,i),s(()=>{e.removeEventListener(`change`,r),window.removeEventListener(`storage`,i)})}),d(()=>{let t=w();if(t===void 0)return;let n=v(),r=document.documentElement;o.disableTransitionOnChange===!0&&g(),e(r,t,n)});let E={theme:b,resolvedTheme:w,systemTheme:T,setTheme(e){let n=typeof e==`function`?e(l(b)):e;if(!t(n))throw TypeError(`setTheme: expected "light", "dark" or "system", received ${String(n)}`);x(n);let r=_();r!==void 0&&i(r,l(u),n)}};return h.Provider({value:E,get children(){return o.children}})}function y(){let e=u(h);if(e===m)throw Error(`useTheme() must be used within <ThemeProvider>`);return e}export{v as ThemeProvider,y as useTheme};
|
|
1
|
+
import{applyTheme as e,isTheme as t,readStoredTheme as n,resolveTheme as r,writeStoredTheme as i}from"./theme-logic.js";import{batch as a,createContext as o,onCleanup as s,onMount as c,untrack as l,useContext as u,useEffect as d,useMemo as f,useSignal as p}from"@shadow-js/core";const m={theme:()=>`system`,resolvedTheme:()=>void 0,systemTheme:()=>void 0,setTheme:()=>void 0},h=o(m,{name:`ShadowUITheme`});function g(){let e=document.createElement(`style`);e.textContent=`*,*::before,*::after{transition:none!important;animation-duration:0s!important}`,document.head.append(e),window.getComputedStyle(document.body).getPropertyValue(`opacity`),window.setTimeout(()=>e.remove(),1)}function _(){try{return window.localStorage}catch(e){if(e instanceof DOMException)return;throw e}}function v(o){let u=()=>o.storageKey??`shadow-ui-theme`,m=()=>o.defaultTheme??`system`,v=()=>o.attribute??`class`,y=()=>o.enableSystem??!0,[b,x]=p(l(m)),[S,C]=p(void 0),w=f(()=>{let e=S();if(e!==void 0)return r(b(),e,y())}),T=f(()=>{let e=S();return e===void 0?void 0:e?`dark`:`light`});c(()=>{let e=window.matchMedia(`(prefers-color-scheme: dark)`);a(()=>{let t=_(),r=t===void 0?void 0:n(t,u());r!==void 0&&x(r),C(e.matches)});let r=e=>{C(e.matches)},i=e=>{(e.key===null||e.key===u())&&x(t(e.newValue)?e.newValue:m())};e.addEventListener(`change`,r),window.addEventListener(`storage`,i),s(()=>{e.removeEventListener(`change`,r),window.removeEventListener(`storage`,i)})}),d(()=>{let t=w();if(t===void 0)return;let n=v(),r=document.documentElement;o.disableTransitionOnChange===!0&&g(),e(r,t,n)});let E={theme:b,resolvedTheme:w,systemTheme:T,setTheme(e){let n=typeof e==`function`?e(l(b)):e;if(!t(n))throw TypeError(`setTheme: expected "light", "dark" or "system", received ${String(n)}`);x(n);let r=_();r!==void 0&&i(r,l(u),n)}};return h.Provider({value:E,get children(){return o.children}})}function y(){let e=u(h);if(e===m)throw Error(`useTheme() must be used within <ThemeProvider>`);return e}export{v as ThemeProvider,y as useTheme};
|
|
2
2
|
//# sourceMappingURL=theme-provider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme-provider.js","names":["MISSING","ThemeContext","createContext","suppressTransitions","style","localStorageOrUndefined","error","ThemeProvider","storageKey","props","defaultTheme","attribute","enableSystem","theme","setThemeState","useSignal","untrack","systemDark","setSystemDark","resolved","useMemo","dark","resolveTheme","system","onMount","query","batch","storage","stored","readStoredTheme","onSystemChange","event","onStorage","isTheme","onCleanup","useEffect","value","attributeName","current","applyTheme","chosen","next","writeStoredTheme","useTheme","useContext"],"sources":["../../../src/theme/theme-provider.ts"],"sourcesContent":["import {\n type Accessor,\n batch,\n createContext,\n onCleanup,\n onMount,\n untrack,\n useContext,\n useEffect,\n useMemo,\n useSignal,\n} from \"@shadow-js/core\";\nimport type { JsxChild } from \"@shadow-js/web/jsx-runtime\";\nimport {\n applyTheme,\n DEFAULT_STORAGE_KEY,\n isTheme,\n type ResolvedTheme,\n readStoredTheme,\n resolveTheme,\n type Theme,\n type ThemeOptions,\n writeStoredTheme,\n} from \"./theme-logic\";\n\n/**\n * What {@link useTheme} returns. Every value is an accessor: call it to read.\n * @since 1.0.0\n */\nexport interface ThemeContextValue {\n /** The user's choice: `\"light\"`, `\"dark\"` or `\"system\"`. Call it to read the value. */\n readonly theme: Accessor<Theme>;\n /**\n * The theme that is shown: `\"light\"` or `\"dark\"`. `undefined` on the server and until the provider\n * has mounted, so markup that depends on it hydrates without a mismatch.\n */\n readonly resolvedTheme: Accessor<ResolvedTheme | undefined>;\n /**\n * The operating system preference: `\"light\"` or `\"dark\"`. `undefined` on the server and until the\n * provider has mounted.\n */\n readonly systemTheme: Accessor<ResolvedTheme | undefined>;\n /**\n * Chooses a theme, or derives one from the previous choice, and stores it under the provider's\n * `storageKey`. Throws a `TypeError` for a value that is not `\"light\"`, `\"dark\"` or `\"system\"`.\n */\n readonly setTheme: (theme: Theme | ((previous: Theme) => Theme)) => void;\n}\n\n/**\n * Props of {@link ThemeProvider}: the {@link ThemeOptions} plus a transition switch and the children.\n * @since 1.0.0\n */\nexport interface ThemeProviderProps extends ThemeOptions {\n /**\n * Stops CSS transitions for one frame while the theme changes, so colors flip at once instead\n * of fading piecemeal.\n * @default false\n */\n readonly disableTransitionOnChange?: boolean | undefined;\n /** The app to render inside the theme context. */\n readonly children?: JsxChild | undefined;\n}\n\nconst MISSING: ThemeContextValue = {\n theme: () => \"system\",\n resolvedTheme: () => undefined,\n systemTheme: () => undefined,\n setTheme: () => undefined,\n};\n\nconst ThemeContext = createContext<ThemeContextValue>(MISSING, { name: \"ShadowUITheme\" });\n\nconst DARK_QUERY = \"(prefers-color-scheme: dark)\";\n\n/** Disables transitions until the next frame; returns nothing, cleans itself up. */\nfunction suppressTransitions(): void {\n const style = document.createElement(\"style\");\n style.textContent =\n \"*,*::before,*::after{transition:none!important;animation-duration:0s!important}\";\n document.head.append(style);\n // Reading a computed style forces the browser to apply the change with transitions off.\n window.getComputedStyle(document.body).getPropertyValue(\"opacity\");\n window.setTimeout(() => style.remove(), 1);\n}\n\n/**\n * The page's `localStorage`, or `undefined` when the browser blocks it: reading the property\n * itself throws a `DOMException` in some sandboxed and locked-down contexts.\n */\nfunction localStorageOrUndefined(): Storage | undefined {\n try {\n return window.localStorage;\n } catch (error) {\n if (error instanceof DOMException) return undefined;\n throw error;\n }\n}\n\n/**\n * Provides the color theme to the app: `\"light\"`, `\"dark\"` or `\"system\"`. The choice is persisted in\n * `localStorage`, applied to `<html>` as a `light` or `dark` class (or a `data-*` attribute), and kept\n * in sync across tabs and with the operating system. The provider renders no element of its own, and\n * nothing touches the DOM until it has mounted, so it is safe to render on the server. Pair it with\n * {@link themeScript} in the document head so the first paint already has the right theme.\n *\n * @param props The {@link ThemeProviderProps}: theme options (`storageKey`, `defaultTheme`,\n * `attribute`, `enableSystem`), `disableTransitionOnChange` and `children`.\n * @returns The children inside the theme context.\n * @example\n * import { ThemeProvider } from \"@shadow-js/ui\";\n *\n * function App() {\n * return <p>Hello</p>;\n * }\n *\n * export const root = (\n * <ThemeProvider defaultTheme=\"system\">\n * <App />\n * </ThemeProvider>\n * );\n * @see {@link useTheme}\n * @since 1.0.0\n */\nexport function ThemeProvider(props: ThemeProviderProps): JsxChild {\n const storageKey = (): string => props.storageKey ?? DEFAULT_STORAGE_KEY;\n const defaultTheme = (): Theme => props.defaultTheme ?? \"system\";\n const attribute = (): \"class\" | `data-${string}` => props.attribute ?? \"class\";\n const enableSystem = (): boolean => props.enableSystem ?? true;\n\n const [theme, setThemeState] = useSignal<Theme>(untrack(defaultTheme));\n const [systemDark, setSystemDark] = useSignal<boolean | undefined>(undefined);\n const resolved = useMemo((): ResolvedTheme | undefined => {\n const dark = systemDark();\n if (dark === undefined) return undefined;\n return resolveTheme(theme(), dark, enableSystem());\n });\n const system = useMemo((): ResolvedTheme | undefined => {\n const dark = systemDark();\n return dark === undefined ? undefined : dark ? \"dark\" : \"light\";\n });\n\n onMount(() => {\n const query = window.matchMedia(DARK_QUERY);\n batch(() => {\n const storage = localStorageOrUndefined();\n const stored = storage === undefined ? undefined : readStoredTheme(storage, storageKey());\n if (stored !== undefined) setThemeState(stored);\n setSystemDark(query.matches);\n });\n const onSystemChange = (event: MediaQueryListEvent): void => {\n setSystemDark(event.matches);\n };\n const onStorage = (event: StorageEvent): void => {\n if (event.key !== storageKey()) return;\n setThemeState(isTheme(event.newValue) ? event.newValue : defaultTheme());\n };\n query.addEventListener(\"change\", onSystemChange);\n window.addEventListener(\"storage\", onStorage);\n onCleanup(() => {\n query.removeEventListener(\"change\", onSystemChange);\n window.removeEventListener(\"storage\", onStorage);\n });\n });\n\n useEffect(() => {\n const value = resolved();\n if (value === undefined) return;\n const attributeName = attribute();\n const current = document.documentElement;\n if (props.disableTransitionOnChange === true) suppressTransitions();\n applyTheme(current, value, attributeName);\n });\n\n const value: ThemeContextValue = {\n theme,\n resolvedTheme: resolved,\n systemTheme: system,\n setTheme(next) {\n const chosen = typeof next === \"function\" ? next(untrack(theme)) : next;\n if (!isTheme(chosen)) {\n throw new TypeError(\n `setTheme: expected \"light\", \"dark\" or \"system\", received ${String(chosen)}`,\n );\n }\n setThemeState(chosen);\n const storage = localStorageOrUndefined();\n if (storage !== undefined) writeStoredTheme(storage, untrack(storageKey), chosen);\n },\n };\n\n return ThemeContext.Provider({\n value,\n get children() {\n return props.children;\n },\n }) as JsxChild;\n}\n\n/**\n * Reads the theme and its setter. Call it inside a component rendered under {@link ThemeProvider}.\n *\n * @returns The {@link ThemeContextValue}: accessors for the chosen, the resolved and the system\n * theme, and `setTheme`.\n * @throws Error when no `ThemeProvider` is above the caller.\n * @example\n * import { useTheme } from \"@shadow-js/ui\";\n *\n * export function ThemeToggle() {\n * const { resolvedTheme, setTheme } = useTheme();\n * return (\n * <button onClick={() => setTheme(resolvedTheme() === \"dark\" ? \"light\" : \"dark\")}>\n * Toggle theme\n * </button>\n * );\n * }\n * @see {@link ThemeProvider}\n * @since 1.0.0\n */\nexport function useTheme(): ThemeContextValue {\n const value = useContext(ThemeContext);\n if (value === MISSING) throw new Error(\"useTheme() must be used within <ThemeProvider>\");\n return value;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"theme-provider.js","names":["MISSING","ThemeContext","createContext","suppressTransitions","style","localStorageOrUndefined","error","ThemeProvider","storageKey","props","defaultTheme","attribute","enableSystem","theme","setThemeState","useSignal","untrack","systemDark","setSystemDark","resolved","useMemo","dark","resolveTheme","system","onMount","query","batch","storage","stored","readStoredTheme","onSystemChange","event","onStorage","isTheme","onCleanup","useEffect","value","attributeName","current","applyTheme","chosen","next","writeStoredTheme","useTheme","useContext"],"sources":["../../../src/theme/theme-provider.ts"],"sourcesContent":["import {\n type Accessor,\n batch,\n createContext,\n onCleanup,\n onMount,\n untrack,\n useContext,\n useEffect,\n useMemo,\n useSignal,\n} from \"@shadow-js/core\";\nimport type { JsxChild } from \"@shadow-js/web/jsx-runtime\";\nimport {\n applyTheme,\n DEFAULT_STORAGE_KEY,\n isTheme,\n type ResolvedTheme,\n readStoredTheme,\n resolveTheme,\n type Theme,\n type ThemeOptions,\n writeStoredTheme,\n} from \"./theme-logic\";\n\n/**\n * What {@link useTheme} returns. Every value is an accessor: call it to read.\n * @since 1.0.0\n */\nexport interface ThemeContextValue {\n /** The user's choice: `\"light\"`, `\"dark\"` or `\"system\"`. Call it to read the value. */\n readonly theme: Accessor<Theme>;\n /**\n * The theme that is shown: `\"light\"` or `\"dark\"`. `undefined` on the server and until the provider\n * has mounted, so markup that depends on it hydrates without a mismatch.\n */\n readonly resolvedTheme: Accessor<ResolvedTheme | undefined>;\n /**\n * The operating system preference: `\"light\"` or `\"dark\"`. `undefined` on the server and until the\n * provider has mounted.\n */\n readonly systemTheme: Accessor<ResolvedTheme | undefined>;\n /**\n * Chooses a theme, or derives one from the previous choice, and stores it under the provider's\n * `storageKey`. Throws a `TypeError` for a value that is not `\"light\"`, `\"dark\"` or `\"system\"`.\n */\n readonly setTheme: (theme: Theme | ((previous: Theme) => Theme)) => void;\n}\n\n/**\n * Props of {@link ThemeProvider}: the {@link ThemeOptions} plus a transition switch and the children.\n * @since 1.0.0\n */\nexport interface ThemeProviderProps extends ThemeOptions {\n /**\n * Stops CSS transitions for one frame while the theme changes, so colors flip at once instead\n * of fading piecemeal.\n * @default false\n */\n readonly disableTransitionOnChange?: boolean | undefined;\n /** The app to render inside the theme context. */\n readonly children?: JsxChild | undefined;\n}\n\nconst MISSING: ThemeContextValue = {\n theme: () => \"system\",\n resolvedTheme: () => undefined,\n systemTheme: () => undefined,\n setTheme: () => undefined,\n};\n\nconst ThemeContext = createContext<ThemeContextValue>(MISSING, { name: \"ShadowUITheme\" });\n\nconst DARK_QUERY = \"(prefers-color-scheme: dark)\";\n\n/** Disables transitions until the next frame; returns nothing, cleans itself up. */\nfunction suppressTransitions(): void {\n const style = document.createElement(\"style\");\n style.textContent =\n \"*,*::before,*::after{transition:none!important;animation-duration:0s!important}\";\n document.head.append(style);\n // Reading a computed style forces the browser to apply the change with transitions off.\n window.getComputedStyle(document.body).getPropertyValue(\"opacity\");\n window.setTimeout(() => style.remove(), 1);\n}\n\n/**\n * The page's `localStorage`, or `undefined` when the browser blocks it: reading the property\n * itself throws a `DOMException` in some sandboxed and locked-down contexts.\n */\nfunction localStorageOrUndefined(): Storage | undefined {\n try {\n return window.localStorage;\n } catch (error) {\n if (error instanceof DOMException) return undefined;\n throw error;\n }\n}\n\n/**\n * Provides the color theme to the app: `\"light\"`, `\"dark\"` or `\"system\"`. The choice is persisted in\n * `localStorage`, applied to `<html>` as a `light` or `dark` class (or a `data-*` attribute), and kept\n * in sync across tabs and with the operating system. The provider renders no element of its own, and\n * nothing touches the DOM until it has mounted, so it is safe to render on the server. Pair it with\n * {@link themeScript} in the document head so the first paint already has the right theme.\n *\n * @param props The {@link ThemeProviderProps}: theme options (`storageKey`, `defaultTheme`,\n * `attribute`, `enableSystem`), `disableTransitionOnChange` and `children`.\n * @returns The children inside the theme context.\n * @example\n * import { ThemeProvider } from \"@shadow-js/ui\";\n *\n * function App() {\n * return <p>Hello</p>;\n * }\n *\n * export const root = (\n * <ThemeProvider defaultTheme=\"system\">\n * <App />\n * </ThemeProvider>\n * );\n * @see {@link useTheme}\n * @since 1.0.0\n */\nexport function ThemeProvider(props: ThemeProviderProps): JsxChild {\n const storageKey = (): string => props.storageKey ?? DEFAULT_STORAGE_KEY;\n const defaultTheme = (): Theme => props.defaultTheme ?? \"system\";\n const attribute = (): \"class\" | `data-${string}` => props.attribute ?? \"class\";\n const enableSystem = (): boolean => props.enableSystem ?? true;\n\n const [theme, setThemeState] = useSignal<Theme>(untrack(defaultTheme));\n const [systemDark, setSystemDark] = useSignal<boolean | undefined>(undefined);\n const resolved = useMemo((): ResolvedTheme | undefined => {\n const dark = systemDark();\n if (dark === undefined) return undefined;\n return resolveTheme(theme(), dark, enableSystem());\n });\n const system = useMemo((): ResolvedTheme | undefined => {\n const dark = systemDark();\n return dark === undefined ? undefined : dark ? \"dark\" : \"light\";\n });\n\n onMount(() => {\n const query = window.matchMedia(DARK_QUERY);\n batch(() => {\n const storage = localStorageOrUndefined();\n const stored = storage === undefined ? undefined : readStoredTheme(storage, storageKey());\n if (stored !== undefined) setThemeState(stored);\n setSystemDark(query.matches);\n });\n const onSystemChange = (event: MediaQueryListEvent): void => {\n setSystemDark(event.matches);\n };\n const onStorage = (event: StorageEvent): void => {\n // A null key is `storage.clear()`: the stored theme is gone, like a removed key.\n if (event.key !== null && event.key !== storageKey()) return;\n setThemeState(isTheme(event.newValue) ? event.newValue : defaultTheme());\n };\n query.addEventListener(\"change\", onSystemChange);\n window.addEventListener(\"storage\", onStorage);\n onCleanup(() => {\n query.removeEventListener(\"change\", onSystemChange);\n window.removeEventListener(\"storage\", onStorage);\n });\n });\n\n useEffect(() => {\n const value = resolved();\n if (value === undefined) return;\n const attributeName = attribute();\n const current = document.documentElement;\n if (props.disableTransitionOnChange === true) suppressTransitions();\n applyTheme(current, value, attributeName);\n });\n\n const value: ThemeContextValue = {\n theme,\n resolvedTheme: resolved,\n systemTheme: system,\n setTheme(next) {\n const chosen = typeof next === \"function\" ? next(untrack(theme)) : next;\n if (!isTheme(chosen)) {\n throw new TypeError(\n `setTheme: expected \"light\", \"dark\" or \"system\", received ${String(chosen)}`,\n );\n }\n setThemeState(chosen);\n const storage = localStorageOrUndefined();\n if (storage !== undefined) writeStoredTheme(storage, untrack(storageKey), chosen);\n },\n };\n\n return ThemeContext.Provider({\n value,\n get children() {\n return props.children;\n },\n }) as JsxChild;\n}\n\n/**\n * Reads the theme and its setter. Call it inside a component rendered under {@link ThemeProvider}.\n *\n * @returns The {@link ThemeContextValue}: accessors for the chosen, the resolved and the system\n * theme, and `setTheme`.\n * @throws Error when no `ThemeProvider` is above the caller.\n * @example\n * import { useTheme } from \"@shadow-js/ui\";\n *\n * export function ThemeToggle() {\n * const { resolvedTheme, setTheme } = useTheme();\n * return (\n * <button onClick={() => setTheme(resolvedTheme() === \"dark\" ? \"light\" : \"dark\")}>\n * Toggle theme\n * </button>\n * );\n * }\n * @see {@link ThemeProvider}\n * @since 1.0.0\n */\nexport function useTheme(): ThemeContextValue {\n const value = useContext(ThemeContext);\n if (value === MISSING) throw new Error(\"useTheme() must be used within <ThemeProvider>\");\n return value;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shadow-js/ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "shadcn-style components you add to your project with a CLI, styled with Tailwind v4",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -60,18 +60,18 @@
|
|
|
60
60
|
"check": "npm run lint && npm run typecheck && npm run build && npm test && npm run test:perf && npm run test:gc"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
|
-
"@shadow-js/core": "^1.0.
|
|
64
|
-
"@shadow-js/primitives": "^1.0.
|
|
65
|
-
"@shadow-js/web": "^1.0.
|
|
63
|
+
"@shadow-js/core": "^1.0.3",
|
|
64
|
+
"@shadow-js/primitives": "^1.0.3",
|
|
65
|
+
"@shadow-js/web": "^1.0.3"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@biomejs/biome": "2.5.15",
|
|
69
|
-
"@shadow-js/compiler": "^1.0.
|
|
70
|
-
"@shadow-js/core": "^1.0.
|
|
71
|
-
"@shadow-js/forms": "^1.0.
|
|
72
|
-
"@shadow-js/primitives": "^1.0.
|
|
73
|
-
"@shadow-js/testing": "^1.0.
|
|
74
|
-
"@shadow-js/web": "^1.0.
|
|
69
|
+
"@shadow-js/compiler": "^1.0.3",
|
|
70
|
+
"@shadow-js/core": "^1.0.3",
|
|
71
|
+
"@shadow-js/forms": "^1.0.3",
|
|
72
|
+
"@shadow-js/primitives": "^1.0.3",
|
|
73
|
+
"@shadow-js/testing": "^1.0.3",
|
|
74
|
+
"@shadow-js/web": "^1.0.3",
|
|
75
75
|
"@tailwindcss/node": "4.3.3",
|
|
76
76
|
"@types/node": "^24.0.0",
|
|
77
77
|
"fast-check": "^4.10.2",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"vitest": "5.0.3"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@shadow-js/table": "1.0.
|
|
87
|
+
"@shadow-js/table": "1.0.3",
|
|
88
88
|
"class-variance-authority": "0.7.1",
|
|
89
89
|
"clsx": "2.1.1",
|
|
90
90
|
"tailwind-merge": "3.7.0"
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { onCleanup, onMount, splitProps, useEffect, useSignal } from "@shadow-js/core";
|
|
2
2
|
import {
|
|
3
|
+
callHandler,
|
|
3
4
|
createPartContext,
|
|
4
5
|
type Direction,
|
|
6
|
+
mergeElementProps,
|
|
5
7
|
useDirectionAround,
|
|
6
8
|
useRefSignal,
|
|
7
9
|
} from "@shadow-js/primitives";
|
|
@@ -99,6 +101,7 @@ interface CarouselContextValue {
|
|
|
99
101
|
readonly setViewport: (element: HTMLElement | undefined) => void;
|
|
100
102
|
readonly claimSlide: () => number;
|
|
101
103
|
readonly registerCount: () => void;
|
|
104
|
+
readonly releaseSlide: () => void;
|
|
102
105
|
readonly pause: (paused: boolean) => void;
|
|
103
106
|
readonly labels: () => ResolvedLabels;
|
|
104
107
|
/** True once the carousel is mounted in the browser; the slide names wait for it (see CarouselItem). */
|
|
@@ -306,6 +309,12 @@ function Carousel(props: CarouselProps): JSX.Element {
|
|
|
306
309
|
const [ready, setReady] = useSignal(false);
|
|
307
310
|
let viewport: HTMLElement | undefined;
|
|
308
311
|
let claimed = 0;
|
|
312
|
+
// The slide count the "reInit" listeners were last told, and whether the carousel is gone.
|
|
313
|
+
let reported = 0;
|
|
314
|
+
let disposed = false;
|
|
315
|
+
onCleanup(() => {
|
|
316
|
+
disposed = true;
|
|
317
|
+
});
|
|
309
318
|
// The slide a programmatic move is heading for, until the scroll arrives there. While it is set,
|
|
310
319
|
// the intermediate positions of a smooth scroll do not move the index back.
|
|
311
320
|
let pending: number | undefined;
|
|
@@ -376,6 +385,27 @@ function Carousel(props: CarouselProps): JSX.Element {
|
|
|
376
385
|
if (typeof viewport.scrollTo !== "function") viewport.scrollLeft = position * sign;
|
|
377
386
|
}
|
|
378
387
|
};
|
|
388
|
+
/** Tells the "reInit" listeners when the slide count differs from what they last heard. */
|
|
389
|
+
const announceCount = (): void => {
|
|
390
|
+
if (!ready()) return;
|
|
391
|
+
const total = count();
|
|
392
|
+
if (total === reported) return;
|
|
393
|
+
reported = total;
|
|
394
|
+
// Slides were removed: the current one may be gone.
|
|
395
|
+
if (total > 0 && index() > total - 1) setIndex(total - 1);
|
|
396
|
+
emit("reInit");
|
|
397
|
+
};
|
|
398
|
+
// A list that is replaced removes every slide and adds new ones in the same turn, and a carousel
|
|
399
|
+
// that unmounts removes them all: wait for the final count before telling anyone, once.
|
|
400
|
+
let announcing = false;
|
|
401
|
+
const scheduleAnnounce = (): void => {
|
|
402
|
+
if (!ready() || announcing) return;
|
|
403
|
+
announcing = true;
|
|
404
|
+
queueMicrotask(() => {
|
|
405
|
+
announcing = false;
|
|
406
|
+
if (!disposed) announceCount();
|
|
407
|
+
});
|
|
408
|
+
};
|
|
379
409
|
const canPrevious = (): boolean => loops() || index() > 0;
|
|
380
410
|
const canNext = (): boolean => loops() || index() < count() - 1;
|
|
381
411
|
|
|
@@ -402,9 +432,13 @@ function Carousel(props: CarouselProps): JSX.Element {
|
|
|
402
432
|
return slot;
|
|
403
433
|
},
|
|
404
434
|
registerCount() {
|
|
405
|
-
const changed = claimed !== count();
|
|
406
435
|
setCount(claimed);
|
|
407
|
-
|
|
436
|
+
scheduleAnnounce();
|
|
437
|
+
},
|
|
438
|
+
releaseSlide() {
|
|
439
|
+
claimed = Math.max(0, claimed - 1);
|
|
440
|
+
setCount(claimed);
|
|
441
|
+
scheduleAnnounce();
|
|
408
442
|
},
|
|
409
443
|
pause: (value) => setPaused(value),
|
|
410
444
|
labels,
|
|
@@ -436,6 +470,7 @@ function Carousel(props: CarouselProps): JSX.Element {
|
|
|
436
470
|
onMount(() => {
|
|
437
471
|
setRegionElement(regionNode);
|
|
438
472
|
setReady(true);
|
|
473
|
+
reported = count();
|
|
439
474
|
const start = opts().startIndex;
|
|
440
475
|
if (start !== undefined && start > 0) scrollToIndex(start, true);
|
|
441
476
|
(local.setApi as ((api: CarouselApi) => void) | undefined)?.(api);
|
|
@@ -445,7 +480,8 @@ function Carousel(props: CarouselProps): JSX.Element {
|
|
|
445
480
|
// `autoplay` prop: changing it retimes the interval, and removing it stops the interval.
|
|
446
481
|
useEffect(() => {
|
|
447
482
|
const interval = local.autoplay as number | undefined;
|
|
448
|
-
|
|
483
|
+
// NaN and Infinity must not reach setInterval: a NaN delay fires every tick and an Infinity one wraps to 0.
|
|
484
|
+
if (interval === undefined || !Number.isFinite(interval) || interval <= 0) return undefined;
|
|
449
485
|
if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches)
|
|
450
486
|
return undefined;
|
|
451
487
|
const timer = setInterval(() => {
|
|
@@ -489,12 +525,24 @@ function Carousel(props: CarouselProps): JSX.Element {
|
|
|
489
525
|
class={cn("relative", local.class)}
|
|
490
526
|
onKeyDown={(event: KeyboardEvent) => {
|
|
491
527
|
onKeyDown(event);
|
|
492
|
-
(local.onKeyDown
|
|
528
|
+
callHandler(local.onKeyDown, event);
|
|
529
|
+
}}
|
|
530
|
+
onPointerEnter={(event: PointerEvent) => {
|
|
531
|
+
callHandler(local.onPointerEnter, event);
|
|
532
|
+
setPaused(true);
|
|
533
|
+
}}
|
|
534
|
+
onPointerLeave={(event: PointerEvent) => {
|
|
535
|
+
callHandler(local.onPointerLeave, event);
|
|
536
|
+
setPaused(false);
|
|
537
|
+
}}
|
|
538
|
+
onFocusIn={(event: FocusEvent) => {
|
|
539
|
+
callHandler(local.onFocusIn, event);
|
|
540
|
+
setPaused(true);
|
|
541
|
+
}}
|
|
542
|
+
onFocusOut={(event: FocusEvent) => {
|
|
543
|
+
callHandler(local.onFocusOut, event);
|
|
544
|
+
setPaused(false);
|
|
493
545
|
}}
|
|
494
|
-
onPointerEnter={() => setPaused(true)}
|
|
495
|
-
onPointerLeave={() => setPaused(false)}
|
|
496
|
-
onFocusIn={() => setPaused(true)}
|
|
497
|
-
onFocusOut={() => setPaused(false)}
|
|
498
546
|
{...rest}
|
|
499
547
|
>
|
|
500
548
|
{local.children as JSX.Element}
|
|
@@ -551,15 +599,15 @@ function CarouselContent(props: ComponentProps<"div">): JSX.Element {
|
|
|
551
599
|
local.class,
|
|
552
600
|
)}
|
|
553
601
|
onScroll={(event: Event) => {
|
|
554
|
-
(local.onScroll
|
|
602
|
+
callHandler(local.onScroll, event);
|
|
555
603
|
carousel.scrolled();
|
|
556
604
|
}}
|
|
557
605
|
onWheel={(event: WheelEvent) => {
|
|
558
|
-
(local.onWheel
|
|
606
|
+
callHandler(local.onWheel, event);
|
|
559
607
|
carousel.userMoved();
|
|
560
608
|
}}
|
|
561
609
|
onPointerDown={(event: PointerEvent) => {
|
|
562
|
-
(local.onPointerDown
|
|
610
|
+
callHandler(local.onPointerDown, event);
|
|
563
611
|
carousel.userMoved();
|
|
564
612
|
if (event.button !== 0 || event.pointerType === "touch") return;
|
|
565
613
|
const element = event.currentTarget as HTMLElement;
|
|
@@ -630,12 +678,28 @@ function CarouselItem(props: ComponentProps<"div">): JSX.Element {
|
|
|
630
678
|
// before the next one is counted; so a slide is named only once the carousel is mounted, and
|
|
631
679
|
// the name then uses the final count (UI-CRS-18).
|
|
632
680
|
carousel.registerCount();
|
|
681
|
+
// A slide that goes away (a list that shrinks or is replaced) no longer counts.
|
|
682
|
+
onCleanup(() => carousel.releaseSlide());
|
|
683
|
+
let node: HTMLElement | undefined;
|
|
684
|
+
// The position among the slides that exist now, from the DOM order: the slot a slide claimed when
|
|
685
|
+
// it rendered is stale once another slide has been removed or inserted before it.
|
|
686
|
+
const position = (): number => {
|
|
687
|
+
carousel.count();
|
|
688
|
+
const parent = node?.parentElement;
|
|
689
|
+
if (node === undefined || parent === null || parent === undefined) return slot + 1;
|
|
690
|
+
const slides = Array.from(parent.children).filter(
|
|
691
|
+
(child) => child.getAttribute("data-slot") === "carousel-item",
|
|
692
|
+
);
|
|
693
|
+
const at = slides.indexOf(node);
|
|
694
|
+
return at === -1 ? slot + 1 : at + 1;
|
|
695
|
+
};
|
|
696
|
+
const merged = mergeElementProps({ ref: (element: HTMLElement) => (node = element) }, rest);
|
|
633
697
|
return (
|
|
634
698
|
<div
|
|
635
699
|
role="group"
|
|
636
700
|
aria-roledescription="slide"
|
|
637
701
|
aria-label={
|
|
638
|
-
carousel.ready() ? carousel.labels().slide(
|
|
702
|
+
carousel.ready() ? carousel.labels().slide(position(), carousel.count()) : undefined
|
|
639
703
|
}
|
|
640
704
|
data-slot="carousel-item"
|
|
641
705
|
class={cn(
|
|
@@ -643,7 +707,7 @@ function CarouselItem(props: ComponentProps<"div">): JSX.Element {
|
|
|
643
707
|
carousel.orientation() === "vertical" ? "pt-4" : "ps-4",
|
|
644
708
|
local.class,
|
|
645
709
|
)}
|
|
646
|
-
{...
|
|
710
|
+
{...merged}
|
|
647
711
|
/>
|
|
648
712
|
);
|
|
649
713
|
}
|
|
@@ -699,7 +763,7 @@ function CarouselPrevious(props: ComponentProps<"button">): JSX.Element {
|
|
|
699
763
|
)}
|
|
700
764
|
disabled={!carousel.canPrevious()}
|
|
701
765
|
onClick={(event: MouseEvent) => {
|
|
702
|
-
(local.onClick
|
|
766
|
+
callHandler(local.onClick, event);
|
|
703
767
|
carousel.previous();
|
|
704
768
|
}}
|
|
705
769
|
{...rest}
|
|
@@ -764,7 +828,7 @@ function CarouselNext(props: ComponentProps<"button">): JSX.Element {
|
|
|
764
828
|
)}
|
|
765
829
|
disabled={!carousel.canNext()}
|
|
766
830
|
onClick={(event: MouseEvent) => {
|
|
767
|
-
(local.onClick
|
|
831
|
+
callHandler(local.onClick, event);
|
|
768
832
|
carousel.next();
|
|
769
833
|
}}
|
|
770
834
|
{...rest}
|
package/registry/chart/chart.tsx
CHANGED
|
@@ -220,7 +220,11 @@ function ChartContainer(props: ChartContainerProps): JSX.Element {
|
|
|
220
220
|
"children",
|
|
221
221
|
]);
|
|
222
222
|
const uiLocale = useUiLocale();
|
|
223
|
-
|
|
223
|
+
// A NaN or Infinity width would put "NaN" into every path; fall back to the default instead.
|
|
224
|
+
const initial = local.initialWidth as number | undefined;
|
|
225
|
+
const [width, setWidth] = useSignal<number>(
|
|
226
|
+
initial !== undefined && Number.isFinite(initial) ? initial : 600,
|
|
227
|
+
);
|
|
224
228
|
const [element, setElement] = useRefSignal<HTMLElement>();
|
|
225
229
|
let node: HTMLElement | undefined;
|
|
226
230
|
const dir = useDirectionAround(() => local.dir as Direction | undefined, element);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { splitProps } from "@shadow-js/core";
|
|
2
|
+
import { callHandler } from "@shadow-js/primitives";
|
|
2
3
|
import type { ComponentProps, JSX } from "@shadow-js/web";
|
|
3
4
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
5
|
import { cn } from "@/lib/utils";
|
|
@@ -117,7 +118,7 @@ function InputGroupAddon(props: InputGroupAddonProps): JSX.Element {
|
|
|
117
118
|
const [local, rest] = splitProps(props, ["class", "align", "onClick"]);
|
|
118
119
|
// The application's own click handler runs first; the focus move is the default.
|
|
119
120
|
const onClick = (event: MouseEvent): void => {
|
|
120
|
-
(local.onClick
|
|
121
|
+
callHandler(local.onClick, event);
|
|
121
122
|
const target = event.target;
|
|
122
123
|
if (target instanceof Element && target.closest("button") !== null) return;
|
|
123
124
|
const group = (event.currentTarget as HTMLElement).closest('[data-slot="input-group"]');
|