@askrjs/themes 0.0.17 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/THEMING.md +8 -1
- package/dist/components/_internal/block-layout.js +1 -1
- package/dist/components/_internal/block-layout.js.map +1 -1
- package/dist/components/_internal/style.js +13 -22
- package/dist/components/_internal/style.js.map +1 -1
- package/dist/components/grid/grid.js +5 -42
- package/dist/components/grid/grid.js.map +1 -1
- package/dist/components/jsx-types.d.ts +0 -3
- package/dist/components/theme/theme.js +37 -15
- package/dist/components/theme/theme.js.map +1 -1
- package/dist/ssr.js +23 -11
- package/dist/ssr.js.map +1 -1
- package/dist/themes/default/index.css +8 -2
- package/package.json +6 -6
- package/src/components/_internal/block-layout.ts +2 -2
- package/src/components/_internal/style.ts +18 -29
- package/src/components/grid/grid.tsx +6 -57
- package/src/components/jsx-types.ts +0 -5
- package/src/components/theme/theme.tsx +64 -24
- package/src/ssr.ts +45 -20
- package/src/themes/default/styles/display/coverage.css +3 -1
- package/src/themes/default/styles/layout/block.css +4 -0
- package/src/themes/default/tokens.css +1 -1
- package/templates/theme/tokens.css +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.js","names":[],"sources":["../../../src/components/theme/theme.tsx"],"sourcesContent":["import type { JSX } from \"@askrjs/askr/jsx-runtime\";\nimport { defineScope, getSignal, readScope, state } from \"@askrjs/askr\";\nimport type { JSXElement } from \"@askrjs/askr/foundations/structures\";\nimport { Button } from \"@askrjs/ui\";\nimport type { ButtonNativeProps, PressEvent } from \"@askrjs/ui\";\n\nexport const CAT_THEME_NAMES = [\"tabby\", \"ginger\", \"tuxedo\", \"calico\", \"torty\"] as const;\n\nexport type CatThemeName = (typeof CAT_THEME_NAMES)[number];\nexport type ThemeName = \"light\" | \"dark\" | \"system\" | CatThemeName | (string & {});\n\nexport type ThemeOption = {\n value: ThemeName;\n label: string;\n};\n\nexport type ThemeScopeValue = {\n theme: () => ThemeName;\n resolvedSystemTheme: () => \"light\" | \"dark\";\n setTheme: (theme: ThemeName) => void;\n themes: readonly ThemeOption[];\n storageKey: string;\n};\n\nexport type ThemeScopeProps = {\n children?: unknown;\n defaultTheme?: ThemeName;\n themes?: readonly ThemeOption[];\n storageKey?: string;\n};\n\nexport type ThemePickerProps = Omit<\n JSX.IntrinsicElements[\"select\"],\n \"children\" | \"value\" | \"defaultValue\" | \"onChange\"\n> & {\n themes?: readonly ThemeOption[];\n label?: string;\n};\n\nexport type ThemeToggleRenderContext = {\n theme: ThemeName;\n nextTheme: ThemeName;\n};\n\nexport type ThemeToggleProps = Omit<ButtonNativeProps, \"children\" | \"onPress\"> & {\n children?: unknown | ((context: ThemeToggleRenderContext) => unknown);\n lightIcon?: unknown;\n darkIcon?: unknown;\n systemIcon?: unknown;\n themes?: readonly ThemeName[];\n onPress?: (event: PressEvent) => void;\n};\n\nexport const DEFAULT_THEME_OPTIONS: readonly ThemeOption[] = [\n { value: \"system\", label: \"System\" },\n { value: \"light\", label: \"Light\" },\n { value: \"dark\", label: \"Dark\" },\n];\n\nexport const CAT_THEME_OPTIONS: readonly ThemeOption[] = [\n { value: \"tabby\", label: \"Tabby\" },\n { value: \"ginger\", label: \"Ginger\" },\n { value: \"tuxedo\", label: \"Tuxedo\" },\n { value: \"calico\", label: \"Calico\" },\n { value: \"torty\", label: \"Torty\" },\n];\n\nconst DEFAULT_STORAGE_KEY = \"askr-theme\";\nconst STATIC_CHILDREN = Symbol.for(\"askr.static-children\");\nconst STATIC_CHILD_SLOTS_CACHE = Symbol.for(\"__askrStaticChildSlots\");\nconst documentThemeCoordinators = new WeakMap<Document, ThemeCoordinator>();\ntype ThemeCoordinator = ReturnType<typeof createThemeCoordinator>;\ntype InternalThemeScopeValue = ThemeScopeValue & {\n readonly coordinator: ThemeCoordinator | null;\n readonly depth: number;\n};\n\nconst ThemeScopeContext = defineScope<InternalThemeScopeValue>({\n theme: () => \"system\",\n resolvedSystemTheme: () => \"light\",\n setTheme: () => undefined,\n themes: DEFAULT_THEME_OPTIONS,\n storageKey: DEFAULT_STORAGE_KEY,\n coordinator: null,\n depth: -1,\n});\n\nexport function theme(): ThemeScopeValue {\n return readScope(ThemeScopeContext);\n}\n\nexport function ThemeScope(props: ThemeScopeProps): JSX.Element {\n const {\n children,\n defaultTheme = \"system\",\n themes = DEFAULT_THEME_OPTIONS,\n storageKey = DEFAULT_STORAGE_KEY,\n } = props;\n\n const scopeId = state<symbol>(Symbol(\"ThemeScope\"))();\n const scopeSignal = getSignal();\n // The first render must be identical on the server and in the browser.\n // Browser persistence is adopted from the committed root ref, after Askr's\n // hydration verifier has accepted the server markup.\n const themeState = state<ThemeName>(defaultTheme);\n const localResolvedSystemTheme = state<\"light\" | \"dark\">(\"light\");\n const persistenceAdoption = state({ complete: false })();\n const currentTheme = themeState();\n const parentScope = readScope(ThemeScopeContext);\n const ownedCoordinator = state<ThemeCoordinator>(getDefaultThemeCoordinator())();\n const coordinator = parentScope.coordinator ?? ownedCoordinator;\n const scopeDepth = parentScope.depth + 1;\n coordinator.register(scopeId, scopeDepth, currentTheme, scopeSignal);\n\n const setTheme = (nextTheme: ThemeName) => {\n themeState.set(nextTheme);\n writeStoredTheme(storageKey, nextTheme);\n coordinator.activate(scopeId, nextTheme);\n };\n\n const resolvedSystemTheme = parentScope.coordinator\n ? parentScope.resolvedSystemTheme\n : localResolvedSystemTheme;\n const value: InternalThemeScopeValue = {\n theme: themeState,\n resolvedSystemTheme,\n setTheme,\n themes,\n storageKey,\n coordinator,\n depth: scopeDepth,\n };\n\n return (\n <ThemeScopeContext value={value}>\n <div\n data-slot=\"theme-scope\"\n ref={\n parentScope.coordinator === null\n ? (element: HTMLElement | null) => {\n coordinator.attach(\n element,\n (nextTheme) => localResolvedSystemTheme.set(nextTheme),\n scopeSignal,\n );\n if (!element || persistenceAdoption.complete) return;\n persistenceAdoption.complete = true;\n const storedTheme = readStoredTheme(storageKey);\n if (storedTheme && storedTheme !== themeState()) {\n themeState.set(storedTheme);\n coordinator.activate(scopeId, storedTheme);\n }\n }\n : undefined\n }\n >\n {children}\n </div>\n </ThemeScopeContext>\n );\n}\n\nfunction getDefaultThemeCoordinator(): ThemeCoordinator {\n if (typeof document === \"undefined\") {\n return createThemeCoordinator();\n }\n\n const existing = documentThemeCoordinators.get(document);\n if (existing) return existing;\n\n const coordinator = createThemeCoordinator();\n documentThemeCoordinators.set(document, coordinator);\n return coordinator;\n}\n\nfunction createThemeCoordinator() {\n const scopes = new Map<\n symbol,\n {\n depth: number;\n sequence: number;\n theme: ThemeName;\n signal: AbortSignal;\n }\n >();\n let nextSequence = 0;\n let explicitOwner: symbol | undefined;\n let root: Node | null = null;\n let scheduled = false;\n\n const target = (): HTMLElement | null => {\n if (root?.nodeType === 9) return (root as Document).documentElement;\n if (root && \"host\" in root) return (root as ShadowRoot).host as HTMLElement;\n return typeof document === \"undefined\" ? null : document.documentElement;\n };\n const syncActive = (): void => {\n if (explicitOwner !== undefined) return;\n let candidate: { depth: number; sequence: number; theme: ThemeName } | undefined;\n for (const scope of scopes.values()) {\n if (\n !candidate ||\n scope.depth > candidate.depth ||\n (scope.depth === candidate.depth && scope.sequence > candidate.sequence)\n ) {\n candidate = scope;\n }\n }\n if (candidate) syncThemeTarget(target(), candidate.theme);\n };\n const schedule = (): void => {\n if (typeof document === \"undefined\" || scheduled) return;\n scheduled = true;\n setTimeout(() => {\n scheduled = false;\n syncActive();\n }, 0);\n };\n\n return Object.freeze({\n attach(\n element: HTMLElement | null,\n onResolvedSystemTheme: (themeName: \"light\" | \"dark\") => void,\n signal: AbortSignal,\n ) {\n if (element) root = element.getRootNode();\n if (element && typeof window !== \"undefined\" && typeof window.matchMedia === \"function\") {\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\");\n const update = () => onResolvedSystemTheme(media.matches ? \"dark\" : \"light\");\n update();\n media.addEventListener?.(\"change\", update);\n signal.addEventListener(\"abort\", () => media.removeEventListener?.(\"change\", update), {\n once: true,\n });\n }\n schedule();\n },\n register(id: symbol, depth: number, themeName: ThemeName, signal: AbortSignal) {\n const existing = scopes.get(id);\n scopes.set(id, {\n depth,\n sequence: existing?.sequence ?? nextSequence++,\n theme: themeName,\n signal,\n });\n if (!existing) {\n signal.addEventListener(\n \"abort\",\n () => {\n scopes.delete(id);\n if (explicitOwner === id) explicitOwner = undefined;\n schedule();\n },\n { once: true },\n );\n }\n schedule();\n },\n activate(id: symbol, themeName: ThemeName) {\n const scope = scopes.get(id);\n if (scope) scope.theme = themeName;\n explicitOwner = id;\n syncThemeTarget(target(), themeName);\n },\n });\n}\n\nexport function ThemePicker(props: ThemePickerProps): JSX.Element {\n const activeTheme = theme();\n const { themes = activeTheme.themes, label = \"Theme\", ...rest } = props;\n const currentTheme = activeTheme.theme();\n\n return (\n <select\n {...rest}\n aria-label={rest[\"aria-label\"] ?? label}\n data-slot=\"theme-picker\"\n value={currentTheme}\n onChange={(event: Event) => {\n const target = getThemePickerTarget(event);\n if (target) {\n activeTheme.setTheme(target.value as ThemeName);\n }\n }}\n >\n {themes.map((option) => (\n <option key={option.value} value={option.value} selected={option.value === currentTheme}>\n {option.label}\n </option>\n ))}\n </select>\n );\n}\n\nfunction getThemePickerTarget(event: Event): HTMLSelectElement | null {\n if (typeof HTMLSelectElement === \"undefined\") {\n return null;\n }\n\n const path = typeof event.composedPath === \"function\" ? event.composedPath() : [];\n const candidates = [event.target, event.currentTarget, ...path];\n\n for (const candidate of candidates) {\n if (candidate instanceof HTMLSelectElement) {\n return candidate;\n }\n }\n\n return null;\n}\n\nexport function ThemeToggle(props: ThemeToggleProps): JSX.Element {\n const activeTheme = theme();\n const {\n children,\n lightIcon,\n darkIcon,\n systemIcon,\n themes = [\"light\", \"dark\"],\n onPress,\n ...rest\n } = props;\n\n const currentTheme = activeTheme.theme();\n const nextTheme = getNextTheme(currentTheme, themes, activeTheme.resolvedSystemTheme());\n const renderContext = { theme: currentTheme, nextTheme };\n const ariaLabel = (rest as Record<string, unknown>)[\"aria-label\"];\n const themedIcon = resolveThemeToggleIcon(currentTheme, nextTheme, {\n lightIcon,\n darkIcon,\n systemIcon,\n });\n const renderedIcon = cloneThemeToggleIcon(themedIcon, currentTheme);\n const renderedIconSlots =\n renderThemeToggleIconSlots(currentTheme, {\n lightIcon,\n darkIcon,\n systemIcon,\n }) ?? renderedIcon;\n const content =\n typeof children === \"function\" ? children(renderContext) : (children ?? renderedIconSlots);\n\n return (\n <Button\n {...(rest as ButtonNativeProps)}\n aria-label={typeof ariaLabel === \"string\" ? ariaLabel : `Switch to ${nextTheme} theme`}\n data-theme-control=\"toggle\"\n data-theme-choice={currentTheme}\n data-next-theme={nextTheme}\n onPress={(event) => {\n onPress?.(event);\n if (!event.defaultPrevented && !Object.is(nextTheme, currentTheme)) {\n activeTheme.setTheme(nextTheme);\n }\n }}\n >\n <span data-slot=\"theme-toggle-content\">{content}</span>\n </Button>\n );\n}\n\nfunction getNextTheme(\n currentTheme: ThemeName,\n themes: readonly ThemeName[],\n resolvedSystemTheme: \"light\" | \"dark\" = \"light\",\n): ThemeName {\n if (themes.length === 0) return currentTheme;\n const index = themes.indexOf(currentTheme);\n if (index < 0 && currentTheme === \"system\") {\n if (themes.includes(\"light\") && themes.includes(\"dark\")) {\n return resolvedSystemTheme === \"dark\" ? \"light\" : \"dark\";\n }\n }\n return themes[index >= 0 && index < themes.length - 1 ? index + 1 : 0]!;\n}\n\nfunction getThemeIcon(\n theme: ThemeName,\n icons: Pick<ThemeToggleProps, \"lightIcon\" | \"darkIcon\" | \"systemIcon\">,\n): unknown {\n if (theme === \"light\") return icons.lightIcon;\n if (theme === \"dark\") return icons.darkIcon;\n if (theme === \"system\") return icons.systemIcon;\n return undefined;\n}\n\nexport function resolveThemeToggleIcon(\n theme: ThemeName,\n nextTheme: ThemeName,\n icons: Pick<ThemeToggleProps, \"lightIcon\" | \"darkIcon\" | \"systemIcon\">,\n): unknown {\n return getThemeIcon(theme, icons) ?? getThemeIcon(nextTheme, icons);\n}\n\nfunction renderThemeToggleIconSlots(\n theme: ThemeName,\n icons: Pick<ThemeToggleProps, \"lightIcon\" | \"darkIcon\" | \"systemIcon\">,\n): unknown {\n if (getThemeIcon(theme, icons) === undefined) {\n return undefined;\n }\n\n const slots = [\n [\"light\", icons.lightIcon],\n [\"dark\", icons.darkIcon],\n [\"system\", icons.systemIcon],\n ] as const;\n const availableSlots = slots.filter(([, icon]) => icon !== undefined && icon !== null);\n\n if (availableSlots.length <= 1) {\n return undefined;\n }\n\n return availableSlots.map(([slotTheme, icon]) => (\n <span\n key={slotTheme}\n data-slot=\"theme-toggle-icon\"\n data-theme-toggle-icon={slotTheme}\n hidden={slotTheme === theme ? undefined : true}\n >\n {cloneThemeToggleIcon(icon, slotTheme)}\n </span>\n ));\n}\n\nfunction isJSXElement(value: unknown): value is JSXElement {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"$$typeof\" in value &&\n \"type\" in value &&\n \"props\" in value\n );\n}\n\nfunction cloneThemeToggleIcon(icon: unknown, key?: string): unknown {\n if (Array.isArray(icon)) {\n const clonedChildren = icon.map((child) => cloneThemeToggleIcon(child));\n if ((icon as unknown as Record<symbol, unknown>)[STATIC_CHILDREN] === true) {\n Object.defineProperty(clonedChildren, STATIC_CHILDREN, {\n value: true,\n configurable: true,\n });\n }\n return clonedChildren;\n }\n\n if (!isJSXElement(icon)) return icon;\n\n const props = icon.props as Record<string, unknown> | undefined;\n const clonedProps = props ? { ...props } : {};\n\n if (\"children\" in clonedProps) {\n clonedProps.children = cloneThemeToggleIcon(clonedProps.children);\n }\n\n const iconKey = (icon.key ?? key ?? null) as string | number | null;\n const clonedIcon = {\n ...icon,\n key: iconKey,\n props: clonedProps,\n };\n\n delete (clonedIcon as Record<symbol, unknown>)[STATIC_CHILD_SLOTS_CACHE];\n return clonedIcon;\n}\n\nfunction syncThemeTarget(\n html: HTMLElement | null,\n themeChoice: ThemeName | null | undefined,\n): void {\n if (!html) return;\n\n if (themeChoice == null) {\n html.removeAttribute(\"data-theme\");\n html.removeAttribute(\"data-theme-choice\");\n return;\n }\n\n html.setAttribute(\"data-theme-choice\", themeChoice);\n\n if (themeChoice === \"system\") {\n html.removeAttribute(\"data-theme\");\n } else {\n html.setAttribute(\"data-theme\", themeChoice);\n }\n}\n\nfunction readStoredTheme(storageKey: string): ThemeName | undefined {\n if (typeof window === \"undefined\") return undefined;\n try {\n const storedTheme = window.localStorage.getItem(storageKey);\n return storedTheme ? (storedTheme as ThemeName) : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction writeStoredTheme(storageKey: string, theme: ThemeName): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(storageKey, theme);\n } catch {\n // Storage can be unavailable in private or locked-down browser contexts.\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"theme.js","names":[],"sources":["../../../src/components/theme/theme.tsx"],"sourcesContent":["import type { JSX } from \"@askrjs/askr/jsx-runtime\";\nimport { defineScope, getSignal, readScope, state } from \"@askrjs/askr\";\nimport { cloneElement, isElement, type JSXElement } from \"@askrjs/askr/foundations/structures\";\nimport { Button } from \"@askrjs/ui\";\nimport type { ButtonNativeProps, PressEvent } from \"@askrjs/ui\";\n\nexport const CAT_THEME_NAMES = [\"tabby\", \"ginger\", \"tuxedo\", \"calico\", \"torty\"] as const;\n\nexport type CatThemeName = (typeof CAT_THEME_NAMES)[number];\nexport type ThemeName = \"light\" | \"dark\" | \"system\" | CatThemeName | (string & {});\n\nexport type ThemeOption = {\n value: ThemeName;\n label: string;\n};\n\nexport type ThemeScopeValue = {\n theme: () => ThemeName;\n resolvedSystemTheme: () => \"light\" | \"dark\";\n setTheme: (theme: ThemeName) => void;\n themes: readonly ThemeOption[];\n storageKey: string;\n};\n\nexport type ThemeScopeProps = {\n children?: unknown;\n defaultTheme?: ThemeName;\n themes?: readonly ThemeOption[];\n storageKey?: string;\n};\n\nexport type ThemePickerProps = Omit<\n JSX.IntrinsicElements[\"select\"],\n \"children\" | \"value\" | \"defaultValue\" | \"onChange\"\n> & {\n themes?: readonly ThemeOption[];\n label?: string;\n};\n\nexport type ThemeToggleRenderContext = {\n theme: ThemeName;\n nextTheme: ThemeName;\n};\n\nexport type ThemeToggleProps = Omit<ButtonNativeProps, \"children\" | \"onPress\"> & {\n children?: unknown | ((context: ThemeToggleRenderContext) => unknown);\n lightIcon?: unknown;\n darkIcon?: unknown;\n systemIcon?: unknown;\n themes?: readonly ThemeName[];\n onPress?: (event: PressEvent) => void;\n};\n\nexport const DEFAULT_THEME_OPTIONS: readonly ThemeOption[] = [\n { value: \"system\", label: \"System\" },\n { value: \"light\", label: \"Light\" },\n { value: \"dark\", label: \"Dark\" },\n];\n\nexport const CAT_THEME_OPTIONS: readonly ThemeOption[] = [\n { value: \"tabby\", label: \"Tabby\" },\n { value: \"ginger\", label: \"Ginger\" },\n { value: \"tuxedo\", label: \"Tuxedo\" },\n { value: \"calico\", label: \"Calico\" },\n { value: \"torty\", label: \"Torty\" },\n];\n\nconst DEFAULT_STORAGE_KEY = \"askr-theme\";\nconst STATIC_CHILDREN = Symbol.for(\"askr.static-children\");\nconst documentThemeCoordinators = new WeakMap<Document, ThemeCoordinator>();\ntype ThemeCoordinator = ReturnType<typeof createThemeCoordinator>;\ntype InternalThemeScopeValue = ThemeScopeValue & {\n readonly coordinator: ThemeCoordinator | null;\n readonly depth: number;\n};\n\nconst ThemeScopeContext = defineScope<InternalThemeScopeValue>({\n theme: () => \"system\",\n resolvedSystemTheme: () => \"light\",\n setTheme: () => undefined,\n themes: DEFAULT_THEME_OPTIONS,\n storageKey: DEFAULT_STORAGE_KEY,\n coordinator: null,\n depth: -1,\n});\n\nexport function theme(): ThemeScopeValue {\n return readScope(ThemeScopeContext);\n}\n\nexport function ThemeScope(props: ThemeScopeProps): JSX.Element {\n const {\n children,\n defaultTheme = \"system\",\n themes = DEFAULT_THEME_OPTIONS,\n storageKey = DEFAULT_STORAGE_KEY,\n } = props;\n\n const scopeId = state<symbol>(Symbol(\"ThemeScope\"))();\n const scopeSignal = getSignal();\n // The first render must be identical on the server and in the browser.\n // Browser persistence is adopted from the committed root ref, after Askr's\n // hydration verifier has accepted the server markup.\n const themeState = state<ThemeName>(defaultTheme);\n const localResolvedSystemTheme = state<\"light\" | \"dark\">(\"light\");\n const persistenceAdoption = state({ complete: false })();\n const currentTheme = themeState();\n const parentScope = readScope(ThemeScopeContext);\n const ownedCoordinator = state<ThemeCoordinator>(getDefaultThemeCoordinator())();\n const coordinator = parentScope.coordinator ?? ownedCoordinator;\n const scopeDepth = parentScope.depth + 1;\n coordinator.register(scopeId, scopeDepth, currentTheme, scopeSignal, (nextTheme) => {\n if (themeState() !== nextTheme) themeState.set(nextTheme);\n });\n\n const setTheme = (nextTheme: ThemeName) => {\n themeState.set(nextTheme);\n writeStoredTheme(storageKey, nextTheme);\n coordinator.activate(scopeId, nextTheme);\n };\n\n const resolvedSystemTheme = parentScope.coordinator\n ? parentScope.resolvedSystemTheme\n : localResolvedSystemTheme;\n const value: InternalThemeScopeValue = {\n theme: themeState,\n resolvedSystemTheme,\n setTheme,\n themes,\n storageKey,\n coordinator,\n depth: scopeDepth,\n };\n\n return (\n <ThemeScopeContext value={value}>\n <div\n data-slot=\"theme-scope\"\n ref={\n parentScope.coordinator === null\n ? (element: HTMLElement | null) => {\n coordinator.attach(\n element,\n (nextTheme) => localResolvedSystemTheme.set(nextTheme),\n scopeSignal,\n );\n if (element && typeof window !== \"undefined\") {\n const onStorage = (event: StorageEvent) => {\n let storageMatches = event.storageArea == null;\n try {\n storageMatches ||= event.storageArea === window.localStorage;\n } catch {\n // Locked-down/private contexts may deny access to localStorage.\n }\n if (!storageMatches || event.key !== storageKey) {\n return;\n }\n const nextTheme = event.newValue as ThemeName | null;\n if (!nextTheme) return;\n themeState.set(nextTheme);\n coordinator.activate(scopeId, nextTheme);\n };\n window.addEventListener(\"storage\", onStorage);\n scopeSignal.addEventListener(\n \"abort\",\n () => window.removeEventListener(\"storage\", onStorage),\n { once: true },\n );\n }\n if (!element || persistenceAdoption.complete) return;\n persistenceAdoption.complete = true;\n const storedTheme = readStoredTheme(storageKey);\n if (storedTheme && storedTheme !== themeState()) {\n themeState.set(storedTheme);\n coordinator.activate(scopeId, storedTheme);\n }\n }\n : undefined\n }\n >\n {children}\n </div>\n </ThemeScopeContext>\n );\n}\n\nfunction getDefaultThemeCoordinator(): ThemeCoordinator {\n if (typeof document === \"undefined\") {\n return createThemeCoordinator();\n }\n\n const existing = documentThemeCoordinators.get(document);\n if (existing) return existing;\n\n const coordinator = createThemeCoordinator();\n documentThemeCoordinators.set(document, coordinator);\n return coordinator;\n}\n\nfunction removeEmptyGeneratedStyleRegistries(root: Node | null): void {\n const ownerDocument =\n root?.nodeType === 9\n ? (root as Document)\n : (root?.ownerDocument ?? (typeof document === \"undefined\" ? null : document));\n if (!ownerDocument) return;\n\n for (const registry of ownerDocument.querySelectorAll<HTMLStyleElement>(\n \"style[data-askr-style-registry]\",\n )) {\n if (!registry.textContent?.trim()) registry.remove();\n }\n}\n\nfunction createThemeCoordinator() {\n const scopes = new Map<\n symbol,\n {\n depth: number;\n sequence: number;\n theme: ThemeName;\n signal: AbortSignal;\n onThemeChange: (themeName: ThemeName) => void;\n }\n >();\n let nextSequence = 0;\n let explicitOwner: symbol | undefined;\n let root: Node | null = null;\n let scheduled = false;\n\n const target = (): HTMLElement | null => {\n if (root?.nodeType === 9) return (root as Document).documentElement;\n if (root && \"host\" in root) return (root as ShadowRoot).host as HTMLElement;\n return typeof document === \"undefined\" ? null : document.documentElement;\n };\n const syncActive = (): void => {\n if (explicitOwner !== undefined) return;\n let candidate: { depth: number; sequence: number; theme: ThemeName } | undefined;\n for (const scope of scopes.values()) {\n if (\n !candidate ||\n scope.depth > candidate.depth ||\n (scope.depth === candidate.depth && scope.sequence > candidate.sequence)\n ) {\n candidate = scope;\n }\n }\n if (candidate) syncThemeTarget(target(), candidate.theme);\n };\n const schedule = (): void => {\n if (typeof document === \"undefined\" || scheduled) return;\n scheduled = true;\n setTimeout(() => {\n scheduled = false;\n syncActive();\n if (scopes.size === 0) removeEmptyGeneratedStyleRegistries(root);\n }, 0);\n };\n\n return Object.freeze({\n attach(\n element: HTMLElement | null,\n onResolvedSystemTheme: (themeName: \"light\" | \"dark\") => void,\n signal: AbortSignal,\n ) {\n if (element) root = element.getRootNode();\n if (element && typeof window !== \"undefined\" && typeof window.matchMedia === \"function\") {\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\");\n const update = () => onResolvedSystemTheme(media.matches ? \"dark\" : \"light\");\n update();\n media.addEventListener?.(\"change\", update);\n signal.addEventListener(\"abort\", () => media.removeEventListener?.(\"change\", update), {\n once: true,\n });\n }\n schedule();\n },\n register(\n id: symbol,\n depth: number,\n themeName: ThemeName,\n signal: AbortSignal,\n onThemeChange: (themeName: ThemeName) => void,\n ) {\n const existing = scopes.get(id);\n scopes.set(id, {\n depth,\n sequence: existing?.sequence ?? nextSequence++,\n theme: themeName,\n signal,\n onThemeChange,\n });\n if (!existing) {\n signal.addEventListener(\n \"abort\",\n () => {\n scopes.delete(id);\n if (explicitOwner === id) explicitOwner = undefined;\n schedule();\n },\n { once: true },\n );\n }\n schedule();\n },\n activate(id: symbol, themeName: ThemeName) {\n const scope = scopes.get(id);\n if (scope) scope.theme = themeName;\n explicitOwner = id;\n const ownerDepth = scope?.depth;\n for (const [scopeId, registered] of scopes) {\n if (scopeId !== id && registered.depth === ownerDepth) {\n registered.theme = themeName;\n registered.onThemeChange(themeName);\n }\n }\n syncThemeTarget(target(), themeName);\n },\n });\n}\n\nexport function ThemePicker(props: ThemePickerProps): JSX.Element {\n const activeTheme = theme();\n const { themes = activeTheme.themes, label = \"Theme\", ...rest } = props;\n const currentTheme = activeTheme.theme();\n\n return (\n <select\n {...rest}\n aria-label={rest[\"aria-label\"] ?? label}\n data-slot=\"theme-picker\"\n value={currentTheme}\n onChange={(event: Event) => {\n const target = getThemePickerTarget(event);\n if (target) {\n activeTheme.setTheme(target.value as ThemeName);\n }\n }}\n >\n {themes.map((option) => (\n <option key={option.value} value={option.value} selected={option.value === currentTheme}>\n {option.label}\n </option>\n ))}\n </select>\n );\n}\n\nfunction getThemePickerTarget(event: Event): HTMLSelectElement | null {\n if (typeof HTMLSelectElement === \"undefined\") {\n return null;\n }\n\n const path = typeof event.composedPath === \"function\" ? event.composedPath() : [];\n const candidates = [event.target, event.currentTarget, ...path];\n\n for (const candidate of candidates) {\n if (candidate instanceof HTMLSelectElement) {\n return candidate;\n }\n }\n\n return null;\n}\n\nexport function ThemeToggle(props: ThemeToggleProps): JSX.Element {\n const activeTheme = theme();\n const {\n children,\n lightIcon,\n darkIcon,\n systemIcon,\n themes = [\"light\", \"dark\"],\n onPress,\n ...rest\n } = props;\n\n const currentTheme = activeTheme.theme();\n const nextTheme = getNextTheme(currentTheme, themes, activeTheme.resolvedSystemTheme());\n const renderContext = { theme: currentTheme, nextTheme };\n const ariaLabel = (rest as Record<string, unknown>)[\"aria-label\"];\n const themedIcon = resolveThemeToggleIcon(currentTheme, nextTheme, {\n lightIcon,\n darkIcon,\n systemIcon,\n });\n const renderedIcon = cloneThemeToggleIcon(themedIcon, currentTheme);\n const renderedIconSlots =\n renderThemeToggleIconSlots(currentTheme, {\n lightIcon,\n darkIcon,\n systemIcon,\n }) ?? renderedIcon;\n const content =\n typeof children === \"function\" ? children(renderContext) : (children ?? renderedIconSlots);\n\n return (\n <Button\n {...(rest as ButtonNativeProps)}\n aria-label={typeof ariaLabel === \"string\" ? ariaLabel : `Switch to ${nextTheme} theme`}\n data-theme-control=\"toggle\"\n data-theme-choice={currentTheme}\n data-next-theme={nextTheme}\n onPress={(event) => {\n onPress?.(event);\n if (!event.defaultPrevented && !Object.is(nextTheme, currentTheme)) {\n activeTheme.setTheme(nextTheme);\n }\n }}\n >\n <span data-slot=\"theme-toggle-content\">{content}</span>\n </Button>\n );\n}\n\nfunction getNextTheme(\n currentTheme: ThemeName,\n themes: readonly ThemeName[],\n resolvedSystemTheme: \"light\" | \"dark\" = \"light\",\n): ThemeName {\n if (themes.length === 0) return currentTheme;\n const index = themes.indexOf(currentTheme);\n if (index < 0 && currentTheme === \"system\") {\n if (themes.includes(\"light\") && themes.includes(\"dark\")) {\n return resolvedSystemTheme === \"dark\" ? \"light\" : \"dark\";\n }\n }\n return themes[index >= 0 && index < themes.length - 1 ? index + 1 : 0]!;\n}\n\nfunction getThemeIcon(\n theme: ThemeName,\n icons: Pick<ThemeToggleProps, \"lightIcon\" | \"darkIcon\" | \"systemIcon\">,\n): unknown {\n if (theme === \"light\") return icons.lightIcon;\n if (theme === \"dark\") return icons.darkIcon;\n if (theme === \"system\") return icons.systemIcon;\n return undefined;\n}\n\nexport function resolveThemeToggleIcon(\n theme: ThemeName,\n nextTheme: ThemeName,\n icons: Pick<ThemeToggleProps, \"lightIcon\" | \"darkIcon\" | \"systemIcon\">,\n): unknown {\n return getThemeIcon(theme, icons) ?? getThemeIcon(nextTheme, icons);\n}\n\nfunction renderThemeToggleIconSlots(\n theme: ThemeName,\n icons: Pick<ThemeToggleProps, \"lightIcon\" | \"darkIcon\" | \"systemIcon\">,\n): unknown {\n if (getThemeIcon(theme, icons) === undefined) {\n return undefined;\n }\n\n const slots = [\n [\"light\", icons.lightIcon],\n [\"dark\", icons.darkIcon],\n [\"system\", icons.systemIcon],\n ] as const;\n const availableSlots = slots.filter(([, icon]) => icon !== undefined && icon !== null);\n\n if (availableSlots.length <= 1) {\n return undefined;\n }\n\n return availableSlots.map(([slotTheme, icon]) => (\n <span\n key={slotTheme}\n data-slot=\"theme-toggle-icon\"\n data-theme-toggle-icon={slotTheme}\n hidden={slotTheme === theme ? undefined : true}\n >\n {cloneThemeToggleIcon(icon, slotTheme)}\n </span>\n ));\n}\n\nfunction cloneThemeToggleIcon(icon: unknown, key?: string): unknown {\n if (Array.isArray(icon)) {\n const clonedChildren = icon.map((child) => cloneThemeToggleIcon(child));\n if ((icon as unknown as Record<symbol, unknown>)[STATIC_CHILDREN] === true) {\n Object.defineProperty(clonedChildren, STATIC_CHILDREN, {\n value: true,\n configurable: true,\n });\n }\n return clonedChildren;\n }\n\n if (!isElement(icon)) return icon;\n\n const props = icon.props as Record<string, unknown> | undefined;\n const clonedProps = props ? { ...props } : {};\n\n if (\"children\" in clonedProps) {\n clonedProps.children = cloneThemeToggleIcon(clonedProps.children);\n }\n\n const clonedIcon = cloneElement(icon, clonedProps);\n return {\n ...clonedIcon,\n key: icon.key ?? key ?? null,\n } satisfies JSXElement;\n}\n\nfunction syncThemeTarget(\n html: HTMLElement | null,\n themeChoice: ThemeName | null | undefined,\n): void {\n if (!html) return;\n\n if (themeChoice == null) {\n html.removeAttribute(\"data-theme\");\n html.removeAttribute(\"data-theme-choice\");\n return;\n }\n\n html.setAttribute(\"data-theme-choice\", themeChoice);\n\n if (themeChoice === \"system\") {\n html.removeAttribute(\"data-theme\");\n } else {\n html.setAttribute(\"data-theme\", themeChoice);\n }\n}\n\nfunction readStoredTheme(storageKey: string): ThemeName | undefined {\n if (typeof window === \"undefined\") return undefined;\n try {\n const storedTheme = window.localStorage.getItem(storageKey);\n return storedTheme ? (storedTheme as ThemeName) : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction writeStoredTheme(storageKey: string, theme: ThemeName): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(storageKey, theme);\n } catch {\n // Storage can be unavailable in private or locked-down browser contexts.\n }\n}\n"],"mappings":"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"}
|
package/dist/ssr.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
import { styleRulesForHtml } from "./components/_internal/style.js";
|
|
2
1
|
//#region src/ssr.ts
|
|
3
2
|
const STYLE_REGISTRY_ATTR = "data-askr-style-registry";
|
|
3
|
+
const STYLE_CLASS_PREFIX = "ak-style-";
|
|
4
|
+
const CLASS_ATTRIBUTE_PATTERN = /\sclass=(?:"([^"]*)"|'([^']*)')/g;
|
|
5
|
+
const MAX_STYLE_RULES = 512;
|
|
4
6
|
function escapeHtmlAttribute(value) {
|
|
5
7
|
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
6
8
|
}
|
|
7
9
|
function escapeStyleRawText(value) {
|
|
8
10
|
return value.replace(/<\/style/gi, "<\\/style");
|
|
9
11
|
}
|
|
12
|
+
function generatedStyleClasses(html) {
|
|
13
|
+
const classes = /* @__PURE__ */ new Set();
|
|
14
|
+
for (const attribute of html.matchAll(CLASS_ATTRIBUTE_PATTERN)) for (const className of (attribute[1] ?? attribute[2] ?? "").split(/\s+/)) if (className.startsWith(STYLE_CLASS_PREFIX)) classes.add(className);
|
|
15
|
+
return classes;
|
|
16
|
+
}
|
|
10
17
|
function findHeadEnd(html) {
|
|
11
18
|
let index = 0;
|
|
12
19
|
while (index < html.length) {
|
|
@@ -44,16 +51,21 @@ function withThemeStyles(documentRenderer) {
|
|
|
44
51
|
return (args) => {
|
|
45
52
|
const documentHtml = documentRenderer(args);
|
|
46
53
|
const registeredStyles = args.context.styles;
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
const generatedClasses = generatedStyleClasses(documentHtml);
|
|
55
|
+
if (registeredStyles === void 0) {
|
|
56
|
+
if (generatedClasses.size > 0) throw new Error("Generated theme classes require request-local SSR style registrations from @askrjs/askr.");
|
|
57
|
+
return documentHtml;
|
|
58
|
+
}
|
|
59
|
+
const styles = /* @__PURE__ */ new Map();
|
|
60
|
+
for (const style of registeredStyles) {
|
|
61
|
+
if (!style.id || !style.cssText) throw new TypeError("Invalid SSR theme style registration.");
|
|
62
|
+
const existing = styles.get(style.id);
|
|
63
|
+
if (existing !== void 0 && existing !== style.cssText) throw new RangeError(`SSR style registration collision for ${JSON.stringify(style.id)}.`);
|
|
64
|
+
styles.set(style.id, style.cssText);
|
|
65
|
+
if (styles.size > MAX_STYLE_RULES) throw new RangeError("Theme style registry capacity exceeded.");
|
|
66
|
+
}
|
|
67
|
+
for (const className of generatedClasses) if (!styles.has(className)) throw new Error(`Missing request-local SSR style registration for ${JSON.stringify(className)}.`);
|
|
68
|
+
const rules = Array.from(styles.values());
|
|
57
69
|
if (rules.length === 0) return documentHtml;
|
|
58
70
|
const nonce = args.context.cspNonce === void 0 ? "" : ` nonce="${escapeHtmlAttribute(args.context.cspNonce)}"`;
|
|
59
71
|
return injectIntoHead(documentHtml, `<style ${STYLE_REGISTRY_ATTR}="true"${nonce}>${escapeStyleRawText(rules.join("\n"))}\n</style>`);
|
package/dist/ssr.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr.js","names":[],"sources":["../src/ssr.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"ssr.js","names":[],"sources":["../src/ssr.ts"],"sourcesContent":["const STYLE_REGISTRY_ATTR = \"data-askr-style-registry\";\nconst STYLE_CLASS_PREFIX = \"ak-style-\";\nconst CLASS_ATTRIBUTE_PATTERN = /\\sclass=(?:\"([^\"]*)\"|'([^']*)')/g;\nconst MAX_STYLE_RULES = 512;\n\ntype DocumentRenderArgsLike = {\n appHtml: string;\n context: {\n cspNonce?: string;\n styles?: readonly { id: string; cssText: string }[];\n };\n};\n\nfunction escapeHtmlAttribute(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/\"/g, \""\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\n}\n\nfunction escapeStyleRawText(value: string): string {\n return value.replace(/<\\/style/gi, \"<\\\\/style\");\n}\n\nfunction generatedStyleClasses(html: string): Set<string> {\n const classes = new Set<string>();\n for (const attribute of html.matchAll(CLASS_ATTRIBUTE_PATTERN)) {\n for (const className of (attribute[1] ?? attribute[2] ?? \"\").split(/\\s+/)) {\n if (className.startsWith(STYLE_CLASS_PREFIX)) classes.add(className);\n }\n }\n return classes;\n}\n\nfunction findHeadEnd(html: string): number {\n let index = 0;\n while (index < html.length) {\n if (html.startsWith(\"<!--\", index)) {\n const commentEnd = html.indexOf(\"-->\", index + 4);\n index = commentEnd < 0 ? html.length : commentEnd + 3;\n continue;\n }\n\n const rawTextStart = html.slice(index).match(/^<\\s*(script|style|title|textarea)(?:\\s|>)/i);\n if (rawTextStart) {\n const rawTextEndPattern = new RegExp(`</\\\\s*${rawTextStart[1]}\\\\s*>`, \"ig\");\n rawTextEndPattern.lastIndex = index + rawTextStart[0].length;\n const rawTextEnd = rawTextEndPattern.exec(html);\n if (!rawTextEnd) return -1;\n index = rawTextEnd.index + rawTextEnd[0].length;\n continue;\n }\n\n const headEnd = html.slice(index).match(/^<\\/\\s*head\\s*>/i);\n if (headEnd) return index;\n index += 1;\n }\n return -1;\n}\n\nfunction injectIntoHead(html: string, content: string): string {\n const headEnd = findHeadEnd(html);\n if (headEnd >= 0) {\n return `${html.slice(0, headEnd)}${content}${html.slice(headEnd)}`;\n }\n\n const bodyStart = html.search(/<body(?:\\s[^>]*)?>/i);\n if (bodyStart >= 0) {\n return `${html.slice(0, bodyStart)}<head>${content}</head>${html.slice(bodyStart)}`;\n }\n\n return `${content}${html}`;\n}\n\n/**\n * Wrap an Askr SSR/SSG document renderer so generated theme rules used by the\n * rendered app are available before hydration.\n */\nexport function withThemeStyles<TArgs extends DocumentRenderArgsLike>(\n documentRenderer: (args: TArgs) => string,\n): (args: TArgs) => string {\n return (args) => {\n const documentHtml = documentRenderer(args);\n const registeredStyles = args.context.styles;\n const generatedClasses = generatedStyleClasses(documentHtml);\n if (registeredStyles === undefined) {\n if (generatedClasses.size > 0) {\n throw new Error(\n \"Generated theme classes require request-local SSR style registrations from @askrjs/askr.\",\n );\n }\n return documentHtml;\n }\n\n const styles = new Map<string, string>();\n for (const style of registeredStyles) {\n if (!style.id || !style.cssText) {\n throw new TypeError(\"Invalid SSR theme style registration.\");\n }\n const existing = styles.get(style.id);\n if (existing !== undefined && existing !== style.cssText) {\n throw new RangeError(`SSR style registration collision for ${JSON.stringify(style.id)}.`);\n }\n styles.set(style.id, style.cssText);\n if (styles.size > MAX_STYLE_RULES) {\n throw new RangeError(\"Theme style registry capacity exceeded.\");\n }\n }\n for (const className of generatedClasses) {\n if (!styles.has(className)) {\n throw new Error(\n `Missing request-local SSR style registration for ${JSON.stringify(className)}.`,\n );\n }\n }\n const rules = Array.from(styles.values());\n if (rules.length === 0) return documentHtml;\n\n const nonce =\n args.context.cspNonce === undefined\n ? \"\"\n : ` nonce=\"${escapeHtmlAttribute(args.context.cspNonce)}\"`;\n const registry = `<style ${STYLE_REGISTRY_ATTR}=\"true\"${nonce}>${escapeStyleRawText(rules.join(\"\\n\"))}\\n</style>`;\n return injectIntoHead(documentHtml, registry);\n };\n}\n"],"mappings":"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"}
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
--ak-section-2: var(--ak-space-5);
|
|
146
146
|
--ak-section-3: var(--ak-space-6);
|
|
147
147
|
--ak-section-4: var(--ak-space-7);
|
|
148
|
-
--ak-z-dropdown:
|
|
148
|
+
--ak-z-dropdown: 1500;
|
|
149
149
|
--ak-z-index-base: auto;
|
|
150
150
|
--ak-z-index-sm: 10;
|
|
151
151
|
--ak-z-index-md: 100;
|
|
@@ -1011,16 +1011,20 @@
|
|
|
1011
1011
|
}
|
|
1012
1012
|
|
|
1013
1013
|
:where([data-slot="nav-group-label"]) {
|
|
1014
|
+
max-inline-size: 100%;
|
|
1014
1015
|
color: var(--ak-color-text-muted);
|
|
1015
1016
|
font-size: var(--ak-font-size-xs);
|
|
1016
1017
|
font-weight: var(--ak-font-weight-medium);
|
|
1017
1018
|
line-height: var(--ak-line-height-tight);
|
|
1019
|
+
overflow-wrap: anywhere;
|
|
1018
1020
|
margin: 0;
|
|
1019
1021
|
}
|
|
1020
1022
|
|
|
1021
1023
|
:where([data-slot="nav-item"]) {
|
|
1022
1024
|
min-block-size: 2rem;
|
|
1025
|
+
max-inline-size: 100%;
|
|
1023
1026
|
color: var(--ak-color-text-muted);
|
|
1027
|
+
white-space: normal;
|
|
1024
1028
|
transition: background var(--ak-duration-fast) var(--ak-ease-standard),
|
|
1025
1029
|
color var(--ak-duration-fast) var(--ak-ease-standard);
|
|
1026
1030
|
text-decoration: none;
|
|
@@ -4330,7 +4334,9 @@
|
|
|
4330
4334
|
}
|
|
4331
4335
|
|
|
4332
4336
|
:where([data-slot="theme-picker"]) {
|
|
4333
|
-
|
|
4337
|
+
appearance: none;
|
|
4338
|
+
block-size: var(--ak-density-control-height-md, 2.25rem);
|
|
4339
|
+
min-block-size: var(--ak-density-control-height-md, 2.25rem);
|
|
4334
4340
|
min-inline-size: min(8rem, 100%);
|
|
4335
4341
|
max-inline-size: 100%;
|
|
4336
4342
|
padding-inline: var(--ak-density-control-padding-x-md) 2.1rem;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askrjs/themes",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"description": "Default theme tokens, styles, and component presets for Askr apps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"askr",
|
|
@@ -332,8 +332,8 @@
|
|
|
332
332
|
"test:checks": "vp test run -c vitest.test.checks.config.ts"
|
|
333
333
|
},
|
|
334
334
|
"devDependencies": {
|
|
335
|
-
"@askrjs/askr": ">=0.0.
|
|
336
|
-
"@askrjs/ui": ">=0.0.
|
|
335
|
+
"@askrjs/askr": ">=0.0.85 <0.1.0",
|
|
336
|
+
"@askrjs/ui": ">=0.0.24 <0.1.0",
|
|
337
337
|
"@askrjs/vite": ">=0.0.5 <0.1.0",
|
|
338
338
|
"@tsdown/css": "0.22.8",
|
|
339
339
|
"@types/node": "^26.0.0",
|
|
@@ -346,11 +346,11 @@
|
|
|
346
346
|
"vite-plus": "^0.2.4"
|
|
347
347
|
},
|
|
348
348
|
"peerDependencies": {
|
|
349
|
-
"@askrjs/askr": ">=0.0.
|
|
350
|
-
"@askrjs/ui": ">=0.0.
|
|
349
|
+
"@askrjs/askr": ">=0.0.85 <0.1.0",
|
|
350
|
+
"@askrjs/ui": ">=0.0.24 <0.1.0"
|
|
351
351
|
},
|
|
352
352
|
"engines": {
|
|
353
|
-
"node": ">=
|
|
353
|
+
"node": ">=24.15.0"
|
|
354
354
|
},
|
|
355
355
|
"packageManager": "npm@11.17.0"
|
|
356
356
|
}
|
|
@@ -200,7 +200,7 @@ function resolveTokenValue(
|
|
|
200
200
|
return tokens[trimmed] ?? trimmed;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
function resolveSpaceValue(value: string | number): string | number {
|
|
203
|
+
export function resolveSpaceValue(value: string | number): string | number {
|
|
204
204
|
return resolveTokenValue(value, SPACE_TOKEN_MAP);
|
|
205
205
|
}
|
|
206
206
|
|
|
@@ -243,7 +243,7 @@ function resolveShadowValue(value: BlockShadow): string | undefined {
|
|
|
243
243
|
return String(resolveTokenValue(value, SHADOW_TOKEN_MAP));
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
function setResponsiveVar<T>(
|
|
246
|
+
export function setResponsiveVar<T>(
|
|
247
247
|
styles: Record<string, string | number>,
|
|
248
248
|
property: string,
|
|
249
249
|
value: ResponsiveValue<T> | undefined,
|
|
@@ -12,6 +12,8 @@ const CSS_ALLOWED_FUNCTIONS = new Set([
|
|
|
12
12
|
"min",
|
|
13
13
|
"max",
|
|
14
14
|
"clamp",
|
|
15
|
+
"minmax",
|
|
16
|
+
"repeat",
|
|
15
17
|
"rgb",
|
|
16
18
|
"rgba",
|
|
17
19
|
"hsl",
|
|
@@ -174,7 +176,7 @@ export function mergeCssVar(style: unknown, name: string, value: string): string
|
|
|
174
176
|
const STYLE_REGISTRY_ATTR = "data-askr-style-registry";
|
|
175
177
|
const STYLE_CLASS_PREFIX = "ak-style-";
|
|
176
178
|
const MAX_STYLE_RULES = 512;
|
|
177
|
-
const
|
|
179
|
+
const MAX_COLLISION_CACHE = 4096;
|
|
178
180
|
|
|
179
181
|
type StyleRule = {
|
|
180
182
|
className: string;
|
|
@@ -182,7 +184,7 @@ type StyleRule = {
|
|
|
182
184
|
rule: string;
|
|
183
185
|
};
|
|
184
186
|
|
|
185
|
-
const
|
|
187
|
+
const styleCollisionCache = new Map<string, StyleRule>();
|
|
186
188
|
type StyleRegistry = {
|
|
187
189
|
element: HTMLStyleElement;
|
|
188
190
|
ruleCount: number;
|
|
@@ -219,7 +221,7 @@ function escapeStyleRawText(value: string): string {
|
|
|
219
221
|
|
|
220
222
|
function styleRuleFor(declarations: string): StyleRule {
|
|
221
223
|
const className = styleClassName(declarations);
|
|
222
|
-
const existing =
|
|
224
|
+
const existing = styleCollisionCache.get(className);
|
|
223
225
|
if (existing) {
|
|
224
226
|
if (existing.declarations !== declarations) {
|
|
225
227
|
throw new RangeError("Theme style class collision detected.");
|
|
@@ -236,12 +238,12 @@ function styleRuleFor(declarations: string): StyleRule {
|
|
|
236
238
|
}
|
|
237
239
|
|
|
238
240
|
function rememberStyleRule(entry: StyleRule): void {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
while (
|
|
242
|
-
const oldest =
|
|
241
|
+
styleCollisionCache.delete(entry.className);
|
|
242
|
+
styleCollisionCache.set(entry.className, entry);
|
|
243
|
+
while (styleCollisionCache.size > MAX_COLLISION_CACHE) {
|
|
244
|
+
const oldest = styleCollisionCache.keys().next().value;
|
|
243
245
|
if (oldest === undefined) break;
|
|
244
|
-
|
|
246
|
+
styleCollisionCache.delete(oldest);
|
|
245
247
|
}
|
|
246
248
|
}
|
|
247
249
|
|
|
@@ -265,10 +267,15 @@ function ensureStyleRegistry(nonce: string | undefined): StyleRegistry | null {
|
|
|
265
267
|
const current = documentRegistries.get(key);
|
|
266
268
|
if (current?.element.isConnected) return current;
|
|
267
269
|
|
|
270
|
+
const existingStyleElements = Array.from(
|
|
271
|
+
document.querySelectorAll<HTMLStyleElement>(`style[${STYLE_REGISTRY_ATTR}]`),
|
|
272
|
+
);
|
|
268
273
|
const styleElement =
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
274
|
+
existingStyleElements.find((element) => (element.nonce || undefined) === nonce) ??
|
|
275
|
+
(nonce === undefined && existingStyleElements.length === 1
|
|
276
|
+
? existingStyleElements[0]
|
|
277
|
+
: undefined) ??
|
|
278
|
+
document.createElement("style");
|
|
272
279
|
if (!styleElement.isConnected) {
|
|
273
280
|
styleElement.setAttribute(STYLE_REGISTRY_ATTR, "true");
|
|
274
281
|
if (nonce !== undefined) styleElement.nonce = nonce;
|
|
@@ -320,21 +327,3 @@ export function styleDeclarationsToClass(declarations: string | undefined): stri
|
|
|
320
327
|
|
|
321
328
|
return entry.className;
|
|
322
329
|
}
|
|
323
|
-
|
|
324
|
-
export function styleRulesForHtml(html: string): string[] {
|
|
325
|
-
const rules = new Map<string, string>();
|
|
326
|
-
const classAttributePattern = /\sclass=(?:"([^"]*)"|'([^']*)')/g;
|
|
327
|
-
|
|
328
|
-
for (const attribute of html.matchAll(classAttributePattern)) {
|
|
329
|
-
const value = attribute[1] ?? attribute[2] ?? "";
|
|
330
|
-
for (const className of value.split(/\s+/)) {
|
|
331
|
-
const entry = styleRulesByClass.get(className);
|
|
332
|
-
if (entry) rules.set(className, entry.rule);
|
|
333
|
-
if (rules.size > MAX_STYLE_RULES) {
|
|
334
|
-
throw new RangeError("Theme style registry capacity exceeded.");
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return Array.from(rules.values());
|
|
340
|
-
}
|
|
@@ -1,46 +1,13 @@
|
|
|
1
1
|
import type { JSX } from "@askrjs/askr/jsx-runtime";
|
|
2
2
|
import { classes } from "../_internal/classes";
|
|
3
|
-
import { mergeLayoutStyles } from "../_internal/block-layout";
|
|
3
|
+
import { mergeLayoutStyles, resolveSpaceValue, setResponsiveVar } from "../_internal/block-layout";
|
|
4
4
|
import { mergeProps } from "../_internal/merge-props";
|
|
5
5
|
import { intrinsicElement } from "../_internal/jsx";
|
|
6
6
|
import { styleDeclarationsToClass } from "../_internal/style";
|
|
7
|
-
import type { BlockSpace
|
|
7
|
+
import type { BlockSpace } from "../_internal/block-layout";
|
|
8
8
|
import type { GridAlign, GridColumns, GridElement, GridProps } from "./grid.types";
|
|
9
9
|
|
|
10
10
|
const DEFAULT_ELEMENT = "div";
|
|
11
|
-
const BREAKPOINTS: readonly LayoutBreakpoint[] = ["base", "sm", "md", "lg", "xl"];
|
|
12
|
-
|
|
13
|
-
const SPACE_TOKEN_MAP: Record<BlockSpace, string> = {
|
|
14
|
-
"0": "0",
|
|
15
|
-
xs: "var(--ak-space-xs)",
|
|
16
|
-
sm: "var(--ak-space-sm)",
|
|
17
|
-
md: "var(--ak-space-md)",
|
|
18
|
-
lg: "var(--ak-space-lg)",
|
|
19
|
-
xl: "var(--ak-space-xl)",
|
|
20
|
-
"2xl": "var(--ak-space-2xl)",
|
|
21
|
-
"3xl": "var(--ak-space-3xl)",
|
|
22
|
-
page: "var(--ak-layout-page-gutter)",
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
function isResponsiveValue<T>(
|
|
26
|
-
value: ResponsiveValue<T> | undefined,
|
|
27
|
-
): value is Partial<Record<LayoutBreakpoint, T>> {
|
|
28
|
-
return Boolean(
|
|
29
|
-
value &&
|
|
30
|
-
typeof value === "object" &&
|
|
31
|
-
!Array.isArray(value) &&
|
|
32
|
-
BREAKPOINTS.some((breakpoint) => breakpoint in value),
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function normalizeResponsiveValue<T>(
|
|
37
|
-
value: ResponsiveValue<T> | undefined,
|
|
38
|
-
): Partial<Record<LayoutBreakpoint, T>> | undefined {
|
|
39
|
-
if (value === undefined) return undefined;
|
|
40
|
-
if (isResponsiveValue(value)) return value;
|
|
41
|
-
return { base: value };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
11
|
function resolveColumns(value: GridColumns): string {
|
|
45
12
|
if (typeof value === "number") {
|
|
46
13
|
return `repeat(${value}, minmax(0, 1fr))`;
|
|
@@ -51,7 +18,7 @@ function resolveColumns(value: GridColumns): string {
|
|
|
51
18
|
}
|
|
52
19
|
|
|
53
20
|
function resolveGap(value: BlockSpace): string {
|
|
54
|
-
return
|
|
21
|
+
return String(resolveSpaceValue(value));
|
|
55
22
|
}
|
|
56
23
|
|
|
57
24
|
function resolveAlign(value: GridAlign): string {
|
|
@@ -60,24 +27,6 @@ function resolveAlign(value: GridAlign): string {
|
|
|
60
27
|
return value;
|
|
61
28
|
}
|
|
62
29
|
|
|
63
|
-
function setResponsiveVar<T>(
|
|
64
|
-
styles: Record<string, string | number>,
|
|
65
|
-
property: string,
|
|
66
|
-
value: ResponsiveValue<T> | undefined,
|
|
67
|
-
resolve: (value: T) => string | number | undefined,
|
|
68
|
-
) {
|
|
69
|
-
const normalized = normalizeResponsiveValue(value);
|
|
70
|
-
if (!normalized) return;
|
|
71
|
-
|
|
72
|
-
for (const breakpoint of BREAKPOINTS) {
|
|
73
|
-
const breakpointValue = normalized[breakpoint];
|
|
74
|
-
if (breakpointValue === undefined) continue;
|
|
75
|
-
const resolved = resolve(breakpointValue);
|
|
76
|
-
if (resolved === undefined || resolved === "") continue;
|
|
77
|
-
styles[`--ak-grid-${property}-${breakpoint}`] = resolved;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
30
|
export function Grid<TElement extends GridElement = "div">(
|
|
82
31
|
props: GridProps<TElement>,
|
|
83
32
|
): JSX.Element {
|
|
@@ -99,9 +48,9 @@ export function Grid<TElement extends GridElement = "div">(
|
|
|
99
48
|
};
|
|
100
49
|
|
|
101
50
|
const styles: Record<string, string | number> = {};
|
|
102
|
-
setResponsiveVar(styles, "columns", columns, resolveColumns);
|
|
103
|
-
setResponsiveVar(styles, "gap", gap, resolveGap);
|
|
104
|
-
setResponsiveVar(styles, "align", align, resolveAlign);
|
|
51
|
+
setResponsiveVar(styles, "grid-columns", columns, resolveColumns);
|
|
52
|
+
setResponsiveVar(styles, "grid-gap", gap, resolveGap);
|
|
53
|
+
setResponsiveVar(styles, "grid-align", align, resolveAlign);
|
|
105
54
|
|
|
106
55
|
const generatedClass = styleDeclarationsToClass(mergeLayoutStyles(styles, userStyle));
|
|
107
56
|
const finalProps = mergeProps(rest, {
|
|
@@ -5,11 +5,6 @@ declare global {
|
|
|
5
5
|
interface Element extends JSXElement {
|
|
6
6
|
readonly __askrThemesJsxElementBrand?: never;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
interface IntrinsicElements {
|
|
10
|
-
// @ts-ignore The Askr source types already provide a compatible fallback index.
|
|
11
|
-
[element: string]: Record<string, unknown>;
|
|
12
|
-
}
|
|
13
8
|
}
|
|
14
9
|
}
|
|
15
10
|
|