@parallelworks/ui 0.1.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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/components/Callout.d.ts +7 -0
- package/dist/components/DescriptionList.d.ts +18 -0
- package/dist/components/EmptyState.d.ts +11 -0
- package/dist/components/Loader.d.ts +10 -0
- package/dist/components/Modal.d.ts +26 -0
- package/dist/components/StatusBadge.d.ts +23 -0
- package/dist/components/Table.d.ts +52 -0
- package/dist/components/Tooltip.d.ts +15 -0
- package/dist/icons.d.ts +39 -0
- package/dist/icons.js +38 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +421 -0
- package/dist/styles/theme.css +185 -0
- package/dist/styles.css +2 -0
- package/dist/theme/index.d.ts +27 -0
- package/dist/theme/index.js +272 -0
- package/package.json +71 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const THEME_TOKENS: readonly ['--theme-app-bg', '--theme-app', '--theme-panel-bg', '--theme-panel', '--theme-muted-panel-bg', '--theme-muted-panel-color', '--theme-muted-text-color', '--theme-border', '--theme-element', '--theme-element-text', '--theme-hover', '--theme-hover-text', '--theme-link', '--theme-input-bg', '--theme-input', '--theme-input-disabled-bg', '--theme-input-disabled-text', '--theme-card-bg', '--theme-card-border', '--theme-card-hover', '--theme-accent', '--theme-accent-text', '--theme-focus-ring', '--theme-hover-overlay', '--theme-success', '--theme-success-muted', '--theme-error', '--theme-error-muted', '--theme-warning', '--theme-warning-muted', '--theme-info', '--theme-info-muted', '--theme-favorite', '--theme-favorite-muted', '--theme-shadow-sm', '--theme-shadow', '--theme-shadow-lg', '--theme-bg', '--theme-sidebar-text', '--theme-sidebar-hover', '--theme-sidebar-hover-text', '--theme-sidebar-active-bg', '--theme-sidebar-active-text'];
|
|
2
|
+
export type ThemeToken = (typeof THEME_TOKENS)[number];
|
|
3
|
+
export type ThemeVariables = Record<ThemeToken, string>;
|
|
4
|
+
export interface ThemeSeed {
|
|
5
|
+
/** Accent color as a hex string, e.g. '#06354f'. */
|
|
6
|
+
accent: string;
|
|
7
|
+
/** Base background color as a hex string. */
|
|
8
|
+
background: string;
|
|
9
|
+
/** How far foregrounds and borders sit from the background. 1 is normal; sensible range is roughly 0.5–1.5. */
|
|
10
|
+
contrast?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface SurfaceSeeds {
|
|
13
|
+
interface: ThemeSeed;
|
|
14
|
+
sidebar?: ThemeSeed;
|
|
15
|
+
}
|
|
16
|
+
export declare function contrastRatio(a: string, b: string): number;
|
|
17
|
+
/** Uses the WCAG luminance threshold 0.179, matching how org themes classify backgrounds. */
|
|
18
|
+
export declare function isDarkColor(hex: string): boolean;
|
|
19
|
+
export declare function deriveTheme(seed: ThemeSeed | SurfaceSeeds): ThemeVariables;
|
|
20
|
+
export interface ThemePreset {
|
|
21
|
+
name: string;
|
|
22
|
+
label: string;
|
|
23
|
+
seed: SurfaceSeeds;
|
|
24
|
+
}
|
|
25
|
+
export declare const THEME_PRESETS: readonly ThemePreset[];
|
|
26
|
+
export declare const DEFAULT_PRESET: ThemePreset;
|
|
27
|
+
export declare function applyTheme(el: HTMLElement, vars: Partial<ThemeVariables>): void;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
//#region src/theme/index.ts
|
|
2
|
+
var THEME_TOKENS = [
|
|
3
|
+
"--theme-app-bg",
|
|
4
|
+
"--theme-app",
|
|
5
|
+
"--theme-panel-bg",
|
|
6
|
+
"--theme-panel",
|
|
7
|
+
"--theme-muted-panel-bg",
|
|
8
|
+
"--theme-muted-panel-color",
|
|
9
|
+
"--theme-muted-text-color",
|
|
10
|
+
"--theme-border",
|
|
11
|
+
"--theme-element",
|
|
12
|
+
"--theme-element-text",
|
|
13
|
+
"--theme-hover",
|
|
14
|
+
"--theme-hover-text",
|
|
15
|
+
"--theme-link",
|
|
16
|
+
"--theme-input-bg",
|
|
17
|
+
"--theme-input",
|
|
18
|
+
"--theme-input-disabled-bg",
|
|
19
|
+
"--theme-input-disabled-text",
|
|
20
|
+
"--theme-card-bg",
|
|
21
|
+
"--theme-card-border",
|
|
22
|
+
"--theme-card-hover",
|
|
23
|
+
"--theme-accent",
|
|
24
|
+
"--theme-accent-text",
|
|
25
|
+
"--theme-focus-ring",
|
|
26
|
+
"--theme-hover-overlay",
|
|
27
|
+
"--theme-success",
|
|
28
|
+
"--theme-success-muted",
|
|
29
|
+
"--theme-error",
|
|
30
|
+
"--theme-error-muted",
|
|
31
|
+
"--theme-warning",
|
|
32
|
+
"--theme-warning-muted",
|
|
33
|
+
"--theme-info",
|
|
34
|
+
"--theme-info-muted",
|
|
35
|
+
"--theme-favorite",
|
|
36
|
+
"--theme-favorite-muted",
|
|
37
|
+
"--theme-shadow-sm",
|
|
38
|
+
"--theme-shadow",
|
|
39
|
+
"--theme-shadow-lg",
|
|
40
|
+
"--theme-bg",
|
|
41
|
+
"--theme-sidebar-text",
|
|
42
|
+
"--theme-sidebar-hover",
|
|
43
|
+
"--theme-sidebar-hover-text",
|
|
44
|
+
"--theme-sidebar-active-bg",
|
|
45
|
+
"--theme-sidebar-active-text"
|
|
46
|
+
];
|
|
47
|
+
function parseHex(hex) {
|
|
48
|
+
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
49
|
+
if (!m) throw new Error(`Invalid hex color: ${hex}`);
|
|
50
|
+
return [
|
|
51
|
+
Number.parseInt(m[1], 16),
|
|
52
|
+
Number.parseInt(m[2], 16),
|
|
53
|
+
Number.parseInt(m[3], 16)
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
function toHex([r, g, b]) {
|
|
57
|
+
const c = (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0");
|
|
58
|
+
return `#${c(r)}${c(g)}${c(b)}`;
|
|
59
|
+
}
|
|
60
|
+
/** Linear interpolation from `a` toward `b` by `t` in sRGB. */
|
|
61
|
+
function mix(a, b, t) {
|
|
62
|
+
const ca = parseHex(a);
|
|
63
|
+
const cb = parseHex(b);
|
|
64
|
+
return toHex([
|
|
65
|
+
ca[0] + (cb[0] - ca[0]) * t,
|
|
66
|
+
ca[1] + (cb[1] - ca[1]) * t,
|
|
67
|
+
ca[2] + (cb[2] - ca[2]) * t
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
function alpha(hex, a) {
|
|
71
|
+
const [r, g, b] = parseHex(hex);
|
|
72
|
+
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
73
|
+
}
|
|
74
|
+
function relativeLuminance(hex) {
|
|
75
|
+
const [r, g, b] = parseHex(hex).map((v) => {
|
|
76
|
+
const s = v / 255;
|
|
77
|
+
return s <= .03928 ? s / 12.92 : ((s + .055) / 1.055) ** 2.4;
|
|
78
|
+
});
|
|
79
|
+
return .2126 * r + .7152 * g + .0722 * b;
|
|
80
|
+
}
|
|
81
|
+
function contrastRatio(a, b) {
|
|
82
|
+
const la = relativeLuminance(a);
|
|
83
|
+
const lb = relativeLuminance(b);
|
|
84
|
+
const [hi, lo] = la > lb ? [la, lb] : [lb, la];
|
|
85
|
+
return (hi + .05) / (lo + .05);
|
|
86
|
+
}
|
|
87
|
+
/** Uses the WCAG luminance threshold 0.179, matching how org themes classify backgrounds. */
|
|
88
|
+
function isDarkColor(hex) {
|
|
89
|
+
return relativeLuminance(hex) < .179;
|
|
90
|
+
}
|
|
91
|
+
/** White or black, whichever reads better on the given color. */
|
|
92
|
+
function readableOn(hex) {
|
|
93
|
+
return contrastRatio(hex, "#ffffff") >= contrastRatio(hex, "#000000") ? "#ffffff" : "#000000";
|
|
94
|
+
}
|
|
95
|
+
/** Nudges `color` toward the readable pole of `bg` until it clears `ratio`. */
|
|
96
|
+
function ensureReadable(color, bg, ratio) {
|
|
97
|
+
const pole = isDarkColor(bg) ? "#ffffff" : "#000000";
|
|
98
|
+
let out = color;
|
|
99
|
+
for (let t = 0; t <= 1 && contrastRatio(out, bg) < ratio; t += .05) out = mix(color, pole, t);
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
function clampContrast(contrast) {
|
|
103
|
+
return Math.max(.25, Math.min(2, contrast ?? 1));
|
|
104
|
+
}
|
|
105
|
+
var STATUS_HUES = {
|
|
106
|
+
success: {
|
|
107
|
+
onLight: "#16a34a",
|
|
108
|
+
onDark: "#22c55e"
|
|
109
|
+
},
|
|
110
|
+
error: {
|
|
111
|
+
onLight: "#dc2626",
|
|
112
|
+
onDark: "#ef4444"
|
|
113
|
+
},
|
|
114
|
+
warning: {
|
|
115
|
+
onLight: "#d97706",
|
|
116
|
+
onDark: "#f59e0b"
|
|
117
|
+
},
|
|
118
|
+
info: {
|
|
119
|
+
onLight: "#2563eb",
|
|
120
|
+
onDark: "#3b82f6"
|
|
121
|
+
},
|
|
122
|
+
favorite: {
|
|
123
|
+
onLight: "#e11d48",
|
|
124
|
+
onDark: "#f43f5e"
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
function deriveSidebar(seed) {
|
|
128
|
+
const bg = seed.background;
|
|
129
|
+
const c = clampContrast(seed.contrast);
|
|
130
|
+
const dark = isDarkColor(bg);
|
|
131
|
+
const pole = dark ? "#ffffff" : "#000000";
|
|
132
|
+
const text = mix(pole, bg, Math.max(0, 1 - c) * .5);
|
|
133
|
+
const hover = mix(bg, pole, .12 * c);
|
|
134
|
+
const activeBg = dark ? mix(seed.accent, "#ffffff", .85) : seed.accent;
|
|
135
|
+
return {
|
|
136
|
+
bg,
|
|
137
|
+
text,
|
|
138
|
+
hover,
|
|
139
|
+
hoverText: text,
|
|
140
|
+
activeBg,
|
|
141
|
+
activeText: readableOn(activeBg)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function deriveTheme(seed) {
|
|
145
|
+
const surfaces = "interface" in seed ? seed : { interface: seed };
|
|
146
|
+
const { accent, background: bg } = surfaces.interface;
|
|
147
|
+
const c = clampContrast(surfaces.interface.contrast);
|
|
148
|
+
const dark = isDarkColor(bg);
|
|
149
|
+
const pole = dark ? "#ffffff" : "#000000";
|
|
150
|
+
const fg = mix(pole, bg, Math.max(0, 1 - c) * .5);
|
|
151
|
+
const panelBg = dark ? mix(bg, "#ffffff", .045) : mix(bg, "#ffffff", .8);
|
|
152
|
+
const mutedPanelBg = dark ? mix(bg, "#ffffff", .09) : mix(bg, "#ffffff", .5);
|
|
153
|
+
const border = mix(bg, pole, (dark ? .16 : .16) * c);
|
|
154
|
+
const mutedText = mix(fg, bg, .42);
|
|
155
|
+
const hover = dark ? mix(bg, "#ffffff", .08) : mix(bg, "#000000", .04);
|
|
156
|
+
const inputBg = dark ? mix(bg, "#ffffff", .08) : mix(bg, "#ffffff", .65);
|
|
157
|
+
const link = ensureReadable(accent, bg, 4.5);
|
|
158
|
+
const accentText = readableOn(accent);
|
|
159
|
+
const cardBg = panelBg;
|
|
160
|
+
let sidebarBg = panelBg;
|
|
161
|
+
if (!dark) sidebarBg = isDarkColor(accent) ? accent : mix(accent, "#000000", .4);
|
|
162
|
+
const sidebar = deriveSidebar(surfaces.sidebar ?? {
|
|
163
|
+
accent,
|
|
164
|
+
background: sidebarBg,
|
|
165
|
+
contrast: c
|
|
166
|
+
});
|
|
167
|
+
const status = (name) => ensureReadable(dark ? STATUS_HUES[name].onDark : STATUS_HUES[name].onLight, bg, 3);
|
|
168
|
+
const shadowInk = dark ? .35 : .07;
|
|
169
|
+
return {
|
|
170
|
+
"--theme-app-bg": bg,
|
|
171
|
+
"--theme-app": fg,
|
|
172
|
+
"--theme-panel-bg": panelBg,
|
|
173
|
+
"--theme-panel": fg,
|
|
174
|
+
"--theme-muted-panel-bg": mutedPanelBg,
|
|
175
|
+
"--theme-muted-panel-color": mutedText,
|
|
176
|
+
"--theme-muted-text-color": mutedText,
|
|
177
|
+
"--theme-border": border,
|
|
178
|
+
"--theme-element": accent,
|
|
179
|
+
"--theme-element-text": accentText,
|
|
180
|
+
"--theme-hover": hover,
|
|
181
|
+
"--theme-hover-text": fg,
|
|
182
|
+
"--theme-link": link,
|
|
183
|
+
"--theme-input-bg": inputBg,
|
|
184
|
+
"--theme-input": link,
|
|
185
|
+
"--theme-input-disabled-bg": mix(inputBg, bg, .5),
|
|
186
|
+
"--theme-input-disabled-text": mix(fg, bg, .6),
|
|
187
|
+
"--theme-card-bg": cardBg,
|
|
188
|
+
"--theme-card-border": alpha(pole, dark ? .06 : .04),
|
|
189
|
+
"--theme-card-hover": dark ? mix(cardBg, "#ffffff", .05) : mix(cardBg, "#000000", .02),
|
|
190
|
+
"--theme-accent": accent,
|
|
191
|
+
"--theme-accent-text": accentText,
|
|
192
|
+
"--theme-focus-ring": alpha(link, dark ? .25 : .2),
|
|
193
|
+
"--theme-hover-overlay": alpha(pole, dark ? .03 : .02),
|
|
194
|
+
"--theme-success": status("success"),
|
|
195
|
+
"--theme-success-muted": alpha(status("success"), dark ? .15 : .1),
|
|
196
|
+
"--theme-error": status("error"),
|
|
197
|
+
"--theme-error-muted": alpha(status("error"), dark ? .15 : .1),
|
|
198
|
+
"--theme-warning": status("warning"),
|
|
199
|
+
"--theme-warning-muted": alpha(status("warning"), dark ? .15 : .1),
|
|
200
|
+
"--theme-info": status("info"),
|
|
201
|
+
"--theme-info-muted": alpha(status("info"), dark ? .15 : .1),
|
|
202
|
+
"--theme-favorite": status("favorite"),
|
|
203
|
+
"--theme-favorite-muted": alpha(status("favorite"), dark ? .15 : .1),
|
|
204
|
+
"--theme-shadow-sm": `0 1px 2px rgba(0, 0, 0, ${shadowInk})`,
|
|
205
|
+
"--theme-shadow": `0 4px 6px -1px rgba(0, 0, 0, ${shadowInk}), 0 2px 4px -2px rgba(0, 0, 0, ${shadowInk})`,
|
|
206
|
+
"--theme-shadow-lg": `0 10px 15px -3px rgba(0, 0, 0, ${shadowInk}), 0 4px 6px -4px rgba(0, 0, 0, ${shadowInk})`,
|
|
207
|
+
"--theme-bg": sidebar.bg,
|
|
208
|
+
"--theme-sidebar-text": sidebar.text,
|
|
209
|
+
"--theme-sidebar-hover": sidebar.hover,
|
|
210
|
+
"--theme-sidebar-hover-text": sidebar.hoverText,
|
|
211
|
+
"--theme-sidebar-active-bg": sidebar.activeBg,
|
|
212
|
+
"--theme-sidebar-active-text": sidebar.activeText
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
var THEME_PRESETS = [
|
|
216
|
+
{
|
|
217
|
+
name: "activate",
|
|
218
|
+
label: "ACTIVATE",
|
|
219
|
+
seed: {
|
|
220
|
+
interface: {
|
|
221
|
+
accent: "#06354f",
|
|
222
|
+
background: "#f3f4f6"
|
|
223
|
+
},
|
|
224
|
+
sidebar: {
|
|
225
|
+
accent: "#06354f",
|
|
226
|
+
background: "#06354f"
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "light",
|
|
232
|
+
label: "Light",
|
|
233
|
+
seed: { interface: {
|
|
234
|
+
accent: "#0d6efd",
|
|
235
|
+
background: "#ffffff"
|
|
236
|
+
} }
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "dark",
|
|
240
|
+
label: "Dark",
|
|
241
|
+
seed: {
|
|
242
|
+
interface: {
|
|
243
|
+
accent: "#2f81f7",
|
|
244
|
+
background: "#0d1117"
|
|
245
|
+
},
|
|
246
|
+
sidebar: {
|
|
247
|
+
accent: "#2f81f7",
|
|
248
|
+
background: "#161b22"
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "rose-pine",
|
|
254
|
+
label: "Rosé Pine",
|
|
255
|
+
seed: {
|
|
256
|
+
interface: {
|
|
257
|
+
accent: "#c4a7e7",
|
|
258
|
+
background: "#191724"
|
|
259
|
+
},
|
|
260
|
+
sidebar: {
|
|
261
|
+
accent: "#c4a7e7",
|
|
262
|
+
background: "#1f1d2e"
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
];
|
|
267
|
+
var DEFAULT_PRESET = THEME_PRESETS[0];
|
|
268
|
+
function applyTheme(el, vars) {
|
|
269
|
+
for (const [token, value] of Object.entries(vars)) if (value !== void 0) el.style.setProperty(token, value);
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
export { DEFAULT_PRESET, THEME_PRESETS, THEME_TOKENS, applyTheme, contrastRatio, deriveTheme, isDarkColor };
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@parallelworks/ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared UI foundation for Parallel Works packages: theme contract, icons, and core primitives",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Parallel Works <support@parallelworks.com>",
|
|
7
|
+
"homepage": "https://parallelworks.com/docs",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./icons": {
|
|
15
|
+
"types": "./dist/icons.d.ts",
|
|
16
|
+
"import": "./dist/icons.js"
|
|
17
|
+
},
|
|
18
|
+
"./theme": {
|
|
19
|
+
"types": "./dist/theme/index.d.ts",
|
|
20
|
+
"import": "./dist/theme/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./theme.css": "./dist/styles/theme.css",
|
|
23
|
+
"./styles.css": "./dist/styles.css"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"registry": "https://registry.npmjs.org"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": "^19.0.0",
|
|
33
|
+
"react-dom": "^19.0.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"classnames": "^2.5.1",
|
|
37
|
+
"react-icons": "5.6.0",
|
|
38
|
+
"react-tooltip": "^6.0.7"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
42
|
+
"@tailwindcss/cli": "^4.3.0",
|
|
43
|
+
"@tailwindcss/vite": "^4.3.0",
|
|
44
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
45
|
+
"@testing-library/react": "^16.3.2",
|
|
46
|
+
"@types/node": "^24.3.1",
|
|
47
|
+
"@types/react": "19.2.17",
|
|
48
|
+
"@types/react-dom": "19.2.3",
|
|
49
|
+
"@vitejs/plugin-react": "^6.0.2",
|
|
50
|
+
"jsdom": "^26.1.0",
|
|
51
|
+
"publint": "^0.3.14",
|
|
52
|
+
"react": "19.2.8",
|
|
53
|
+
"react-dom": "19.2.8",
|
|
54
|
+
"tailwindcss": "^4.3.0",
|
|
55
|
+
"typescript": "^7.0.0",
|
|
56
|
+
"vite": "^8.0.16",
|
|
57
|
+
"vitest": "^4.1.8"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "vite build && pnpm build:types && pnpm build:css",
|
|
61
|
+
"build:types": "tsc -p tsconfig.build.json && node scripts/declaration-extensions.mjs",
|
|
62
|
+
"build:css": "mkdir -p dist/styles && cp src/styles/theme.css dist/styles/ && tailwindcss -i ./src/styles.css -o ./dist/styles.css --minify",
|
|
63
|
+
"clean": "rm -rf dist",
|
|
64
|
+
"check-types": "tsc --noEmit",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"lint:package": "publint --strict",
|
|
67
|
+
"verify:pack": "node scripts/verify-pack.mjs"
|
|
68
|
+
},
|
|
69
|
+
"main": "./dist/index.js",
|
|
70
|
+
"types": "./dist/index.d.ts"
|
|
71
|
+
}
|