@askrjs/themes 0.0.8 → 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.
Files changed (35) hide show
  1. package/dist/components/_internal/box-layout.js +1 -1
  2. package/dist/components/_internal/jsx.js.map +1 -1
  3. package/dist/components/_internal/layout.js +2 -7
  4. package/dist/components/_internal/layout.js.map +1 -1
  5. package/dist/components/_internal/style.js +25 -1
  6. package/dist/components/_internal/style.js.map +1 -1
  7. package/dist/components/aspect-ratio/aspect-ratio.js +1 -1
  8. package/dist/components/navbar/navbar.js +23 -12
  9. package/dist/components/navbar/navbar.js.map +1 -1
  10. package/dist/components/shell/shell-responsive.js +50 -27
  11. package/dist/components/shell/shell-responsive.js.map +1 -1
  12. package/dist/components/sidebar/sidebar.js +38 -24
  13. package/dist/components/sidebar/sidebar.js.map +1 -1
  14. package/dist/components/spacer/spacer.js +1 -1
  15. package/dist/components/theme/theme.js +47 -10
  16. package/dist/components/theme/theme.js.map +1 -1
  17. package/dist/themes/default/index.css +19 -3
  18. package/package.json +5 -5
  19. package/src/components/_internal/jsx.ts +18 -6
  20. package/src/components/_internal/layout.ts +3 -12
  21. package/src/components/_internal/style.ts +42 -4
  22. package/src/components/navbar/navbar.tsx +32 -17
  23. package/src/components/shell/shell-responsive.tsx +82 -66
  24. package/src/components/sidebar/sidebar.tsx +52 -34
  25. package/src/components/theme/theme.tsx +80 -12
  26. package/src/themes/default/styles/display/avatar.css +7 -1
  27. package/src/themes/default/styles/display/badge.css +2 -1
  28. package/src/themes/default/styles/display/table.css +2 -0
  29. package/src/themes/default/styles/forms/select.css +6 -0
  30. package/src/themes/default/styles/shell/navbar.css +2 -1
  31. package/templates/theme/styles/display/avatar.css +7 -1
  32. package/templates/theme/styles/display/badge.css +2 -1
  33. package/templates/theme/styles/display/table.css +2 -0
  34. package/templates/theme/styles/forms/select.css +6 -0
  35. package/templates/theme/styles/shell/navbar.css +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"sidebar.js","names":[],"sources":["../../../src/components/sidebar/sidebar.tsx"],"sourcesContent":["import { state } from \"@askrjs/askr\";\nimport { classes } from \"../_internal/classes\";\nimport { isJsxElement, toChildArray } from \"../_internal/jsx\";\nimport { mergeProps } from \"../_internal/merge-props\";\nimport { NavBrand } from \"../navbar/navbar\";\nimport {\n isShellPanelChild,\n renderKeyedShellChildren,\n renderKeyedShellPanelChildren,\n ShellPanelWatcher,\n ShellResponsiveWatcher,\n} from \"../shell/shell-responsive\";\nimport { SidebarContext, SidebarResponsiveContext } from \"./sidebar.context\";\nimport { SidebarPanel } from \"./sidebar-panel\";\nimport { SidebarToggle } from \"./sidebar-toggle\";\nimport { isSidebarCollapsed, resolveSidebarCollapsible } from \"./sidebar.shared\";\nimport type { SidebarProps } from \"./sidebar.types\";\nimport type { CollapseIconPlacement } from \"../shell/shell-nav.types\";\n\ntype SidebarChildren = {\n brand?: unknown;\n items: unknown[];\n};\n\ntype SidebarToggleConfig = {\n collapsedIcon?: unknown;\n expandedIcon?: unknown;\n};\n\nfunction splitSidebarChildren(children: unknown[]): SidebarChildren {\n const brandIndex = children.findIndex((child) => isJsxElement(child) && child.type === NavBrand);\n\n if (brandIndex === -1) {\n return { items: children };\n }\n\n return {\n brand: children[brandIndex],\n items: children.filter((_, index) => index !== brandIndex),\n };\n}\n\nfunction isSidebarToggleChild(child: unknown): boolean {\n return isJsxElement(child) && child.type === SidebarToggle;\n}\n\nfunction splitSidebarToggle(children: unknown[]): {\n items: unknown[];\n toggle?: SidebarToggleConfig;\n} {\n const toggleIndex = children.findIndex((child) => isSidebarToggleChild(child));\n\n if (toggleIndex === -1) {\n return { items: children };\n }\n\n const toggleChild = children[toggleIndex] as { props?: SidebarToggleConfig } | undefined;\n\n return {\n items: children.filter((_, index) => index !== toggleIndex),\n toggle: toggleChild?.props,\n };\n}\n\nfunction renderSidebarMenuContents(\n label: string,\n icon: unknown,\n iconPlacement: CollapseIconPlacement,\n): JSX.Element {\n const renderedIcon = (\n <span class=\"sidebar-toggle-icon\" data-slot=\"sidebar-toggle-icon\" aria-hidden=\"true\">\n {icon ?? (\n <span class=\"sidebar-toggle-glyph sidebar-toggle-glyph--menu\">\n <span />\n <span />\n <span />\n </span>\n )}\n </span>\n );\n const renderedLabel = (\n <span class=\"sidebar-toggle-label\" data-slot=\"sidebar-toggle-label\">\n {label}\n </span>\n );\n\n if (iconPlacement === \"end\") {\n return (\n <>\n {renderedLabel}\n {renderedIcon}\n </>\n );\n }\n\n return (\n <>\n {renderedIcon}\n {renderedLabel}\n </>\n );\n}\n\nfunction renderSidebarRailContents(\n label: string,\n collapsed: boolean,\n toggle: SidebarToggleConfig | undefined,\n): JSX.Element {\n const icon = collapsed ? toggle?.collapsedIcon : toggle?.expandedIcon;\n\n return (\n <>\n <span\n class=\"sidebar-rail-toggle-icon\"\n data-slot=\"sidebar-rail-toggle-icon\"\n aria-hidden=\"true\"\n >\n {icon ?? (\n <span\n class={classes(\n \"sidebar-toggle-glyph\",\n \"sidebar-toggle-glyph--rail\",\n collapsed ? \"is-collapsed\" : \"is-expanded\",\n )}\n />\n )}\n </span>\n <span class=\"sidebar-rail-toggle-label\" data-slot=\"sidebar-rail-toggle-label\">\n {label}\n </span>\n </>\n );\n}\n\nexport function Sidebar(props: SidebarProps): JSX.Element {\n const {\n children,\n collapsed: controlledCollapsed,\n breakpoint,\n collapseIcon,\n collapseIconPlacement = \"start\",\n collapseLabel,\n collapsible,\n defaultCollapsed = false,\n id,\n \"aria-label\": ariaLabel,\n onCollapsedChange,\n class: className,\n ref,\n ...rest\n } = props;\n const effectiveCollapseLabel =\n collapseLabel ?? (typeof ariaLabel === \"string\" ? ariaLabel : undefined) ?? \"Menu\";\n const childItems = toChildArray(children);\n const explicitPanelItems = childItems.filter((child) => isShellPanelChild(child, SidebarPanel));\n const contentItems = childItems.filter((child) => !isShellPanelChild(child, SidebarPanel));\n const isResponsive = breakpoint !== undefined;\n const responsiveCollapsedState = state(isResponsive ? isSidebarCollapsed(breakpoint) : false);\n const drawerOpenState = state(false);\n const panelId = typeof id === \"string\" ? `${id}-panel` : undefined;\n const collapseBehavior = resolveSidebarCollapsible(collapsible);\n const isRailCollapsible = collapseBehavior === \"icon\";\n const railCollapsedState = state(defaultCollapsed);\n const responsiveCollapsed = responsiveCollapsedState();\n const drawerOpen = drawerOpenState();\n const internalRailCollapsed = railCollapsedState();\n const isMobile = isResponsive ? responsiveCollapsed : false;\n const isDrawerOpen = isResponsive && isMobile && drawerOpen;\n const isRailCollapsed = isRailCollapsible\n ? (controlledCollapsed ?? internalRailCollapsed ?? false)\n : false;\n const closeDrawer = () => drawerOpenState.set(false);\n const closeDrawerOnNavActivation = (event: MouseEvent) => {\n const target = event.target;\n\n if (!(target instanceof Element)) {\n return;\n }\n\n if (target.closest('a[data-slot=\"nav-link\"], a[data-slot=\"nav-item\"], a.navbar-item')) {\n closeDrawer();\n }\n };\n const toggleDrawer = () => {\n drawerOpenState.set(!drawerOpenState());\n };\n const setRailCollapsed = (nextCollapsed: boolean) => {\n if (!isRailCollapsible) {\n return;\n }\n\n if (controlledCollapsed === undefined) {\n railCollapsedState.set(nextCollapsed);\n }\n\n onCollapsedChange?.(nextCollapsed);\n };\n const toggleRail = () => {\n if (!isRailCollapsible) {\n return;\n }\n\n setRailCollapsed(!isRailCollapsed);\n };\n const { items: drawerSourceItems, toggle: railToggleConfig } = splitSidebarToggle(contentItems);\n const { brand: drawerBrand, items: drawerItems } = splitSidebarChildren(drawerSourceItems);\n const desktopChildren = renderKeyedShellChildren(drawerSourceItems, \"sidebar-desktop\");\n const drawerChildren = renderKeyedShellChildren(drawerItems, \"sidebar-drawer\");\n\n const contextValue = {\n active: () => isResponsive,\n collapseLabel: () => effectiveCollapseLabel,\n closePanel: closeDrawer,\n iconCollapsed: () => isRailCollapsed,\n isRailCollapsible: () => isRailCollapsible,\n orientation: () => \"vertical\" as const,\n panelId: () => panelId,\n panelOpen: () => isDrawerOpen,\n togglePanel: toggleDrawer,\n toggleRail,\n };\n const panelContext = {\n active: isResponsive,\n collapseLabel: effectiveCollapseLabel,\n onClose: closeDrawer,\n open: isDrawerOpen,\n panelId,\n };\n const panels =\n explicitPanelItems.length > 0 ? (\n renderKeyedShellPanelChildren(explicitPanelItems, \"sidebar-panel\", panelContext)\n ) : isResponsive ? (\n <SidebarPanel\n active={isResponsive}\n brand={drawerBrand}\n collapseLabel={effectiveCollapseLabel}\n onClose={closeDrawer}\n onClick={closeDrawerOnNavActivation}\n open={isDrawerOpen}\n panelId={panelId}\n >\n {drawerChildren}\n </SidebarPanel>\n ) : null;\n\n const finalProps = mergeProps(rest, {\n ref,\n \"aria-label\": ariaLabel,\n class: classes(\"sidebar\", className),\n \"data-collapse-below\": breakpoint,\n \"data-collapsible\": collapseBehavior === \"none\" ? undefined : collapseBehavior,\n \"data-icon-collapsed\": isRailCollapsible ? (isRailCollapsed ? \"true\" : \"false\") : undefined,\n \"data-mobile-open\": isResponsive ? (drawerOpen ? \"true\" : \"false\") : undefined,\n \"data-orientation\": \"vertical\",\n \"data-responsive-collapsed\": isMobile ? \"true\" : \"false\",\n \"data-slot\": \"sidebar\",\n });\n\n return (\n <SidebarContext.Scope value={{ orientation: () => \"vertical\" as const }}>\n <SidebarResponsiveContext.Scope value={contextValue}>\n <nav {...finalProps}>\n {isResponsive ? (\n <div class=\"sidebar-mobile\" data-slot=\"sidebar-mobile\">\n <button\n aria-controls={panelId}\n aria-expanded={isDrawerOpen ? \"true\" : \"false\"}\n aria-label={effectiveCollapseLabel}\n class=\"sidebar-toggle\"\n data-slot=\"sidebar-toggle\"\n data-state={isDrawerOpen ? \"open\" : \"closed\"}\n data-variant=\"mobile\"\n type=\"button\"\n onClick={toggleDrawer}\n >\n {renderSidebarMenuContents(\n effectiveCollapseLabel,\n collapseIcon,\n collapseIconPlacement,\n )}\n </button>\n </div>\n ) : null}\n\n <div class=\"sidebar-shell\" data-orientation=\"vertical\" data-slot=\"sidebar-shell\">\n {isRailCollapsible && !isMobile ? (\n <div\n class=\"sidebar-rail\"\n data-slot=\"sidebar-rail\"\n data-state={isRailCollapsed ? \"collapsed\" : \"expanded\"}\n >\n <button\n aria-expanded={isRailCollapsed ? \"false\" : \"true\"}\n aria-label={\n isRailCollapsed\n ? `Expand ${effectiveCollapseLabel}`\n : `Collapse ${effectiveCollapseLabel}`\n }\n class=\"sidebar-rail-toggle\"\n data-slot=\"sidebar-rail-toggle\"\n data-state={isRailCollapsed ? \"collapsed\" : \"expanded\"}\n type=\"button\"\n onClick={toggleRail}\n >\n {renderSidebarRailContents(\n effectiveCollapseLabel,\n isRailCollapsed,\n railToggleConfig,\n )}\n </button>\n </div>\n ) : null}\n {desktopChildren}\n </div>\n\n {panels}\n\n {isResponsive && isDrawerOpen ? (\n <button\n aria-label={`Close ${effectiveCollapseLabel}`}\n class=\"sidebar-backdrop\"\n data-slot=\"sidebar-backdrop\"\n data-state=\"open\"\n tabIndex={-1}\n type=\"button\"\n onClick={closeDrawer}\n />\n ) : null}\n\n {isResponsive ? (\n <ShellResponsiveWatcher\n breakpoint={breakpoint}\n isCollapsed={isSidebarCollapsed}\n onResize={(nextCollapsed) => {\n responsiveCollapsedState.set(nextCollapsed);\n }}\n onExpand={() => {\n drawerOpenState.set(false);\n }}\n />\n ) : null}\n\n <ShellPanelWatcher\n contentId={panelId}\n open={isDrawerOpen}\n panelSlot=\"sidebar-panel\"\n onClose={() => {\n closeDrawer();\n }}\n />\n </nav>\n </SidebarResponsiveContext.Scope>\n </SidebarContext.Scope>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sidebar.js","names":[],"sources":["../../../src/components/sidebar/sidebar.tsx"],"sourcesContent":["import { state } from \"@askrjs/askr\";\nimport { classes } from \"../_internal/classes\";\nimport { isJsxElement, toChildArray } from \"../_internal/jsx\";\nimport { mergeProps } from \"../_internal/merge-props\";\nimport { NavBrand } from \"../navbar/navbar\";\nimport {\n isShellPanelChild,\n renderKeyedShellChildren,\n renderKeyedShellPanelChildren,\n ShellPanelWatcher,\n ShellResponsiveWatcher,\n} from \"../shell/shell-responsive\";\nimport { SidebarContext, SidebarResponsiveContext } from \"./sidebar.context\";\nimport { SidebarPanel } from \"./sidebar-panel\";\nimport { SidebarToggle } from \"./sidebar-toggle\";\nimport { isSidebarCollapsed, resolveSidebarCollapsible } from \"./sidebar.shared\";\nimport type { SidebarProps } from \"./sidebar.types\";\nimport type { CollapseIconPlacement } from \"../shell/shell-nav.types\";\n\ntype SidebarChildren = {\n brand?: unknown;\n drawerItems: unknown[];\n drawerSourceItems: unknown[];\n explicitPanelItems: unknown[];\n toggle?: SidebarToggleConfig;\n};\n\ntype SidebarToggleConfig = {\n collapsedIcon?: unknown;\n expandedIcon?: unknown;\n};\n\nfunction isSidebarToggleChild(child: unknown): boolean {\n return isJsxElement(child) && child.type === SidebarToggle;\n}\n\nfunction splitSidebarChildren(children: unknown[]): SidebarChildren {\n const explicitPanelItems: unknown[] = [];\n const drawerSourceItems: unknown[] = [];\n let drawerItems: unknown[] | undefined;\n let brand: unknown;\n let hasBrand = false;\n let hasToggle = false;\n let toggle: SidebarToggleConfig | undefined;\n\n for (const child of children) {\n if (isShellPanelChild(child, SidebarPanel)) {\n explicitPanelItems.push(child);\n continue;\n }\n\n if (!hasToggle && isSidebarToggleChild(child)) {\n hasToggle = true;\n toggle = (child as { props?: SidebarToggleConfig } | undefined)?.props;\n continue;\n }\n\n drawerSourceItems.push(child);\n\n if (!hasBrand && isJsxElement(child) && child.type === NavBrand) {\n brand = child;\n hasBrand = true;\n drawerItems ??= drawerSourceItems.slice(0, drawerSourceItems.length - 1);\n continue;\n }\n\n drawerItems?.push(child);\n }\n\n return {\n brand: hasBrand ? brand : undefined,\n drawerItems: drawerItems ?? drawerSourceItems,\n drawerSourceItems,\n explicitPanelItems,\n toggle,\n };\n}\n\nfunction renderSidebarMenuContents(\n label: string,\n icon: unknown,\n iconPlacement: CollapseIconPlacement,\n): JSX.Element {\n const renderedIcon = (\n <span class=\"sidebar-toggle-icon\" data-slot=\"sidebar-toggle-icon\" aria-hidden=\"true\">\n {icon ?? (\n <span class=\"sidebar-toggle-glyph sidebar-toggle-glyph--menu\">\n <span />\n <span />\n <span />\n </span>\n )}\n </span>\n );\n const renderedLabel = (\n <span class=\"sidebar-toggle-label\" data-slot=\"sidebar-toggle-label\">\n {label}\n </span>\n );\n\n if (iconPlacement === \"end\") {\n return (\n <>\n {renderedLabel}\n {renderedIcon}\n </>\n );\n }\n\n return (\n <>\n {renderedIcon}\n {renderedLabel}\n </>\n );\n}\n\nfunction renderSidebarRailContents(\n label: string,\n collapsed: boolean,\n toggle: SidebarToggleConfig | undefined,\n): JSX.Element {\n const icon = collapsed ? toggle?.collapsedIcon : toggle?.expandedIcon;\n\n return (\n <>\n <span\n class=\"sidebar-rail-toggle-icon\"\n data-slot=\"sidebar-rail-toggle-icon\"\n aria-hidden=\"true\"\n >\n {icon ?? (\n <span\n class={classes(\n \"sidebar-toggle-glyph\",\n \"sidebar-toggle-glyph--rail\",\n collapsed ? \"is-collapsed\" : \"is-expanded\",\n )}\n />\n )}\n </span>\n <span class=\"sidebar-rail-toggle-label\" data-slot=\"sidebar-rail-toggle-label\">\n {label}\n </span>\n </>\n );\n}\n\nexport function Sidebar(props: SidebarProps): JSX.Element {\n const {\n children,\n collapsed: controlledCollapsed,\n breakpoint,\n collapseIcon,\n collapseIconPlacement = \"start\",\n collapseLabel,\n collapsible,\n defaultCollapsed = false,\n id,\n \"aria-label\": ariaLabel,\n onCollapsedChange,\n class: className,\n ref,\n ...rest\n } = props;\n const effectiveCollapseLabel =\n collapseLabel ?? (typeof ariaLabel === \"string\" ? ariaLabel : undefined) ?? \"Menu\";\n const childItems = toChildArray(children);\n const sidebarChildren = splitSidebarChildren(childItems);\n const isResponsive = breakpoint !== undefined;\n const responsiveCollapsedState = state(isResponsive ? isSidebarCollapsed(breakpoint) : false);\n const drawerOpenState = state(false);\n const panelId = typeof id === \"string\" ? `${id}-panel` : undefined;\n const collapseBehavior = resolveSidebarCollapsible(collapsible);\n const isRailCollapsible = collapseBehavior === \"icon\";\n const railCollapsedState = state(defaultCollapsed);\n const responsiveCollapsed = responsiveCollapsedState();\n const drawerOpen = drawerOpenState();\n const internalRailCollapsed = railCollapsedState();\n const isMobile = isResponsive ? responsiveCollapsed : false;\n const isDrawerOpen = isResponsive && isMobile && drawerOpen;\n const isRailCollapsed = isRailCollapsible\n ? (controlledCollapsed ?? internalRailCollapsed ?? false)\n : false;\n const closeDrawer = () => drawerOpenState.set(false);\n const closeDrawerOnNavActivation = (event: MouseEvent) => {\n const target = event.target;\n\n if (!(target instanceof Element)) {\n return;\n }\n\n if (target.closest('a[data-slot=\"nav-link\"], a[data-slot=\"nav-item\"], a.navbar-item')) {\n closeDrawer();\n }\n };\n const toggleDrawer = () => {\n drawerOpenState.set(!drawerOpenState());\n };\n const setRailCollapsed = (nextCollapsed: boolean) => {\n if (!isRailCollapsible) {\n return;\n }\n\n if (controlledCollapsed === undefined) {\n railCollapsedState.set(nextCollapsed);\n }\n\n onCollapsedChange?.(nextCollapsed);\n };\n const toggleRail = () => {\n if (!isRailCollapsible) {\n return;\n }\n\n setRailCollapsed(!isRailCollapsed);\n };\n const desktopChildren = renderKeyedShellChildren(\n sidebarChildren.drawerSourceItems,\n \"sidebar-desktop\",\n );\n const drawerChildren = renderKeyedShellChildren(sidebarChildren.drawerItems, \"sidebar-drawer\");\n\n const contextValue = {\n active: () => isResponsive,\n collapseLabel: () => effectiveCollapseLabel,\n closePanel: closeDrawer,\n iconCollapsed: () => isRailCollapsed,\n isRailCollapsible: () => isRailCollapsible,\n orientation: () => \"vertical\" as const,\n panelId: () => panelId,\n panelOpen: () => isDrawerOpen,\n togglePanel: toggleDrawer,\n toggleRail,\n };\n const panelContext = {\n active: isResponsive,\n collapseLabel: effectiveCollapseLabel,\n onClose: closeDrawer,\n open: isDrawerOpen,\n panelId,\n };\n const panels =\n sidebarChildren.explicitPanelItems.length > 0 ? (\n renderKeyedShellPanelChildren(\n sidebarChildren.explicitPanelItems,\n \"sidebar-panel\",\n panelContext,\n )\n ) : isResponsive ? (\n <SidebarPanel\n active={isResponsive}\n brand={sidebarChildren.brand}\n collapseLabel={effectiveCollapseLabel}\n onClose={closeDrawer}\n onClick={closeDrawerOnNavActivation}\n open={isDrawerOpen}\n panelId={panelId}\n >\n {drawerChildren}\n </SidebarPanel>\n ) : null;\n\n const finalProps = mergeProps(rest, {\n ref,\n \"aria-label\": ariaLabel,\n class: classes(\"sidebar\", className),\n \"data-collapse-below\": breakpoint,\n \"data-collapsible\": collapseBehavior === \"none\" ? undefined : collapseBehavior,\n \"data-icon-collapsed\": isRailCollapsible ? (isRailCollapsed ? \"true\" : \"false\") : undefined,\n \"data-mobile-open\": isResponsive ? (drawerOpen ? \"true\" : \"false\") : undefined,\n \"data-orientation\": \"vertical\",\n \"data-responsive-collapsed\": isMobile ? \"true\" : \"false\",\n \"data-slot\": \"sidebar\",\n });\n\n return (\n <SidebarContext.Scope value={{ orientation: () => \"vertical\" as const }}>\n <SidebarResponsiveContext.Scope value={contextValue}>\n <nav {...finalProps}>\n {isResponsive ? (\n <div class=\"sidebar-mobile\" data-slot=\"sidebar-mobile\">\n <button\n aria-controls={panelId}\n aria-expanded={isDrawerOpen ? \"true\" : \"false\"}\n aria-label={effectiveCollapseLabel}\n class=\"sidebar-toggle\"\n data-slot=\"sidebar-toggle\"\n data-state={isDrawerOpen ? \"open\" : \"closed\"}\n data-variant=\"mobile\"\n type=\"button\"\n onClick={toggleDrawer}\n >\n {renderSidebarMenuContents(\n effectiveCollapseLabel,\n collapseIcon,\n collapseIconPlacement,\n )}\n </button>\n </div>\n ) : null}\n\n <div class=\"sidebar-shell\" data-orientation=\"vertical\" data-slot=\"sidebar-shell\">\n {isRailCollapsible && !isMobile ? (\n <div\n class=\"sidebar-rail\"\n data-slot=\"sidebar-rail\"\n data-state={isRailCollapsed ? \"collapsed\" : \"expanded\"}\n >\n <button\n aria-expanded={isRailCollapsed ? \"false\" : \"true\"}\n aria-label={\n isRailCollapsed\n ? `Expand ${effectiveCollapseLabel}`\n : `Collapse ${effectiveCollapseLabel}`\n }\n class=\"sidebar-rail-toggle\"\n data-slot=\"sidebar-rail-toggle\"\n data-state={isRailCollapsed ? \"collapsed\" : \"expanded\"}\n type=\"button\"\n onClick={toggleRail}\n >\n {renderSidebarRailContents(\n effectiveCollapseLabel,\n isRailCollapsed,\n sidebarChildren.toggle,\n )}\n </button>\n </div>\n ) : null}\n {desktopChildren}\n </div>\n\n {panels}\n\n {isResponsive && isDrawerOpen ? (\n <button\n aria-label={`Close ${effectiveCollapseLabel}`}\n class=\"sidebar-backdrop\"\n data-slot=\"sidebar-backdrop\"\n data-state=\"open\"\n tabIndex={-1}\n type=\"button\"\n onClick={closeDrawer}\n />\n ) : null}\n\n {isResponsive ? (\n <ShellResponsiveWatcher\n breakpoint={breakpoint}\n isCollapsed={isSidebarCollapsed}\n onResize={(nextCollapsed) => {\n responsiveCollapsedState.set(nextCollapsed);\n }}\n onExpand={() => {\n drawerOpenState.set(false);\n }}\n />\n ) : null}\n\n <ShellPanelWatcher\n contentId={panelId}\n open={isDrawerOpen}\n panelSlot=\"sidebar-panel\"\n onClose={() => {\n closeDrawer();\n }}\n />\n </nav>\n </SidebarResponsiveContext.Scope>\n </SidebarContext.Scope>\n );\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { mergeProps } from "../_internal/merge-props.js";
2
- import { isCssLength, mergeLayoutStyles } from "../_internal/layout.js";
3
2
  import { styleDeclarationsToClass } from "../_internal/style.js";
3
+ import { isCssLength, mergeLayoutStyles } from "../_internal/layout.js";
4
4
  import { isJsxElement, toChildArray } from "../_internal/jsx.js";
5
5
  import { jsx } from "@askrjs/askr/jsx-runtime";
6
6
  import { Slot } from "@askrjs/askr/foundations";
@@ -1,6 +1,6 @@
1
1
  import { Button } from "@askrjs/ui";
2
2
  import { jsx, jsxs } from "@askrjs/askr/jsx-runtime";
3
- import { defineContext, readContext, state } from "@askrjs/askr";
3
+ import { defineContext, getSignal, readContext, state } from "@askrjs/askr";
4
4
  import { resource } from "@askrjs/askr/resources";
5
5
  //#region src/components/theme/theme.tsx
6
6
  const CAT_THEME_NAMES = [
@@ -47,6 +47,9 @@ const CAT_THEME_OPTIONS = [
47
47
  }
48
48
  ];
49
49
  const DEFAULT_STORAGE_KEY = "askr-theme";
50
+ let rootThemeRevision = 0;
51
+ let explicitRootThemeOwner;
52
+ const registeredProviderSignals = /* @__PURE__ */ new WeakSet();
50
53
  const ThemeContext = defineContext({
51
54
  theme: () => "system",
52
55
  setTheme: () => void 0,
@@ -58,11 +61,21 @@ function useTheme() {
58
61
  }
59
62
  function ThemeProvider(props) {
60
63
  const { children, defaultTheme = "system", themes = DEFAULT_THEME_OPTIONS, storageKey = DEFAULT_STORAGE_KEY } = props;
64
+ const providerId = state(Symbol("ThemeProvider"))();
65
+ const providerSignal = getSignal();
61
66
  const themeState = state(readStoredTheme(storageKey) ?? defaultTheme);
62
67
  const currentTheme = themeState();
68
+ if (!registeredProviderSignals.has(providerSignal)) {
69
+ registeredProviderSignals.add(providerSignal);
70
+ providerSignal.addEventListener("abort", () => {
71
+ if (explicitRootThemeOwner === providerId) explicitRootThemeOwner = void 0;
72
+ }, { once: true });
73
+ }
63
74
  const setTheme = (nextTheme) => {
64
75
  themeState.set(nextTheme);
65
76
  writeStoredTheme(storageKey, nextTheme);
77
+ explicitRootThemeOwner = providerId;
78
+ rootThemeRevision += 1;
66
79
  syncThemeRoot(nextTheme);
67
80
  };
68
81
  const value = {
@@ -73,21 +86,29 @@ function ThemeProvider(props) {
73
86
  };
74
87
  return /* @__PURE__ */ jsxs(ThemeContext.Scope, {
75
88
  value,
76
- children: [/* @__PURE__ */ jsx(ThemeRootSync, { theme: currentTheme }), /* @__PURE__ */ jsx("div", {
89
+ children: [/* @__PURE__ */ jsx(ThemeRootSync, {
90
+ providerId,
91
+ theme: currentTheme
92
+ }), /* @__PURE__ */ jsx("div", {
77
93
  "data-slot": "theme-provider",
78
94
  children
79
95
  })]
80
96
  });
81
97
  }
82
98
  function ThemeRootSync(props) {
83
- const { theme } = props;
99
+ const { providerId, theme } = props;
100
+ const expectedRootThemeRevision = rootThemeRevision;
84
101
  resource(({ signal }) => {
85
102
  if (typeof window === "undefined") {
86
103
  syncThemeRoot(theme);
87
104
  return null;
88
105
  }
89
106
  const timeoutId = window.setTimeout(() => {
90
- if (!signal.aborted) syncThemeRoot(theme);
107
+ if (!signal.aborted) syncThemeRoot(theme, {
108
+ expectedRevision: expectedRootThemeRevision,
109
+ passive: true,
110
+ providerId
111
+ });
91
112
  }, 0);
92
113
  signal.addEventListener("abort", () => {
93
114
  window.clearTimeout(timeoutId);
@@ -99,21 +120,34 @@ function ThemeRootSync(props) {
99
120
  function ThemePicker(props) {
100
121
  const theme = useTheme();
101
122
  const { themes = theme.themes, label = "Theme", ...rest } = props;
123
+ const currentTheme = theme.theme();
102
124
  return /* @__PURE__ */ jsx("select", {
103
125
  ...rest,
104
126
  "aria-label": rest["aria-label"] ?? label,
105
127
  "data-slot": "theme-picker",
106
- value: theme.theme(),
128
+ value: currentTheme,
107
129
  onChange: (event) => {
108
- const target = event.currentTarget;
109
- theme.setTheme(target.value);
130
+ const target = getThemePickerTarget(event);
131
+ if (target) theme.setTheme(target.value);
110
132
  },
111
133
  children: themes.map((option) => /* @__PURE__ */ jsx("option", {
112
134
  value: option.value,
135
+ selected: option.value === currentTheme,
113
136
  children: option.label
114
137
  }, option.value))
115
138
  });
116
139
  }
140
+ function getThemePickerTarget(event) {
141
+ if (typeof HTMLSelectElement === "undefined") return null;
142
+ const path = typeof event.composedPath === "function" ? event.composedPath() : [];
143
+ const candidates = [
144
+ event.target,
145
+ event.currentTarget,
146
+ ...path
147
+ ];
148
+ for (const candidate of candidates) if (candidate instanceof HTMLSelectElement) return candidate;
149
+ return null;
150
+ }
117
151
  function ThemeToggle(props) {
118
152
  const theme = useTheme();
119
153
  const { children, lightIcon, darkIcon, systemIcon, themes = ["light", "dark"], onPress, ...rest } = props;
@@ -138,7 +172,7 @@ function ThemeToggle(props) {
138
172
  "data-next-theme": nextTheme,
139
173
  onPress: (event) => {
140
174
  onPress?.(event);
141
- if (!event.defaultPrevented) theme.setTheme(nextTheme);
175
+ if (!event.defaultPrevented && !Object.is(nextTheme, currentTheme)) theme.setTheme(nextTheme);
142
176
  },
143
177
  children: /* @__PURE__ */ jsx("span", {
144
178
  "data-slot": "theme-toggle-content",
@@ -171,9 +205,11 @@ function cloneThemeToggleIcon(icon) {
171
205
  props: { ...props }
172
206
  };
173
207
  }
174
- function syncThemeRoot(themeChoice) {
208
+ function syncThemeRoot(themeChoice, options) {
175
209
  if (typeof document === "undefined") return;
176
210
  const html = document.documentElement;
211
+ if (options?.passive && options.expectedRevision !== void 0 && options.expectedRevision !== rootThemeRevision) return;
212
+ if (options?.passive && explicitRootThemeOwner !== void 0 && explicitRootThemeOwner !== options.providerId) return;
177
213
  if (themeChoice == null) {
178
214
  html.removeAttribute("data-theme");
179
215
  html.removeAttribute("data-theme-choice");
@@ -186,7 +222,8 @@ function syncThemeRoot(themeChoice) {
186
222
  function readStoredTheme(storageKey) {
187
223
  if (typeof window === "undefined") return void 0;
188
224
  try {
189
- return window.localStorage.getItem(storageKey) ?? void 0;
225
+ const storedTheme = window.localStorage.getItem(storageKey);
226
+ return storedTheme ? storedTheme : void 0;
190
227
  } catch {
191
228
  return;
192
229
  }
@@ -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 const currentTheme = themeState();\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 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 <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(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
- @media (width >= 48rem) {
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);
@@ -3679,16 +3680,22 @@
3679
3680
  }
3680
3681
 
3681
3682
  :where(.select-item, [data-slot="select-item"]) {
3683
+ appearance: none;
3684
+ inline-size: 100%;
3682
3685
  min-height: var(--ak-density-control-height-md);
3683
3686
  padding: 0 var(--ak-density-control-padding-x-md);
3684
3687
  border-radius: var(--ak-radius-md);
3685
3688
  color: var(--ak-color-text);
3686
3689
  cursor: pointer;
3690
+ font-family: inherit;
3687
3691
  font-size: var(--ak-font-size-sm);
3688
3692
  font-weight: var(--ak-font-weight-medium);
3689
3693
  overflow-wrap: anywhere;
3694
+ text-align: start;
3690
3695
  transition: background var(--ak-duration-fast) var(--ak-ease-standard),
3691
3696
  color var(--ak-duration-fast) var(--ak-ease-standard);
3697
+ background: none;
3698
+ border: 0;
3692
3699
  align-items: center;
3693
3700
  display: flex;
3694
3701
  }
@@ -4065,7 +4072,8 @@
4065
4072
  font-size: var(--ak-font-size-xs);
4066
4073
  font-weight: var(--ak-font-weight-semibold);
4067
4074
  letter-spacing: 0;
4068
- overflow-wrap: anywhere;
4075
+ overflow-wrap: normal;
4076
+ word-break: normal;
4069
4077
  align-items: center;
4070
4078
  gap: .35rem;
4071
4079
  padding: 0 .55rem;
@@ -4164,14 +4172,20 @@
4164
4172
  }
4165
4173
 
4166
4174
  :where(.avatar-fallback, [data-slot="avatar-fallback"]) {
4175
+ box-sizing: border-box;
4167
4176
  block-size: 100%;
4168
4177
  inline-size: 100%;
4178
+ min-inline-size: 0;
4179
+ max-inline-size: 100%;
4169
4180
  font-size: var(--ak-font-size-sm);
4170
4181
  font-weight: var(--ak-font-weight-semibold);
4171
- overflow-wrap: anywhere;
4182
+ overflow-wrap: normal;
4183
+ text-overflow: ellipsis;
4184
+ white-space: nowrap;
4172
4185
  place-items: center;
4173
4186
  line-height: 1;
4174
4187
  display: inline-grid;
4188
+ overflow: hidden;
4175
4189
  }
4176
4190
 
4177
4191
  :where([data-slot="separator"]) {
@@ -4248,7 +4262,9 @@
4248
4262
  color: var(--ak-color-text-muted);
4249
4263
  font-weight: var(--ak-font-weight-semibold);
4250
4264
  letter-spacing: .04em;
4265
+ overflow-wrap: normal;
4251
4266
  text-transform: uppercase;
4267
+ word-break: normal;
4252
4268
  white-space: normal;
4253
4269
  }
4254
4270
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askrjs/themes",
3
- "version": "0.0.8",
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",
@@ -144,13 +144,13 @@
144
144
  "@askrjs/ui": ">=0.0.1 <0.1.0",
145
145
  "@askrjs/vite": ">=0.0.1 <0.1.0",
146
146
  "@tsdown/css": "0.22.3",
147
- "@types/node": "^25.9.3",
147
+ "@types/node": "^26.0.0",
148
148
  "@vitest/browser-playwright": "^4.1.9",
149
149
  "cross-env": "^10.1.0",
150
150
  "jsdom": "^29.1.1",
151
151
  "playwright": "^1.61.0",
152
152
  "typescript": "^6.0.3",
153
- "vite-plus": "^0.2.0"
153
+ "vite-plus": "^0.2.1"
154
154
  },
155
155
  "peerDependencies": {
156
156
  "@askrjs/askr": ">=0.0.1 <0.1.0",
@@ -53,9 +53,11 @@ export function collectJsxElements(
53
53
  ): JSXElement[] {
54
54
  const result: JSXElement[] = [];
55
55
 
56
- const visit = (value: unknown) => {
56
+ function visit(value: unknown): void {
57
57
  if (Array.isArray(value)) {
58
- value.forEach(visit);
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.startsWith("on")) {
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].localeCompare(keys[0]) < 0) {
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((left, right) => left.localeCompare(right));
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 = objectToCssString(merged);
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 = Object.entries(style as Record<string, unknown>)
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
 
@@ -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
- * Extracts the first NavBrand child to display in the mobile panel.
18
- * NavBrand should be the first child of Navbar for it to appear in the mobile menu.
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 withoutNavbarBrand(children: unknown): unknown[] {
25
- return toChildArray(children).filter(
26
- (child) => !(isJsxElement(child) && child.type === NavBrand),
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={panelBrand}
149
+ brand={navbarChildren.brand}
135
150
  collapseLabel={effectiveCollapseLabel}
136
151
  onClose={closePanel}
137
152
  onClick={closePanelOnNavActivation}