@agent-native/toolkit 0.9.0 → 0.10.0
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 +12 -3
- package/agent-native.eject.json +71 -0
- package/dist/chat-history/ChatHistoryRail.d.ts +33 -0
- package/dist/chat-history/ChatHistoryRail.d.ts.map +1 -0
- package/dist/chat-history/ChatHistoryRail.js +35 -0
- package/dist/chat-history/ChatHistoryRail.js.map +1 -0
- package/dist/chat-history/ChatHistoryRail.spec.d.ts +2 -0
- package/dist/chat-history/ChatHistoryRail.spec.d.ts.map +1 -0
- package/dist/chat-history/ChatHistoryRail.spec.js +157 -0
- package/dist/chat-history/ChatHistoryRail.spec.js.map +1 -0
- package/dist/chat-history/index.d.ts +2 -0
- package/dist/chat-history/index.d.ts.map +1 -1
- package/dist/chat-history/index.js +2 -0
- package/dist/chat-history/index.js.map +1 -1
- package/dist/chat-history/useChatHistoryRailController.d.ts +26 -0
- package/dist/chat-history/useChatHistoryRailController.d.ts.map +1 -0
- package/dist/chat-history/useChatHistoryRailController.js +36 -0
- package/dist/chat-history/useChatHistoryRailController.js.map +1 -0
- package/dist/chat-history/useChatHistoryRailController.spec.d.ts +2 -0
- package/dist/chat-history/useChatHistoryRailController.spec.d.ts.map +1 -0
- package/dist/chat-history/useChatHistoryRailController.spec.js +101 -0
- package/dist/chat-history/useChatHistoryRailController.spec.js.map +1 -0
- package/dist/chat-history.css +61 -0
- package/dist/composer/ComposerPlusMenu.js +1 -1
- package/dist/composer/ComposerPlusMenu.js.map +1 -1
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +22 -4
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/RealtimeVoiceMode.d.ts +5 -1
- package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/RealtimeVoiceMode.js +27 -8
- package/dist/composer/RealtimeVoiceMode.js.map +1 -1
- package/dist/composer/RealtimeVoiceMode.spec.js +51 -11
- package/dist/composer/RealtimeVoiceMode.spec.js.map +1 -1
- package/dist/composer/VoiceButton.d.ts +2 -0
- package/dist/composer/VoiceButton.d.ts.map +1 -1
- package/dist/composer/VoiceButton.js +33 -1
- package/dist/composer/VoiceButton.js.map +1 -1
- package/dist/composer/VoiceButton.spec.js +9 -1
- package/dist/composer/VoiceButton.spec.js.map +1 -1
- package/dist/composer/realtime-voice-audio-level.d.ts.map +1 -1
- package/dist/composer/realtime-voice-audio-level.js +2 -1
- package/dist/composer/realtime-voice-audio-level.js.map +1 -1
- package/dist/composer/realtime-voice-audio-level.spec.js +7 -0
- package/dist/composer/realtime-voice-audio-level.spec.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.d.ts +7 -0
- package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.js +95 -23
- package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.spec.js +58 -9
- package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -1
- package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts +7 -0
- package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts.map +1 -0
- package/dist/conformance/__fixtures__/css-in-js-adapter.js +84 -0
- package/dist/conformance/__fixtures__/css-in-js-adapter.js.map +1 -0
- package/dist/conformance/conformance.spec.d.ts +2 -0
- package/dist/conformance/conformance.spec.d.ts.map +1 -0
- package/dist/conformance/conformance.spec.js +58 -0
- package/dist/conformance/conformance.spec.js.map +1 -0
- package/dist/conformance/index.d.ts +4 -0
- package/dist/conformance/index.d.ts.map +1 -0
- package/dist/conformance/index.js +4 -0
- package/dist/conformance/index.js.map +1 -0
- package/dist/conformance/runner.d.ts +12 -0
- package/dist/conformance/runner.d.ts.map +1 -0
- package/dist/conformance/runner.js +441 -0
- package/dist/conformance/runner.js.map +1 -0
- package/dist/conformance/types.d.ts +26 -0
- package/dist/conformance/types.d.ts.map +1 -0
- package/dist/conformance/types.js +13 -0
- package/dist/conformance/types.js.map +1 -0
- package/dist/conformance/version.d.ts +6 -0
- package/dist/conformance/version.d.ts.map +1 -0
- package/dist/conformance/version.js +14 -0
- package/dist/conformance/version.js.map +1 -0
- package/dist/design-system/components.d.ts +64 -0
- package/dist/design-system/components.d.ts.map +1 -0
- package/dist/design-system/components.js +50 -0
- package/dist/design-system/components.js.map +1 -0
- package/dist/design-system/context.d.ts +11 -0
- package/dist/design-system/context.d.ts.map +1 -0
- package/dist/design-system/context.js +17 -0
- package/dist/design-system/context.js.map +1 -0
- package/dist/design-system/default-adapter.d.ts +3 -0
- package/dist/design-system/default-adapter.d.ts.map +1 -0
- package/dist/design-system/default-adapter.js +166 -0
- package/dist/design-system/default-adapter.js.map +1 -0
- package/dist/design-system/default-adapter.spec.d.ts +2 -0
- package/dist/design-system/default-adapter.spec.d.ts.map +1 -0
- package/dist/design-system/default-adapter.spec.js +96 -0
- package/dist/design-system/default-adapter.spec.js.map +1 -0
- package/dist/design-system/definition.d.ts +9 -0
- package/dist/design-system/definition.d.ts.map +1 -0
- package/dist/design-system/definition.js +4 -0
- package/dist/design-system/definition.js.map +1 -0
- package/dist/design-system/design-system.spec.d.ts +2 -0
- package/dist/design-system/design-system.spec.d.ts.map +1 -0
- package/dist/design-system/design-system.spec.js +121 -0
- package/dist/design-system/design-system.spec.js.map +1 -0
- package/dist/design-system/error-boundary.d.ts +18 -0
- package/dist/design-system/error-boundary.d.ts.map +1 -0
- package/dist/design-system/error-boundary.js +16 -0
- package/dist/design-system/error-boundary.js.map +1 -0
- package/dist/design-system/index.d.ts +8 -0
- package/dist/design-system/index.d.ts.map +1 -0
- package/dist/design-system/index.js +8 -0
- package/dist/design-system/index.js.map +1 -0
- package/dist/design-system/theme.d.ts +30 -0
- package/dist/design-system/theme.d.ts.map +1 -0
- package/dist/design-system/theme.js +95 -0
- package/dist/design-system/theme.js.map +1 -0
- package/dist/design-system/theme.spec.d.ts +2 -0
- package/dist/design-system/theme.spec.d.ts.map +1 -0
- package/dist/design-system/theme.spec.js +43 -0
- package/dist/design-system/theme.spec.js.map +1 -0
- package/dist/design-system/types.d.ts +288 -0
- package/dist/design-system/types.d.ts.map +1 -0
- package/dist/design-system/types.js +2 -0
- package/dist/design-system/types.js.map +1 -0
- package/dist/provider.d.ts +4 -3
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +46 -4
- package/dist/provider.js.map +1 -1
- package/dist/styles.css +35 -0
- package/dist/ui/button.d.ts +5 -0
- package/dist/ui/button.d.ts.map +1 -1
- package/dist/ui/button.js +25 -4
- package/dist/ui/button.js.map +1 -1
- package/dist/ui/dialog.d.ts +4 -0
- package/dist/ui/dialog.d.ts.map +1 -1
- package/dist/ui/dialog.js +2 -2
- package/dist/ui/dialog.js.map +1 -1
- package/dist/ui/popover.d.ts +1 -0
- package/dist/ui/popover.d.ts.map +1 -1
- package/dist/ui/popover.js +2 -2
- package/dist/ui/popover.js.map +1 -1
- package/dist/ui/select.d.ts +3 -1
- package/dist/ui/select.d.ts.map +1 -1
- package/dist/ui/select.js +1 -1
- package/dist/ui/select.js.map +1 -1
- package/dist/ui/sidebar.d.ts +6 -2
- package/dist/ui/sidebar.d.ts.map +1 -1
- package/dist/ui/sidebar.js +12 -15
- package/dist/ui/sidebar.js.map +1 -1
- package/dist/ui/tooltip.d.ts +3 -1
- package/dist/ui/tooltip.d.ts.map +1 -1
- package/dist/ui/tooltip.js +2 -2
- package/dist/ui/tooltip.js.map +1 -1
- package/package.json +31 -1
- package/src/chat-history/ChatHistoryRail.spec.tsx +312 -0
- package/src/chat-history/ChatHistoryRail.tsx +150 -0
- package/src/chat-history/index.ts +14 -0
- package/src/chat-history/useChatHistoryRailController.spec.tsx +131 -0
- package/src/chat-history/useChatHistoryRailController.ts +76 -0
- package/src/chat-history.css +61 -0
- package/src/composer/ComposerPlusMenu.tsx +11 -8
- package/src/composer/PromptComposer.tsx +27 -6
- package/src/composer/RealtimeVoiceMode.spec.tsx +116 -11
- package/src/composer/RealtimeVoiceMode.tsx +106 -36
- package/src/composer/VoiceButton.spec.ts +17 -1
- package/src/composer/VoiceButton.tsx +49 -0
- package/src/composer/realtime-voice-audio-level.spec.ts +15 -0
- package/src/composer/realtime-voice-audio-level.ts +2 -1
- package/src/composer/useRealtimeVoiceMode.spec.ts +82 -8
- package/src/composer/useRealtimeVoiceMode.tsx +112 -25
- package/src/conformance/__fixtures__/css-in-js-adapter.tsx +401 -0
- package/src/conformance/conformance.spec.tsx +83 -0
- package/src/conformance/index.ts +3 -0
- package/src/conformance/runner.tsx +710 -0
- package/src/conformance/types.ts +45 -0
- package/src/conformance/version.ts +22 -0
- package/src/design-system/components.tsx +146 -0
- package/src/design-system/context.tsx +31 -0
- package/src/design-system/default-adapter.spec.tsx +143 -0
- package/src/design-system/default-adapter.tsx +915 -0
- package/src/design-system/definition.ts +14 -0
- package/src/design-system/design-system.spec.tsx +183 -0
- package/src/design-system/error-boundary.tsx +37 -0
- package/src/design-system/index.ts +7 -0
- package/src/design-system/theme.spec.ts +61 -0
- package/src/design-system/theme.ts +145 -0
- package/src/design-system/types.ts +363 -0
- package/src/provider.tsx +98 -5
- package/src/styles.css +35 -0
- package/src/ui/button.tsx +95 -6
- package/src/ui/dialog.tsx +48 -23
- package/src/ui/popover.tsx +16 -2
- package/src/ui/select.tsx +7 -3
- package/src/ui/sidebar.tsx +47 -48
- package/src/ui/tooltip.tsx +7 -3
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DesignSystemTheme } from "./theme.js";
|
|
2
|
+
import type { DesignSystemComponents } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export interface DesignSystemDefinition {
|
|
5
|
+
name?: string;
|
|
6
|
+
components?: Partial<DesignSystemComponents>;
|
|
7
|
+
theme?: DesignSystemTheme;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function defineDesignSystem<
|
|
11
|
+
const Definition extends DesignSystemDefinition,
|
|
12
|
+
>(definition: Definition): Definition {
|
|
13
|
+
return definition;
|
|
14
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act, type ComponentProps } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { ToolkitProvider } from "../provider.js";
|
|
8
|
+
import { Button } from "../ui/button.js";
|
|
9
|
+
import { ActionButton } from "./components.js";
|
|
10
|
+
import { defaultDesignSystemComponents } from "./default-adapter.js";
|
|
11
|
+
import { defineDesignSystem } from "./definition.js";
|
|
12
|
+
import { defineTheme } from "./theme.js";
|
|
13
|
+
import { DESIGN_SYSTEM_CONTRACT_VERSION } from "./types.js";
|
|
14
|
+
|
|
15
|
+
describe("design-system contract", () => {
|
|
16
|
+
let container: HTMLDivElement;
|
|
17
|
+
let root: Root;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
21
|
+
container = document.createElement("div");
|
|
22
|
+
document.body.appendChild(container);
|
|
23
|
+
root = createRoot(container);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
act(() => root.unmount());
|
|
28
|
+
container.remove();
|
|
29
|
+
vi.restoreAllMocks();
|
|
30
|
+
vi.unstubAllGlobals();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("locks the seventeen semantic component names", () => {
|
|
34
|
+
expect(DESIGN_SYSTEM_CONTRACT_VERSION).toBe(1);
|
|
35
|
+
expect(Object.keys(defaultDesignSystemComponents).sort()).toEqual(
|
|
36
|
+
[
|
|
37
|
+
"ActionButton",
|
|
38
|
+
"Avatar",
|
|
39
|
+
"Checkbox",
|
|
40
|
+
"Dialog",
|
|
41
|
+
"IconButton",
|
|
42
|
+
"Menu",
|
|
43
|
+
"Picker",
|
|
44
|
+
"Popover",
|
|
45
|
+
"Skeleton",
|
|
46
|
+
"Spinner",
|
|
47
|
+
"Status",
|
|
48
|
+
"Surface",
|
|
49
|
+
"Switch",
|
|
50
|
+
"Tabs",
|
|
51
|
+
"TextArea",
|
|
52
|
+
"TextField",
|
|
53
|
+
"Tooltip",
|
|
54
|
+
].sort(),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("preserves typed design-system and theme definitions", () => {
|
|
59
|
+
const theme = defineTheme({
|
|
60
|
+
colors: {
|
|
61
|
+
light: { primary: "oklch(55% 0.2 260)" },
|
|
62
|
+
},
|
|
63
|
+
radius: "0.75rem",
|
|
64
|
+
});
|
|
65
|
+
const definition = defineDesignSystem({
|
|
66
|
+
name: "Acme",
|
|
67
|
+
theme,
|
|
68
|
+
components: {},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(definition).toEqual({ name: "Acme", theme, components: {} });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("prefers ActionButton over the legacy Button override", () => {
|
|
75
|
+
const warning = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
76
|
+
const LegacyButton = () => <button data-adapter="legacy" />;
|
|
77
|
+
const CustomActionButton = () => <button data-adapter="semantic" />;
|
|
78
|
+
|
|
79
|
+
act(() => {
|
|
80
|
+
root.render(
|
|
81
|
+
<ToolkitProvider
|
|
82
|
+
components={{ Button: LegacyButton }}
|
|
83
|
+
designSystem={{
|
|
84
|
+
components: { ActionButton: CustomActionButton },
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
<Button>Save</Button>
|
|
88
|
+
</ToolkitProvider>,
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(container.querySelector("[data-adapter=semantic]")).not.toBeNull();
|
|
93
|
+
expect(container.querySelector("[data-adapter=legacy]")).toBeNull();
|
|
94
|
+
expect(warning).toHaveBeenCalledOnce();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("warns when nested providers combine the effective ActionButton APIs", () => {
|
|
98
|
+
const warning = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
99
|
+
const LegacyButton = () => <button data-adapter="legacy" />;
|
|
100
|
+
const CustomActionButton = () => <button data-adapter="semantic" />;
|
|
101
|
+
|
|
102
|
+
act(() => {
|
|
103
|
+
root.render(
|
|
104
|
+
<ToolkitProvider
|
|
105
|
+
designSystem={{
|
|
106
|
+
components: { ActionButton: CustomActionButton },
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<ToolkitProvider components={{ Button: LegacyButton }}>
|
|
110
|
+
<Button>Save</Button>
|
|
111
|
+
</ToolkitProvider>
|
|
112
|
+
</ToolkitProvider>,
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(container.querySelector("[data-adapter=semantic]")).not.toBeNull();
|
|
117
|
+
expect(warning).toHaveBeenCalledOnce();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("forwards semantic intent independently from the legacy visual variant", () => {
|
|
121
|
+
const received = vi.fn();
|
|
122
|
+
const CustomActionButton = (props: ComponentProps<typeof ActionButton>) => {
|
|
123
|
+
received(props.intent, props.emphasis);
|
|
124
|
+
return <button>{props.children}</button>;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
act(() => {
|
|
128
|
+
root.render(
|
|
129
|
+
<ToolkitProvider
|
|
130
|
+
designSystem={{ components: { ActionButton: CustomActionButton } }}
|
|
131
|
+
>
|
|
132
|
+
<Button variant="ghost" intent="danger" emphasis="outline">
|
|
133
|
+
Remove
|
|
134
|
+
</Button>
|
|
135
|
+
</ToolkitProvider>,
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(received).toHaveBeenCalledWith("danger", "outline");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("uses legacy Button as the lowest-precedence ActionButton adapter", () => {
|
|
143
|
+
const LegacyButton = (props: ComponentProps<"button">) => (
|
|
144
|
+
<button {...props} data-adapter="legacy" />
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
act(() => {
|
|
148
|
+
root.render(
|
|
149
|
+
<ToolkitProvider components={{ Button: LegacyButton }}>
|
|
150
|
+
<ActionButton>Save</ActionButton>
|
|
151
|
+
</ToolkitProvider>,
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(container.querySelector("[data-adapter=legacy]")?.textContent).toBe(
|
|
156
|
+
"Save",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("isolates a broken customer component and renders the default control", () => {
|
|
161
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
162
|
+
const BrokenActionButton = () => {
|
|
163
|
+
throw new Error("broken adapter");
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
act(() => {
|
|
167
|
+
root.render(
|
|
168
|
+
<ToolkitProvider
|
|
169
|
+
designSystem={{
|
|
170
|
+
components: { ActionButton: BrokenActionButton },
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<ActionButton>Fallback action</ActionButton>
|
|
174
|
+
</ToolkitProvider>,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(container.querySelector("button")?.textContent).toBe(
|
|
179
|
+
"Fallback action",
|
|
180
|
+
);
|
|
181
|
+
expect(console.error).toHaveBeenCalled();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import type { DesignSystemComponentName } from "./types.js";
|
|
4
|
+
|
|
5
|
+
interface DesignSystemErrorBoundaryProps {
|
|
6
|
+
component: DesignSystemComponentName | string;
|
|
7
|
+
fallback: ReactNode;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface DesignSystemErrorBoundaryState {
|
|
12
|
+
failed: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class DesignSystemErrorBoundary extends Component<
|
|
16
|
+
DesignSystemErrorBoundaryProps,
|
|
17
|
+
DesignSystemErrorBoundaryState
|
|
18
|
+
> {
|
|
19
|
+
state: DesignSystemErrorBoundaryState = { failed: false };
|
|
20
|
+
|
|
21
|
+
static getDerivedStateFromError(): DesignSystemErrorBoundaryState {
|
|
22
|
+
return { failed: true };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
componentDidCatch(error: unknown, info: ErrorInfo) {
|
|
26
|
+
console.error(
|
|
27
|
+
`[agent-native] Custom design-system component ${this.props.component} failed; rendering the default adapter for this control.`,
|
|
28
|
+
error,
|
|
29
|
+
info,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
render() {
|
|
34
|
+
if (this.state.failed) return this.props.fallback;
|
|
35
|
+
return this.props.children;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
normalizeDesignSystemColor,
|
|
5
|
+
normalizeDesignSystemTheme,
|
|
6
|
+
renderDesignSystemThemeCss,
|
|
7
|
+
} from "./theme.js";
|
|
8
|
+
|
|
9
|
+
describe("design system themes", () => {
|
|
10
|
+
it.each([
|
|
11
|
+
["#ff0000", "0 100% 50%"],
|
|
12
|
+
["rgb(0 128 255)", "209.882 100% 50%"],
|
|
13
|
+
["oklch(62% 0.2 250)", "204.933 99.979% 45.91%"],
|
|
14
|
+
["rebeccapurple", "270 50% 40%"],
|
|
15
|
+
["rgb(255 0 0 / 50%)", "0 100% 50% / 0.5"],
|
|
16
|
+
])("normalizes %s to an HSL triplet", (input, output) => {
|
|
17
|
+
expect(normalizeDesignSystemColor(input)).toBe(output);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("falls back to light colors token by token without deriving dark colors", () => {
|
|
21
|
+
const normalized = normalizeDesignSystemTheme({
|
|
22
|
+
colors: {
|
|
23
|
+
light: { background: "white", foreground: "black" },
|
|
24
|
+
dark: { background: "#101010" },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(normalized.dark["--background"]).toBe("0 0% 6.275%");
|
|
29
|
+
expect(normalized.dark["--foreground"]).toBe(
|
|
30
|
+
normalized.light["--foreground"],
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("renders static light and dark token blocks", () => {
|
|
35
|
+
const css = renderDesignSystemThemeCss({
|
|
36
|
+
colors: { light: { primary: "oklch(60% 0.2 250)" } },
|
|
37
|
+
radius: "0.75rem",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(css).toContain(":root:root {");
|
|
41
|
+
expect(css).toContain(":root.dark {");
|
|
42
|
+
expect(css).toContain("--primary:");
|
|
43
|
+
expect(css.match(/--radius: 0.75rem/g)).toHaveLength(2);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it.each(["oklch(62% 0.2 250)", "oklch(70% 0.4 30)", "oklch(60% 0.3 120)"])(
|
|
47
|
+
"maps vivid %s into the sRGB gamut before HSL conversion",
|
|
48
|
+
(input) => {
|
|
49
|
+
const [, saturation] = normalizeDesignSystemColor(input).split(" ");
|
|
50
|
+
expect(Number.parseFloat(saturation)).toBeLessThanOrEqual(100);
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
it("identifies the invalid semantic token", () => {
|
|
55
|
+
expect(() =>
|
|
56
|
+
normalizeDesignSystemTheme({
|
|
57
|
+
colors: { light: { primary: "definitely-not-a-color" } },
|
|
58
|
+
}),
|
|
59
|
+
).toThrow('Invalid design system color for "primary"');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { clampChroma, converter, parse } from "culori";
|
|
2
|
+
|
|
3
|
+
export const DESIGN_SYSTEM_COLOR_TOKENS = [
|
|
4
|
+
"background",
|
|
5
|
+
"foreground",
|
|
6
|
+
"border",
|
|
7
|
+
"input",
|
|
8
|
+
"ring",
|
|
9
|
+
"primary",
|
|
10
|
+
"primary-foreground",
|
|
11
|
+
"secondary",
|
|
12
|
+
"secondary-foreground",
|
|
13
|
+
"destructive",
|
|
14
|
+
"destructive-foreground",
|
|
15
|
+
"muted",
|
|
16
|
+
"muted-foreground",
|
|
17
|
+
"accent",
|
|
18
|
+
"accent-foreground",
|
|
19
|
+
"popover",
|
|
20
|
+
"popover-foreground",
|
|
21
|
+
"card",
|
|
22
|
+
"card-foreground",
|
|
23
|
+
"sidebar-background",
|
|
24
|
+
"sidebar-foreground",
|
|
25
|
+
"sidebar-primary",
|
|
26
|
+
"sidebar-primary-foreground",
|
|
27
|
+
"sidebar-accent",
|
|
28
|
+
"sidebar-accent-foreground",
|
|
29
|
+
"sidebar-border",
|
|
30
|
+
"sidebar-ring",
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
export type DesignSystemColorToken =
|
|
34
|
+
(typeof DESIGN_SYSTEM_COLOR_TOKENS)[number];
|
|
35
|
+
export type DesignSystemColorPalette = Partial<
|
|
36
|
+
Record<DesignSystemColorToken, string>
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
export interface DesignSystemTheme {
|
|
40
|
+
colors: {
|
|
41
|
+
light: DesignSystemColorPalette;
|
|
42
|
+
/** Dark mode falls back token-by-token to the light palette. */
|
|
43
|
+
dark?: DesignSystemColorPalette;
|
|
44
|
+
};
|
|
45
|
+
radius?: string;
|
|
46
|
+
typography?: {
|
|
47
|
+
fontFamily?: string;
|
|
48
|
+
monoFontFamily?: string;
|
|
49
|
+
baseFontSize?: string;
|
|
50
|
+
};
|
|
51
|
+
elevation?: {
|
|
52
|
+
low?: string;
|
|
53
|
+
medium?: string;
|
|
54
|
+
high?: string;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface NormalizedDesignSystemTheme {
|
|
59
|
+
light: Record<string, string>;
|
|
60
|
+
dark: Record<string, string>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const toHsl = converter("hsl");
|
|
64
|
+
|
|
65
|
+
function round(value: number): string {
|
|
66
|
+
return Number(value.toFixed(3)).toString();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function normalizeDesignSystemColor(value: string): string {
|
|
70
|
+
const parsed = parse(value);
|
|
71
|
+
const color = parsed ? toHsl(clampChroma(parsed, "oklch")) : undefined;
|
|
72
|
+
if (!color) {
|
|
73
|
+
throw new Error(`Unsupported design system color: ${value}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const hue = color.h ?? 0;
|
|
77
|
+
const base = `${round(hue)} ${round(color.s * 100)}% ${round(color.l * 100)}%`;
|
|
78
|
+
return color.alpha !== undefined && color.alpha < 1
|
|
79
|
+
? `${base} / ${round(color.alpha)}`
|
|
80
|
+
: base;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizePalette(
|
|
84
|
+
palette: DesignSystemColorPalette,
|
|
85
|
+
): Record<string, string> {
|
|
86
|
+
return Object.fromEntries(
|
|
87
|
+
DESIGN_SYSTEM_COLOR_TOKENS.flatMap((token) => {
|
|
88
|
+
const value = palette[token];
|
|
89
|
+
if (value === undefined) return [];
|
|
90
|
+
try {
|
|
91
|
+
return [[`--${token}`, normalizeDesignSystemColor(value)]];
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Invalid design system color for "${token}": ${value}`,
|
|
95
|
+
{ cause: error },
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function normalizeDesignSystemTheme(
|
|
103
|
+
theme: DesignSystemTheme,
|
|
104
|
+
): NormalizedDesignSystemTheme {
|
|
105
|
+
const light = normalizePalette(theme.colors.light);
|
|
106
|
+
const dark = {
|
|
107
|
+
...light,
|
|
108
|
+
...normalizePalette(theme.colors.dark ?? {}),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const shared: Record<string, string | undefined> = {
|
|
112
|
+
"--radius": theme.radius,
|
|
113
|
+
"--font-family": theme.typography?.fontFamily,
|
|
114
|
+
"--font-mono": theme.typography?.monoFontFamily,
|
|
115
|
+
"--base-font-size": theme.typography?.baseFontSize,
|
|
116
|
+
"--shadow-low": theme.elevation?.low,
|
|
117
|
+
"--shadow-medium": theme.elevation?.medium,
|
|
118
|
+
"--shadow-high": theme.elevation?.high,
|
|
119
|
+
};
|
|
120
|
+
for (const [name, value] of Object.entries(shared)) {
|
|
121
|
+
if (value !== undefined) {
|
|
122
|
+
light[name] = value;
|
|
123
|
+
dark[name] = value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { light, dark };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderDeclarations(values: Record<string, string>): string {
|
|
131
|
+
return Object.entries(values)
|
|
132
|
+
.map(([name, value]) => ` ${name}: ${value};`)
|
|
133
|
+
.join("\n");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function renderDesignSystemThemeCss(theme: DesignSystemTheme): string {
|
|
137
|
+
const normalized = normalizeDesignSystemTheme(theme);
|
|
138
|
+
return `:root:root {\n${renderDeclarations(normalized.light)}\n}\n\n:root.dark {\n${renderDeclarations(normalized.dark)}\n}\n`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function defineTheme<const Theme extends DesignSystemTheme>(
|
|
142
|
+
theme: Theme,
|
|
143
|
+
): Theme {
|
|
144
|
+
return theme;
|
|
145
|
+
}
|