@askrjs/themes 0.0.16 → 0.0.19

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 (179) hide show
  1. package/README.md +25 -0
  2. package/THEMING.md +6 -5
  3. package/dist/components/_internal/block-layout.d.ts +1 -0
  4. package/dist/components/_internal/block-layout.js +14 -11
  5. package/dist/components/_internal/block-layout.js.map +1 -1
  6. package/dist/components/_internal/classes.js +8 -1
  7. package/dist/components/_internal/classes.js.map +1 -1
  8. package/dist/components/_internal/jsx.js.map +1 -1
  9. package/dist/components/_internal/style.js +185 -28
  10. package/dist/components/_internal/style.js.map +1 -1
  11. package/dist/components/alert/alert.d.ts +1 -0
  12. package/dist/components/alert/alert.js.map +1 -1
  13. package/dist/components/alert/alert.types.d.ts +1 -0
  14. package/dist/components/aside/aside.d.ts +1 -0
  15. package/dist/components/aside/aside.js.map +1 -1
  16. package/dist/components/aspect-ratio/aspect-ratio.d.ts +1 -0
  17. package/dist/components/aspect-ratio/aspect-ratio.js.map +1 -1
  18. package/dist/components/aspect-ratio/aspect-ratio.types.d.ts +1 -0
  19. package/dist/components/badge/badge.d.ts +1 -0
  20. package/dist/components/badge/badge.js.map +1 -1
  21. package/dist/components/badge/badge.types.d.ts +1 -0
  22. package/dist/components/block/block.d.ts +1 -0
  23. package/dist/components/block/block.js +2 -2
  24. package/dist/components/block/block.js.map +1 -1
  25. package/dist/components/block/block.types.d.ts +1 -0
  26. package/dist/components/brand/brand.d.ts +1 -0
  27. package/dist/components/brand/brand.js +2 -2
  28. package/dist/components/brand/brand.js.map +1 -1
  29. package/dist/components/brand/brand.types.d.ts +1 -0
  30. package/dist/components/button-group/button-group.d.ts +1 -0
  31. package/dist/components/button-group/button-group.js.map +1 -1
  32. package/dist/components/button-group/button-group.types.d.ts +1 -0
  33. package/dist/components/card/card.d.ts +1 -0
  34. package/dist/components/card/card.js.map +1 -1
  35. package/dist/components/card/card.types.d.ts +1 -0
  36. package/dist/components/catalog.d.ts +1 -0
  37. package/dist/components/catalog.js +22 -13
  38. package/dist/components/catalog.js.map +1 -1
  39. package/dist/components/close/close.d.ts +1 -0
  40. package/dist/components/close/close.js.map +1 -1
  41. package/dist/components/container/container.d.ts +1 -0
  42. package/dist/components/container/container.js.map +1 -1
  43. package/dist/components/empty-state/empty-state.d.ts +1 -0
  44. package/dist/components/empty-state/empty-state.js.map +1 -1
  45. package/dist/components/field/field.d.ts +1 -0
  46. package/dist/components/field/field.js.map +1 -1
  47. package/dist/components/field/field.types.d.ts +1 -0
  48. package/dist/components/footer/footer.d.ts +1 -0
  49. package/dist/components/footer/footer.js.map +1 -1
  50. package/dist/components/footer/footer.types.d.ts +1 -0
  51. package/dist/components/grid/grid.d.ts +1 -0
  52. package/dist/components/grid/grid.js.map +1 -1
  53. package/dist/components/grid/grid.types.d.ts +1 -0
  54. package/dist/components/header/header.d.ts +1 -0
  55. package/dist/components/header/header.js.map +1 -1
  56. package/dist/components/input-group/input-group.d.ts +1 -0
  57. package/dist/components/input-group/input-group.js.map +1 -1
  58. package/dist/components/input-group/input-group.types.d.ts +1 -0
  59. package/dist/components/jsx-types.d.ts +12 -0
  60. package/dist/components/main/main.d.ts +1 -0
  61. package/dist/components/main/main.js.map +1 -1
  62. package/dist/components/nav/nav.d.ts +1 -0
  63. package/dist/components/nav/nav.js +6 -2
  64. package/dist/components/nav/nav.js.map +1 -1
  65. package/dist/components/nav/nav.types.d.ts +2 -0
  66. package/dist/components/navbar/navbar.d.ts +1 -0
  67. package/dist/components/navbar/navbar.js +15 -10
  68. package/dist/components/navbar/navbar.js.map +1 -1
  69. package/dist/components/overlays/dropdown-content.d.ts +1 -0
  70. package/dist/components/overlays/dropdown-content.js.map +1 -1
  71. package/dist/components/page/page.d.ts +1 -0
  72. package/dist/components/page/page.js.map +1 -1
  73. package/dist/components/page-header/page-header.d.ts +1 -0
  74. package/dist/components/page-header/page-header.js.map +1 -1
  75. package/dist/components/section/section.d.ts +1 -0
  76. package/dist/components/section/section.js.map +1 -1
  77. package/dist/components/separator/separator.d.ts +1 -0
  78. package/dist/components/separator/separator.js.map +1 -1
  79. package/dist/components/separator/separator.types.d.ts +1 -0
  80. package/dist/components/sidebar/index.d.ts +2 -2
  81. package/dist/components/sidebar/sidebar.d.ts +2 -1
  82. package/dist/components/sidebar/sidebar.js +39 -13
  83. package/dist/components/sidebar/sidebar.js.map +1 -1
  84. package/dist/components/sidebar/sidebar.types.d.ts +3 -1
  85. package/dist/components/skeleton/skeleton.d.ts +1 -0
  86. package/dist/components/skeleton/skeleton.js.map +1 -1
  87. package/dist/components/skeleton/skeleton.types.d.ts +1 -0
  88. package/dist/components/spinner/spinner.d.ts +1 -0
  89. package/dist/components/spinner/spinner.js.map +1 -1
  90. package/dist/components/stat/stat.d.ts +1 -0
  91. package/dist/components/stat/stat.js.map +1 -1
  92. package/dist/components/stat/stat.types.d.ts +1 -0
  93. package/dist/components/text/text.d.ts +1 -0
  94. package/dist/components/text/text.js.map +1 -1
  95. package/dist/components/text/text.types.d.ts +1 -0
  96. package/dist/components/theme/theme.d.ts +2 -0
  97. package/dist/components/theme/theme.js +58 -19
  98. package/dist/components/theme/theme.js.map +1 -1
  99. package/dist/components/toolbar/toolbar.d.ts +1 -0
  100. package/dist/components/toolbar/toolbar.js.map +1 -1
  101. package/dist/components.d.ts +6 -3
  102. package/dist/core.d.ts +2 -2
  103. package/dist/ssr.d.ts +18 -0
  104. package/dist/ssr.js +63 -0
  105. package/dist/ssr.js.map +1 -0
  106. package/dist/themes/default/index.css +43 -7
  107. package/package.json +7 -3
  108. package/src/components/_internal/block-layout.ts +27 -16
  109. package/src/components/_internal/classes.ts +15 -2
  110. package/src/components/_internal/jsx.ts +1 -1
  111. package/src/components/_internal/style.ts +248 -27
  112. package/src/components/alert/alert.tsx +1 -0
  113. package/src/components/alert/alert.types.ts +1 -0
  114. package/src/components/aside/aside.tsx +1 -0
  115. package/src/components/aspect-ratio/aspect-ratio.tsx +1 -0
  116. package/src/components/aspect-ratio/aspect-ratio.types.ts +1 -0
  117. package/src/components/badge/badge.tsx +1 -0
  118. package/src/components/badge/badge.types.ts +1 -0
  119. package/src/components/block/block.tsx +4 -1
  120. package/src/components/block/block.types.ts +1 -0
  121. package/src/components/brand/brand.tsx +6 -2
  122. package/src/components/brand/brand.types.ts +1 -0
  123. package/src/components/button-group/button-group.tsx +1 -0
  124. package/src/components/button-group/button-group.types.ts +1 -0
  125. package/src/components/card/card.tsx +1 -0
  126. package/src/components/card/card.types.ts +1 -0
  127. package/src/components/catalog.tsx +15 -4
  128. package/src/components/close/close.tsx +1 -0
  129. package/src/components/container/container.tsx +1 -0
  130. package/src/components/empty-state/empty-state.tsx +1 -0
  131. package/src/components/field/field.tsx +1 -0
  132. package/src/components/field/field.types.ts +1 -0
  133. package/src/components/footer/footer.tsx +1 -0
  134. package/src/components/footer/footer.types.ts +1 -0
  135. package/src/components/grid/grid.tsx +1 -0
  136. package/src/components/grid/grid.types.ts +1 -0
  137. package/src/components/header/header.tsx +1 -0
  138. package/src/components/input-group/input-group.tsx +1 -0
  139. package/src/components/input-group/input-group.types.ts +1 -0
  140. package/src/components/jsx-types.ts +16 -0
  141. package/src/components/main/main.tsx +1 -0
  142. package/src/components/nav/nav.tsx +16 -2
  143. package/src/components/nav/nav.types.ts +2 -0
  144. package/src/components/navbar/navbar.tsx +19 -8
  145. package/src/components/overlays/dropdown-content.tsx +1 -0
  146. package/src/components/page/page.tsx +1 -0
  147. package/src/components/page-header/page-header.tsx +1 -0
  148. package/src/components/section/section.tsx +1 -0
  149. package/src/components/separator/separator.tsx +1 -0
  150. package/src/components/separator/separator.types.ts +1 -0
  151. package/src/components/sidebar/index.ts +1 -0
  152. package/src/components/sidebar/sidebar.tsx +45 -20
  153. package/src/components/sidebar/sidebar.types.ts +3 -0
  154. package/src/components/skeleton/skeleton.tsx +1 -0
  155. package/src/components/skeleton/skeleton.types.ts +1 -0
  156. package/src/components/spinner/spinner.tsx +1 -0
  157. package/src/components/stat/stat.tsx +1 -0
  158. package/src/components/stat/stat.types.ts +1 -0
  159. package/src/components/text/text.tsx +1 -0
  160. package/src/components/text/text.types.ts +1 -0
  161. package/src/components/theme/theme.tsx +85 -14
  162. package/src/components/toolbar/toolbar.tsx +1 -0
  163. package/src/components.ts +9 -0
  164. package/src/ssr.ts +102 -0
  165. package/src/themes/default/styles/base/text.css +4 -4
  166. package/src/themes/default/styles/display/coverage.css +3 -1
  167. package/src/themes/default/styles/layout/block.css +4 -0
  168. package/src/themes/default/styles/layout/layout.css +108 -0
  169. package/src/themes/default/styles/shell/navbar.css +4 -1
  170. package/src/themes/default/tokens.css +1 -1
  171. package/src/themes/presets/ginger.css +3 -3
  172. package/templates/theme/styles/base/text.css +4 -4
  173. package/templates/theme/styles/display/progress-circle.css +2 -0
  174. package/templates/theme/styles/forms/input-group.css +2 -1
  175. package/templates/theme/styles/layout/layout.css +108 -0
  176. package/templates/theme/styles/overlays/toast.css +11 -0
  177. package/templates/theme/styles/shell/navbar.css +4 -1
  178. package/templates/theme/styles/shell/sidebar.css +18 -1
  179. package/templates/theme/tokens.css +1 -1
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import { Slot } from "@askrjs/askr/foundations";
2
3
  import { currentRoute, Link, navigate } from "@askrjs/askr/router";
