@askrjs/themes 0.0.11 → 0.0.13

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 (190) hide show
  1. package/README.md +5 -5
  2. package/THEMING.md +2 -2
  3. package/dist/components/_internal/block-layout.d.ts +1 -2
  4. package/dist/components/_internal/block-layout.js +1 -3
  5. package/dist/components/_internal/block-layout.js.map +1 -1
  6. package/dist/components/_internal/classes.js +0 -2
  7. package/dist/components/_internal/jsx.js +9 -3
  8. package/dist/components/_internal/jsx.js.map +1 -1
  9. package/dist/components/_internal/merge-props.js +0 -2
  10. package/dist/components/_internal/pathname.js +0 -2
  11. package/dist/components/_internal/style.js +0 -2
  12. package/dist/components/alert/alert.d.ts +1 -3
  13. package/dist/components/alert/alert.js +0 -2
  14. package/dist/components/alert/alert.js.map +1 -1
  15. package/dist/components/alert/alert.types.d.ts +1 -3
  16. package/dist/components/alert/index.d.ts +2 -1
  17. package/dist/components/aside/aside.d.ts +1 -3
  18. package/dist/components/aside/aside.js +0 -2
  19. package/dist/components/aside/aside.types.d.ts +2 -2
  20. package/dist/components/aside/index.d.ts +2 -1
  21. package/dist/components/aspect-ratio/aspect-ratio.d.ts +1 -3
  22. package/dist/components/aspect-ratio/aspect-ratio.js +2 -6
  23. package/dist/components/aspect-ratio/aspect-ratio.js.map +1 -1
  24. package/dist/components/aspect-ratio/aspect-ratio.types.d.ts +1 -3
  25. package/dist/components/aspect-ratio/index.d.ts +2 -1
  26. package/dist/components/badge/badge.d.ts +1 -3
  27. package/dist/components/badge/badge.js +2 -6
  28. package/dist/components/badge/badge.js.map +1 -1
  29. package/dist/components/badge/badge.types.d.ts +1 -3
  30. package/dist/components/badge/index.d.ts +2 -1
  31. package/dist/components/block/block.d.ts +1 -3
  32. package/dist/components/block/block.js +2 -6
  33. package/dist/components/block/block.js.map +1 -1
  34. package/dist/components/block/block.types.d.ts +2 -4
  35. package/dist/components/block/index.d.ts +2 -1
  36. package/dist/components/brand/brand.d.ts +1 -3
  37. package/dist/components/brand/brand.js +4 -7
  38. package/dist/components/brand/brand.js.map +1 -1
  39. package/dist/components/brand/brand.types.d.ts +1 -3
  40. package/dist/components/brand/index.d.ts +2 -1
  41. package/dist/components/button-group/button-group.d.ts +1 -3
  42. package/dist/components/button-group/button-group.js +0 -2
  43. package/dist/components/button-group/button-group.js.map +1 -1
  44. package/dist/components/button-group/button-group.types.d.ts +1 -3
  45. package/dist/components/button-group/index.d.ts +2 -1
  46. package/dist/components/card/card.d.ts +1 -3
  47. package/dist/components/card/card.js +0 -2
  48. package/dist/components/card/card.types.d.ts +1 -3
  49. package/dist/components/card/index.d.ts +2 -1
  50. package/dist/components/catalog.d.ts +1 -3
  51. package/dist/components/catalog.js +2 -3
  52. package/dist/components/catalog.js.map +1 -1
  53. package/dist/components/close/close.d.ts +1 -3
  54. package/dist/components/close/close.js +0 -2
  55. package/dist/components/close/close.types.d.ts +1 -3
  56. package/dist/components/close/index.d.ts +2 -1
  57. package/dist/components/container/container.d.ts +1 -3
  58. package/dist/components/container/container.js +0 -2
  59. package/dist/components/container/container.types.d.ts +2 -2
  60. package/dist/components/container/index.d.ts +2 -1
  61. package/dist/components/empty-state/empty-state.d.ts +1 -3
  62. package/dist/components/empty-state/empty-state.js +0 -2
  63. package/dist/components/empty-state/empty-state.types.d.ts +2 -2
  64. package/dist/components/empty-state/index.d.ts +2 -1
  65. package/dist/components/field/field.d.ts +1 -3
  66. package/dist/components/field/field.js +0 -2
  67. package/dist/components/field/field.types.d.ts +1 -3
  68. package/dist/components/field/index.d.ts +2 -1
  69. package/dist/components/footer/footer.d.ts +1 -3
  70. package/dist/components/footer/footer.js +0 -2
  71. package/dist/components/footer/footer.types.d.ts +2 -3
  72. package/dist/components/footer/index.d.ts +2 -1
  73. package/dist/components/grid/grid.d.ts +1 -3
  74. package/dist/components/grid/grid.js +2 -7
  75. package/dist/components/grid/grid.js.map +1 -1
  76. package/dist/components/grid/grid.types.d.ts +1 -3
  77. package/dist/components/grid/index.d.ts +2 -1
  78. package/dist/components/header/header.d.ts +1 -3
  79. package/dist/components/header/header.js +0 -2
  80. package/dist/components/header/header.types.d.ts +2 -2
  81. package/dist/components/header/index.d.ts +2 -1
  82. package/dist/components/input-group/index.d.ts +2 -1
  83. package/dist/components/input-group/input-group.d.ts +1 -3
  84. package/dist/components/input-group/input-group.js +2 -6
  85. package/dist/components/input-group/input-group.js.map +1 -1
  86. package/dist/components/input-group/input-group.types.d.ts +1 -3
  87. package/dist/components/main/index.d.ts +2 -1
  88. package/dist/components/main/main.d.ts +1 -3
  89. package/dist/components/main/main.js +0 -2
  90. package/dist/components/main/main.types.d.ts +2 -2
  91. package/dist/components/nav/index.d.ts +2 -1
  92. package/dist/components/nav/nav.d.ts +1 -3
  93. package/dist/components/nav/nav.js +22 -17
  94. package/dist/components/nav/nav.js.map +1 -1
  95. package/dist/components/nav/nav.types.d.ts +1 -3
  96. package/dist/components/navbar/index.d.ts +3 -1
  97. package/dist/components/navbar/navbar.d.ts +1 -3
  98. package/dist/components/navbar/navbar.js +9 -10
  99. package/dist/components/navbar/navbar.js.map +1 -1
  100. package/dist/components/navbar/navbar.types.d.ts +2 -3
  101. package/dist/components/overlays/dropdown-content.d.ts +1 -3
  102. package/dist/components/overlays/dropdown-content.js +0 -2
  103. package/dist/components/page/index.d.ts +2 -1
  104. package/dist/components/page/page.d.ts +1 -3
  105. package/dist/components/page/page.js +0 -2
  106. package/dist/components/page/page.types.d.ts +2 -2
  107. package/dist/components/page-header/index.d.ts +2 -1
  108. package/dist/components/page-header/page-header.d.ts +1 -3
  109. package/dist/components/page-header/page-header.js +0 -2
  110. package/dist/components/page-header/page-header.types.d.ts +2 -2
  111. package/dist/components/section/index.d.ts +2 -1
  112. package/dist/components/section/section.d.ts +1 -3
  113. package/dist/components/section/section.js +0 -2
  114. package/dist/components/section/section.types.d.ts +2 -2
  115. package/dist/components/separator/index.d.ts +2 -1
  116. package/dist/components/separator/separator.d.ts +1 -3
  117. package/dist/components/separator/separator.js +2 -6
  118. package/dist/components/separator/separator.js.map +1 -1
  119. package/dist/components/separator/separator.types.d.ts +1 -3
  120. package/dist/components/sidebar/index.d.ts +2 -1
  121. package/dist/components/sidebar/sidebar.d.ts +2 -4
  122. package/dist/components/sidebar/sidebar.js +5 -10
  123. package/dist/components/sidebar/sidebar.js.map +1 -1
  124. package/dist/components/sidebar/sidebar.types.d.ts +2 -2
  125. package/dist/components/skeleton/index.d.ts +2 -1
  126. package/dist/components/skeleton/skeleton.d.ts +1 -3
  127. package/dist/components/skeleton/skeleton.js +8 -10
  128. package/dist/components/skeleton/skeleton.js.map +1 -1
  129. package/dist/components/skeleton/skeleton.types.d.ts +1 -3
  130. package/dist/components/spinner/index.d.ts +2 -1
  131. package/dist/components/spinner/spinner.d.ts +1 -3
  132. package/dist/components/spinner/spinner.js +0 -2
  133. package/dist/components/spinner/spinner.types.d.ts +1 -3
  134. package/dist/components/stat/index.d.ts +2 -1
  135. package/dist/components/stat/stat.d.ts +1 -3
  136. package/dist/components/stat/stat.js +0 -2
  137. package/dist/components/stat/stat.types.d.ts +1 -3
  138. package/dist/components/text/index.d.ts +2 -1
  139. package/dist/components/text/text.d.ts +1 -3
  140. package/dist/components/text/text.js +2 -7
  141. package/dist/components/text/text.js.map +1 -1
  142. package/dist/components/text/text.types.d.ts +1 -3
  143. package/dist/components/theme/index.d.ts +2 -1
  144. package/dist/components/theme/theme.d.ts +5 -7
  145. package/dist/components/theme/theme.js +102 -72
  146. package/dist/components/theme/theme.js.map +1 -1
  147. package/dist/components/toolbar/index.d.ts +2 -1
  148. package/dist/components/toolbar/toolbar.d.ts +1 -3
  149. package/dist/components/toolbar/toolbar.js +0 -2
  150. package/dist/components/toolbar/toolbar.types.d.ts +2 -2
  151. package/dist/components.d.ts +34 -4
  152. package/dist/components.js +4 -4
  153. package/dist/controls.d.ts +4 -0
  154. package/dist/core.d.ts +18 -2
  155. package/dist/navs.d.ts +1 -0
  156. package/dist/overlays.d.ts +2 -2
  157. package/dist/overlays.js +2 -2
  158. package/dist/surfaces.d.ts +8 -0
  159. package/dist/theme.d.ts +3 -2
  160. package/dist/theme.js +2 -2
  161. package/dist/themes/default/index.css +3 -3
  162. package/package.json +39 -16
  163. package/src/components/_internal/jsx.ts +11 -0
  164. package/src/components/alert/alert.tsx +1 -1
  165. package/src/components/aspect-ratio/aspect-ratio.tsx +2 -1
  166. package/src/components/badge/badge.tsx +2 -1
  167. package/src/components/block/block.tsx +2 -2
  168. package/src/components/brand/brand.tsx +3 -3
  169. package/src/components/button-group/button-group.tsx +1 -1
  170. package/src/components/catalog.tsx +3 -1
  171. package/src/components/grid/grid.tsx +2 -2
  172. package/src/components/input-group/input-group.tsx +3 -2
  173. package/src/components/nav/nav.tsx +25 -19
  174. package/src/components/navbar/navbar.tsx +22 -14
  175. package/src/components/separator/separator.tsx +2 -1
  176. package/src/components/sidebar/index.ts +1 -1
  177. package/src/components/sidebar/sidebar.tsx +7 -7
  178. package/src/components/skeleton/skeleton.tsx +8 -10
  179. package/src/components/text/text.tsx +2 -2
  180. package/src/components/theme/index.ts +4 -4
  181. package/src/components/theme/theme.tsx +135 -99
  182. package/src/components.ts +4 -4
  183. package/src/overlays.ts +3 -3
  184. package/src/theme.ts +4 -4
  185. package/src/themes/default/styles/display/coverage.css +1 -1
  186. package/src/themes/default/styles/overlays/toast.css +1 -1
  187. package/src/themes/default/styles/shell/sidebar.css +1 -1
  188. package/templates/theme/styles/display/coverage.css +1 -1
  189. package/templates/theme/styles/overlays/toast.css +1 -1
  190. package/templates/theme/styles/shell/sidebar.css +1 -1
