@jasonbelmonti/signal-ui 0.2.0 → 0.3.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 CHANGED
@@ -11,6 +11,10 @@ bun run typecheck
11
11
  bun run build
12
12
  ```
13
13
 
14
+ Pushes to `main` also publish the static Storybook to GitHub Pages via
15
+ `.github/workflows/storybook-pages.yml`, so the theme can be shared without a local build.
16
+ The default URL for this repository is `https://jasonbelmonti.github.io/signal-ui/`.
17
+
14
18
  `bun run build` emits the consumable package contract in `dist/`:
15
19
 
16
20
  - `dist/index.js`
@@ -1,4 +1,4 @@
1
- export { AntdThemeProvider, installStaticAntdTheme } from "./providers/AntdThemeProvider.js";
1
+ export { AntdThemeProvider, installStaticAntdTheme, type AntdThemeProviderProps, type InstallStaticAntdThemeOptions, } from "./providers/AntdThemeProvider.js";
2
2
  export { HashCube } from "./components/HashCube.js";
3
3
  export { GraphCanvas } from "./components/GraphCanvas.js";
4
4
  export type { GraphCanvasProps, GraphCanvasReactFlowProps } from "./components/GraphCanvas.js";
@@ -25,4 +25,5 @@ export { SignalWireframe } from "./components/SignalWireframe.js";
25
25
  export type { PixelCubePathProps, PixelCubePathTone, } from "./components/PixelCubePath.js";
26
26
  export type { PixelCubeLoaderGridSize, PixelCubeLoaderProps, PixelCubeLoaderTone, } from "./components/PixelCubeLoader.js";
27
27
  export type { SignalWireframeProps, SignalWireframeTone, } from "./components/SignalWireframe.js";
28
- export { signalFontStacks, signalPalette, signalTheme } from "./theme/signalTheme.js";
28
+ export { createSignalTheme, createSignalThemeCssVariables, resolveSignalPalette, signalFontStacks, signalPalette, signalTheme, } from "./theme/signalTheme.js";
29
+ export type { HexColor, SignalPalette, SignalThemeColorPreferences, SignalThemePreferences, } from "./theme/signalTheme.js";
package/dist/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { AntdThemeProvider, installStaticAntdTheme } from "./providers/AntdThemeProvider.js";
1
+ export { AntdThemeProvider, installStaticAntdTheme, } from "./providers/AntdThemeProvider.js";
2
2
  export { HashCube } from "./components/HashCube.js";
3
3
  export { GraphCanvas } from "./components/GraphCanvas.js";
4
4
  export { MarkdownTheme } from "./components/MarkdownTheme.js";
@@ -12,4 +12,4 @@ export { SignalStatusTag } from "./components/SignalStatusTag.js";
12
12
  export { PixelCubePath } from "./components/PixelCubePath.js";
13
13
  export { PixelCubeLoader } from "./components/PixelCubeLoader.js";
14
14
  export { SignalWireframe } from "./components/SignalWireframe.js";
15
- export { signalFontStacks, signalPalette, signalTheme } from "./theme/signalTheme.js";
15
+ export { createSignalTheme, createSignalThemeCssVariables, resolveSignalPalette, signalFontStacks, signalPalette, signalTheme, } from "./theme/signalTheme.js";
@@ -1,3 +1,10 @@
1
+ import type { ThemeConfig } from "antd";
1
2
  import type { PropsWithChildren } from "react";