3
4
  import { Block } from "../block";
@@ -103,13 +104,17 @@ function renderRoutedLink(
103
104
  children,
104
105
  href: suppliedHref,
105
106
  to,
107
+ onPress,
106
108
  onClick,
107
109
  ref,
108
110
  class: className,
109
111
  match = "prefix",
110
112
  target,
111
113
  ...rest
112
- } = props as NavLinkProps & { onClick?: (event: MouseEvent) => void };
114
+ } = props as NavLinkProps & {
115
+ onPress?: (event: Event) => void;
116
+ onClick?: (event: MouseEvent) => void;
117
+ };
113
118
  const href = to?.href ?? suppliedHref;
114
119
  if (!href) {
115
120
  throw new Error("Nav link requires href or to.");
@@ -124,6 +129,7 @@ function renderRoutedLink(
124
129
  slot === "nav-item" && resolvedSlot !== "nav-item" ? "nav-item" : undefined;
125
130
  const { "data-slot": _dataSlot, ...childRest } = rest as Record<string, unknown>;
126
131
  void _dataSlot;
132
+ const hasDownload = (rest as Record<string, unknown>).download !== undefined;
127
133
  const currentPathname = getReactiveCurrentPathname();
128
134
  const targetPathname = resolvePathname(href);
129
135
  const routeActive =
@@ -139,12 +145,20 @@ function renderRoutedLink(
139
145
  : {
140
146
  "data-active": undefined,
141
147
  };
142
- const rendersRouterLink = targetPathname !== null && !target && typeof onClick !== "function";
148
+ const rendersRouterLink =
149
+ targetPathname !== null &&
150
+ !target &&
151
+ !hasDownload &&
152
+ typeof onClick !== "function" &&
153
+ typeof onPress !== "function";
143
154
  const childProps: NavLinkProps = to
144
155
  ? ({ ...childRest, to, target } as NavLinkProps)
145
156
  : ({ ...childRest, href, target } as NavLinkProps);
146
157
  const handleClick = (event: MouseEvent) => {
158
+ onPress?.(event);
159
+ if (event.defaultPrevented) return;
147
160
  onClick?.(event);
161
+ if (hasDownload) return;
148
162
 
149
163
  if (!shouldHandleClientNavigation(event, target, targetPathname)) {
150
164
  return;
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import type { JSXElement } from "@askrjs/askr/foundations";
2
3
  import type { Ref } from "@askrjs/askr/foundations/utilities";
3
4
  import type { LinkProps } from "@askrjs/askr/router";
@@ -58,6 +59,7 @@ export type NavItemAsChildProps = Omit<JSX.IntrinsicElements["a"], "children" |
58
59
 
59
60
  export type NavLinkOwnProps = NavItemOwnProps & {
60
61
  match?: NavLinkMatch;
62
+ onPress?: (event: Event) => void;
61
63
  };
62
64
 
63
65
  export type NavLinkProps = LinkProps & NavLinkOwnProps;
@@ -1,4 +1,11 @@
1
- import { Dropdown, DropdownTrigger } from "@askrjs/ui";
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
2
+ import {
3
+ Collapsible,
4
+ CollapsibleContent,
5
+ CollapsibleTrigger,
6
+ Dropdown,
7
+ DropdownTrigger,
8
+ } from "@askrjs/ui";
2
9
  import { Block } from "../block";
3
10
  import { classes } from "../_internal/classes";
4
11
  import { isJsxElement, toChildArray } from "../_internal/jsx";
@@ -70,13 +77,17 @@ export function Navbar(props: NavbarProps): JSX.Element {
70
77
  </LayoutBlock>
71
78
  );
72
79
  const collapse = hasNavContent ? (
73
- <details data-slot="navbar-collapse">
74
- <summary aria-label={collapseLabel} data-slot="navbar-toggle">
75
- {collapseIcon}
76
- <span data-slot="navbar-toggle-label">{collapseLabel}</span>
77
- </summary>
78
- {content}
79
- </details>
80
+ <div data-slot="navbar-collapse">
81
+ <Collapsible>
82
+ <CollapsibleTrigger aria-label={collapseLabel} data-slot="navbar-toggle">
83
+ {collapseIcon}
84
+ <span data-slot="navbar-toggle-label">{collapseLabel}</span>
85
+ </CollapsibleTrigger>
86
+ <CollapsibleContent forceMount asChild data-slot="navbar-content">
87
+ {content}
88
+ </CollapsibleContent>
89
+ </Collapsible>
90
+ </div>
80
91
  ) : null;
81
92
 
82
93
  return (
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import { DropdownContent as UiDropdownContent } from "@askrjs/ui";
2
3
  import type { DropdownContentAsChildProps, DropdownContentProps } from "@askrjs/ui";
3
4
 
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import { Block } from "../block";
2
3
  import { Container } from "../container";
3
4
  import type { PageProps } from "./page.types";
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import { Block } from "../block";
2
3
  import type { PageHeaderProps } from "./page-header.types";
3
4
 
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import { Block } from "../block";
2
3
  import type { SectionProps } from "./section.types";
3
4
 
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import { Slot } from "@askrjs/askr/foundations";
2
3
  import { mergeProps } from "../_internal/merge-props";
3
4
  import { intrinsicElement } from "../_internal/jsx";
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import type { JSXElement } from "@askrjs/askr/foundations";
2
3
  import type { Ref } from "@askrjs/askr/foundations/utilities";
3
4
 
@@ -21,6 +21,7 @@ export type {
21
21
  SidebarCollapsible,
22
22
  SidebarPartProps,
23
23
  SidebarProps,
24
+ SidebarRailProps,
24
25
  SidebarSide,
25
26
  SidebarTooltipSide,
26
27
  SidebarVariant,
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import { Slot } from "@askrjs/askr/foundations";
2
3
  import { Block } from "../block";
3
4
  import { classes } from "../_internal/classes";
@@ -143,16 +144,23 @@ export function SidebarMenuButton(props: SidebarButtonProps): JSX.Element {
143
144
  }
144
145
 
145
146
  export function SidebarMenuAction(props: SidebarButtonProps): JSX.Element {
146
- const { children, class: className, tooltip, tooltipSide, type = "button", ...rest } = props;
147
+ const {
148
+ asChild,
149
+ children,
150
+ class: className,
151
+ tooltip,
152
+ tooltipSide,
153
+ type = "button",
154
+ ...rest
155
+ } = props;
156
+ const finalProps = mergeProps(rest, {
157
+ class: classes(className),
158
+ "data-slot": "sidebar-menu-action",
159
+ ...sidebarTooltipProps(tooltip, tooltipSide),
160
+ });
161
+ if (asChild) return <Slot asChild {...finalProps} children={children as JSX.Element} />;
147
162
  return (
148
- <button
149
- type={type as "button" | "submit" | "reset" | undefined}
150
- {...mergeProps(rest, {
151
- class: classes(className),
152
- "data-slot": "sidebar-menu-action",
153
- ...sidebarTooltipProps(tooltip, tooltipSide),
154
- })}
155
- >
163
+ <button type={type as "button" | "submit" | "reset" | undefined} {...finalProps}>
156
164
  {children}
157
165
  </button>
158
166
  );
@@ -162,21 +170,38 @@ export function SidebarMenuBadge(props: SidebarPartProps): JSX.Element {
162
170
  return sidebarPart(props, "sidebar-menu-badge", "span");
163
171
  }
164
172
 
165
- export function SidebarRail(props: SidebarPartProps): JSX.Element {
166
- return sidebarPart(props, "sidebar-rail", "button");
173
+ export function SidebarRail(props: SidebarButtonProps): JSX.Element {
174
+ const { asChild, children, class: className, type = "button", ...rest } = props;
175
+ const finalProps = mergeProps(rest, {
176
+ class: classes(className),
177
+ "data-slot": "sidebar-rail",
178
+ });
179
+ if (asChild) return <Slot asChild {...finalProps} children={children as JSX.Element} />;
180
+ return (
181
+ <button type={type as "button" | "submit" | "reset" | undefined} {...finalProps}>
182
+ {children}
183
+ </button>
184
+ );
167
185
  }
168
186
 
169
187
  export function SidebarTrigger(props: SidebarButtonProps): JSX.Element {
170
- const { children, class: className, tooltip, tooltipSide, type = "button", ...rest } = props;
188
+ const {
189
+ asChild,
190
+ children,
191
+ class: className,
192
+ tooltip,
193
+ tooltipSide,
194
+ type = "button",
195
+ ...rest
196
+ } = props;
197
+ const finalProps = mergeProps(rest, {
198
+ class: classes("btn btn-ghost btn-icon", className),
199
+ "data-slot": "sidebar-trigger",
200
+ ...sidebarTooltipProps(tooltip, tooltipSide),
201
+ });
202
+ if (asChild) return <Slot asChild {...finalProps} children={children as JSX.Element} />;
171
203
  return (
172
- <button
173
- type={type as "button" | "submit" | "reset" | undefined}
174
- {...mergeProps(rest, {
175
- class: classes("btn btn-ghost btn-icon", className),
176
- "data-slot": "sidebar-trigger",
177
- ...sidebarTooltipProps(tooltip, tooltipSide),
178
- })}
179
- >
204
+ <button type={type as "button" | "submit" | "reset" | undefined} {...finalProps}>
180
205
  {children}
181
206
  </button>
182
207
  );
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import type { BlockElementProps } from "../block";
2
3
 
3
4
  export type SidebarCollapsible = "offcanvas" | "icon" | "none";
@@ -26,3 +27,5 @@ export type SidebarButtonProps = JSX.IntrinsicElements["button"] & {
26
27
  tooltipSide?: SidebarTooltipSide;
27
28
  variant?: "default" | "outline";
28
29
  };
30
+
31
+ export type SidebarRailProps = SidebarButtonProps;
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import { Slot } from "@askrjs/askr/foundations";
2
3
  import { mergeLayoutStyles } from "../_internal/block-layout";
3
4
  import { classes } from "../_internal/classes";
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import type { JSXElement } from "@askrjs/askr/foundations";
2
3
  import type { Ref } from "@askrjs/askr/foundations/utilities";
3
4
 
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import { ProgressCircle, ProgressCircleIndicator } from "@askrjs/ui";
2
3
  import type { SpinnerProps } from "./spinner.types";
3
4
 
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import { classes } from "../_internal/classes";
2
3
  import { mergeProps } from "../_internal/merge-props";
3
4
  import type { StatDescriptionProps, StatLabelProps, StatProps, StatValueProps } from "./stat.types";
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import type { Ref } from "@askrjs/askr/foundations/utilities";
2
3
 
3
4
  type DivProps = Omit<JSX.IntrinsicElements["div"], "children" | "ref">;
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import { classes } from "../_internal/classes";
2
3
  import { mergeProps } from "../_internal/merge-props";
3
4
  import { intrinsicElement } from "../_internal/jsx";
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import type { Ref } from "@askrjs/askr/foundations/utilities";
2
3
 
3
4
  export type TextElement = "p" | "span" | "div" | "strong";
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import { defineScope, getSignal, readScope, state } from "@askrjs/askr";
2
3
  import type { JSXElement } from "@askrjs/askr/foundations/structures";
3
4
  import { Button } from "@askrjs/ui";
@@ -15,6 +16,7 @@ export type ThemeOption = {
15
16
 
16
17
  export type ThemeScopeValue = {
17
18
  theme: () => ThemeName;
19
+ resolvedSystemTheme: () => "light" | "dark";
18
20
  setTheme: (theme: ThemeName) => void;
19
21
  themes: readonly ThemeOption[];
20
22
  storageKey: string;
@@ -66,6 +68,7 @@ export const CAT_THEME_OPTIONS: readonly ThemeOption[] = [
66
68
  const DEFAULT_STORAGE_KEY = "askr-theme";
67
69
  const STATIC_CHILDREN = Symbol.for("askr.static-children");
68
70
  const STATIC_CHILD_SLOTS_CACHE = Symbol.for("__askrStaticChildSlots");
71
+ const documentThemeCoordinators = new WeakMap<Document, ThemeCoordinator>();
69
72
  type ThemeCoordinator = ReturnType<typeof createThemeCoordinator>;
70
73
  type InternalThemeScopeValue = ThemeScopeValue & {
71
74
  readonly coordinator: ThemeCoordinator | null;
@@ -74,6 +77,7 @@ type InternalThemeScopeValue = ThemeScopeValue & {
74
77
 
75
78
  const ThemeScopeContext = defineScope<InternalThemeScopeValue>({
76
79
  theme: () => "system",
80
+ resolvedSystemTheme: () => "light",
77
81
  setTheme: () => undefined,
78
82
  themes: DEFAULT_THEME_OPTIONS,
79
83
  storageKey: DEFAULT_STORAGE_KEY,
@@ -99,13 +103,16 @@ export function ThemeScope(props: ThemeScopeProps): JSX.Element {
99
103
  // Browser persistence is adopted from the committed root ref, after Askr's
100
104
  // hydration verifier has accepted the server markup.
101
105
  const themeState = state<ThemeName>(defaultTheme);
106
+ const localResolvedSystemTheme = state<"light" | "dark">("light");
102
107
  const persistenceAdoption = state({ complete: false })();
103
108
  const currentTheme = themeState();
104
109
  const parentScope = readScope(ThemeScopeContext);
105
- const ownedCoordinator = state<ThemeCoordinator>(createThemeCoordinator())();
110
+ const ownedCoordinator = state<ThemeCoordinator>(getDefaultThemeCoordinator())();
106
111
  const coordinator = parentScope.coordinator ?? ownedCoordinator;
107
112
  const scopeDepth = parentScope.depth + 1;
108
- coordinator.register(scopeId, scopeDepth, currentTheme, scopeSignal);
113
+ coordinator.register(scopeId, scopeDepth, currentTheme, scopeSignal, (nextTheme) => {
114
+ if (themeState() !== nextTheme) themeState.set(nextTheme);
115
+ });
109
116
 
110
117
  const setTheme = (nextTheme: ThemeName) => {
111
118
  themeState.set(nextTheme);
@@ -113,8 +120,12 @@ export function ThemeScope(props: ThemeScopeProps): JSX.Element {
113
120
  coordinator.activate(scopeId, nextTheme);
114
121
  };
115
122
 
123
+ const resolvedSystemTheme = parentScope.coordinator
124
+ ? parentScope.resolvedSystemTheme
125
+ : localResolvedSystemTheme;
116
126
  const value: InternalThemeScopeValue = {
117
127
  theme: themeState,
128
+ resolvedSystemTheme,
118
129
  setTheme,
119
130
  themes,
120
131
  storageKey,
@@ -129,7 +140,34 @@ export function ThemeScope(props: ThemeScopeProps): JSX.Element {
129
140
  ref={
130
141
  parentScope.coordinator === null
131
142
  ? (element: HTMLElement | null) => {
132
- coordinator.attach(element);
143
+ coordinator.attach(
144
+ element,
145
+ (nextTheme) => localResolvedSystemTheme.set(nextTheme),
146
+ scopeSignal,
147
+ );
148
+ if (element && typeof window !== "undefined") {
149
+ const onStorage = (event: StorageEvent) => {
150
+ let storageMatches = event.storageArea == null;
151
+ try {
152
+ storageMatches ||= event.storageArea === window.localStorage;
153
+ } catch {
154
+ // Locked-down/private contexts may deny access to localStorage.
155
+ }
156
+ if (!storageMatches || event.key !== storageKey) {
157
+ return;
158
+ }
159
+ const nextTheme = event.newValue as ThemeName | null;
160
+ if (!nextTheme) return;
161
+ themeState.set(nextTheme);
162
+ coordinator.activate(scopeId, nextTheme);
163
+ };
164
+ window.addEventListener("storage", onStorage);
165
+ scopeSignal.addEventListener(
166
+ "abort",
167
+ () => window.removeEventListener("storage", onStorage),
168
+ { once: true },
169
+ );
170
+ }
133
171
  if (!element || persistenceAdoption.complete) return;
134
172
  persistenceAdoption.complete = true;
135
173
  const storedTheme = readStoredTheme(storageKey);
@@ -147,6 +185,19 @@ export function ThemeScope(props: ThemeScopeProps): JSX.Element {
147
185
  );
148
186
  }
149
187
 
188
+ function getDefaultThemeCoordinator(): ThemeCoordinator {
189
+ if (typeof document === "undefined") {
190
+ return createThemeCoordinator();
191
+ }
192
+
193
+ const existing = documentThemeCoordinators.get(document);
194
+ if (existing) return existing;
195
+
196
+ const coordinator = createThemeCoordinator();
197
+ documentThemeCoordinators.set(document, coordinator);
198
+ return coordinator;
199
+ }
200
+
150
201
  function createThemeCoordinator() {
151
202
  const scopes = new Map<
152
203
  symbol,
@@ -155,6 +206,7 @@ function createThemeCoordinator() {
155
206
  sequence: number;
156
207
  theme: ThemeName;
157
208
  signal: AbortSignal;
209
+ onThemeChange: (themeName: ThemeName) => void;
158
210
  }
159
211
  >();
160
212
  let nextSequence = 0;
@@ -191,17 +243,37 @@ function createThemeCoordinator() {
191
243
  };
192
244
 
193
245
  return Object.freeze({
194
- attach(element: HTMLElement | null) {
246
+ attach(
247
+ element: HTMLElement | null,
248
+ onResolvedSystemTheme: (themeName: "light" | "dark") => void,
249
+ signal: AbortSignal,
250
+ ) {
195
251
  if (element) root = element.getRootNode();
252
+ if (element && typeof window !== "undefined" && typeof window.matchMedia === "function") {
253
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
254
+ const update = () => onResolvedSystemTheme(media.matches ? "dark" : "light");
255
+ update();
256
+ media.addEventListener?.("change", update);
257
+ signal.addEventListener("abort", () => media.removeEventListener?.("change", update), {
258
+ once: true,
259
+ });
260
+ }
196
261
  schedule();
197
262
  },
198
- register(id: symbol, depth: number, themeName: ThemeName, signal: AbortSignal) {
263
+ register(
264
+ id: symbol,
265
+ depth: number,
266
+ themeName: ThemeName,
267
+ signal: AbortSignal,
268
+ onThemeChange: (themeName: ThemeName) => void,
269
+ ) {
199
270
  const existing = scopes.get(id);
200
271
  scopes.set(id, {
201
272
  depth,
202
273
  sequence: existing?.sequence ?? nextSequence++,
203
274
  theme: themeName,
204
275
  signal,
276
+ onThemeChange,
205
277
  });
206
278
  if (!existing) {
207
279
  signal.addEventListener(
@@ -220,6 +292,13 @@ function createThemeCoordinator() {
220
292
  const scope = scopes.get(id);
221
293
  if (scope) scope.theme = themeName;
222
294
  explicitOwner = id;
295
+ const ownerDepth = scope?.depth;
296
+ for (const [scopeId, registered] of scopes) {
297
+ if (scopeId !== id && registered.depth === ownerDepth) {
298
+ registered.theme = themeName;
299
+ registered.onThemeChange(themeName);
300
+ }
301
+ }
223
302
  syncThemeTarget(target(), themeName);
224
303
  },
225
304
  });
@@ -282,7 +361,7 @@ export function ThemeToggle(props: ThemeToggleProps): JSX.Element {
282
361
  } = props;
283
362
 
284
363
  const currentTheme = activeTheme.theme();
285
- const nextTheme = getNextTheme(currentTheme, themes, getResolvedSystemTheme());
364
+ const nextTheme = getNextTheme(currentTheme, themes, activeTheme.resolvedSystemTheme());
286
365
  const renderContext = { theme: currentTheme, nextTheme };
287
366
  const ariaLabel = (rest as Record<string, unknown>)["aria-label"];
288
367
  const themedIcon = resolveThemeToggleIcon(currentTheme, nextTheme, {
@@ -334,14 +413,6 @@ function getNextTheme(
334
413
  return themes[index >= 0 && index < themes.length - 1 ? index + 1 : 0]!;
335
414
  }
336
415
 
337
- function getResolvedSystemTheme(): "light" | "dark" {
338
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
339
- return "light";
340
- }
341
-
342
- return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
343
- }
344
-
345
416
  function getThemeIcon(
346
417
  theme: ThemeName,
347
418
  icons: Pick<ThemeToggleProps, "lightIcon" | "darkIcon" | "systemIcon">,
@@ -1,3 +1,4 @@
1
+ import type { JSX } from "@askrjs/askr/jsx-runtime";
1
2
  import { Block } from "../block";
2
3
  import type { ToolbarProps } from "./toolbar.types";
3
4
 
package/src/components.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  import "./themes/default/index.css";
2
+ import "./components/jsx-types";
3
+
4
+ // Keep the UI type surface explicit. A wildcard type re-export makes the
5
+ // generated declaration bundle claim ownership of UI-only catalog values
6
+ // (for example DataTable), which are intentionally sourced from our catalog
7
+ // wrappers instead.
8
+ export type { DialogProps } from "@askrjs/ui";
2
9
 
3
10
  export {
4
11
  Accordion,
@@ -271,6 +278,7 @@ export type * from "./components/container";
271
278
  export type * from "./components/empty-state";
272
279
  export type * from "./components/field";
273
280
  export type * from "./components/footer";
281
+ export type * from "./components/grid";
274
282
  export type * from "./components/header";
275
283
  export type * from "./components/input-group";
276
284
  export type * from "./components/main";
@@ -286,4 +294,5 @@ export type * from "./components/skeleton";
286
294
  export type * from "./components/spinner";
287
295
  export type * from "./components/toolbar";
288
296
  export type * from "./components/theme";
297
+ export type * from "./components/text";
289
298
  export type * from "./components/catalog";
package/src/ssr.ts ADDED
@@ -0,0 +1,102 @@
1
+ import { styleRulesForHtml } from "./components/_internal/style";
2
+
3
+ const STYLE_REGISTRY_ATTR = "data-askr-style-registry";
4
+
5
+ type DocumentRenderArgsLike = {
6
+ appHtml: string;
7
+ context: {
8
+ cspNonce?: string;
9
+ styles?: readonly { id: string; cssText: string }[];
10
+ };
11
+ };
12
+
13
+ function escapeHtmlAttribute(value: string): string {
14
+ return value
15
+ .replace(/&/g, "&amp;")
16
+ .replace(/"/g, "&quot;")
17
+ .replace(/</g, "&lt;")
18
+ .replace(/>/g, "&gt;");
19
+ }
20
+
21
+ function escapeStyleRawText(value: string): string {
22
+ return value.replace(/<\/style/gi, "<\\/style");
23
+ }
24
+
25
+ function findHeadEnd(html: string): number {
26
+ let index = 0;
27
+ while (index < html.length) {
28
+ if (html.startsWith("<!--", index)) {
29
+ const commentEnd = html.indexOf("-->", index + 4);
30
+ index = commentEnd < 0 ? html.length : commentEnd + 3;
31
+ continue;
32
+ }
33
+
34
+ const rawTextStart = html.slice(index).match(/^<\s*(script|style|title|textarea)(?:\s|>)/i);
35
+ if (rawTextStart) {
36
+ const rawTextEndPattern = new RegExp(`</\\s*${rawTextStart[1]}\\s*>`, "ig");
37
+ rawTextEndPattern.lastIndex = index + rawTextStart[0].length;
38
+ const rawTextEnd = rawTextEndPattern.exec(html);
39
+ if (!rawTextEnd) return -1;
40
+ index = rawTextEnd.index + rawTextEnd[0].length;
41
+ continue;
42
+ }
43
+
44
+ const headEnd = html.slice(index).match(/^<\/\s*head\s*>/i);
45
+ if (headEnd) return index;
46
+ index += 1;
47
+ }
48
+ return -1;
49
+ }
50
+
51
+ function injectIntoHead(html: string, content: string): string {
52
+ const headEnd = findHeadEnd(html);
53
+ if (headEnd >= 0) {
54
+ return `${html.slice(0, headEnd)}${content}${html.slice(headEnd)}`;
55
+ }
56
+
57
+ const bodyStart = html.search(/<body(?:\s[^>]*)?>/i);
58
+ if (bodyStart >= 0) {
59
+ return `${html.slice(0, bodyStart)}<head>${content}</head>${html.slice(bodyStart)}`;
60
+ }
61
+
62
+ return `${content}${html}`;
63
+ }
64
+
65
+ /**
66
+ * Wrap an Askr SSR/SSG document renderer so generated theme rules used by the
67
+ * rendered app are available before hydration.
68
+ */
69
+ export function withThemeStyles<TArgs extends DocumentRenderArgsLike>(
70
+ documentRenderer: (args: TArgs) => string,
71
+ ): (args: TArgs) => string {
72
+ return (args) => {
73
+ const documentHtml = documentRenderer(args);
74
+ const registeredStyles = args.context.styles;
75
+ const rules = registeredStyles
76
+ ? (() => {
77
+ const styles = new Map<string, string>();
78
+ for (const style of registeredStyles) {
79
+ if (!style.id || !style.cssText) {
80
+ throw new TypeError("Invalid SSR theme style registration.");
81
+ }
82
+ const existing = styles.get(style.id);
83
+ if (existing !== undefined && existing !== style.cssText) {
84
+ throw new RangeError(
85
+ `SSR style registration collision for ${JSON.stringify(style.id)}.`,
86
+ );
87
+ }
88
+ styles.set(style.id, style.cssText);
89
+ }
90
+ return Array.from(styles.values());
91
+ })()
92
+ : styleRulesForHtml(args.appHtml);
93
+ if (rules.length === 0) return documentHtml;
94
+
95
+ const nonce =
96
+ args.context.cspNonce === undefined
97
+ ? ""
98
+ : ` nonce="${escapeHtmlAttribute(args.context.cspNonce)}"`;
99
+ const registry = `<style ${STYLE_REGISTRY_ATTR}="true"${nonce}>${escapeStyleRawText(rules.join("\n"))}\n</style>`;
100
+ return injectIntoHead(documentHtml, registry);
101
+ };
102
+ }
@@ -26,19 +26,19 @@
26
26
  }
27
27
 
28
28
  :where([data-slot="text"][data-tone="success"]) {
29
- color: var(--ak-color-success);
29
+ color: var(--ak-color-success-ink);
30
30
  }
31
31
 
32
32
  :where([data-slot="text"][data-tone="warning"]) {
33
- color: var(--ak-color-warning);
33
+ color: var(--ak-color-warning-ink);
34
34
  }
35
35
 
36
36
  :where([data-slot="text"][data-tone="danger"]) {
37
- color: var(--ak-color-danger);
37
+ color: var(--ak-color-danger-ink);
38
38
  }
39
39
 
40
40
  :where([data-slot="text"][data-tone="info"]) {
41
- color: var(--ak-color-info);
41
+ color: var(--ak-color-info-ink);
42
42
  }
43
43
 
44
44
  :where([data-slot="text"][data-weight="normal"]) {
@@ -6,8 +6,10 @@
6
6
  }
7
7
 
8
8
  :where([data-slot="theme-picker"]) {
9
+ appearance: none;
9
10
  display: inline-flex;
10
- min-block-size: var(--ak-density-control-height-md);
11
+ block-size: var(--ak-density-control-height-md, 2.25rem);
12
+ min-block-size: var(--ak-density-control-height-md, 2.25rem);
11
13
  min-inline-size: min(8rem, 100%);
12
14
  max-inline-size: 100%;
13
15
  padding-inline: var(--ak-density-control-padding-x-md) 2.1rem;