@@ -6,6 +6,7 @@ import { DropdownContent } from "../overlays/dropdown-content";
6
6
  import type { NavBrandProps, NavDropdownProps, NavGroupProps, NavbarProps } from "./navbar.types";
7
7
 
8
8
  const ASKR_FRAGMENT = Symbol.for("askr.fragment");
9
+ const LayoutBlock = Block as (props: Record<string, unknown>) => JSX.Element;
9
10
 
10
11
  function flattenNavbarChildren(children: unknown): unknown[] {
11
12
  return toChildArray(children).flatMap((child) => {
@@ -45,7 +46,7 @@ export function Navbar(props: NavbarProps): JSX.Element {
45
46
 
46
47
  if (!collapseAt) {
47
48
  return (
48
- <Block
49
+ <LayoutBlock
49
50
  as="nav"
50
51
  direction="row"
51
52
  align="center"
@@ -55,7 +56,7 @@ export function Navbar(props: NavbarProps): JSX.Element {
55
56
  data-slot="navbar"
56
57
  >
57
58
  {children}
58
- </Block>
59
+ </LayoutBlock>
59
60
  );
60
61
  }
61
62
 
@@ -64,9 +65,9 @@ export function Navbar(props: NavbarProps): JSX.Element {
64
65
  const navChildren = navbarChildren.filter((child) => !isNavBrandChild(child));
65
66
  const hasNavContent = hasRenderableChildren(navChildren);
66
67
  const content = (
67
- <Block direction="row" align="center" gap="md" width="full" data-slot="navbar-content">
68
+ <LayoutBlock direction="row" align="center" gap="md" width="full" data-slot="navbar-content">
68
69
  {navChildren}
69
- </Block>
70
+ </LayoutBlock>
70
71
  );
71
72
  const collapse = hasNavContent ? (
72
73
  <details data-slot="navbar-collapse">
@@ -79,7 +80,7 @@ export function Navbar(props: NavbarProps): JSX.Element {
79
80
  ) : null;
80
81
 
81
82
  return (
82
- <Block
83
+ <LayoutBlock
83
84
  as="nav"
84
85
  direction="row"
85
86
  align="center"
@@ -90,7 +91,7 @@ export function Navbar(props: NavbarProps): JSX.Element {
90
91
  data-slot="navbar"
91
92
  >
92
93
  {[...brandChildren, collapse]}
93
- </Block>
94
+ </LayoutBlock>
94
95
  );
95
96
  }
96
97
 
@@ -98,9 +99,16 @@ export function NavBrand(props: NavBrandProps): JSX.Element {
98
99
  const { children, ...rest } = props;
99
100
 
100
101
  return (
101
- <Block direction="row" align="center" gap="sm" shrink={false} {...rest} data-slot="nav-brand">
102
+ <LayoutBlock
103
+ direction="row"
104
+ align="center"
105
+ gap="sm"
106
+ shrink={false}
107
+ {...rest}
108
+ data-slot="nav-brand"
109
+ >
102
110
  {children}
103
- </Block>
111
+ </LayoutBlock>
104
112
  );
105
113
  }
106
114
 
@@ -108,12 +116,12 @@ export function NavGroup(props: NavGroupProps): JSX.Element {
108
116
  const { align, children, label, title = label, ...rest } = props;
109
117
 
110
118
  return (
111
- <Block align={align} gap="sm" {...rest} data-align={align} data-slot="nav-group">
119
+ <LayoutBlock align={align} gap="sm" {...rest} data-align={align} data-slot="nav-group">
112
120
  {title !== undefined ? <div data-slot="nav-group-label">{title}</div> : null}
113
- <Block gap="xs" data-slot="nav-group-body">
121
+ <LayoutBlock gap="xs" data-slot="nav-group-body">
114
122
  {children}
115
- </Block>
116
- </Block>
123
+ </LayoutBlock>
124
+ </LayoutBlock>
117
125
  );
118
126
  }
119
127
 
@@ -121,7 +129,7 @@ export function NavDropdown(props: NavDropdownProps): JSX.Element {
121
129
  const { align = "end", children, label, side = "bottom", sideOffset = 6, ...rest } = props;
122
130
 
123
131
  return (
124
- <Block direction="row" align="center" data-slot="nav-dropdown">
132
+ <LayoutBlock direction="row" align="center" data-slot="nav-dropdown">
125
133
  <Dropdown {...rest}>
126
134
  <DropdownTrigger class={classes("nav-item")} data-slot="nav-dropdown-trigger">
127
135
  {label}
@@ -136,6 +144,6 @@ export function NavDropdown(props: NavDropdownProps): JSX.Element {
136
144
  {children}
137
145
  </DropdownContent>
138
146
  </Dropdown>
139
- </Block>
147
+ </LayoutBlock>
140
148
  );
141
149
  }
@@ -1,5 +1,6 @@
1
1
  import { Slot } from "@askrjs/askr/foundations";
2
2
  import { mergeProps } from "../_internal/merge-props";
3
+ import { intrinsicElement } from "../_internal/jsx";
3
4
  import type { SeparatorAsChildProps, SeparatorNativeProps } from "./separator.types";
4
5
 
5
6
  export function Separator(props: SeparatorNativeProps): JSX.Element;
@@ -19,5 +20,5 @@ export function Separator(props: SeparatorNativeProps | SeparatorAsChildProps) {
19
20
  return <Slot asChild {...finalProps} children={children} />;
20
21
  }
21
22
 
22
- return <div {...finalProps}>{children}</div>;
23
+ return intrinsicElement("div", finalProps, children);
23
24
  }
@@ -12,7 +12,7 @@ export {
12
12
  SidebarMenuBadge,
13
13
  SidebarMenuButton,
14
14
  SidebarMenuItem,
15
- SidebarProvider,
15
+ SidebarScope,
16
16
  SidebarRail,
17
17
  SidebarTrigger,
18
18
  } from "./sidebar";
@@ -2,6 +2,7 @@ import { Slot } from "@askrjs/askr/foundations";
2
2
  import { Block } from "../block";
3
3
  import { classes } from "../_internal/classes";
4
4
  import { mergeProps } from "../_internal/merge-props";
5
+ import { intrinsicElement } from "../_internal/jsx";
5
6
  import type {
6
7
  SidebarButtonProps,
7
8
  SidebarPartProps,
@@ -25,7 +26,6 @@ function sidebarPart(
25
26
  element: keyof JSX.IntrinsicElements = "div",
26
27
  ): JSX.Element {
27
28
  const { as, asChild, children, class: className, ...rest } = props;
28
- const Element = (as ?? element) as "div";
29
29
  const finalProps = mergeProps(rest, {
30
30
  class: className,
31
31
  "data-slot": slot,
@@ -35,7 +35,7 @@ function sidebarPart(
35
35
  return <Slot asChild {...finalProps} children={children as JSX.Element} />;
36
36
  }
37
37
 
38
- return <Element {...finalProps}>{children}</Element>;
38
+ return intrinsicElement(String(as ?? element), finalProps, children);
39
39
  }
40
40
 
41
41
  export function Sidebar(props: SidebarProps): JSX.Element {
@@ -69,8 +69,8 @@ export function Sidebar(props: SidebarProps): JSX.Element {
69
69
  );
70
70
  }
71
71
 
72
- export function SidebarProvider(props: SidebarPartProps): JSX.Element {
73
- return sidebarPart(props, "sidebar-provider");
72
+ export function SidebarScope(props: SidebarPartProps): JSX.Element {
73
+ return sidebarPart(props, "sidebar-scope");
74
74
  }
75
75
 
76
76
  export function SidebarInset(props: SidebarPartProps): JSX.Element {
@@ -136,7 +136,7 @@ export function SidebarMenuButton(props: SidebarButtonProps): JSX.Element {
136
136
  }
137
137
 
138
138
  return (
139
- <button type={type} {...finalProps}>
139
+ <button type={type as "button" | "submit" | "reset" | undefined} {...finalProps}>
140
140
  {children}
141
141
  </button>
142
142
  );
@@ -146,7 +146,7 @@ export function SidebarMenuAction(props: SidebarButtonProps): JSX.Element {
146
146
  const { children, class: className, tooltip, tooltipSide, type = "button", ...rest } = props;
147
147
  return (
148
148
  <button
149
- type={type}
149
+ type={type as "button" | "submit" | "reset" | undefined}
150
150
  {...mergeProps(rest, {
151
151
  class: classes(className),
152
152
  "data-slot": "sidebar-menu-action",
@@ -170,7 +170,7 @@ export function SidebarTrigger(props: SidebarButtonProps): JSX.Element {
170
170
  const { children, class: className, tooltip, tooltipSide, type = "button", ...rest } = props;
171
171
  return (
172
172
  <button
173
- type={type}
173
+ type={type as "button" | "submit" | "reset" | undefined}
174
174
  {...mergeProps(rest, {
175
175
  class: classes("btn btn-ghost btn-icon", className),
176
176
  "data-slot": "sidebar-trigger",
@@ -2,6 +2,7 @@ import { Slot } from "@askrjs/askr/foundations";
2
2
  import { mergeLayoutStyles } from "../_internal/block-layout";
3
3
  import { classes } from "../_internal/classes";
4
4
  import { mergeProps } from "../_internal/merge-props";
5
+ import { intrinsicElement } from "../_internal/jsx";
5
6
  import { styleDeclarationsToClass } from "../_internal/style";
6
7
  import type { SkeletonAsChildProps, SkeletonProps } from "./skeleton.types";
7
8
 
@@ -28,15 +29,12 @@ export function Skeleton(props: SkeletonProps | SkeletonAsChildProps) {
28
29
  className?: unknown;
29
30
  style?: unknown;
30
31
  };
31
- const layoutClass = styleDeclarationsToClass(
32
- mergeLayoutStyles(
33
- {
34
- blockSize: resolveSkeletonDimension(height),
35
- inlineSize: resolveSkeletonDimension(width),
36
- },
37
- style,
38
- ),
39
- );
32
+ const dimensions: Record<string, string | number> = {};
33
+ const blockSize = resolveSkeletonDimension(height);
34
+ const inlineSize = resolveSkeletonDimension(width);
35
+ if (blockSize !== undefined) dimensions.blockSize = blockSize;
36
+ if (inlineSize !== undefined) dimensions.inlineSize = inlineSize;
37
+ const layoutClass = styleDeclarationsToClass(mergeLayoutStyles(dimensions, style));
40
38
  const finalProps = mergeProps(rest, {
41
39
  ref,
42
40
  class: classes(classProp, className, layoutClass),
@@ -49,5 +47,5 @@ export function Skeleton(props: SkeletonProps | SkeletonAsChildProps) {
49
47
  return <Slot asChild {...finalProps} children={children} />;
50
48
  }
51
49
 
52
- return <div {...finalProps}>{children}</div>;
50
+ return intrinsicElement("div", finalProps, children);
53
51
  }
@@ -1,5 +1,6 @@
1
1
  import { classes } from "../_internal/classes";
2
2
  import { mergeProps } from "../_internal/merge-props";
3
+ import { intrinsicElement } from "../_internal/jsx";
3
4
  import type { TextElement, TextProps } from "./text.types";
4
5
 
5
6
  const DEFAULT_ELEMENT = "p";
@@ -34,6 +35,5 @@ export function Text<TElement extends TextElement = "p">(props: TextProps<TEleme
34
35
  "data-truncate": truncate ? "true" : undefined,
35
36
  });
36
37
 
37
- const Element = (as ?? DEFAULT_ELEMENT) as keyof JSX.IntrinsicElements;
38
- return <Element {...finalProps}>{children}</Element>;
38
+ return intrinsicElement(as ?? DEFAULT_ELEMENT, finalProps, children);
39
39
  }
@@ -3,17 +3,17 @@ export {
3
3
  CAT_THEME_OPTIONS,
4
4
  DEFAULT_THEME_OPTIONS,
5
5
  ThemePicker,
6
- ThemeProvider,
6
+ ThemeScope,
7
7
  ThemeToggle,
8
- useTheme,
8
+ theme,
9
9
  } from "./theme";
10
10
  export type {
11
11
  CatThemeName,
12
- ThemeContextValue,
12
+ ThemeScopeValue,
13
13
  ThemeName,
14
14
  ThemeOption,
15
15
  ThemePickerProps,
16
- ThemeProviderProps,
16
+ ThemeScopeProps,
17
17
  ThemeToggleProps,
18
18
  ThemeToggleRenderContext,
19
19
  } from "./theme";
@@ -1,6 +1,5 @@
1
- import { defineContext, getSignal, readContext, state } from "@askrjs/askr";
1
+ import { defineScope, getSignal, readScope, state } from "@askrjs/askr";
2
2
  import type { JSXElement } from "@askrjs/askr/foundations/structures";
3
- import { resource } from "@askrjs/askr/resources";
4
3
  import { Button } from "@askrjs/ui";
5
4
  import type { ButtonNativeProps, PressEvent } from "@askrjs/ui";
6
5
 
@@ -14,14 +13,14 @@ export type ThemeOption = {
14
13
  label: string;
15
14
  };
16
15
 
17
- export type ThemeContextValue = {
16
+ export type ThemeScopeValue = {
18
17
  theme: () => ThemeName;
19
18
  setTheme: (theme: ThemeName) => void;
20
19
  themes: readonly ThemeOption[];
21
20
  storageKey: string;
22
21
  };
23
22
 
24
- export type ThemeProviderProps = {
23
+ export type ThemeScopeProps = {
25
24
  children?: unknown;
26
25
  defaultTheme?: ThemeName;
27
26
  themes?: readonly ThemeOption[];
@@ -67,23 +66,26 @@ export const CAT_THEME_OPTIONS: readonly ThemeOption[] = [
67
66
  const DEFAULT_STORAGE_KEY = "askr-theme";
68
67
  const STATIC_CHILDREN = Symbol.for("askr.static-children");
69
68
  const STATIC_CHILD_SLOTS_CACHE = Symbol.for("__askrStaticChildSlots");
70
- // Passive mount sync should not overwrite a newer user/provider-initiated change.
71
- let rootThemeRevision = 0;
72
- let explicitRootThemeOwner: symbol | undefined;
73
- const registeredProviderSignals = new WeakSet<AbortSignal>();
69
+ type ThemeCoordinator = ReturnType<typeof createThemeCoordinator>;
70
+ type InternalThemeScopeValue = ThemeScopeValue & {
71
+ readonly coordinator: ThemeCoordinator | null;
72
+ readonly depth: number;
73
+ };
74
74
 
75
- const ThemeContext = defineContext<ThemeContextValue>({
75
+ const ThemeScopeContext = defineScope<InternalThemeScopeValue>({
76
76
  theme: () => "system",
77
77
  setTheme: () => undefined,
78
78
  themes: DEFAULT_THEME_OPTIONS,
79
79
  storageKey: DEFAULT_STORAGE_KEY,
80
+ coordinator: null,
81
+ depth: -1,
80
82
  });
81
83
 
82
- export function useTheme(): ThemeContextValue {
83
- return readContext(ThemeContext);
84
+ export function theme(): ThemeScopeValue {
85
+ return readScope(ThemeScopeContext);
84
86
  }
85
87
 
86
- export function ThemeProvider(props: ThemeProviderProps): JSX.Element {
88
+ export function ThemeScope(props: ThemeScopeProps): JSX.Element {
87
89
  const {
88
90
  children,
89
91
  defaultTheme = "system",
@@ -91,88 +93,142 @@ export function ThemeProvider(props: ThemeProviderProps): JSX.Element {
91
93
  storageKey = DEFAULT_STORAGE_KEY,
92
94
  } = props;
93
95
 
94
- const providerId = state<symbol>(Symbol("ThemeProvider"))();
95
- const providerSignal = getSignal();
96
- const themeState = state<ThemeName>(readStoredTheme(storageKey) ?? defaultTheme);
96
+ const scopeId = state<symbol>(Symbol("ThemeScope"))();
97
+ const scopeSignal = getSignal();
98
+ // The first render must be identical on the server and in the browser.
99
+ // Browser persistence is adopted from the committed root ref, after Askr's
100
+ // hydration verifier has accepted the server markup.
101
+ const themeState = state<ThemeName>(defaultTheme);
102
+ const persistenceAdoption = state({ complete: false })();
97
103
  const currentTheme = themeState();
98
-
99
- if (!registeredProviderSignals.has(providerSignal)) {
100
- registeredProviderSignals.add(providerSignal);
101
- providerSignal.addEventListener(
102
- "abort",
103
- () => {
104
- if (explicitRootThemeOwner === providerId) {
105
- explicitRootThemeOwner = undefined;
106
- }
107
- },
108
- { once: true },
109
- );
110
- }
104
+ const parentScope = readScope(ThemeScopeContext);
105
+ const ownedCoordinator = state<ThemeCoordinator>(createThemeCoordinator())();
106
+ const coordinator = parentScope.coordinator ?? ownedCoordinator;
107
+ const scopeDepth = parentScope.depth + 1;
108
+ coordinator.register(scopeId, scopeDepth, currentTheme, scopeSignal);
111
109
 
112
110
  const setTheme = (nextTheme: ThemeName) => {
113
111
  themeState.set(nextTheme);
114
112
  writeStoredTheme(storageKey, nextTheme);
115
- explicitRootThemeOwner = providerId;
116
- rootThemeRevision += 1;
117
- syncThemeRoot(nextTheme);
113
+ coordinator.activate(scopeId, nextTheme);
118
114
  };
119
115
 
120
- const value: ThemeContextValue = {
116
+ const value: InternalThemeScopeValue = {
121
117
  theme: themeState,
122
118
  setTheme,
123
119
  themes,
124
120
  storageKey,
121
+ coordinator,
122
+ depth: scopeDepth,
125
123
  };
126
124
 
127
125
  return (
128
- <ThemeContext.Scope value={value}>
129
- <ThemeRootSync providerId={providerId} theme={currentTheme} />
130
- <div data-slot="theme-provider">{children}</div>
131
- </ThemeContext.Scope>
126
+ <ThemeScopeContext value={value}>
127
+ <div
128
+ data-slot="theme-scope"
129
+ ref={
130
+ parentScope.coordinator === null
131
+ ? (element: HTMLElement | null) => {
132
+ coordinator.attach(element);
133
+ if (!element || persistenceAdoption.complete) return;
134
+ persistenceAdoption.complete = true;
135
+ const storedTheme = readStoredTheme(storageKey);
136
+ if (storedTheme && storedTheme !== themeState()) {
137
+ themeState.set(storedTheme);
138
+ coordinator.activate(scopeId, storedTheme);
139
+ }
140
+ }
141
+ : undefined
142
+ }
143
+ >
144
+ {children}
145
+ </div>
146
+ </ThemeScopeContext>
132
147
  );
133
148
  }
134
149
 
135
- function ThemeRootSync(props: { providerId: symbol; theme: ThemeName }): JSX.Element | null {
136
- const { providerId, theme } = props;
137
- const expectedRootThemeRevision = rootThemeRevision;
138
-
139
- resource(
140
- ({ signal }: { signal: AbortSignal }) => {
141
- if (typeof window === "undefined") {
142
- syncThemeRoot(theme);
143
- return null;
150
+ function createThemeCoordinator() {
151
+ const scopes = new Map<
152
+ symbol,
153
+ {
154
+ depth: number;
155
+ sequence: number;
156
+ theme: ThemeName;
157
+ signal: AbortSignal;
158
+ }
159
+ >();
160
+ let nextSequence = 0;
161
+ let explicitOwner: symbol | undefined;
162
+ let root: Node | null = null;
163
+ let scheduled = false;
164
+
165
+ const target = (): HTMLElement | null => {
166
+ if (root?.nodeType === 9) return (root as Document).documentElement;
167
+ if (root && "host" in root) return (root as ShadowRoot).host as HTMLElement;
168
+ return typeof document === "undefined" ? null : document.documentElement;
169
+ };
170
+ const syncActive = (): void => {
171
+ if (explicitOwner !== undefined) return;
172
+ let candidate: { depth: number; sequence: number; theme: ThemeName } | undefined;
173
+ for (const scope of scopes.values()) {
174
+ if (
175
+ !candidate ||
176
+ scope.depth > candidate.depth ||
177
+ (scope.depth === candidate.depth && scope.sequence > candidate.sequence)
178
+ ) {
179
+ candidate = scope;
144
180
  }
181
+ }
182
+ if (candidate) syncThemeTarget(target(), candidate.theme);
183
+ };
184
+ const schedule = (): void => {
185
+ if (typeof document === "undefined" || scheduled) return;
186
+ scheduled = true;
187
+ setTimeout(() => {
188
+ scheduled = false;
189
+ syncActive();
190
+ }, 0);
191
+ };
145
192
 
146
- const timeoutId = window.setTimeout(() => {
147
- if (!signal.aborted) {
148
- syncThemeRoot(theme, {
149
- expectedRevision: expectedRootThemeRevision,
150
- passive: true,
151
- providerId,
152
- });
153
- }
154
- }, 0);
155
-
156
- signal.addEventListener(
157
- "abort",
158
- () => {
159
- window.clearTimeout(timeoutId);
160
- },
161
- { once: true },
162
- );
163
-
164
- return null;
193
+ return Object.freeze({
194
+ attach(element: HTMLElement | null) {
195
+ if (element) root = element.getRootNode();
196
+ schedule();
165
197
  },
166
- [theme],
167
- );
168
-
169
- return null;
198
+ register(id: symbol, depth: number, themeName: ThemeName, signal: AbortSignal) {
199
+ const existing = scopes.get(id);
200
+ scopes.set(id, {
201
+ depth,
202
+ sequence: existing?.sequence ?? nextSequence++,
203
+ theme: themeName,
204
+ signal,
205
+ });
206
+ if (!existing) {
207
+ signal.addEventListener(
208
+ "abort",
209
+ () => {
210
+ scopes.delete(id);
211
+ if (explicitOwner === id) explicitOwner = undefined;
212
+ schedule();
213
+ },
214
+ { once: true },
215
+ );
216
+ }
217
+ schedule();
218
+ },
219
+ activate(id: symbol, themeName: ThemeName) {
220
+ const scope = scopes.get(id);
221
+ if (scope) scope.theme = themeName;
222
+ explicitOwner = id;
223
+ syncThemeTarget(target(), themeName);
224
+ },
225
+ });
170
226
  }
171
227
 
172
228
  export function ThemePicker(props: ThemePickerProps): JSX.Element {
173
- const theme = useTheme();
174
- const { themes = theme.themes, label = "Theme", ...rest } = props;
175
- const currentTheme = theme.theme();
229
+ const activeTheme = theme();
230
+ const { themes = activeTheme.themes, label = "Theme", ...rest } = props;
231
+ const currentTheme = activeTheme.theme();
176
232
 
177
233
  return (
178
234
  <select
@@ -183,7 +239,7 @@ export function ThemePicker(props: ThemePickerProps): JSX.Element {
183
239
  onChange={(event: Event) => {
184
240
  const target = getThemePickerTarget(event);
185
241
  if (target) {
186
- theme.setTheme(target.value as ThemeName);
242
+ activeTheme.setTheme(target.value as ThemeName);
187
243
  }
188
244
  }}
189
245
  >
@@ -214,7 +270,7 @@ function getThemePickerTarget(event: Event): HTMLSelectElement | null {
214
270
  }
215
271
 
216
272
  export function ThemeToggle(props: ThemeToggleProps): JSX.Element {
217
- const theme = useTheme();
273
+ const activeTheme = theme();
218
274
  const {
219
275
  children,
220
276
  lightIcon,
@@ -225,7 +281,7 @@ export function ThemeToggle(props: ThemeToggleProps): JSX.Element {
225
281
  ...rest
226
282
  } = props;
227
283
 
228
- const currentTheme = theme.theme();
284
+ const currentTheme = activeTheme.theme();
229
285
  const nextTheme = getNextTheme(currentTheme, themes, getResolvedSystemTheme());
230
286
  const renderContext = { theme: currentTheme, nextTheme };
231
287
  const ariaLabel = (rest as Record<string, unknown>)["aria-label"];
@@ -254,7 +310,7 @@ export function ThemeToggle(props: ThemeToggleProps): JSX.Element {
254
310
  onPress={(event) => {
255
311
  onPress?.(event);
256
312
  if (!event.defaultPrevented && !Object.is(nextTheme, currentTheme)) {
257
- theme.setTheme(nextTheme);
313
+ activeTheme.setTheme(nextTheme);
258
314
  }
259
315
  }}
260
316
  >
@@ -348,7 +404,7 @@ function isJSXElement(value: unknown): value is JSXElement {
348
404
  function cloneThemeToggleIcon(icon: unknown, key?: string): unknown {
349
405
  if (Array.isArray(icon)) {
350
406
  const clonedChildren = icon.map((child) => cloneThemeToggleIcon(child));
351
- if ((icon as Record<symbol, unknown>)[STATIC_CHILDREN] === true) {
407
+ if ((icon as unknown as Record<symbol, unknown>)[STATIC_CHILDREN] === true) {
352
408
  Object.defineProperty(clonedChildren, STATIC_CHILDREN, {
353
409
  value: true,
354
410
  configurable: true,
@@ -377,31 +433,11 @@ function cloneThemeToggleIcon(icon: unknown, key?: string): unknown {
377
433
  return clonedIcon;
378
434
  }
379
435
 
380
- function syncThemeRoot(
436
+ function syncThemeTarget(
437
+ html: HTMLElement | null,
381
438
  themeChoice: ThemeName | null | undefined,
382
- options?: { expectedRevision?: number; passive?: boolean; providerId?: symbol },
383
439
  ): void {
384
- if (typeof document === "undefined") {
385
- return;
386
- }
387
-
388
- const html = document.documentElement;
389
-
390
- if (
391
- options?.passive &&
392
- options.expectedRevision !== undefined &&
393
- options.expectedRevision !== rootThemeRevision
394
- ) {
395
- return;
396
- }
397
-
398
- if (
399
- options?.passive &&
400
- explicitRootThemeOwner !== undefined &&
401
- explicitRootThemeOwner !== options.providerId
402
- ) {
403
- return;
404
- }
440
+ if (!html) return;
405
441
 
406
442
  if (themeChoice == null) {
407
443
  html.removeAttribute("data-theme");
package/src/components.ts CHANGED
@@ -141,7 +141,7 @@ export {
141
141
  ToastAction,
142
142
  ToastClose,
143
143
  ToastDescription,
144
- ToastProvider,
144
+ ToastHost,
145
145
  ToastTitle,
146
146
  ToastViewport,
147
147
  Toggle,
@@ -240,7 +240,7 @@ export {
240
240
  SidebarMenuBadge,
241
241
  SidebarMenuButton,
242
242
  SidebarMenuItem,
243
- SidebarProvider,
243
+ SidebarScope,
244
244
  SidebarRail,
245
245
  SidebarTrigger,
246
246
  } from "./components/sidebar";
@@ -254,9 +254,9 @@ export {
254
254
  CAT_THEME_OPTIONS,
255
255
  DEFAULT_THEME_OPTIONS,
256
256
  ThemePicker,
257
- ThemeProvider,
257
+ ThemeScope,
258
258
  ThemeToggle,
259
- useTheme,
259
+ theme,
260
260
  } from "./components/theme";
261
261
  export * from "./components/catalog";
262
262
 
package/src/overlays.ts CHANGED
@@ -34,7 +34,7 @@ export {
34
34
  ToastAction,
35
35
  ToastClose,
36
36
  ToastDescription,
37
- ToastProvider,
37
+ ToastHost,
38
38
  ToastTitle,
39
39
  ToastViewport,
40
40
  Tooltip,
@@ -115,8 +115,8 @@ export type {
115
115
  ToastDescriptionProps,
116
116
  ToastOwnProps,
117
117
  ToastProps,
118
- ToastProviderOwnProps,
119
- ToastProviderProps,
118
+ ToastHostOwnProps,
119
+ ToastHostProps,
120
120
  ToastTitleAsChildProps,
121
121
  ToastTitleProps,
122
122
  ToastViewportAsChildProps,
package/src/theme.ts CHANGED
@@ -5,17 +5,17 @@ export {
5
5
  CAT_THEME_OPTIONS,
6
6
  DEFAULT_THEME_OPTIONS,
7
7
  ThemePicker,
8
- ThemeProvider,
8
+ ThemeScope,
9
9
  ThemeToggle,
10
- useTheme,
10
+ theme,
11
11
  } from "./components/theme";
12
12
  export type {
13
13
  CatThemeName,
14
- ThemeContextValue,
14
+ ThemeScopeValue,
15
15
  ThemeName,
16
16
  ThemeOption,
17
17
  ThemePickerProps,
18
- ThemeProviderProps,
18
+ ThemeScopeProps,
19
19
  ThemeToggleProps,
20
20
  ThemeToggleRenderContext,
21
21
  } from "./components/theme";