2
- export declare function AntdThemeProvider({ children }: PropsWithChildren): import("react/jsx-runtime").JSX.Element;
3
- export declare function installStaticAntdTheme(): void;
3
+ import { type SignalThemePreferences } from "../theme/signalTheme.js";
4
+ export type AntdThemeProviderProps = PropsWithChildren<{
5
+ theme?: ThemeConfig;
6
+ themePreferences?: SignalThemePreferences;
7
+ }>;
8
+ export declare function AntdThemeProvider({ children, theme, themePreferences, }: AntdThemeProviderProps): import("react/jsx-runtime").JSX.Element;
9
+ export type InstallStaticAntdThemeOptions = Pick<AntdThemeProviderProps, "theme" | "themePreferences">;
10
+ export declare function installStaticAntdTheme(options?: InstallStaticAntdThemeOptions): void;
@@ -1,11 +1,101 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { App, ConfigProvider } from "antd";
3
- import { signalTheme } from "../theme/signalTheme.js";
4
- export function AntdThemeProvider({ children }) {
5
- return (_jsx(ConfigProvider, { theme: signalTheme, children: _jsx(App, { children: children }) }));
3
+ import { useInsertionEffect } from "react";
4
+ import { useRef } from "react";
5
+ import { mergeThemeConfig } from "../theme/mergeThemeConfig.js";
6
+ import { createSignalTheme, createSignalThemeCssVariables, signalTheme, } from "../theme/signalTheme.js";
7
+ // Portals read vars from documentElement, so keep the newest themed provider authoritative
8
+ // and restore the previous layer when a nested/themed preview unmounts.
9
+ const activeDocumentThemeEntries = [];
10
+ const documentThemeBaseline = new Map();
11
+ function resolveAntdTheme(theme, themePreferences) {
12
+ const baseTheme = themePreferences ? createSignalTheme(themePreferences) : signalTheme;
13
+ return theme ? mergeThemeConfig(baseTheme, theme) : baseTheme;
6
14
  }
7
- export function installStaticAntdTheme() {
15
+ function syncDocumentThemeVariables() {
16
+ if (typeof document === "undefined") {
17
+ return;
18
+ }
19
+ const rootStyle = document.documentElement.style;
20
+ const activeVariables = activeDocumentThemeEntries.at(-1)?.variables;
21
+ const variableNames = new Set(documentThemeBaseline.keys());
22
+ for (const entry of activeDocumentThemeEntries) {
23
+ for (const name of Object.keys(entry.variables)) {
24
+ variableNames.add(name);
25
+ }
26
+ }
27
+ for (const name of variableNames) {
28
+ const value = activeVariables?.[name];
29
+ if (value !== undefined) {
30
+ rootStyle.setProperty(name, String(value));
31
+ continue;
32
+ }
33
+ const baselineValue = documentThemeBaseline.get(name);
34
+ if (baselineValue) {
35
+ rootStyle.setProperty(name, baselineValue);
36
+ }
37
+ else {
38
+ rootStyle.removeProperty(name);
39
+ }
40
+ }
41
+ if (!activeVariables) {
42
+ documentThemeBaseline.clear();
43
+ }
44
+ }
45
+ function upsertDocumentThemeVariables(id, variables) {
46
+ if (typeof document === "undefined") {
47
+ return;
48
+ }
49
+ const rootStyle = document.documentElement.style;
50
+ for (const name of Object.keys(variables)) {
51
+ if (!documentThemeBaseline.has(name)) {
52
+ documentThemeBaseline.set(name, rootStyle.getPropertyValue(name));
53
+ }
54
+ }
55
+ const existingEntryIndex = activeDocumentThemeEntries.findIndex((entry) => entry.id === id);
56
+ if (existingEntryIndex === -1) {
57
+ activeDocumentThemeEntries.push({ id, variables });
58
+ }
59
+ else {
60
+ activeDocumentThemeEntries[existingEntryIndex] = { id, variables };
61
+ }
62
+ syncDocumentThemeVariables();
63
+ }
64
+ function removeDocumentThemeVariables(id) {
65
+ const existingEntryIndex = activeDocumentThemeEntries.findIndex((entry) => entry.id === id);
66
+ if (existingEntryIndex === -1) {
67
+ return;
68
+ }
69
+ activeDocumentThemeEntries.splice(existingEntryIndex, 1);
70
+ syncDocumentThemeVariables();
71
+ }
72
+ function useDocumentThemeVariables(themePreferences, themeConfig) {
73
+ const themeEntryIdRef = useRef(null);
74
+ if (!themeEntryIdRef.current) {
75
+ themeEntryIdRef.current = Symbol("signal-theme-vars");
76
+ }
77
+ useInsertionEffect(() => {
78
+ if ((!themePreferences && !themeConfig) || typeof document === "undefined") {
79
+ return;
80
+ }
81
+ const themeVariables = createSignalThemeCssVariables(themePreferences, themeConfig);
82
+ const themeEntryId = themeEntryIdRef.current;
83
+ if (!themeEntryId) {
84
+ return;
85
+ }
86
+ upsertDocumentThemeVariables(themeEntryId, themeVariables);
87
+ return () => {
88
+ removeDocumentThemeVariables(themeEntryId);
89
+ };
90
+ }, [themeConfig, themePreferences]);
91
+ }
92
+ export function AntdThemeProvider({ children, theme, themePreferences, }) {
93
+ const resolvedTheme = resolveAntdTheme(theme, themePreferences);
94
+ useDocumentThemeVariables(themePreferences, theme ? resolvedTheme : undefined);
95
+ return (_jsx(ConfigProvider, { theme: resolvedTheme, children: _jsx(App, { children: children }) }));
96
+ }
97
+ export function installStaticAntdTheme(options = {}) {
8
98
  ConfigProvider.config({
9
- holderRender: (children) => _jsx(AntdThemeProvider, { children: children }),
99
+ holderRender: (children) => _jsx(AntdThemeProvider, { ...options, children: children }),
10
100
  });
11
101
  }
@@ -0,0 +1,11 @@
1
+ export type HexColor = `#${string}`;
2
+ export declare function isHexColor(color: string | undefined): color is HexColor;
3
+ export declare function normalizeHexColor(color: string | undefined): HexColor | null;
4
+ export declare function resolveHexColor(color: string | undefined, fallback: HexColor): HexColor;
5
+ export declare function normalizeColor(color: string | undefined): HexColor | null;
6
+ export declare function normalizeCssColor(color: string | undefined): string | null;
7
+ export declare function mixHexColors(color: string, mixWith: string, mixAmount: number): string;
8
+ export declare function darkenHexColor(color: string, amount: number): string;
9
+ export declare function lightenHexColor(color: string, amount: number): string;
10
+ export declare function withAlpha(color: string, alpha: number): string;
11
+ export declare function toRgbTriplet(color: string): string | null;
@@ -0,0 +1,334 @@
1
+ let colorResolutionElement = null;
2
+ function clamp(value, min, max) {
3
+ return Math.min(Math.max(value, min), max);
4
+ }
5
+ function expandHexColor(hex) {
6
+ if (!/^[0-9a-fA-F]+$/.test(hex)) {
7
+ return null;
8
+ }
9
+ if (hex.length === 3 || hex.length === 4) {
10
+ return hex
11
+ .split("")
12
+ .map((segment) => `${segment}${segment}`)
13
+ .join("");
14
+ }
15
+ if (hex.length === 6 || hex.length === 8) {
16
+ return hex;
17
+ }
18
+ return null;
19
+ }
20
+ export function isHexColor(color) {
21
+ if (!color) {
22
+ return false;
23
+ }
24
+ const normalized = color.trim();
25
+ return /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(normalized);
26
+ }
27
+ export function normalizeHexColor(color) {
28
+ if (!isHexColor(color)) {
29
+ return null;
30
+ }
31
+ const parsed = parseHexColor(color);
32
+ return parsed ? formatHexColor(parsed) : null;
33
+ }
34
+ export function resolveHexColor(color, fallback) {
35
+ return normalizeHexColor(color) ?? fallback;
36
+ }
37
+ function parseRgbChannel(channel) {
38
+ const normalized = channel.trim().replace(/%$/, "");
39
+ const value = Number.parseFloat(normalized);
40
+ if (!Number.isFinite(value)) {
41
+ return null;
42
+ }
43
+ return channel.trim().endsWith("%") ? (value / 100) * 255 : value;
44
+ }
45
+ function parseHue(hue) {
46
+ const normalized = hue.trim().toLowerCase();
47
+ const value = Number.parseFloat(normalized);
48
+ if (!Number.isFinite(value)) {
49
+ return null;
50
+ }
51
+ if (normalized.endsWith("turn")) {
52
+ return value * 360;
53
+ }
54
+ if (normalized.endsWith("rad")) {
55
+ return (value * 180) / Math.PI;
56
+ }
57
+ return value;
58
+ }
59
+ function parsePercentChannel(channel) {
60
+ const normalized = channel.trim();
61
+ if (!normalized.endsWith("%")) {
62
+ return null;
63
+ }
64
+ const value = Number.parseFloat(normalized.slice(0, -1));
65
+ if (!Number.isFinite(value)) {
66
+ return null;
67
+ }
68
+ return clamp(value, 0, 100) / 100;
69
+ }
70
+ function parseAlphaChannel(channel) {
71
+ const normalized = channel.trim();
72
+ if (!normalized) {
73
+ return null;
74
+ }
75
+ if (normalized.endsWith("%")) {
76
+ const value = Number.parseFloat(normalized.slice(0, -1));
77
+ if (!Number.isFinite(value)) {
78
+ return null;
79
+ }
80
+ return clamp(value, 0, 100) / 100;
81
+ }
82
+ const value = Number.parseFloat(normalized);
83
+ if (!Number.isFinite(value)) {
84
+ return null;
85
+ }
86
+ return clamp(value, 0, 1);
87
+ }
88
+ function parseFunctionalColorArguments(color) {
89
+ const match = color.trim().match(/^[a-z]+\((.*)\)$/i);
90
+ const contents = match?.[1];
91
+ if (!contents) {
92
+ return null;
93
+ }
94
+ return contents
95
+ .trim()
96
+ .replace(/\s*\/\s*[^, ]+\s*$/, "")
97
+ .replace(/,/g, " ")
98
+ .split(/\s+/)
99
+ .filter(Boolean);
100
+ }
101
+ function parseFunctionalColorChannels(color) {
102
+ const match = color.trim().match(/^[a-z]+\((.*)\)$/i);
103
+ const contents = match?.[1]?.trim();
104
+ if (!contents) {
105
+ return null;
106
+ }
107
+ let channelContents = contents;
108
+ let alphaChannel;
109
+ let alphaProvided = false;
110
+ if (channelContents.includes("/")) {
111
+ const [channels, alpha] = channelContents.split("/", 2);
112
+ channelContents = channels?.trim() ?? "";
113
+ alphaChannel = alpha?.trim();
114
+ alphaProvided = true;
115
+ }
116
+ const channels = channelContents.replace(/,/g, " ").split(/\s+/).filter(Boolean);
117
+ if (!alphaChannel && channels.length === 4) {
118
+ alphaChannel = channels.pop();
119
+ alphaProvided = true;
120
+ }
121
+ const alpha = alphaChannel ? parseAlphaChannel(alphaChannel) : null;
122
+ if (alphaProvided && alpha === null) {
123
+ return null;
124
+ }
125
+ return {
126
+ alpha,
127
+ channels,
128
+ };
129
+ }
130
+ function parseRgbColor(color) {
131
+ const match = color.trim().match(/^rgba?\(/i);
132
+ if (!match) {
133
+ return null;
134
+ }
135
+ const parsedChannels = parseFunctionalColorChannels(color);
136
+ const channels = parsedChannels?.channels;
137
+ if (!channels || channels.length < 3) {
138
+ return null;
139
+ }
140
+ const [redChannel, greenChannel, blueChannel] = channels;
141
+ if (!redChannel || !greenChannel || !blueChannel) {
142
+ return null;
143
+ }
144
+ const red = parseRgbChannel(redChannel);
145
+ const green = parseRgbChannel(greenChannel);
146
+ const blue = parseRgbChannel(blueChannel);
147
+ if (red === null || green === null || blue === null) {
148
+ return null;
149
+ }
150
+ if (parsedChannels?.alpha === undefined) {
151
+ return null;
152
+ }
153
+ return { red, green, blue, alpha: parsedChannels.alpha };
154
+ }
155
+ function hslToRgb(hue, saturation, lightness) {
156
+ const normalizedHue = ((hue % 360) + 360) % 360;
157
+ const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;
158
+ const segment = normalizedHue / 60;
159
+ const second = chroma * (1 - Math.abs((segment % 2) - 1));
160
+ const matchLightness = lightness - chroma / 2;
161
+ let red = 0;
162
+ let green = 0;
163
+ let blue = 0;
164
+ if (segment >= 0 && segment < 1) {
165
+ red = chroma;
166
+ green = second;
167
+ }
168
+ else if (segment < 2) {
169
+ red = second;
170
+ green = chroma;
171
+ }
172
+ else if (segment < 3) {
173
+ green = chroma;
174
+ blue = second;
175
+ }
176
+ else if (segment < 4) {
177
+ green = second;
178
+ blue = chroma;
179
+ }
180
+ else if (segment < 5) {
181
+ red = second;
182
+ blue = chroma;
183
+ }
184
+ else {
185
+ red = chroma;
186
+ blue = second;
187
+ }
188
+ return {
189
+ red: (red + matchLightness) * 255,
190
+ green: (green + matchLightness) * 255,
191
+ blue: (blue + matchLightness) * 255,
192
+ };
193
+ }
194
+ function parseHslColor(color) {
195
+ const match = color.trim().match(/^hsla?\(/i);
196
+ if (!match) {
197
+ return null;
198
+ }
199
+ const parsedChannels = parseFunctionalColorChannels(color);
200
+ const channels = parsedChannels?.channels;
201
+ if (!channels || channels.length < 3) {
202
+ return null;
203
+ }
204
+ const [hueChannel, saturationChannel, lightnessChannel] = channels;
205
+ if (!hueChannel || !saturationChannel || !lightnessChannel) {
206
+ return null;
207
+ }
208
+ const hue = parseHue(hueChannel);
209
+ const saturation = parsePercentChannel(saturationChannel);
210
+ const lightness = parsePercentChannel(lightnessChannel);
211
+ if (hue === null || saturation === null || lightness === null) {
212
+ return null;
213
+ }
214
+ if (parsedChannels?.alpha === undefined) {
215
+ return null;
216
+ }
217
+ return { ...hslToRgb(hue, saturation, lightness), alpha: parsedChannels.alpha };
218
+ }
219
+ function parseHexColor(color) {
220
+ const normalized = color.trim().replace(/^#/, "");
221
+ const hex = expandHexColor(normalized);
222
+ if (!hex) {
223
+ return null;
224
+ }
225
+ const red = Number.parseInt(hex.slice(0, 2), 16);
226
+ const green = Number.parseInt(hex.slice(2, 4), 16);
227
+ const blue = Number.parseInt(hex.slice(4, 6), 16);
228
+ return { red, green, blue };
229
+ }
230
+ function parseHexColorWithAlpha(color) {
231
+ const normalized = color.trim().replace(/^#/, "");
232
+ const hex = expandHexColor(normalized);
233
+ const rgb = parseHexColor(color);
234
+ if (!hex || !rgb) {
235
+ return null;
236
+ }
237
+ if (normalized.length === 4 || normalized.length === 8) {
238
+ const alphaHex = hex.slice(6, 8);
239
+ const alpha = Number.parseInt(alphaHex, 16) / 255;
240
+ return { ...rgb, alpha };
241
+ }
242
+ return { ...rgb, alpha: null };
243
+ }
244
+ function parseCssColor(color) {
245
+ return parseHexColorWithAlpha(color) ?? parseRgbColor(color) ?? parseHslColor(color) ?? parseBrowserColor(color);
246
+ }
247
+ function getColorResolutionElement() {
248
+ if (typeof document === "undefined") {
249
+ return null;
250
+ }
251
+ if (!colorResolutionElement) {
252
+ colorResolutionElement = document.createElement("div");
253
+ colorResolutionElement.setAttribute("aria-hidden", "true");
254
+ colorResolutionElement.style.display = "none";
255
+ document.documentElement.append(colorResolutionElement);
256
+ }
257
+ else if (!colorResolutionElement.isConnected) {
258
+ document.documentElement.append(colorResolutionElement);
259
+ }
260
+ return colorResolutionElement;
261
+ }
262
+ function parseBrowserColor(color) {
263
+ const resolutionElement = getColorResolutionElement();
264
+ if (!resolutionElement) {
265
+ return null;
266
+ }
267
+ resolutionElement.style.color = "";
268
+ resolutionElement.style.color = color;
269
+ if (!resolutionElement.style.color) {
270
+ return null;
271
+ }
272
+ return parseRgbColor(getComputedStyle(resolutionElement).color);
273
+ }
274
+ function formatHexColor({ red, green, blue }) {
275
+ return `#${[red, green, blue]
276
+ .map((channel) => clamp(Math.round(channel), 0, 255).toString(16).padStart(2, "0"))
277
+ .join("")}`;
278
+ }
279
+ function formatCssColor({ red, green, blue, alpha }) {
280
+ if (alpha === null || alpha >= 1) {
281
+ return formatHexColor({ red, green, blue });
282
+ }
283
+ return `rgba(${clamp(Math.round(red), 0, 255)}, ${clamp(Math.round(green), 0, 255)}, ${clamp(Math.round(blue), 0, 255)}, ${alpha})`;
284
+ }
285
+ export function normalizeColor(color) {
286
+ if (!color) {
287
+ return null;
288
+ }
289
+ const parsed = parseCssColor(color);
290
+ if (!parsed || (parsed.alpha !== null && parsed.alpha < 1)) {
291
+ return null;
292
+ }
293
+ return formatHexColor(parsed);
294
+ }
295
+ export function normalizeCssColor(color) {
296
+ if (!color) {
297
+ return null;
298
+ }
299
+ const parsed = parseCssColor(color);
300
+ return parsed ? formatCssColor(parsed) : null;
301
+ }
302
+ export function mixHexColors(color, mixWith, mixAmount) {
303
+ const source = parseHexColor(color);
304
+ const target = parseHexColor(mixWith);
305
+ if (!source || !target) {
306
+ return color;
307
+ }
308
+ const weight = clamp(mixAmount, 0, 1);
309
+ return formatHexColor({
310
+ red: source.red + (target.red - source.red) * weight,
311
+ green: source.green + (target.green - source.green) * weight,
312
+ blue: source.blue + (target.blue - source.blue) * weight,
313
+ });
314
+ }
315
+ export function darkenHexColor(color, amount) {
316
+ return mixHexColors(color, "#000000", amount);
317
+ }
318
+ export function lightenHexColor(color, amount) {
319
+ return mixHexColors(color, "#ffffff", amount);
320
+ }
321
+ export function withAlpha(color, alpha) {
322
+ const parsed = parseCssColor(color);
323
+ if (!parsed) {
324
+ return color;
325
+ }
326
+ return `rgba(${parsed.red}, ${parsed.green}, ${parsed.blue}, ${clamp(alpha, 0, 1)})`;
327
+ }
328
+ export function toRgbTriplet(color) {
329
+ const parsed = parseCssColor(color);
330
+ if (!parsed) {
331
+ return null;
332
+ }
333
+ return `${parsed.red} ${parsed.green} ${parsed.blue}`;
334
+ }
@@ -0,0 +1,8 @@
1
+ import type { ThemeConfig } from "antd";
2
+ import type { CSSProperties } from "react";
3
+ import type { SignalPalette as ResolvedSignalPalette } from "./signalPalette.js";
4
+ import type { SignalThemePreferences } from "./signalThemePreferences.js";
5
+ export declare function resolveSignalPalette(preferences?: SignalThemePreferences): ResolvedSignalPalette;
6
+ export type SignalThemeStyleVariables = CSSProperties & Record<`--signal-ui-${string}`, string | number>;
7
+ export declare function createSignalThemeCssVariables(preferences?: SignalThemePreferences, themeConfig?: ThemeConfig): SignalThemeStyleVariables;
8
+ export declare function createSignalTheme(preferences?: SignalThemePreferences): ThemeConfig;