@tangle-network/brand 1.27.1 → 1.28.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/CHANGELOG.md +30 -0
- package/dist/index.d.ts +18 -22
- package/dist/index.js +20 -22
- package/dist/knot.d.ts +1 -3
- package/dist/knot.js +1 -2
- package/dist/social.d.ts +1 -3
- package/dist/social.js +1 -2
- package/package.json +4 -4
- package/src/palette.generated.ts +18 -18
- package/src/styles/css-test-utils.test.ts +10 -7
- package/src/styles/css-test-utils.ts +38 -13
- package/src/styles/focus.test.ts +4 -15
- package/src/styles/legacy-light.css +193 -337
- package/src/styles/named-themes.css +16 -24
- package/src/styles/named-themes.test.ts +35 -98
- package/src/styles/surface-ladder.test.ts +1 -1
- package/src/styles/theme-scopes.ts +4 -2
- package/src/styles/theme.css +12 -10
- package/src/styles/tokens.css +259 -430
- package/src/styles/tokens.test.ts +35 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.28.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 069f422: Each colour in the canonical spine is declared once, as a role, and every other
|
|
8
|
+
name of that colour aliases it: planes derive from the `--hsl-*` channels, the
|
|
9
|
+
four indigos (`--hsl-primary` fill, `--brand-primary` mark, `--brand-cool` line,
|
|
10
|
+
`--accent-text` ink) carry every `brand-*`, `md3-primary*` and gradient name, and
|
|
11
|
+
one ink per status hue carries its `surface-*`, `status-*`, `md3-*`, `code-*`,
|
|
12
|
+
`syntax-*` and shadcn channel. Every public token name is kept.
|
|
13
|
+
|
|
14
|
+
Values that moved, all toward the hand-tuned inks: dark `--status-*`,
|
|
15
|
+
`--hsl-success`, `--hsl-warning`, `--hsl-info` and `--hsl-destructive` are the
|
|
16
|
+
pastel `--surface-*-text` inks instead of saturated fills (a status fill now
|
|
17
|
+
carries dark text, as the destructive one did); light `--hsl-warning` is the
|
|
18
|
+
`--surface-warning-text` orange instead of the 1.6:1 yellow; `--brand-glow` is
|
|
19
|
+
the accent ink (`#A5B4FC` dark, the bright indigo in light, so prose links read
|
|
20
|
+
on paper); `--brand-vibrant` is the brand mark; syntax and `--code-*` colours are
|
|
21
|
+
the status and tone inks, with keywords on the categorical violet instead of the
|
|
22
|
+
accent; `--hsl-primary` and `--btn-primary-bg` agree at `#4c3fc6`; the sidebar
|
|
23
|
+
hover and border are the spine's L3 plane and hairline; one-unit plane drifts
|
|
24
|
+
(`#12111f`, `#f6f7fa`, `#f1f2f7`, `#c7c6d6`) collapse onto their channels.
|
|
25
|
+
|
|
26
|
+
Tailwind's `bg-brand`, `text-brand-glow` and `bg-status-*` utilities now follow
|
|
27
|
+
the mode and the named theme instead of one static value. `tangle-dark` declares
|
|
28
|
+
nothing: `tokens.css` seeds the complete dark spine on its selector. A shared
|
|
29
|
+
`scripts/css-resolve.mjs` reads aliases for the generators and the style tests,
|
|
30
|
+
and `tokens.test.ts` holds a ratchet on hex literals per spine (28 dark, 25 light,
|
|
31
|
+
outside the generated tones).
|
|
32
|
+
|
|
3
33
|
## 1.27.1
|
|
4
34
|
|
|
5
35
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { knot, knotGradientIds, knotSvg } from "./knot.js";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
//#region src/logo.d.ts
|
|
4
3
|
interface LogoProps {
|
|
5
4
|
size?: "sm" | "md" | "lg" | "xl";
|
|
6
5
|
variant?: "full" | "icon";
|
|
@@ -15,8 +14,6 @@ interface TangleKnotProps {
|
|
|
15
14
|
}
|
|
16
15
|
export declare function TangleKnot({ size, className, label }: TangleKnotProps): React.JSX.Element;
|
|
17
16
|
export declare function Logo({ size, variant, suffix, className }: LogoProps): React.JSX.Element;
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region src/palette.generated.d.ts
|
|
20
17
|
/** A theme's resolved roles: hex strings, or rgba() where the token is translucent. */
|
|
21
18
|
interface BrandPalette {
|
|
22
19
|
canvas: string;
|
|
@@ -56,19 +53,19 @@ export declare const palettes: {
|
|
|
56
53
|
readonly light: {
|
|
57
54
|
readonly canvas: "#eceef3";
|
|
58
55
|
readonly card: "#ffffff";
|
|
59
|
-
readonly well: "#
|
|
60
|
-
readonly nested: "#
|
|
56
|
+
readonly well: "#f5f6fa";
|
|
57
|
+
readonly nested: "#f2f3f8";
|
|
61
58
|
readonly overlay: "#ffffff";
|
|
62
59
|
readonly deepest: "#ffffff";
|
|
63
|
-
readonly hairline: "#
|
|
60
|
+
readonly hairline: "#c8c7d6";
|
|
64
61
|
readonly ink: "#2d2d2d";
|
|
65
62
|
readonly inkSecondary: "#3f3f3f";
|
|
66
63
|
readonly inkMuted: "#4c4c4c";
|
|
67
|
-
readonly accent: "#
|
|
64
|
+
readonly accent: "#4c3fc6";
|
|
68
65
|
readonly accentHover: "#5a50d6";
|
|
69
66
|
readonly onAccent: "#ffffff";
|
|
70
67
|
readonly accentText: "#3c32e9";
|
|
71
|
-
readonly danger: "#
|
|
68
|
+
readonly danger: "#b91c1c";
|
|
72
69
|
readonly successBg: "#ecfdf5";
|
|
73
70
|
readonly successBorder: "#a7f3d0";
|
|
74
71
|
readonly successText: "#047857";
|
|
@@ -83,13 +80,13 @@ export declare const palettes: {
|
|
|
83
80
|
readonly infoText: "#0369a1";
|
|
84
81
|
readonly brandVibrant: "#6366f1";
|
|
85
82
|
readonly brandCool: "#818cf8";
|
|
86
|
-
readonly brandGlow: "#
|
|
83
|
+
readonly brandGlow: "#3c32e9";
|
|
87
84
|
readonly brandPurple: "#6366f1";
|
|
88
85
|
};
|
|
89
86
|
readonly dark: {
|
|
90
87
|
readonly canvas: "#0a0a14";
|
|
91
88
|
readonly card: "#191826";
|
|
92
|
-
readonly well: "#
|
|
89
|
+
readonly well: "#11101e";
|
|
93
90
|
readonly nested: "#221f33";
|
|
94
91
|
readonly overlay: "#2c2942";
|
|
95
92
|
readonly deepest: "#040408";
|
|
@@ -97,11 +94,11 @@ export declare const palettes: {
|
|
|
97
94
|
readonly ink: "#e6e6e6";
|
|
98
95
|
readonly inkSecondary: "#c7c7c7";
|
|
99
96
|
readonly inkMuted: "#b5b5b5";
|
|
100
|
-
readonly accent: "#
|
|
97
|
+
readonly accent: "#4c3fc6";
|
|
101
98
|
readonly accentHover: "#5a50d6";
|
|
102
99
|
readonly onAccent: "#ffffff";
|
|
103
100
|
readonly accentText: "#a5b4fc";
|
|
104
|
-
readonly danger: "#
|
|
101
|
+
readonly danger: "#ffada9";
|
|
105
102
|
readonly successBg: "#2f5146";
|
|
106
103
|
readonly successBorder: "#3c7760";
|
|
107
104
|
readonly successText: "#5ed7a4";
|
|
@@ -114,9 +111,9 @@ export declare const palettes: {
|
|
|
114
111
|
readonly infoBg: "#2d4e67";
|
|
115
112
|
readonly infoBorder: "#3d7292";
|
|
116
113
|
readonly infoText: "#67cdff";
|
|
117
|
-
readonly brandVibrant: "#
|
|
114
|
+
readonly brandVibrant: "#7c6cf6";
|
|
118
115
|
readonly brandCool: "#818cf8";
|
|
119
|
-
readonly brandGlow: "#
|
|
116
|
+
readonly brandGlow: "#a5b4fc";
|
|
120
117
|
readonly brandPurple: "#c7d2fe";
|
|
121
118
|
};
|
|
122
119
|
readonly websiteLight: {
|
|
@@ -130,11 +127,11 @@ export declare const palettes: {
|
|
|
130
127
|
readonly ink: "#191c24";
|
|
131
128
|
readonly inkSecondary: "#3a3a50";
|
|
132
129
|
readonly inkMuted: "#5e5e7a";
|
|
133
|
-
readonly accent: "#
|
|
130
|
+
readonly accent: "#4c3fc6";
|
|
134
131
|
readonly accentHover: "#5a50d6";
|
|
135
132
|
readonly onAccent: "#ffffff";
|
|
136
133
|
readonly accentText: "#3c32e9";
|
|
137
|
-
readonly danger: "#
|
|
134
|
+
readonly danger: "#b91c1c";
|
|
138
135
|
readonly successBg: "#ecfdf5";
|
|
139
136
|
readonly successBorder: "#a7f3d0";
|
|
140
137
|
readonly successText: "#047857";
|
|
@@ -149,7 +146,7 @@ export declare const palettes: {
|
|
|
149
146
|
readonly infoText: "#0369a1";
|
|
150
147
|
readonly brandVibrant: "#6366f1";
|
|
151
148
|
readonly brandCool: "#818cf8";
|
|
152
|
-
readonly brandGlow: "#
|
|
149
|
+
readonly brandGlow: "#3c32e9";
|
|
153
150
|
readonly brandPurple: "#6366f1";
|
|
154
151
|
};
|
|
155
152
|
readonly websiteDark: {
|
|
@@ -163,11 +160,11 @@ export declare const palettes: {
|
|
|
163
160
|
readonly ink: "#fdfdfd";
|
|
164
161
|
readonly inkSecondary: "#aaa8b8";
|
|
165
162
|
readonly inkMuted: "#9496bc";
|
|
166
|
-
readonly accent: "#
|
|
163
|
+
readonly accent: "#4c3fc6";
|
|
167
164
|
readonly accentHover: "#5a50d6";
|
|
168
165
|
readonly onAccent: "#ffffff";
|
|
169
166
|
readonly accentText: "#a5b4fc";
|
|
170
|
-
readonly danger: "#
|
|
167
|
+
readonly danger: "#ffada9";
|
|
171
168
|
readonly successBg: "#2f5146";
|
|
172
169
|
readonly successBorder: "#3c7760";
|
|
173
170
|
readonly successText: "#5ed7a4";
|
|
@@ -180,11 +177,10 @@ export declare const palettes: {
|
|
|
180
177
|
readonly infoBg: "#2d4e67";
|
|
181
178
|
readonly infoBorder: "#3d7292";
|
|
182
179
|
readonly infoText: "#67cdff";
|
|
183
|
-
readonly brandVibrant: "#
|
|
180
|
+
readonly brandVibrant: "#7c6cf6";
|
|
184
181
|
readonly brandCool: "#818cf8";
|
|
185
|
-
readonly brandGlow: "#
|
|
182
|
+
readonly brandGlow: "#a5b4fc";
|
|
186
183
|
readonly brandPurple: "#c7d2fe";
|
|
187
184
|
};
|
|
188
185
|
};
|
|
189
|
-
//#endregion
|
|
190
186
|
export { type BrandPalette, type LogoProps, type TangleKnotProps, knot, knotGradientIds, knotSvg };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { knot, knotGradientIds, knotSvg } from "./knot.js";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
-
|
|
4
|
+
// src/logo.tsx
|
|
5
5
|
const sizeConfig = {
|
|
6
6
|
sm: {
|
|
7
7
|
icon: 24,
|
|
@@ -88,26 +88,25 @@ function Logo({ size = "md", variant = "full", suffix = null, className }) {
|
|
|
88
88
|
})]
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
|
-
|
|
92
|
-
//#region src/palette.generated.ts
|
|
91
|
+
// src/palette.generated.ts
|
|
93
92
|
/** Resolved palettes for the canonical light and dark themes and the website named theme. */
|
|
94
93
|
const palettes = {
|
|
95
94
|
light: {
|
|
96
95
|
canvas: "#eceef3",
|
|
97
96
|
card: "#ffffff",
|
|
98
|
-
well: "#
|
|
99
|
-
nested: "#
|
|
97
|
+
well: "#f5f6fa",
|
|
98
|
+
nested: "#f2f3f8",
|
|
100
99
|
overlay: "#ffffff",
|
|
101
100
|
deepest: "#ffffff",
|
|
102
|
-
hairline: "#
|
|
101
|
+
hairline: "#c8c7d6",
|
|
103
102
|
ink: "#2d2d2d",
|
|
104
103
|
inkSecondary: "#3f3f3f",
|
|
105
104
|
inkMuted: "#4c4c4c",
|
|
106
|
-
accent: "#
|
|
105
|
+
accent: "#4c3fc6",
|
|
107
106
|
accentHover: "#5a50d6",
|
|
108
107
|
onAccent: "#ffffff",
|
|
109
108
|
accentText: "#3c32e9",
|
|
110
|
-
danger: "#
|
|
109
|
+
danger: "#b91c1c",
|
|
111
110
|
successBg: "#ecfdf5",
|
|
112
111
|
successBorder: "#a7f3d0",
|
|
113
112
|
successText: "#047857",
|
|
@@ -122,13 +121,13 @@ const palettes = {
|
|
|
122
121
|
infoText: "#0369a1",
|
|
123
122
|
brandVibrant: "#6366f1",
|
|
124
123
|
brandCool: "#818cf8",
|
|
125
|
-
brandGlow: "#
|
|
124
|
+
brandGlow: "#3c32e9",
|
|
126
125
|
brandPurple: "#6366f1"
|
|
127
126
|
},
|
|
128
127
|
dark: {
|
|
129
128
|
canvas: "#0a0a14",
|
|
130
129
|
card: "#191826",
|
|
131
|
-
well: "#
|
|
130
|
+
well: "#11101e",
|
|
132
131
|
nested: "#221f33",
|
|
133
132
|
overlay: "#2c2942",
|
|
134
133
|
deepest: "#040408",
|
|
@@ -136,11 +135,11 @@ const palettes = {
|
|
|
136
135
|
ink: "#e6e6e6",
|
|
137
136
|
inkSecondary: "#c7c7c7",
|
|
138
137
|
inkMuted: "#b5b5b5",
|
|
139
|
-
accent: "#
|
|
138
|
+
accent: "#4c3fc6",
|
|
140
139
|
accentHover: "#5a50d6",
|
|
141
140
|
onAccent: "#ffffff",
|
|
142
141
|
accentText: "#a5b4fc",
|
|
143
|
-
danger: "#
|
|
142
|
+
danger: "#ffada9",
|
|
144
143
|
successBg: "#2f5146",
|
|
145
144
|
successBorder: "#3c7760",
|
|
146
145
|
successText: "#5ed7a4",
|
|
@@ -153,9 +152,9 @@ const palettes = {
|
|
|
153
152
|
infoBg: "#2d4e67",
|
|
154
153
|
infoBorder: "#3d7292",
|
|
155
154
|
infoText: "#67cdff",
|
|
156
|
-
brandVibrant: "#
|
|
155
|
+
brandVibrant: "#7c6cf6",
|
|
157
156
|
brandCool: "#818cf8",
|
|
158
|
-
brandGlow: "#
|
|
157
|
+
brandGlow: "#a5b4fc",
|
|
159
158
|
brandPurple: "#c7d2fe"
|
|
160
159
|
},
|
|
161
160
|
websiteLight: {
|
|
@@ -169,11 +168,11 @@ const palettes = {
|
|
|
169
168
|
ink: "#191c24",
|
|
170
169
|
inkSecondary: "#3a3a50",
|
|
171
170
|
inkMuted: "#5e5e7a",
|
|
172
|
-
accent: "#
|
|
171
|
+
accent: "#4c3fc6",
|
|
173
172
|
accentHover: "#5a50d6",
|
|
174
173
|
onAccent: "#ffffff",
|
|
175
174
|
accentText: "#3c32e9",
|
|
176
|
-
danger: "#
|
|
175
|
+
danger: "#b91c1c",
|
|
177
176
|
successBg: "#ecfdf5",
|
|
178
177
|
successBorder: "#a7f3d0",
|
|
179
178
|
successText: "#047857",
|
|
@@ -188,7 +187,7 @@ const palettes = {
|
|
|
188
187
|
infoText: "#0369a1",
|
|
189
188
|
brandVibrant: "#6366f1",
|
|
190
189
|
brandCool: "#818cf8",
|
|
191
|
-
brandGlow: "#
|
|
190
|
+
brandGlow: "#3c32e9",
|
|
192
191
|
brandPurple: "#6366f1"
|
|
193
192
|
},
|
|
194
193
|
websiteDark: {
|
|
@@ -202,11 +201,11 @@ const palettes = {
|
|
|
202
201
|
ink: "#fdfdfd",
|
|
203
202
|
inkSecondary: "#aaa8b8",
|
|
204
203
|
inkMuted: "#9496bc",
|
|
205
|
-
accent: "#
|
|
204
|
+
accent: "#4c3fc6",
|
|
206
205
|
accentHover: "#5a50d6",
|
|
207
206
|
onAccent: "#ffffff",
|
|
208
207
|
accentText: "#a5b4fc",
|
|
209
|
-
danger: "#
|
|
208
|
+
danger: "#ffada9",
|
|
210
209
|
successBg: "#2f5146",
|
|
211
210
|
successBorder: "#3c7760",
|
|
212
211
|
successText: "#5ed7a4",
|
|
@@ -219,11 +218,10 @@ const palettes = {
|
|
|
219
218
|
infoBg: "#2d4e67",
|
|
220
219
|
infoBorder: "#3d7292",
|
|
221
220
|
infoText: "#67cdff",
|
|
222
|
-
brandVibrant: "#
|
|
221
|
+
brandVibrant: "#7c6cf6",
|
|
223
222
|
brandCool: "#818cf8",
|
|
224
|
-
brandGlow: "#
|
|
223
|
+
brandGlow: "#a5b4fc",
|
|
225
224
|
brandPurple: "#c7d2fe"
|
|
226
225
|
}
|
|
227
226
|
};
|
|
228
|
-
//#endregion
|
|
229
227
|
export { Logo, TangleKnot, knot, knotGradientIds, knotSvg, palettes };
|
package/dist/knot.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region src/knot.d.ts
|
|
2
1
|
/** Canonical geometry. This entry has no React or browser dependencies. */
|
|
3
2
|
export declare const knot: {
|
|
4
3
|
readonly viewBox: "20 45 156 140";
|
|
@@ -29,5 +28,4 @@ export declare function knotSvg({ id, size, label }: {
|
|
|
29
28
|
id: string;
|
|
30
29
|
size?: number;
|
|
31
30
|
label?: string;
|
|
32
|
-
}): string;
|
|
33
|
-
//#endregion
|
|
31
|
+
}): string;
|
package/dist/knot.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/knot.ts
|
|
2
2
|
/** Canonical geometry. This entry has no React or browser dependencies. */
|
|
3
3
|
const knot = {
|
|
4
4
|
viewBox: "20 45 156 140",
|
|
@@ -41,5 +41,4 @@ function knotSvg({ id, size = 32, label }) {
|
|
|
41
41
|
const accessibility = label ? `role="img" aria-label="${escape(label)}"` : "aria-hidden=\"true\"";
|
|
42
42
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="${knot.viewBox}" fill="none" focusable="false" ${accessibility}><defs>${knot.gradients.map((g, i) => `<linearGradient id="${ids[i]}" x1="${g.x1}" y1="${g.y1}" x2="${g.x2}" y2="${g.y2}" gradientUnits="userSpaceOnUse">${knot.stops.map((color, index) => `<stop offset="${index}" stop-color="${color}"/>`).join("")}</linearGradient>`).join("")}</defs>${knot.paths.map((path, i) => `<path d="${path.d}" opacity="${path.opacity}" fill="url(#${ids[i]})"/>`).join("")}</svg>`;
|
|
43
43
|
}
|
|
44
|
-
//#endregion
|
|
45
44
|
export { knot, knotGradientIds, knotSvg };
|
package/dist/social.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region src/social.d.ts
|
|
2
1
|
export interface SocialImage {
|
|
3
2
|
url: string;
|
|
4
3
|
type: "image/png" | "image/jpeg" | "image/webp";
|
|
@@ -36,5 +35,4 @@ export type SocialMetaTag = {
|
|
|
36
35
|
export declare function socialMetadata(page: SocialPage): {
|
|
37
36
|
canonicalUrl: string;
|
|
38
37
|
tags: SocialMetaTag[];
|
|
39
|
-
};
|
|
40
|
-
//#endregion
|
|
38
|
+
};
|
package/dist/social.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/social.ts
|
|
2
2
|
function httpsUrl(value) {
|
|
3
3
|
const url = new URL(value);
|
|
4
4
|
if (url.protocol !== "https:" || url.username || url.password) throw new Error("Social metadata requires HTTPS URLs without credentials");
|
|
@@ -105,5 +105,4 @@ function socialMetadata(page) {
|
|
|
105
105
|
]
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
-
//#endregion
|
|
109
108
|
export { socialMetadata };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/brand",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.0",
|
|
4
4
|
"description": "Tangle brand system — design tokens, logo, Tailwind v4 theme. Single source of truth for every Tangle app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -73,10 +73,10 @@
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@storybook/react": "^10.6.
|
|
76
|
+
"@storybook/react": "^10.6.3",
|
|
77
77
|
"@types/react": "^19.3.0",
|
|
78
|
-
"react": "^19.3.
|
|
79
|
-
"tsdown": "
|
|
78
|
+
"react": "^19.3.1",
|
|
79
|
+
"tsdown": "0.23.1",
|
|
80
80
|
"typescript": "^7.0.2"
|
|
81
81
|
},
|
|
82
82
|
"keywords": [
|
package/src/palette.generated.ts
CHANGED
|
@@ -42,19 +42,19 @@ export const palettes = {
|
|
|
42
42
|
light: {
|
|
43
43
|
canvas: "#eceef3",
|
|
44
44
|
card: "#ffffff",
|
|
45
|
-
well: "#
|
|
46
|
-
nested: "#
|
|
45
|
+
well: "#f5f6fa",
|
|
46
|
+
nested: "#f2f3f8",
|
|
47
47
|
overlay: "#ffffff",
|
|
48
48
|
deepest: "#ffffff",
|
|
49
|
-
hairline: "#
|
|
49
|
+
hairline: "#c8c7d6",
|
|
50
50
|
ink: "#2d2d2d",
|
|
51
51
|
inkSecondary: "#3f3f3f",
|
|
52
52
|
inkMuted: "#4c4c4c",
|
|
53
|
-
accent: "#
|
|
53
|
+
accent: "#4c3fc6",
|
|
54
54
|
accentHover: "#5a50d6",
|
|
55
55
|
onAccent: "#ffffff",
|
|
56
56
|
accentText: "#3c32e9",
|
|
57
|
-
danger: "#
|
|
57
|
+
danger: "#b91c1c",
|
|
58
58
|
successBg: "#ecfdf5",
|
|
59
59
|
successBorder: "#a7f3d0",
|
|
60
60
|
successText: "#047857",
|
|
@@ -69,13 +69,13 @@ export const palettes = {
|
|
|
69
69
|
infoText: "#0369a1",
|
|
70
70
|
brandVibrant: "#6366f1",
|
|
71
71
|
brandCool: "#818cf8",
|
|
72
|
-
brandGlow: "#
|
|
72
|
+
brandGlow: "#3c32e9",
|
|
73
73
|
brandPurple: "#6366f1",
|
|
74
74
|
},
|
|
75
75
|
dark: {
|
|
76
76
|
canvas: "#0a0a14",
|
|
77
77
|
card: "#191826",
|
|
78
|
-
well: "#
|
|
78
|
+
well: "#11101e",
|
|
79
79
|
nested: "#221f33",
|
|
80
80
|
overlay: "#2c2942",
|
|
81
81
|
deepest: "#040408",
|
|
@@ -83,11 +83,11 @@ export const palettes = {
|
|
|
83
83
|
ink: "#e6e6e6",
|
|
84
84
|
inkSecondary: "#c7c7c7",
|
|
85
85
|
inkMuted: "#b5b5b5",
|
|
86
|
-
accent: "#
|
|
86
|
+
accent: "#4c3fc6",
|
|
87
87
|
accentHover: "#5a50d6",
|
|
88
88
|
onAccent: "#ffffff",
|
|
89
89
|
accentText: "#a5b4fc",
|
|
90
|
-
danger: "#
|
|
90
|
+
danger: "#ffada9",
|
|
91
91
|
successBg: "#2f5146",
|
|
92
92
|
successBorder: "#3c7760",
|
|
93
93
|
successText: "#5ed7a4",
|
|
@@ -100,9 +100,9 @@ export const palettes = {
|
|
|
100
100
|
infoBg: "#2d4e67",
|
|
101
101
|
infoBorder: "#3d7292",
|
|
102
102
|
infoText: "#67cdff",
|
|
103
|
-
brandVibrant: "#
|
|
103
|
+
brandVibrant: "#7c6cf6",
|
|
104
104
|
brandCool: "#818cf8",
|
|
105
|
-
brandGlow: "#
|
|
105
|
+
brandGlow: "#a5b4fc",
|
|
106
106
|
brandPurple: "#c7d2fe",
|
|
107
107
|
},
|
|
108
108
|
websiteLight: {
|
|
@@ -116,11 +116,11 @@ export const palettes = {
|
|
|
116
116
|
ink: "#191c24",
|
|
117
117
|
inkSecondary: "#3a3a50",
|
|
118
118
|
inkMuted: "#5e5e7a",
|
|
119
|
-
accent: "#
|
|
119
|
+
accent: "#4c3fc6",
|
|
120
120
|
accentHover: "#5a50d6",
|
|
121
121
|
onAccent: "#ffffff",
|
|
122
122
|
accentText: "#3c32e9",
|
|
123
|
-
danger: "#
|
|
123
|
+
danger: "#b91c1c",
|
|
124
124
|
successBg: "#ecfdf5",
|
|
125
125
|
successBorder: "#a7f3d0",
|
|
126
126
|
successText: "#047857",
|
|
@@ -135,7 +135,7 @@ export const palettes = {
|
|
|
135
135
|
infoText: "#0369a1",
|
|
136
136
|
brandVibrant: "#6366f1",
|
|
137
137
|
brandCool: "#818cf8",
|
|
138
|
-
brandGlow: "#
|
|
138
|
+
brandGlow: "#3c32e9",
|
|
139
139
|
brandPurple: "#6366f1",
|
|
140
140
|
},
|
|
141
141
|
websiteDark: {
|
|
@@ -149,11 +149,11 @@ export const palettes = {
|
|
|
149
149
|
ink: "#fdfdfd",
|
|
150
150
|
inkSecondary: "#aaa8b8",
|
|
151
151
|
inkMuted: "#9496bc",
|
|
152
|
-
accent: "#
|
|
152
|
+
accent: "#4c3fc6",
|
|
153
153
|
accentHover: "#5a50d6",
|
|
154
154
|
onAccent: "#ffffff",
|
|
155
155
|
accentText: "#a5b4fc",
|
|
156
|
-
danger: "#
|
|
156
|
+
danger: "#ffada9",
|
|
157
157
|
successBg: "#2f5146",
|
|
158
158
|
successBorder: "#3c7760",
|
|
159
159
|
successText: "#5ed7a4",
|
|
@@ -166,9 +166,9 @@ export const palettes = {
|
|
|
166
166
|
infoBg: "#2d4e67",
|
|
167
167
|
infoBorder: "#3d7292",
|
|
168
168
|
infoText: "#67cdff",
|
|
169
|
-
brandVibrant: "#
|
|
169
|
+
brandVibrant: "#7c6cf6",
|
|
170
170
|
brandCool: "#818cf8",
|
|
171
|
-
brandGlow: "#
|
|
171
|
+
brandGlow: "#a5b4fc",
|
|
172
172
|
brandPurple: "#c7d2fe",
|
|
173
173
|
},
|
|
174
174
|
} as const satisfies Record<string, BrandPalette>;
|
|
@@ -60,11 +60,12 @@ describe("hexRelativeLuminanceIn", () => {
|
|
|
60
60
|
expect(card).toBeGreaterThan(canvas);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
it("
|
|
64
|
-
expect(
|
|
65
|
-
|
|
66
|
-
);
|
|
67
|
-
expect(() => hexRelativeLuminanceIn("--a:
|
|
63
|
+
it("follows an alias to the colour it paints, and names an alias that leads nowhere", () => {
|
|
64
|
+
expect(hexRelativeLuminanceIn("--a: var(--b); --b: #fff;", "a")).toBe(1);
|
|
65
|
+
expect(hexRelativeLuminanceIn("--a: hsl(var(--b)); --b: 0 0% 100%;", "a")).toBe(1);
|
|
66
|
+
expect(() => hexRelativeLuminanceIn("--a: var(--b);", "a")).toThrow(/--b is not defined/);
|
|
67
|
+
expect(() => hexRelativeLuminanceIn("--a: rgba(0, 0, 0, 0.5);", "a")).toThrow(/not a hex color/);
|
|
68
|
+
expect(() => hexRelativeLuminanceIn("--a: #fff;", "b")).toThrow(/not defined/);
|
|
68
69
|
});
|
|
69
70
|
});
|
|
70
71
|
|
|
@@ -73,7 +74,9 @@ describe("hslIn", () => {
|
|
|
73
74
|
expect(hslIn("--a: 240 33% 6%;", "a")).toEqual({ h: 240, s: 33, l: 6 });
|
|
74
75
|
});
|
|
75
76
|
|
|
76
|
-
it("throws when the slot is absent", () => {
|
|
77
|
-
expect(
|
|
77
|
+
it("follows an alias to its triple, and throws when the slot is absent or not a triple", () => {
|
|
78
|
+
expect(hslIn("--a: var(--b); --b: 240 33% 6%;", "a")).toEqual({ h: 240, s: 33, l: 6 });
|
|
79
|
+
expect(() => hslIn("--a: 240 33% 6%;", "b")).toThrow(/not defined/);
|
|
80
|
+
expect(() => hslIn("--a: #fff;", "a")).toThrow(/not an HSL triple/);
|
|
78
81
|
});
|
|
79
82
|
});
|
|
@@ -6,6 +6,32 @@
|
|
|
6
6
|
* importing file's context.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { block as resolverBlock, decls, resolveValue } from "../../scripts/css-resolve.mjs";
|
|
12
|
+
|
|
13
|
+
/** The canonical dark spine's declarations: what every other block inherits. */
|
|
14
|
+
const BASELINE: Record<string, string> = decls(
|
|
15
|
+
resolverBlock(
|
|
16
|
+
readFileSync(path.resolve(import.meta.dirname, "tokens.css"), "utf8"),
|
|
17
|
+
".dark",
|
|
18
|
+
),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A token's concrete value in a block, with aliases followed: `var(--x)`
|
|
23
|
+
* chains and `hsl(var(--x))` wrappers, each hop reading the block first and
|
|
24
|
+
* the canonical dark baseline second, which is how the browser resolves a
|
|
25
|
+
* custom property declared on the same element as the light override.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveIn(css: string | string[], token: string): string {
|
|
28
|
+
// A chain of blocks models the cascade on one element: the first block that
|
|
29
|
+
// declares a name wins, as a named theme's own rule beats the spine it sits on.
|
|
30
|
+
const sources = Array.isArray(css) ? css : [css];
|
|
31
|
+
const scope = Object.assign({}, ...sources.map(decls).reverse());
|
|
32
|
+
return resolveValue(scope, token, BASELINE);
|
|
33
|
+
}
|
|
34
|
+
|
|
9
35
|
/** Escape every regex metacharacter, so a selector is matched literally. */
|
|
10
36
|
function escapeRegex(literal: string): string {
|
|
11
37
|
return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -78,15 +104,12 @@ export function declIn(css: string, token: string): string {
|
|
|
78
104
|
|
|
79
105
|
/** Read an `H S% L%` channel triple. */
|
|
80
106
|
export function hslIn(
|
|
81
|
-
css: string,
|
|
107
|
+
css: string | string[],
|
|
82
108
|
token: string,
|
|
83
109
|
): { h: number; s: number; l: number } {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
),
|
|
88
|
-
);
|
|
89
|
-
if (!m) throw new Error(`missing HSL slot --${token}`);
|
|
110
|
+
const value = resolveIn(css, token);
|
|
111
|
+
const m = /^(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%$/.exec(value);
|
|
112
|
+
if (!m) throw new Error(`--${token} is not an HSL triple (found "${value}")`);
|
|
90
113
|
return { h: Number(m[1]), s: Number(m[2]), l: Number(m[3]) };
|
|
91
114
|
}
|
|
92
115
|
|
|
@@ -99,11 +122,13 @@ type Rgb = [number, number, number];
|
|
|
99
122
|
* separate question, answered by `compositeOver`). A token that exists but is not
|
|
100
123
|
* hex reports exactly that, rather than claiming it is missing.
|
|
101
124
|
*/
|
|
102
|
-
export function hexIn(css: string, token: string): Rgb {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
125
|
+
export function hexIn(css: string | string[], token: string): Rgb {
|
|
126
|
+
const value = resolveIn(css, token);
|
|
127
|
+
// A plane authored as channels resolves to a triple; read it as the colour it paints.
|
|
128
|
+
const triple = /^(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%$/.exec(value);
|
|
129
|
+
if (triple) {
|
|
130
|
+
return hslToRgb({ h: Number(triple[1]), s: Number(triple[2]), l: Number(triple[3]) }).map(Math.round) as Rgb;
|
|
131
|
+
}
|
|
107
132
|
const hex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/.exec(value);
|
|
108
133
|
if (!hex) {
|
|
109
134
|
throw new Error(`--${token} is not a hex color (found "${value}")`);
|
|
@@ -181,7 +206,7 @@ export function contrastRatio(a: Rgb, b: Rgb): number {
|
|
|
181
206
|
* below" assertion made on HSL L would be measuring the wrong thing. Hex parsing
|
|
182
207
|
* (and its error reporting) is `hexIn`'s.
|
|
183
208
|
*/
|
|
184
|
-
export function hexRelativeLuminanceIn(css: string, token: string): number {
|
|
209
|
+
export function hexRelativeLuminanceIn(css: string | string[], token: string): number {
|
|
185
210
|
return relativeLuminance(hexIn(css, token));
|
|
186
211
|
}
|
|
187
212
|
|
package/src/styles/focus.test.ts
CHANGED
|
@@ -43,18 +43,6 @@ const dangerAlpha = alphaOf(
|
|
|
43
43
|
/--focus-border-danger:\s*color-mix\(in srgb, var\(--surface-danger-text\) ([\d.]+)(%), transparent\)/,
|
|
44
44
|
);
|
|
45
45
|
|
|
46
|
-
/** Reads a token from the first source that declares it, as the cascade would. */
|
|
47
|
-
function effective<T>(sources: string[], readToken: (css: string) => T): T {
|
|
48
|
-
for (const css of sources) {
|
|
49
|
-
try {
|
|
50
|
-
return readToken(css);
|
|
51
|
-
} catch {
|
|
52
|
-
// Not declared here; fall through to the scope it inherits from.
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
throw new Error("token resolves to nothing");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
46
|
const lightStatus = blocksIn(themes, '[data-theme="tangle-light"]').join("\n");
|
|
59
47
|
const scopes: Array<{ name: string; sources: string[] }> = [
|
|
60
48
|
{ name: "canonical dark", sources: [DARK] },
|
|
@@ -89,12 +77,13 @@ describe("the focus palette resolves in every theme scope", () => {
|
|
|
89
77
|
describe("a focused field's border clears 3:1 against the field", () => {
|
|
90
78
|
for (const { name, sources } of scopes) {
|
|
91
79
|
it(name, () => {
|
|
92
|
-
|
|
93
|
-
const
|
|
80
|
+
// The scope chain models the cascade: a theme's own rule, then the spine it inherits.
|
|
81
|
+
const field = hslToRgb(hslIn(sources, "hsl-card"));
|
|
82
|
+
const ring = hslToRgb(hslIn(sources, "hsl-ring"));
|
|
94
83
|
const border = compositeOver(ring, field, borderAlpha);
|
|
95
84
|
expect(contrastRatio(border, field), "focus border on field").toBeGreaterThanOrEqual(3);
|
|
96
85
|
|
|
97
|
-
const danger =
|
|
86
|
+
const danger = hexIn(sources, "surface-danger-text");
|
|
98
87
|
const dangerBorder = compositeOver(danger, field, dangerAlpha);
|
|
99
88
|
expect(contrastRatio(dangerBorder, field), "danger focus border on field").toBeGreaterThanOrEqual(3);
|
|
100
89
|
});
|