@askrjs/themes 0.0.7 → 0.0.9
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/THEMING.md +5 -1
- package/dist/components/_internal/box-layout.js +1 -1
- package/dist/components/_internal/classes.js.map +1 -1
- package/dist/components/_internal/jsx.js.map +1 -1
- package/dist/components/_internal/layout.js +2 -7
- package/dist/components/_internal/layout.js.map +1 -1
- package/dist/components/_internal/style.js +25 -1
- package/dist/components/_internal/style.js.map +1 -1
- package/dist/components/aspect-ratio/aspect-ratio.js +1 -1
- package/dist/components/block/block.js.map +1 -1
- package/dist/components/container/container.js.map +1 -1
- package/dist/components/flex/flex.js.map +1 -1
- package/dist/components/nav/nav.js.map +1 -1
- package/dist/components/navbar/navbar-panel.js +10 -16
- package/dist/components/navbar/navbar-panel.js.map +1 -1
- package/dist/components/navbar/navbar.js +23 -12
- package/dist/components/navbar/navbar.js.map +1 -1
- package/dist/components/section/section.js.map +1 -1
- package/dist/components/shell/shell-responsive.js +51 -20
- package/dist/components/shell/shell-responsive.js.map +1 -1
- package/dist/components/sidebar/sidebar-panel.js +14 -24
- package/dist/components/sidebar/sidebar-panel.js.map +1 -1
- package/dist/components/sidebar/sidebar.js +38 -24
- package/dist/components/sidebar/sidebar.js.map +1 -1
- package/dist/components/spacer/spacer.js +1 -1
- package/dist/components/spacer/spacer.js.map +1 -1
- package/dist/components/theme/theme.js +55 -13
- package/dist/components/theme/theme.js.map +1 -1
- package/dist/themes/default/index.css +35 -3
- package/package.json +11 -10
- package/src/components/_internal/jsx.ts +18 -6
- package/src/components/_internal/layout.ts +3 -12
- package/src/components/_internal/style.ts +42 -4
- package/src/components/navbar/navbar-panel.tsx +11 -16
- package/src/components/navbar/navbar.tsx +32 -17
- package/src/components/shell/shell-responsive.tsx +79 -29
- package/src/components/sidebar/sidebar-panel.tsx +15 -24
- package/src/components/sidebar/sidebar.tsx +52 -34
- package/src/components/theme/theme.tsx +88 -16
- package/src/themes/default/styles/actions/button.css +16 -0
- package/src/themes/default/styles/display/avatar.css +7 -1
- package/src/themes/default/styles/display/badge.css +2 -1
- package/src/themes/default/styles/display/table.css +2 -0
- package/src/themes/default/styles/forms/select.css +6 -0
- package/src/themes/default/styles/shell/navbar.css +2 -1
- package/templates/theme/styles/actions/button.css +16 -0
- package/templates/theme/styles/display/avatar.css +7 -1
- package/templates/theme/styles/display/badge.css +2 -1
- package/templates/theme/styles/display/table.css +2 -0
- package/templates/theme/styles/forms/select.css +6 -0
- package/templates/theme/styles/shell/navbar.css +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.js","names":[],"sources":["../../../src/components/theme/theme.tsx"],"sourcesContent":["import { defineContext, readContext, state } from \"@askrjs/askr\";\nimport type { JSXElement } from \"@askrjs/askr/foundations/structures\";\nimport { resource } from \"@askrjs/askr/resources\";\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 ThemeContextValue = {\n theme: () => ThemeName;\n setTheme: (theme: ThemeName) => void;\n themes: readonly ThemeOption[];\n storageKey: string;\n};\n\nexport type ThemeProviderProps = {\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\";\n\nconst ThemeContext = defineContext<ThemeContextValue>({\n theme: () => \"system\",\n setTheme: () => undefined,\n themes: DEFAULT_THEME_OPTIONS,\n storageKey: DEFAULT_STORAGE_KEY,\n});\n\nexport function useTheme(): ThemeContextValue {\n return readContext(ThemeContext);\n}\n\nexport function ThemeProvider(props: ThemeProviderProps): JSX.Element {\n const {\n children,\n defaultTheme = \"system\",\n themes = DEFAULT_THEME_OPTIONS,\n storageKey = DEFAULT_STORAGE_KEY,\n } = props;\n\n const themeState = state<ThemeName>(readStoredTheme(storageKey) ?? defaultTheme);\n\n const setTheme = (nextTheme: ThemeName) => {\n themeState.set(nextTheme);\n writeStoredTheme(storageKey, nextTheme);\n syncThemeRoot(nextTheme);\n };\n\n const value: ThemeContextValue = {\n theme: themeState,\n setTheme,\n themes,\n storageKey,\n };\n\n const currentTheme = themeState();\n\n return (\n <ThemeContext.Scope value={value}>\n <ThemeRootSync theme={currentTheme} />\n <div data-slot=\"theme-provider\">{children}</div>\n </ThemeContext.Scope>\n );\n}\n\nfunction ThemeRootSync(props: { theme: ThemeName }): JSX.Element | null {\n const { theme } = props;\n\n resource(\n ({ signal }: { signal: AbortSignal }) => {\n if (typeof window === \"undefined\") {\n syncThemeRoot(theme);\n return null;\n }\n\n const timeoutId = window.setTimeout(() => {\n if (!signal.aborted) {\n syncThemeRoot(theme);\n }\n }, 0);\n\n signal.addEventListener(\n \"abort\",\n () => {\n window.clearTimeout(timeoutId);\n },\n { once: true },\n );\n\n return null;\n },\n [theme],\n );\n\n return null;\n}\n\nexport function ThemePicker(props: ThemePickerProps): JSX.Element {\n const theme = useTheme();\n const { themes = theme.themes, label = \"Theme\", ...rest } = props;\n\n return (\n <select\n {...rest}\n aria-label={rest[\"aria-label\"] ?? label}\n data-slot=\"theme-picker\"\n value={theme.theme()}\n onChange={(event: Event) => {\n const target = event.currentTarget as HTMLSelectElement;\n theme.setTheme(target.value as ThemeName);\n }}\n >\n {themes.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n );\n}\n\nexport function ThemeToggle(props: ThemeToggleProps): JSX.Element {\n const theme = useTheme();\n const {\n children,\n lightIcon,\n darkIcon,\n systemIcon,\n themes = [\"light\", \"dark\"],\n onPress,\n ...rest\n } = props;\n\n const currentTheme = theme.theme();\n const nextTheme = getNextTheme(currentTheme, themes);\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);\n const content =\n typeof children === \"function\" ? children(renderContext) : (children ?? renderedIcon);\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) theme.setTheme(nextTheme);\n }}\n >\n {content}\n </Button>\n );\n}\n\nfunction getNextTheme(currentTheme: ThemeName, themes: readonly ThemeName[]): ThemeName {\n if (themes.length === 0) return currentTheme;\n const index = themes.indexOf(currentTheme);\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 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): unknown {\n if (!isJSXElement(icon)) return icon;\n\n return {\n ...icon,\n props: { ...(icon.props as Record<string, unknown> | undefined) },\n };\n}\n\nfunction syncThemeRoot(themeChoice: ThemeName | null | undefined): void {\n if (typeof document === \"undefined\") {\n return;\n }\n\n const html = document.documentElement;\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 return (window.localStorage.getItem(storageKey) as ThemeName | null) ?? 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 { defineContext, getSignal, readContext, state } from \"@askrjs/askr\";\nimport type { JSXElement } from \"@askrjs/askr/foundations/structures\";\nimport { resource } from \"@askrjs/askr/resources\";\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 ThemeContextValue = {\n theme: () => ThemeName;\n setTheme: (theme: ThemeName) => void;\n themes: readonly ThemeOption[];\n storageKey: string;\n};\n\nexport type ThemeProviderProps = {\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\";\n// Passive mount sync should not overwrite a newer user/provider-initiated change.\nlet rootThemeRevision = 0;\nlet explicitRootThemeOwner: symbol | undefined;\nconst registeredProviderSignals = new WeakSet<AbortSignal>();\n\nconst ThemeContext = defineContext<ThemeContextValue>({\n theme: () => \"system\",\n setTheme: () => undefined,\n themes: DEFAULT_THEME_OPTIONS,\n storageKey: DEFAULT_STORAGE_KEY,\n});\n\nexport function useTheme(): ThemeContextValue {\n return readContext(ThemeContext);\n}\n\nexport function ThemeProvider(props: ThemeProviderProps): JSX.Element {\n const {\n children,\n defaultTheme = \"system\",\n themes = DEFAULT_THEME_OPTIONS,\n storageKey = DEFAULT_STORAGE_KEY,\n } = props;\n\n const providerId = state<symbol>(Symbol(\"ThemeProvider\"))();\n const providerSignal = getSignal();\n const themeState = state<ThemeName>(readStoredTheme(storageKey) ?? defaultTheme);\n const currentTheme = themeState();\n\n if (!registeredProviderSignals.has(providerSignal)) {\n registeredProviderSignals.add(providerSignal);\n providerSignal.addEventListener(\n \"abort\",\n () => {\n if (explicitRootThemeOwner === providerId) {\n explicitRootThemeOwner = undefined;\n }\n },\n { once: true },\n );\n }\n\n const setTheme = (nextTheme: ThemeName) => {\n themeState.set(nextTheme);\n writeStoredTheme(storageKey, nextTheme);\n explicitRootThemeOwner = providerId;\n rootThemeRevision += 1;\n syncThemeRoot(nextTheme);\n };\n\n const value: ThemeContextValue = {\n theme: themeState,\n setTheme,\n themes,\n storageKey,\n };\n\n return (\n <ThemeContext.Scope value={value}>\n <ThemeRootSync providerId={providerId} theme={currentTheme} />\n <div data-slot=\"theme-provider\">{children}</div>\n </ThemeContext.Scope>\n );\n}\n\nfunction ThemeRootSync(props: { providerId: symbol; theme: ThemeName }): JSX.Element | null {\n const { providerId, theme } = props;\n const expectedRootThemeRevision = rootThemeRevision;\n\n resource(\n ({ signal }: { signal: AbortSignal }) => {\n if (typeof window === \"undefined\") {\n syncThemeRoot(theme);\n return null;\n }\n\n const timeoutId = window.setTimeout(() => {\n if (!signal.aborted) {\n syncThemeRoot(theme, {\n expectedRevision: expectedRootThemeRevision,\n passive: true,\n providerId,\n });\n }\n }, 0);\n\n signal.addEventListener(\n \"abort\",\n () => {\n window.clearTimeout(timeoutId);\n },\n { once: true },\n );\n\n return null;\n },\n [theme],\n );\n\n return null;\n}\n\nexport function ThemePicker(props: ThemePickerProps): JSX.Element {\n const theme = useTheme();\n const { themes = theme.themes, label = \"Theme\", ...rest } = props;\n const currentTheme = theme.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 theme.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 theme = useTheme();\n const {\n children,\n lightIcon,\n darkIcon,\n systemIcon,\n themes = [\"light\", \"dark\"],\n onPress,\n ...rest\n } = props;\n\n const currentTheme = theme.theme();\n const nextTheme = getNextTheme(currentTheme, themes);\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);\n const content =\n typeof children === \"function\" ? children(renderContext) : (children ?? renderedIcon);\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 theme.setTheme(nextTheme);\n }\n }}\n >\n <span data-slot=\"theme-toggle-content\">{content}</span>\n </Button>\n );\n}\n\nfunction getNextTheme(currentTheme: ThemeName, themes: readonly ThemeName[]): ThemeName {\n if (themes.length === 0) return currentTheme;\n const index = themes.indexOf(currentTheme);\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 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): unknown {\n if (!isJSXElement(icon)) return icon;\n\n const props = icon.props as Record<string, unknown> | undefined;\n if (!props || Object.keys(props).length === 0) {\n return icon;\n }\n\n return {\n ...icon,\n props: { ...props },\n };\n}\n\nfunction syncThemeRoot(\n themeChoice: ThemeName | null | undefined,\n options?: { expectedRevision?: number; passive?: boolean; providerId?: symbol },\n): void {\n if (typeof document === \"undefined\") {\n return;\n }\n\n const html = document.documentElement;\n\n if (\n options?.passive &&\n options.expectedRevision !== undefined &&\n options.expectedRevision !== rootThemeRevision\n ) {\n return;\n }\n\n if (\n options?.passive &&\n explicitRootThemeOwner !== undefined &&\n explicitRootThemeOwner !== options.providerId\n ) {\n return;\n }\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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"}
|
|
@@ -1648,6 +1648,7 @@
|
|
|
1648
1648
|
display: flex;
|
|
1649
1649
|
position: relative;
|
|
1650
1650
|
overflow: hidden;
|
|
1651
|
+
container-type: inline-size;
|
|
1651
1652
|
}
|
|
1652
1653
|
|
|
1653
1654
|
:where(.navbar-shell, [data-slot="navbar-shell"]) {
|
|
@@ -1779,7 +1780,7 @@
|
|
|
1779
1780
|
margin-inline-start: 0;
|
|
1780
1781
|
}
|
|
1781
1782
|
|
|
1782
|
-
@
|
|
1783
|
+
@container (width >= 48rem) {
|
|
1783
1784
|
:where(.navbar-shell, [data-slot="navbar-shell"]):has( > :where(.navbar-group[data-align="center"], [data-slot="navbar-group"][data-align="center"])) {
|
|
1784
1785
|
align-items: center;
|
|
1785
1786
|
column-gap: var(--ak-space-md);
|
|
@@ -2628,6 +2629,22 @@
|
|
|
2628
2629
|
display: inline-flex;
|
|
2629
2630
|
}
|
|
2630
2631
|
|
|
2632
|
+
:where([data-slot="theme-toggle-content"]) {
|
|
2633
|
+
--_ak-theme-toggle-icon-size: var(--ak-theme-toggle-icon-size, var(--ak-icon-size, 1em));
|
|
2634
|
+
flex: none;
|
|
2635
|
+
justify-content: center;
|
|
2636
|
+
align-items: center;
|
|
2637
|
+
min-inline-size: 0;
|
|
2638
|
+
display: inline-flex;
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
:where([data-slot="theme-toggle-content"]) > :where([data-slot="icon"], svg) {
|
|
2642
|
+
inline-size: var(--_ak-theme-toggle-icon-size);
|
|
2643
|
+
block-size: var(--_ak-theme-toggle-icon-size);
|
|
2644
|
+
color: currentColor;
|
|
2645
|
+
flex: none;
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2631
2648
|
:where(.btn) > :where([data-slot="icon"], svg), :where([data-slot="button"]) > :where([data-slot="icon"], svg) {
|
|
2632
2649
|
color: currentColor;
|
|
2633
2650
|
flex: none;
|
|
@@ -3663,16 +3680,22 @@
|
|
|
3663
3680
|
}
|
|
3664
3681
|
|
|
3665
3682
|
:where(.select-item, [data-slot="select-item"]) {
|
|
3683
|
+
appearance: none;
|
|
3684
|
+
inline-size: 100%;
|
|
3666
3685
|
min-height: var(--ak-density-control-height-md);
|
|
3667
3686
|
padding: 0 var(--ak-density-control-padding-x-md);
|
|
3668
3687
|
border-radius: var(--ak-radius-md);
|
|
3669
3688
|
color: var(--ak-color-text);
|
|
3670
3689
|
cursor: pointer;
|
|
3690
|
+
font-family: inherit;
|
|
3671
3691
|
font-size: var(--ak-font-size-sm);
|
|
3672
3692
|
font-weight: var(--ak-font-weight-medium);
|
|
3673
3693
|
overflow-wrap: anywhere;
|
|
3694
|
+
text-align: start;
|
|
3674
3695
|
transition: background var(--ak-duration-fast) var(--ak-ease-standard),
|
|
3675
3696
|
color var(--ak-duration-fast) var(--ak-ease-standard);
|
|
3697
|
+
background: none;
|
|
3698
|
+
border: 0;
|
|
3676
3699
|
align-items: center;
|
|
3677
3700
|
display: flex;
|
|
3678
3701
|
}
|
|
@@ -4049,7 +4072,8 @@
|
|
|
4049
4072
|
font-size: var(--ak-font-size-xs);
|
|
4050
4073
|
font-weight: var(--ak-font-weight-semibold);
|
|
4051
4074
|
letter-spacing: 0;
|
|
4052
|
-
overflow-wrap:
|
|
4075
|
+
overflow-wrap: normal;
|
|
4076
|
+
word-break: normal;
|
|
4053
4077
|
align-items: center;
|
|
4054
4078
|
gap: .35rem;
|
|
4055
4079
|
padding: 0 .55rem;
|
|
@@ -4148,14 +4172,20 @@
|
|
|
4148
4172
|
}
|
|
4149
4173
|
|
|
4150
4174
|
:where(.avatar-fallback, [data-slot="avatar-fallback"]) {
|
|
4175
|
+
box-sizing: border-box;
|
|
4151
4176
|
block-size: 100%;
|
|
4152
4177
|
inline-size: 100%;
|
|
4178
|
+
min-inline-size: 0;
|
|
4179
|
+
max-inline-size: 100%;
|
|
4153
4180
|
font-size: var(--ak-font-size-sm);
|
|
4154
4181
|
font-weight: var(--ak-font-weight-semibold);
|
|
4155
|
-
overflow-wrap:
|
|
4182
|
+
overflow-wrap: normal;
|
|
4183
|
+
text-overflow: ellipsis;
|
|
4184
|
+
white-space: nowrap;
|
|
4156
4185
|
place-items: center;
|
|
4157
4186
|
line-height: 1;
|
|
4158
4187
|
display: inline-grid;
|
|
4188
|
+
overflow: hidden;
|
|
4159
4189
|
}
|
|
4160
4190
|
|
|
4161
4191
|
:where([data-slot="separator"]) {
|
|
@@ -4232,7 +4262,9 @@
|
|
|
4232
4262
|
color: var(--ak-color-text-muted);
|
|
4233
4263
|
font-weight: var(--ak-font-weight-semibold);
|
|
4234
4264
|
letter-spacing: .04em;
|
|
4265
|
+
overflow-wrap: normal;
|
|
4235
4266
|
text-transform: uppercase;
|
|
4267
|
+
word-break: normal;
|
|
4236
4268
|
white-space: normal;
|
|
4237
4269
|
}
|
|
4238
4270
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askrjs/themes",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Default theme tokens, styles, and themed layout wrappers for Askr apps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"askr",
|
|
@@ -124,11 +124,11 @@
|
|
|
124
124
|
"scripts": {
|
|
125
125
|
"new:theme": "node ./scripts/new-theme.js",
|
|
126
126
|
"clean": "npx --yes rimraf dist node_modules",
|
|
127
|
-
"bench": "npm run bench:tier2",
|
|
127
|
+
"bench": "npm run bench:tier1 && npm run bench:tier2 && npm run bench:tier3 && npm run bench:tier4",
|
|
128
128
|
"bench:tier1": "cross-env NODE_ENV=production vp test bench --run --reporter=default --config vitest.bench.tier1.config.ts",
|
|
129
129
|
"bench:tier2": "cross-env NODE_ENV=production vp test bench --run --reporter=default --config vitest.bench.tier2.config.ts",
|
|
130
130
|
"bench:tier3": "cross-env NODE_ENV=production vp test bench --run --reporter=default --config vitest.bench.tier3.config.ts",
|
|
131
|
-
"bench:tier4": "cross-env NODE_ENV=production vp test bench --run --reporter=default --config vitest.bench.tier4.config.ts",
|
|
131
|
+
"bench:tier4": "cross-env NODE_ENV=production vp test bench --run --reporter=default --config vitest.bench.tier4.config.ts --mode production",
|
|
132
132
|
"test": "npm run test:unit && npm run test:checks && npm run test:jsdom && npm run test:browser",
|
|
133
133
|
"test:unit": "vp test run -c vitest.test.unit.config.ts",
|
|
134
134
|
"test:jsdom": "cross-env NODE_ENV=production vp test run -c vitest.test.jsdom.config.ts",
|
|
@@ -142,21 +142,22 @@
|
|
|
142
142
|
"devDependencies": {
|
|
143
143
|
"@askrjs/askr": ">=0.0.1 <0.1.0",
|
|
144
144
|
"@askrjs/ui": ">=0.0.1 <0.1.0",
|
|
145
|
-
"@askrjs/vite": ">=0.0.
|
|
146
|
-
"@tsdown/css": "0.22.
|
|
147
|
-
"@types/node": "^
|
|
145
|
+
"@askrjs/vite": ">=0.0.1 <0.1.0",
|
|
146
|
+
"@tsdown/css": "0.22.3",
|
|
147
|
+
"@types/node": "^26.0.0",
|
|
148
|
+
"@vitest/browser-playwright": "^4.1.9",
|
|
148
149
|
"cross-env": "^10.1.0",
|
|
149
150
|
"jsdom": "^29.1.1",
|
|
150
|
-
"playwright": "^1.
|
|
151
|
+
"playwright": "^1.61.0",
|
|
151
152
|
"typescript": "^6.0.3",
|
|
152
|
-
"vite-plus": "^0.1
|
|
153
|
+
"vite-plus": "^0.2.1"
|
|
153
154
|
},
|
|
154
155
|
"peerDependencies": {
|
|
155
156
|
"@askrjs/askr": ">=0.0.1 <0.1.0",
|
|
156
|
-
"@askrjs/ui": ">=0.0.
|
|
157
|
+
"@askrjs/ui": ">=0.0.10 <0.1.0"
|
|
157
158
|
},
|
|
158
159
|
"engines": {
|
|
159
160
|
"node": ">=18"
|
|
160
161
|
},
|
|
161
|
-
"packageManager": "npm@11.
|
|
162
|
+
"packageManager": "npm@11.17.0"
|
|
162
163
|
}
|
|
@@ -53,9 +53,11 @@ export function collectJsxElements(
|
|
|
53
53
|
): JSXElement[] {
|
|
54
54
|
const result: JSXElement[] = [];
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
function visit(value: unknown): void {
|
|
57
57
|
if (Array.isArray(value)) {
|
|
58
|
-
value.
|
|
58
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
59
|
+
visit(value[index]);
|
|
60
|
+
}
|
|
59
61
|
return;
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -68,13 +70,23 @@ export function collectJsxElements(
|
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
visit(value.props?.children);
|
|
71
|
-
}
|
|
73
|
+
}
|
|
72
74
|
|
|
73
75
|
visit(children);
|
|
74
76
|
|
|
75
77
|
return result;
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
function isEventHandlerLikeKey(key: string): boolean {
|
|
81
|
+
return key.length >= 2 && key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function compareKeys(left: string, right: string): number {
|
|
85
|
+
if (left < right) return -1;
|
|
86
|
+
if (left > right) return 1;
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
78
90
|
function serializeArray(value: readonly unknown[]): string {
|
|
79
91
|
if (value.length === 0) {
|
|
80
92
|
return "";
|
|
@@ -101,7 +113,7 @@ function serializeSerializableProps(props: Record<string, unknown>): string {
|
|
|
101
113
|
const keys: string[] = [];
|
|
102
114
|
|
|
103
115
|
for (const key of Object.keys(props)) {
|
|
104
|
-
if (key === "children" || key === "ref" || key
|
|
116
|
+
if (key === "children" || key === "ref" || isEventHandlerLikeKey(key)) {
|
|
105
117
|
continue;
|
|
106
118
|
}
|
|
107
119
|
|
|
@@ -127,14 +139,14 @@ function serializeSerializableProps(props: Record<string, unknown>): string {
|
|
|
127
139
|
}
|
|
128
140
|
|
|
129
141
|
if (keys.length === 2) {
|
|
130
|
-
if (keys[1]
|
|
142
|
+
if (keys[1] < keys[0]) {
|
|
131
143
|
[keys[0], keys[1]] = [keys[1], keys[0]];
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
return `${keys[0]}:${String(props[keys[0]])},${keys[1]}:${String(props[keys[1]])}`;
|
|
135
147
|
}
|
|
136
148
|
|
|
137
|
-
keys.sort(
|
|
149
|
+
keys.sort(compareKeys);
|
|
138
150
|
|
|
139
151
|
let result = `${keys[0]}:${String(props[keys[0]])}`;
|
|
140
152
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { serializeCssDeclarations } from "./style";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Layout helpers shared by headless layout primitives.
|
|
3
5
|
*/
|
|
@@ -190,7 +192,7 @@ export function mergeLayoutStyles(
|
|
|
190
192
|
Object.assign(merged, user as Record<string, unknown>);
|
|
191
193
|
}
|
|
192
194
|
|
|
193
|
-
const mergedString =
|
|
195
|
+
const mergedString = serializeCssDeclarations(merged);
|
|
194
196
|
|
|
195
197
|
if (typeof user === "string" && user.trim()) {
|
|
196
198
|
return mergedString ? `${mergedString};${user.trim()}` : user.trim();
|
|
@@ -198,14 +200,3 @@ export function mergeLayoutStyles(
|
|
|
198
200
|
|
|
199
201
|
return mergedString || undefined;
|
|
200
202
|
}
|
|
201
|
-
|
|
202
|
-
function camelToKebab(s: string): string {
|
|
203
|
-
return s.replace(/([A-Z])/g, (c) => `-${c.toLowerCase()}`);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function objectToCssString(styles: Record<string, unknown>): string {
|
|
207
|
-
return Object.entries(styles)
|
|
208
|
-
.filter(([, value]) => value !== undefined && value !== null)
|
|
209
|
-
.map(([key, value]) => `${camelToKebab(key)}:${String(value)}`)
|
|
210
|
-
.join(";");
|
|
211
|
-
}
|
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
const cssPropertyNameCache = new Map<string, string>();
|
|
2
|
+
|
|
3
|
+
function cssPropertyName(name: string): string {
|
|
4
|
+
let cached = cssPropertyNameCache.get(name);
|
|
5
|
+
if (cached !== undefined) {
|
|
6
|
+
return cached;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let result = "";
|
|
10
|
+
|
|
11
|
+
for (let index = 0; index < name.length; index += 1) {
|
|
12
|
+
const code = name.charCodeAt(index);
|
|
13
|
+
|
|
14
|
+
if (code >= 65 && code <= 90) {
|
|
15
|
+
result += `-${String.fromCharCode(code + 32)}`;
|
|
16
|
+
} else {
|
|
17
|
+
result += name[index];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
cssPropertyNameCache.set(name, result);
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function serializeCssDeclarations(styles: Record<string, unknown>): string {
|
|
26
|
+
const keys = Object.keys(styles);
|
|
27
|
+
let result = "";
|
|
28
|
+
|
|
29
|
+
for (const key of keys) {
|
|
30
|
+
const value = styles[key];
|
|
31
|
+
if (value === undefined || value === null) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const declaration = `${cssPropertyName(key)}:${String(value)}`;
|
|
36
|
+
result = result ? `${result};${declaration}` : declaration;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
1
42
|
export function mergeCssVar(style: unknown, name: string, value: string): string {
|
|
2
43
|
const decl = `${name}:${value}`;
|
|
3
44
|
|
|
@@ -7,10 +48,7 @@ export function mergeCssVar(style: unknown, name: string, value: string): string
|
|
|
7
48
|
}
|
|
8
49
|
|
|
9
50
|
if (style && typeof style === "object") {
|
|
10
|
-
const entries =
|
|
11
|
-
.filter(([, v]) => v !== undefined && v !== null)
|
|
12
|
-
.map(([k, v]) => `${k.replace(/([A-Z])/g, (c) => `-${c.toLowerCase()}`)}:${String(v)}`)
|
|
13
|
-
.join(";");
|
|
51
|
+
const entries = serializeCssDeclarations(style as Record<string, unknown>);
|
|
14
52
|
return entries ? `${entries};${decl}` : decl;
|
|
15
53
|
}
|
|
16
54
|
|
|
@@ -3,6 +3,15 @@ import { renderShellPanel } from "../shell/shell-panel";
|
|
|
3
3
|
import { NavbarResponsiveContext } from "./navbar.context";
|
|
4
4
|
import type { NavbarPanelProps } from "./navbar.types";
|
|
5
5
|
|
|
6
|
+
const DEFAULT_NAVBAR_RESPONSIVE_CONTEXT = {
|
|
7
|
+
active: () => false,
|
|
8
|
+
collapseLabel: () => "Menu",
|
|
9
|
+
closePanel: () => undefined,
|
|
10
|
+
panelId: () => undefined,
|
|
11
|
+
panelOpen: () => false,
|
|
12
|
+
togglePanel: () => undefined,
|
|
13
|
+
};
|
|
14
|
+
|
|
6
15
|
export function NavbarPanel(props: NavbarPanelProps): JSX.Element | null {
|
|
7
16
|
const {
|
|
8
17
|
active: activeProp,
|
|
@@ -27,24 +36,10 @@ export function NavbarPanel(props: NavbarPanelProps): JSX.Element | null {
|
|
|
27
36
|
try {
|
|
28
37
|
return readContext(NavbarResponsiveContext);
|
|
29
38
|
} catch {
|
|
30
|
-
return
|
|
31
|
-
active: () => false,
|
|
32
|
-
collapseLabel: () => "Menu",
|
|
33
|
-
closePanel: () => undefined,
|
|
34
|
-
panelId: () => undefined,
|
|
35
|
-
panelOpen: () => false,
|
|
36
|
-
togglePanel: () => undefined,
|
|
37
|
-
};
|
|
39
|
+
return DEFAULT_NAVBAR_RESPONSIVE_CONTEXT;
|
|
38
40
|
}
|
|
39
41
|
})()
|
|
40
|
-
:
|
|
41
|
-
active: () => false,
|
|
42
|
-
collapseLabel: () => "Menu",
|
|
43
|
-
closePanel: () => undefined,
|
|
44
|
-
panelId: () => undefined,
|
|
45
|
-
panelOpen: () => false,
|
|
46
|
-
togglePanel: () => undefined,
|
|
47
|
-
};
|
|
42
|
+
: DEFAULT_NAVBAR_RESPONSIVE_CONTEXT;
|
|
48
43
|
const active = activeProp ?? responsive.active();
|
|
49
44
|
const open = openProp ?? responsive.panelOpen();
|
|
50
45
|
const panelId = panelIdProp ?? responsive.panelId();
|
|
@@ -13,18 +13,36 @@ import { NavbarResponsiveContext } from "./navbar.context";
|
|
|
13
13
|
import { isNavbarCollapsed, resolveNavGroupAlign } from "./navbar.shared";
|
|
14
14
|
import type { NavBrandProps, NavGroupProps, NavToggleProps, NavbarProps } from "./navbar.types";
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
function findNavbarBrand(children: unknown): unknown {
|
|
21
|
-
return toChildArray(children).find((child) => isJsxElement(child) && child.type === NavBrand);
|
|
22
|
-
}
|
|
16
|
+
type NavbarChildren = {
|
|
17
|
+
brand?: unknown;
|
|
18
|
+
panelItems: unknown[];
|
|
19
|
+
};
|
|
23
20
|
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
function splitNavbarChildren(children: unknown[]): NavbarChildren {
|
|
22
|
+
let brand: unknown;
|
|
23
|
+
let hasBrand = false;
|
|
24
|
+
let panelItems: unknown[] | undefined;
|
|
25
|
+
|
|
26
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
27
|
+
const child = children[index];
|
|
28
|
+
|
|
29
|
+
if (isJsxElement(child) && child.type === NavBrand) {
|
|
30
|
+
if (!hasBrand) {
|
|
31
|
+
brand = child;
|
|
32
|
+
hasBrand = true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
panelItems ??= children.slice(0, index);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
panelItems?.push(child);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
brand: hasBrand ? brand : undefined,
|
|
44
|
+
panelItems: panelItems ?? children,
|
|
45
|
+
};
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
/**
|
|
@@ -59,11 +77,9 @@ export function Navbar(props: NavbarProps): JSX.Element {
|
|
|
59
77
|
} = props;
|
|
60
78
|
const effectiveCollapseLabel = collapseLabel ?? "Menu";
|
|
61
79
|
const responsiveChildren = toChildArray(children);
|
|
80
|
+
const navbarChildren = splitNavbarChildren(responsiveChildren);
|
|
62
81
|
const desktopChildren = renderKeyedShellChildren(responsiveChildren, "navbar-desktop");
|
|
63
|
-
const panelChildren = renderKeyedShellChildren(
|
|
64
|
-
withoutNavbarBrand(responsiveChildren),
|
|
65
|
-
"navbar-panel",
|
|
66
|
-
);
|
|
82
|
+
const panelChildren = renderKeyedShellChildren(navbarChildren.panelItems, "navbar-panel");
|
|
67
83
|
// Extract brand to display in mobile panel header
|
|
68
84
|
const responsiveCollapsedState =
|
|
69
85
|
breakpoint !== undefined ? state(isNavbarCollapsed(breakpoint)) : undefined;
|
|
@@ -95,7 +111,6 @@ export function Navbar(props: NavbarProps): JSX.Element {
|
|
|
95
111
|
panelOpen: () => Boolean(responsiveCollapsedState?.() && mobileMenuOpen),
|
|
96
112
|
togglePanel,
|
|
97
113
|
};
|
|
98
|
-
const panelBrand = findNavbarBrand(responsiveChildren);
|
|
99
114
|
const finalProps = mergeProps(rest, {
|
|
100
115
|
"aria-label": ariaLabel,
|
|
101
116
|
ref,
|
|
@@ -131,7 +146,7 @@ export function Navbar(props: NavbarProps): JSX.Element {
|
|
|
131
146
|
{breakpoint !== undefined ? (
|
|
132
147
|
<NavbarPanel
|
|
133
148
|
active={breakpoint !== undefined}
|
|
134
|
-
brand={
|
|
149
|
+
brand={navbarChildren.brand}
|
|
135
150
|
collapseLabel={effectiveCollapseLabel}
|
|
136
151
|
onClose={closePanel}
|
|
137
152
|
onClick={closePanelOnNavActivation}
|
|
@@ -46,60 +46,110 @@ export function isShellPanelChild(child: unknown, panelComponent: unknown): chil
|
|
|
46
46
|
return isJsxElement(child) && child.type === panelComponent;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
function renderShellChildKey(child: unknown, index: number, keyPrefix: string): string {
|
|
50
|
+
return isJsxElement(child) && child.key != null ? String(child.key) : `${keyPrefix}-${index}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function keyMissingShellChildren(children: unknown[], keyPrefix: string): unknown[] {
|
|
54
|
+
let keyedChildren: unknown[] | undefined;
|
|
55
|
+
|
|
56
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
57
|
+
const child = children[index];
|
|
58
|
+
|
|
59
|
+
if (isJsxElement(child) && child.key == null) {
|
|
60
|
+
keyedChildren ??= children.slice(0, index);
|
|
61
|
+
keyedChildren.push({
|
|
62
|
+
...child,
|
|
63
|
+
key: `${keyPrefix}-${index}`,
|
|
64
|
+
});
|
|
65
|
+
continue;
|
|
53
66
|
}
|
|
54
67
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
keyedChildren?.push(child);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return keyedChildren ?? children;
|
|
72
|
+
}
|
|
60
73
|
|
|
74
|
+
function hasPanelProps(
|
|
75
|
+
props: Record<string, unknown> | undefined,
|
|
76
|
+
panelProps: ShellResponsivePanelProps,
|
|
77
|
+
): boolean {
|
|
61
78
|
return (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
>
|
|
68
|
-
{(child) => child as never}
|
|
69
|
-
</For>
|
|
79
|
+
props?.active === panelProps.active &&
|
|
80
|
+
props?.collapseLabel === panelProps.collapseLabel &&
|
|
81
|
+
props?.onClose === panelProps.onClose &&
|
|
82
|
+
props?.open === panelProps.open &&
|
|
83
|
+
props?.panelId === panelProps.panelId
|
|
70
84
|
);
|
|
71
85
|
}
|
|
72
86
|
|
|
73
|
-
|
|
74
|
-
children: unknown,
|
|
87
|
+
function applyShellPanelProps(
|
|
88
|
+
children: unknown[],
|
|
75
89
|
keyPrefix: string,
|
|
76
90
|
panelProps: ShellResponsivePanelProps,
|
|
77
|
-
):
|
|
78
|
-
|
|
91
|
+
): unknown[] {
|
|
92
|
+
let panelChildren: unknown[] | undefined;
|
|
93
|
+
|
|
94
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
95
|
+
const child = children[index];
|
|
96
|
+
|
|
79
97
|
if (!isJsxElement(child)) {
|
|
80
|
-
|
|
98
|
+
panelChildren?.push(child);
|
|
99
|
+
continue;
|
|
81
100
|
}
|
|
82
101
|
|
|
83
|
-
|
|
102
|
+
const props = child.props as Record<string, unknown> | undefined;
|
|
103
|
+
|
|
104
|
+
if (hasPanelProps(props, panelProps)) {
|
|
105
|
+
panelChildren?.push(child);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
panelChildren ??= children.slice(0, index);
|
|
110
|
+
panelChildren.push({
|
|
84
111
|
...child,
|
|
85
112
|
key: child.key != null ? child.key : `${keyPrefix}-${index}`,
|
|
86
113
|
props: {
|
|
87
|
-
...
|
|
114
|
+
...props,
|
|
88
115
|
active: panelProps.active,
|
|
89
116
|
collapseLabel: panelProps.collapseLabel,
|
|
90
117
|
onClose: panelProps.onClose,
|
|
91
118
|
open: panelProps.open,
|
|
92
119
|
panelId: panelProps.panelId,
|
|
93
120
|
},
|
|
94
|
-
};
|
|
95
|
-
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return panelChildren ?? children;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function renderKeyedShellChildren(children: unknown, keyPrefix: string): JSX.Element {
|
|
128
|
+
const childList = toChildArray(children);
|
|
129
|
+
const keyedChildren = keyMissingShellChildren(childList, keyPrefix);
|
|
96
130
|
|
|
97
131
|
return (
|
|
98
132
|
<For
|
|
99
133
|
each={() => keyedChildren}
|
|
100
|
-
by={(child, index) =>
|
|
101
|
-
|
|
102
|
-
}
|
|
134
|
+
by={(child, index) => renderShellChildKey(child, index, keyPrefix)}
|
|
135
|
+
>
|
|
136
|
+
{(child) => child as never}
|
|
137
|
+
</For>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function renderKeyedShellPanelChildren(
|
|
142
|
+
children: unknown,
|
|
143
|
+
keyPrefix: string,
|
|
144
|
+
panelProps: ShellResponsivePanelProps,
|
|
145
|
+
): JSX.Element {
|
|
146
|
+
const childList = toChildArray(children);
|
|
147
|
+
const keyedChildren = applyShellPanelProps(childList, keyPrefix, panelProps);
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<For
|
|
151
|
+
each={() => keyedChildren}
|
|
152
|
+
by={(child, index) => renderShellChildKey(child, index, keyPrefix)}
|
|
103
153
|
>
|
|
104
154
|
{(child) => child as never}
|
|
105
155
|
</For>
|