@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.
- package/README.md +25 -0
- package/THEMING.md +6 -5
- package/dist/components/_internal/block-layout.d.ts +1 -0
- package/dist/components/_internal/block-layout.js +14 -11
- package/dist/components/_internal/block-layout.js.map +1 -1
- package/dist/components/_internal/classes.js +8 -1
- package/dist/components/_internal/classes.js.map +1 -1
- package/dist/components/_internal/jsx.js.map +1 -1
- package/dist/components/_internal/style.js +185 -28
- package/dist/components/_internal/style.js.map +1 -1
- package/dist/components/alert/alert.d.ts +1 -0
- package/dist/components/alert/alert.js.map +1 -1
- package/dist/components/alert/alert.types.d.ts +1 -0
- package/dist/components/aside/aside.d.ts +1 -0
- package/dist/components/aside/aside.js.map +1 -1
- package/dist/components/aspect-ratio/aspect-ratio.d.ts +1 -0
- package/dist/components/aspect-ratio/aspect-ratio.js.map +1 -1
- package/dist/components/aspect-ratio/aspect-ratio.types.d.ts +1 -0
- package/dist/components/badge/badge.d.ts +1 -0
- package/dist/components/badge/badge.js.map +1 -1
- package/dist/components/badge/badge.types.d.ts +1 -0
- package/dist/components/block/block.d.ts +1 -0
- package/dist/components/block/block.js +2 -2
- package/dist/components/block/block.js.map +1 -1
- package/dist/components/block/block.types.d.ts +1 -0
- package/dist/components/brand/brand.d.ts +1 -0
- package/dist/components/brand/brand.js +2 -2
- package/dist/components/brand/brand.js.map +1 -1
- package/dist/components/brand/brand.types.d.ts +1 -0
- package/dist/components/button-group/button-group.d.ts +1 -0
- package/dist/components/button-group/button-group.js.map +1 -1
- package/dist/components/button-group/button-group.types.d.ts +1 -0
- package/dist/components/card/card.d.ts +1 -0
- package/dist/components/card/card.js.map +1 -1
- package/dist/components/card/card.types.d.ts +1 -0
- package/dist/components/catalog.d.ts +1 -0
- package/dist/components/catalog.js +22 -13
- package/dist/components/catalog.js.map +1 -1
- package/dist/components/close/close.d.ts +1 -0
- package/dist/components/close/close.js.map +1 -1
- package/dist/components/container/container.d.ts +1 -0
- package/dist/components/container/container.js.map +1 -1
- package/dist/components/empty-state/empty-state.d.ts +1 -0
- package/dist/components/empty-state/empty-state.js.map +1 -1
- package/dist/components/field/field.d.ts +1 -0
- package/dist/components/field/field.js.map +1 -1
- package/dist/components/field/field.types.d.ts +1 -0
- package/dist/components/footer/footer.d.ts +1 -0
- package/dist/components/footer/footer.js.map +1 -1
- package/dist/components/footer/footer.types.d.ts +1 -0
- package/dist/components/grid/grid.d.ts +1 -0
- package/dist/components/grid/grid.js.map +1 -1
- package/dist/components/grid/grid.types.d.ts +1 -0
- package/dist/components/header/header.d.ts +1 -0
- package/dist/components/header/header.js.map +1 -1
- package/dist/components/input-group/input-group.d.ts +1 -0
- package/dist/components/input-group/input-group.js.map +1 -1
- package/dist/components/input-group/input-group.types.d.ts +1 -0
- package/dist/components/jsx-types.d.ts +12 -0
- package/dist/components/main/main.d.ts +1 -0
- package/dist/components/main/main.js.map +1 -1
- package/dist/components/nav/nav.d.ts +1 -0
- package/dist/components/nav/nav.js +6 -2
- package/dist/components/nav/nav.js.map +1 -1
- package/dist/components/nav/nav.types.d.ts +2 -0
- package/dist/components/navbar/navbar.d.ts +1 -0
- package/dist/components/navbar/navbar.js +15 -10
- package/dist/components/navbar/navbar.js.map +1 -1
- package/dist/components/overlays/dropdown-content.d.ts +1 -0
- package/dist/components/overlays/dropdown-content.js.map +1 -1
- package/dist/components/page/page.d.ts +1 -0
- package/dist/components/page/page.js.map +1 -1
- package/dist/components/page-header/page-header.d.ts +1 -0
- package/dist/components/page-header/page-header.js.map +1 -1
- package/dist/components/section/section.d.ts +1 -0
- package/dist/components/section/section.js.map +1 -1
- package/dist/components/separator/separator.d.ts +1 -0
- package/dist/components/separator/separator.js.map +1 -1
- package/dist/components/separator/separator.types.d.ts +1 -0
- package/dist/components/sidebar/index.d.ts +2 -2
- package/dist/components/sidebar/sidebar.d.ts +2 -1
- package/dist/components/sidebar/sidebar.js +39 -13
- package/dist/components/sidebar/sidebar.js.map +1 -1
- package/dist/components/sidebar/sidebar.types.d.ts +3 -1
- package/dist/components/skeleton/skeleton.d.ts +1 -0
- package/dist/components/skeleton/skeleton.js.map +1 -1
- package/dist/components/skeleton/skeleton.types.d.ts +1 -0
- package/dist/components/spinner/spinner.d.ts +1 -0
- package/dist/components/spinner/spinner.js.map +1 -1
- package/dist/components/stat/stat.d.ts +1 -0
- package/dist/components/stat/stat.js.map +1 -1
- package/dist/components/stat/stat.types.d.ts +1 -0
- package/dist/components/text/text.d.ts +1 -0
- package/dist/components/text/text.js.map +1 -1
- package/dist/components/text/text.types.d.ts +1 -0
- package/dist/components/theme/theme.d.ts +2 -0
- package/dist/components/theme/theme.js +58 -19
- package/dist/components/theme/theme.js.map +1 -1
- package/dist/components/toolbar/toolbar.d.ts +1 -0
- package/dist/components/toolbar/toolbar.js.map +1 -1
- package/dist/components.d.ts +6 -3
- package/dist/core.d.ts +2 -2
- package/dist/ssr.d.ts +18 -0
- package/dist/ssr.js +63 -0
- package/dist/ssr.js.map +1 -0
- package/dist/themes/default/index.css +43 -7
- package/package.json +7 -3
- package/src/components/_internal/block-layout.ts +27 -16
- package/src/components/_internal/classes.ts +15 -2
- package/src/components/_internal/jsx.ts +1 -1
- package/src/components/_internal/style.ts +248 -27
- package/src/components/alert/alert.tsx +1 -0
- package/src/components/alert/alert.types.ts +1 -0
- package/src/components/aside/aside.tsx +1 -0
- package/src/components/aspect-ratio/aspect-ratio.tsx +1 -0
- package/src/components/aspect-ratio/aspect-ratio.types.ts +1 -0
- package/src/components/badge/badge.tsx +1 -0
- package/src/components/badge/badge.types.ts +1 -0
- package/src/components/block/block.tsx +4 -1
- package/src/components/block/block.types.ts +1 -0
- package/src/components/brand/brand.tsx +6 -2
- package/src/components/brand/brand.types.ts +1 -0
- package/src/components/button-group/button-group.tsx +1 -0
- package/src/components/button-group/button-group.types.ts +1 -0
- package/src/components/card/card.tsx +1 -0
- package/src/components/card/card.types.ts +1 -0
- package/src/components/catalog.tsx +15 -4
- package/src/components/close/close.tsx +1 -0
- package/src/components/container/container.tsx +1 -0
- package/src/components/empty-state/empty-state.tsx +1 -0
- package/src/components/field/field.tsx +1 -0
- package/src/components/field/field.types.ts +1 -0
- package/src/components/footer/footer.tsx +1 -0
- package/src/components/footer/footer.types.ts +1 -0
- package/src/components/grid/grid.tsx +1 -0
- package/src/components/grid/grid.types.ts +1 -0
- package/src/components/header/header.tsx +1 -0
- package/src/components/input-group/input-group.tsx +1 -0
- package/src/components/input-group/input-group.types.ts +1 -0
- package/src/components/jsx-types.ts +16 -0
- package/src/components/main/main.tsx +1 -0
- package/src/components/nav/nav.tsx +16 -2
- package/src/components/nav/nav.types.ts +2 -0
- package/src/components/navbar/navbar.tsx +19 -8
- package/src/components/overlays/dropdown-content.tsx +1 -0
- package/src/components/page/page.tsx +1 -0
- package/src/components/page-header/page-header.tsx +1 -0
- package/src/components/section/section.tsx +1 -0
- package/src/components/separator/separator.tsx +1 -0
- package/src/components/separator/separator.types.ts +1 -0
- package/src/components/sidebar/index.ts +1 -0
- package/src/components/sidebar/sidebar.tsx +45 -20
- package/src/components/sidebar/sidebar.types.ts +3 -0
- package/src/components/skeleton/skeleton.tsx +1 -0
- package/src/components/skeleton/skeleton.types.ts +1 -0
- package/src/components/spinner/spinner.tsx +1 -0
- package/src/components/stat/stat.tsx +1 -0
- package/src/components/stat/stat.types.ts +1 -0
- package/src/components/text/text.tsx +1 -0
- package/src/components/text/text.types.ts +1 -0
- package/src/components/theme/theme.tsx +85 -14
- package/src/components/toolbar/toolbar.tsx +1 -0
- package/src/components.ts +9 -0
- package/src/ssr.ts +102 -0
- package/src/themes/default/styles/base/text.css +4 -4
- package/src/themes/default/styles/display/coverage.css +3 -1
- package/src/themes/default/styles/layout/block.css +4 -0
- package/src/themes/default/styles/layout/layout.css +108 -0
- package/src/themes/default/styles/shell/navbar.css +4 -1
- package/src/themes/default/tokens.css +1 -1
- package/src/themes/presets/ginger.css +3 -3
- package/templates/theme/styles/base/text.css +4 -4
- package/templates/theme/styles/display/progress-circle.css +2 -0
- package/templates/theme/styles/forms/input-group.css +2 -1
- package/templates/theme/styles/layout/layout.css +108 -0
- package/templates/theme/styles/overlays/toast.css +11 -0
- package/templates/theme/styles/shell/navbar.css +4 -1
- package/templates/theme/styles/shell/sidebar.css +18 -1
- 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 & {
|
|
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 =
|
|
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 {
|
|
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
|
-
<
|
|
74
|
-
<
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 { 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 {
|
|
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:
|
|
166
|
-
|
|
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 {
|
|
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 { 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>(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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">,
|
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, "&")
|
|
16
|
+
.replace(/"/g, """)
|
|
17
|
+
.replace(/</g, "<")
|
|
18
|
+
.replace(/>/g, ">");
|
|
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
|
-
|
|
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;
|