@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.
- package/dist/components/_internal/box-layout.js +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/navbar/navbar.js +23 -12
- package/dist/components/navbar/navbar.js.map +1 -1
- package/dist/components/shell/shell-responsive.js +50 -27
- package/dist/components/shell/shell-responsive.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/theme/theme.js +47 -10
- package/dist/components/theme/theme.js.map +1 -1
- package/dist/themes/default/index.css +19 -3
- package/package.json +5 -5
- 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.tsx +32 -17
- package/src/components/shell/shell-responsive.tsx +82 -66
- package/src/components/sidebar/sidebar.tsx +52 -34
- package/src/components/theme/theme.tsx +80 -12
- 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/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":"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, {
|
|
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:
|
|
128
|
+
value: currentTheme,
|
|
107
129
|
onChange: (event) => {
|
|
108
|
-
const target = event
|
|
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
|
-
|
|
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
|
-
@
|
|
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:
|
|
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:
|
|
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.
|
|
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": "^
|
|
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.
|
|
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
|
-
|
|
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
|
|
|
@@ -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}
|