@kanso-labs/react-native-kanso-ui 0.0.0-stage → 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.md +20 -0
- package/README.md +205 -2
- package/dist/button/index.d.ts +56 -0
- package/dist/button/index.js +169 -0
- package/dist/button/index.js.map +1 -0
- package/dist/button/palettes.js +47 -0
- package/dist/button/palettes.js.map +1 -0
- package/dist/chip/index.js +194 -0
- package/dist/chip/index.js.map +1 -0
- package/dist/chip/styles.js +36 -0
- package/dist/chip/styles.js.map +1 -0
- package/dist/clipboard/index.js +58 -0
- package/dist/clipboard/index.js.map +1 -0
- package/dist/components/avatar/index.d.ts +82 -0
- package/dist/components/avatar/index.js +62 -0
- package/dist/components/avatar/index.js.map +1 -0
- package/dist/components/button/index.d.ts +87 -0
- package/dist/components/button/index.js +377 -0
- package/dist/components/button/index.js.map +1 -0
- package/dist/components/card/index.d.ts +78 -0
- package/dist/components/card/index.js +315 -0
- package/dist/components/card/index.js.map +1 -0
- package/dist/components/checkbox/index.d.ts +76 -0
- package/dist/components/checkbox/index.js +360 -0
- package/dist/components/checkbox/index.js.map +1 -0
- package/dist/components/chip/index.d.ts +84 -0
- package/dist/components/chip/index.js +257 -0
- package/dist/components/chip/index.js.map +1 -0
- package/dist/components/chip-group/index.d.ts +143 -0
- package/dist/components/chip-group/index.js +669 -0
- package/dist/components/chip-group/index.js.map +1 -0
- package/dist/components/copy-field/index.d.ts +60 -0
- package/dist/components/copy-field/index.js +277 -0
- package/dist/components/copy-field/index.js.map +1 -0
- package/dist/components/currency/index.d.ts +69 -0
- package/dist/components/currency/index.js +162 -0
- package/dist/components/currency/index.js.map +1 -0
- package/dist/components/form/index.d.ts +84 -0
- package/dist/components/form/index.js +95 -0
- package/dist/components/form/index.js.map +1 -0
- package/dist/components/icon-button/index.d.ts +68 -0
- package/dist/components/icon-button/index.js +289 -0
- package/dist/components/icon-button/index.js.map +1 -0
- package/dist/components/link/index.d.ts +59 -0
- package/dist/components/link/index.js +191 -0
- package/dist/components/link/index.js.map +1 -0
- package/dist/components/number-field/index.d.ts +151 -0
- package/dist/components/number-field/index.js +832 -0
- package/dist/components/number-field/index.js.map +1 -0
- package/dist/components/product-icon/index.d.ts +70 -0
- package/dist/components/product-icon/index.js +60 -0
- package/dist/components/product-icon/index.js.map +1 -0
- package/dist/components/radio-group/index.d.ts +105 -0
- package/dist/components/radio-group/index.js +597 -0
- package/dist/components/radio-group/index.js.map +1 -0
- package/dist/components/search-field/index.d.ts +98 -0
- package/dist/components/search-field/index.js +521 -0
- package/dist/components/search-field/index.js.map +1 -0
- package/dist/components/segmented-button/index.d.ts +113 -0
- package/dist/components/segmented-button/index.js +937 -0
- package/dist/components/segmented-button/index.js.map +1 -0
- package/dist/components/separator/index.d.ts +45 -0
- package/dist/components/separator/index.js +109 -0
- package/dist/components/separator/index.js.map +1 -0
- package/dist/components/stack/index.d.ts +70 -0
- package/dist/components/stack/index.js +107 -0
- package/dist/components/stack/index.js.map +1 -0
- package/dist/components/switch/index.d.ts +76 -0
- package/dist/components/switch/index.js +591 -0
- package/dist/components/switch/index.js.map +1 -0
- package/dist/components/text/index.d.ts +44 -0
- package/dist/components/text/index.js +78 -0
- package/dist/components/text/index.js.map +1 -0
- package/dist/components/text-field/index.d.ts +146 -0
- package/dist/components/text-field/index.js +228 -0
- package/dist/components/text-field/index.js.map +1 -0
- package/dist/control/index.js +117 -0
- package/dist/control/index.js.map +1 -0
- package/dist/control/styles.js +36 -0
- package/dist/control/styles.js.map +1 -0
- package/dist/field/index.d.ts +13 -0
- package/dist/field/index.js +1007 -0
- package/dist/field/index.js.map +1 -0
- package/dist/field/styles.js +131 -0
- package/dist/field/styles.js.map +1 -0
- package/dist/form/index.d.ts +24 -0
- package/dist/form/index.js +209 -0
- package/dist/form/index.js.map +1 -0
- package/dist/glyphs/icon-style.d.ts +25 -0
- package/dist/glyphs/icon-style.js +22 -0
- package/dist/glyphs/icon-style.js.map +1 -0
- package/dist/glyphs/index.js +170 -0
- package/dist/glyphs/index.js.map +1 -0
- package/dist/glyphs/styles.js +7 -0
- package/dist/glyphs/styles.js.map +1 -0
- package/dist/hooks/useWindowSizeClass.d.ts +25 -0
- package/dist/hooks/useWindowSizeClass.js +39 -0
- package/dist/hooks/useWindowSizeClass.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +27 -0
- package/dist/mark/image.js +61 -0
- package/dist/mark/image.js.map +1 -0
- package/dist/mark/index.d.ts +9 -0
- package/dist/mark/index.js +234 -0
- package/dist/mark/index.js.map +1 -0
- package/dist/mark/initials.js +65 -0
- package/dist/mark/initials.js.map +1 -0
- package/dist/mark/styles.d.ts +1 -0
- package/dist/mark/styles.js +73 -0
- package/dist/mark/styles.js.map +1 -0
- package/dist/number/parser.js +321 -0
- package/dist/number/parser.js.map +1 -0
- package/dist/number/state.js +235 -0
- package/dist/number/state.js.map +1 -0
- package/dist/overlay/host.js +111 -0
- package/dist/overlay/host.js.map +1 -0
- package/dist/press/index.js +350 -0
- package/dist/press/index.js.map +1 -0
- package/dist/press/styles.js +31 -0
- package/dist/press/styles.js.map +1 -0
- package/dist/router/index.d.ts +28 -0
- package/dist/router/index.js +82 -0
- package/dist/router/index.js.map +1 -0
- package/dist/theme/color.js +25 -0
- package/dist/theme/color.js.map +1 -0
- package/dist/theme/resolve.d.ts +6 -0
- package/dist/theme/resolve.js +310 -0
- package/dist/theme/resolve.js.map +1 -0
- package/dist/theme/scope.d.ts +42 -0
- package/dist/theme/scope.js +80 -0
- package/dist/theme/scope.js.map +1 -0
- package/dist/theme/styles.js +39 -0
- package/dist/theme/styles.js.map +1 -0
- package/dist/theme-scope.d.ts +56 -0
- package/dist/theme-scope.js +127 -0
- package/dist/theme-scope.js.map +1 -0
- package/dist/tokens/defaults.d.ts +407 -0
- package/dist/tokens/defaults.js +403 -0
- package/dist/tokens/defaults.js.map +1 -0
- package/dist/typography/fonts.js +84 -0
- package/dist/typography/fonts.js.map +1 -0
- package/dist/typography/roles.d.ts +10 -0
- package/dist/typography/roles.js +82 -0
- package/dist/typography/roles.js.map +1 -0
- package/package.json +132 -4
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
//#region src/theme/resolve.ts
|
|
2
|
+
const CATEGORIES = [
|
|
3
|
+
["color", "colors"],
|
|
4
|
+
["motion", "motion"],
|
|
5
|
+
["radius", "radii"],
|
|
6
|
+
["shadow", "shadows"],
|
|
7
|
+
["sizing", "sizing"],
|
|
8
|
+
["spacing", "spacing"],
|
|
9
|
+
["state-layer-opacity", "stateLayerOpacity"],
|
|
10
|
+
["typography", "typography"]
|
|
11
|
+
];
|
|
12
|
+
const EASING_KEYWORDS = {
|
|
13
|
+
ease: [
|
|
14
|
+
.25,
|
|
15
|
+
.1,
|
|
16
|
+
.25,
|
|
17
|
+
1
|
|
18
|
+
],
|
|
19
|
+
"ease-in": [
|
|
20
|
+
.42,
|
|
21
|
+
0,
|
|
22
|
+
1,
|
|
23
|
+
1
|
|
24
|
+
],
|
|
25
|
+
"ease-in-out": [
|
|
26
|
+
.42,
|
|
27
|
+
0,
|
|
28
|
+
.58,
|
|
29
|
+
1
|
|
30
|
+
],
|
|
31
|
+
"ease-out": [
|
|
32
|
+
0,
|
|
33
|
+
0,
|
|
34
|
+
.58,
|
|
35
|
+
1
|
|
36
|
+
],
|
|
37
|
+
linear: [
|
|
38
|
+
0,
|
|
39
|
+
0,
|
|
40
|
+
1,
|
|
41
|
+
1
|
|
42
|
+
]
|
|
43
|
+
};
|
|
44
|
+
const FONT_WEIGHT_KEYWORDS = {
|
|
45
|
+
bold: 700,
|
|
46
|
+
normal: 400
|
|
47
|
+
};
|
|
48
|
+
const UNSUPPORTED_FUNCTION = /\b(attr|calc|clamp|color-mix|env|light-dark|max|min)\(/u;
|
|
49
|
+
const NUMBER = String.raw`-?(?:\d+\.?\d*|\.\d+)`;
|
|
50
|
+
function camelCase(kebab) {
|
|
51
|
+
return kebab.replaceAll(/-([a-z0-9])/gu, (_, letter) => letter.toUpperCase());
|
|
52
|
+
}
|
|
53
|
+
/** The group a property belongs to, the key it takes there, and its kind. */
|
|
54
|
+
function classify(property) {
|
|
55
|
+
if (!property.startsWith("--kui-")) return;
|
|
56
|
+
const name = property.slice(6);
|
|
57
|
+
const match = CATEGORIES.find(([category]) => name.startsWith(`${category}-`));
|
|
58
|
+
if (match === void 0) return;
|
|
59
|
+
const [category, group] = match;
|
|
60
|
+
const rest = name.slice(category.length + 1);
|
|
61
|
+
const kind = kindOf(category, rest);
|
|
62
|
+
return kind === void 0 ? void 0 : {
|
|
63
|
+
group,
|
|
64
|
+
key: camelCase(rest),
|
|
65
|
+
kind
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** The index of the parenthesis closing the one just before `from`. */
|
|
69
|
+
function closingParenthesis(value, from) {
|
|
70
|
+
let depth = 1;
|
|
71
|
+
for (let index = from; index < value.length; index++) if (value[index] === "(") depth++;
|
|
72
|
+
else if (value[index] === ")") {
|
|
73
|
+
depth--;
|
|
74
|
+
if (depth === 0) return index;
|
|
75
|
+
}
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
function kindOf(category, rest) {
|
|
79
|
+
switch (category) {
|
|
80
|
+
case "color": return "color";
|
|
81
|
+
case "motion":
|
|
82
|
+
if (rest.startsWith("duration-")) return "duration";
|
|
83
|
+
return rest.startsWith("easing-") ? "easing" : void 0;
|
|
84
|
+
case "radius":
|
|
85
|
+
case "sizing":
|
|
86
|
+
case "spacing": return "length";
|
|
87
|
+
case "shadow": return "shadow";
|
|
88
|
+
case "state-layer-opacity": return "opacity";
|
|
89
|
+
default: return typographyKindOf(rest);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function parseColor(value) {
|
|
93
|
+
return /^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/iu.test(value) || /^(rgba?|hsla?|hwb)\([^()]*\)$/iu.test(value) || /^[a-z]+$/iu.test(value) ? value : void 0;
|
|
94
|
+
}
|
|
95
|
+
function parseDuration(value) {
|
|
96
|
+
const match = /^(\d+\.?\d*|\.\d+)(ms|s)$/u.exec(value);
|
|
97
|
+
if (match === null) return;
|
|
98
|
+
const duration = Number(match[1]);
|
|
99
|
+
return match[2] === "s" ? duration * 1e3 : duration;
|
|
100
|
+
}
|
|
101
|
+
function parseEasing(value) {
|
|
102
|
+
const keyword = EASING_KEYWORDS[value];
|
|
103
|
+
if (keyword !== void 0) return keyword;
|
|
104
|
+
const match = /^cubic-bezier\(([^()]*)\)$/u.exec(value);
|
|
105
|
+
if (match === null) return;
|
|
106
|
+
const points = match[1].split(",").map((point) => point.trim());
|
|
107
|
+
if (points.length !== 4 || !points.every((point) => new RegExp(`^${NUMBER}$`, "u").test(point))) return;
|
|
108
|
+
const [x1, y1, x2, y2] = points.map(Number);
|
|
109
|
+
return [x1, x2].every((x) => x >= 0 && x <= 1) ? [
|
|
110
|
+
x1,
|
|
111
|
+
y1,
|
|
112
|
+
x2,
|
|
113
|
+
y2
|
|
114
|
+
] : void 0;
|
|
115
|
+
}
|
|
116
|
+
function parseFontFamily(value) {
|
|
117
|
+
const families = splitTopLevel(value).map((family) => family.trim().replace(/^(["'])(.*)\1$/u, "$2"));
|
|
118
|
+
return families.every((family) => family !== "" && !family.includes("(")) ? families : void 0;
|
|
119
|
+
}
|
|
120
|
+
function parseFontWeight(value) {
|
|
121
|
+
const keyword = FONT_WEIGHT_KEYWORDS[value];
|
|
122
|
+
if (keyword !== void 0) return keyword;
|
|
123
|
+
if (!/^\d+(\.\d+)?$/u.test(value)) return;
|
|
124
|
+
const weight = Number(value);
|
|
125
|
+
return weight >= 1 && weight <= 1e3 ? weight : void 0;
|
|
126
|
+
}
|
|
127
|
+
function parseLength(value) {
|
|
128
|
+
const match = new RegExp(`^(${NUMBER})(px)?$`, "u").exec(value);
|
|
129
|
+
if (match === null) return;
|
|
130
|
+
const length = Number(match[1]);
|
|
131
|
+
return match[2] !== "px" && length !== 0 ? void 0 : length;
|
|
132
|
+
}
|
|
133
|
+
function parseOpacity(value) {
|
|
134
|
+
const match = new RegExp(`^(${NUMBER})(%)?$`, "u").exec(value);
|
|
135
|
+
if (match === null) return;
|
|
136
|
+
const opacity = Number(match[1]) / (match[2] === "%" ? 100 : 1);
|
|
137
|
+
return Math.min(Math.max(opacity, 0), 1);
|
|
138
|
+
}
|
|
139
|
+
function parseShadow(value) {
|
|
140
|
+
return value === "" ? void 0 : value;
|
|
141
|
+
}
|
|
142
|
+
/** Splits a value at its top-level commas, leaving any inside parentheses. */
|
|
143
|
+
function splitTopLevel(value) {
|
|
144
|
+
const parts = [];
|
|
145
|
+
let depth = 0;
|
|
146
|
+
let quote;
|
|
147
|
+
let start = 0;
|
|
148
|
+
for (let index = 0; index < value.length; index++) {
|
|
149
|
+
const character = value[index];
|
|
150
|
+
if (quote !== void 0) quote = character === quote ? void 0 : quote;
|
|
151
|
+
else if (character === "\"" || character === "'") quote = character;
|
|
152
|
+
else if (character === "(") depth++;
|
|
153
|
+
else if (character === ")") depth--;
|
|
154
|
+
else if (character === "," && depth === 0) {
|
|
155
|
+
parts.push(value.slice(start, index));
|
|
156
|
+
start = index + 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
parts.push(value.slice(start));
|
|
160
|
+
return parts;
|
|
161
|
+
}
|
|
162
|
+
function typographyKindOf(rest) {
|
|
163
|
+
if (rest.startsWith("font-family-") || rest.endsWith("-font")) return "fontFamily";
|
|
164
|
+
if (rest.startsWith("weight-") || rest.endsWith("-weight")) return "fontWeight";
|
|
165
|
+
return [
|
|
166
|
+
"-line-height",
|
|
167
|
+
"-size",
|
|
168
|
+
"-tracking"
|
|
169
|
+
].some((suffix) => rest.endsWith(suffix)) ? "length" : void 0;
|
|
170
|
+
}
|
|
171
|
+
const PARSERS = {
|
|
172
|
+
color: [parseColor, "is not a colour React Native can draw"],
|
|
173
|
+
duration: [parseDuration, "is not a duration in ms or s"],
|
|
174
|
+
easing: [parseEasing, "is not a cubic-bezier() curve or an easing keyword"],
|
|
175
|
+
fontFamily: [parseFontFamily, "is not a font family list"],
|
|
176
|
+
fontWeight: [parseFontWeight, "is not a font weight from 1 to 1000"],
|
|
177
|
+
length: [parseLength, "is not a length in px"],
|
|
178
|
+
opacity: [parseOpacity, "is not a number or a percentage"],
|
|
179
|
+
shadow: [parseShadow, "is empty"]
|
|
180
|
+
};
|
|
181
|
+
/** The properties a theme declares, leaving out any it lists as undefined. */
|
|
182
|
+
function declarations(properties) {
|
|
183
|
+
return new Map(Object.entries(properties).filter((entry) => entry[1] !== void 0));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Turns a theme in CSS syntax into the values React Native styles take.
|
|
187
|
+
*
|
|
188
|
+
* `defaults` is the whole contract, and `overrides` replace any of it, as a
|
|
189
|
+
* consumer's declarations replace the library's on web. A `var()` reference
|
|
190
|
+
* resolves against both, with its fallback where the property it names has no
|
|
191
|
+
* usable value, so a role pointing at a family or a weight follows an override
|
|
192
|
+
* of that family or weight as it does in a browser.
|
|
193
|
+
*
|
|
194
|
+
* A value native cannot use, such as a `calc()` or a `color-mix()`, leaves its
|
|
195
|
+
* property at the default instead of dropping it, and a property the defaults
|
|
196
|
+
* do not declare is ignored. Each is listed in `problems` by name, for the
|
|
197
|
+
* caller to report.
|
|
198
|
+
*/
|
|
199
|
+
function resolveTheme(defaults, overrides = {}) {
|
|
200
|
+
const declared = declarations(defaults);
|
|
201
|
+
const replaced = declarations(overrides);
|
|
202
|
+
const problems = [];
|
|
203
|
+
const settled = /* @__PURE__ */ new Map();
|
|
204
|
+
const settling = [];
|
|
205
|
+
const cyclic = /* @__PURE__ */ new Set();
|
|
206
|
+
function substitute(value) {
|
|
207
|
+
const start = value.indexOf("var(");
|
|
208
|
+
if (start === -1) return value;
|
|
209
|
+
const end = closingParenthesis(value, start + 4);
|
|
210
|
+
if (end === -1) return { reason: "has a var() that is never closed" };
|
|
211
|
+
const [name, ...rest] = splitTopLevel(value.slice(start + 4, end));
|
|
212
|
+
const property = name.trim();
|
|
213
|
+
const fallback = rest.length === 0 ? void 0 : rest.join(",").trim();
|
|
214
|
+
const referenced = declared.has(property) ? settle(property) : null;
|
|
215
|
+
let replacement;
|
|
216
|
+
if (referenced !== null) replacement = referenced.raw;
|
|
217
|
+
else if (fallback === void 0) replacement = { reason: `refers to ${property}, which has no usable value` };
|
|
218
|
+
else replacement = substitute(fallback);
|
|
219
|
+
if (typeof replacement !== "string") return replacement;
|
|
220
|
+
return substitute(`${value.slice(0, start)}${replacement}${value.slice(end + 1)}`);
|
|
221
|
+
}
|
|
222
|
+
function evaluate(kind, value) {
|
|
223
|
+
const raw = substitute(value.trim());
|
|
224
|
+
if (typeof raw !== "string") return raw;
|
|
225
|
+
const unsupported = UNSUPPORTED_FUNCTION.exec(raw);
|
|
226
|
+
if (unsupported !== null) return { reason: `uses ${unsupported[1]}(), which native cannot evaluate` };
|
|
227
|
+
const [read, reason] = PARSERS[kind];
|
|
228
|
+
const parsed = read(raw);
|
|
229
|
+
return parsed === void 0 ? { reason } : {
|
|
230
|
+
raw,
|
|
231
|
+
value: parsed
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** Evaluates one declaration, listing it as a problem if it fails. */
|
|
235
|
+
function attempt(property, kind, value) {
|
|
236
|
+
if (value === void 0) return null;
|
|
237
|
+
const result = evaluate(kind, value);
|
|
238
|
+
if ("reason" in result) {
|
|
239
|
+
problems.push({
|
|
240
|
+
property,
|
|
241
|
+
reason: result.reason,
|
|
242
|
+
value
|
|
243
|
+
});
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
function settle(property) {
|
|
249
|
+
const known = settled.get(property);
|
|
250
|
+
if (known !== void 0) return known;
|
|
251
|
+
const classified = classify(property);
|
|
252
|
+
if (classified === void 0) return null;
|
|
253
|
+
const index = settling.indexOf(property);
|
|
254
|
+
if (index !== -1) {
|
|
255
|
+
for (const member of settling.slice(index)) cyclic.add(member);
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
settling.push(property);
|
|
259
|
+
const value = replaced.get(property);
|
|
260
|
+
let result = attempt(property, classified.kind, value);
|
|
261
|
+
if (result !== null && value !== void 0 && cyclic.has(property)) {
|
|
262
|
+
problems.push({
|
|
263
|
+
property,
|
|
264
|
+
reason: "refers back to itself through var()",
|
|
265
|
+
value
|
|
266
|
+
});
|
|
267
|
+
result = null;
|
|
268
|
+
}
|
|
269
|
+
result ??= attempt(property, classified.kind, declared.get(property));
|
|
270
|
+
settling.pop();
|
|
271
|
+
settled.set(property, result);
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
const theme = {
|
|
275
|
+
colors: {},
|
|
276
|
+
motion: {},
|
|
277
|
+
radii: {},
|
|
278
|
+
shadows: {},
|
|
279
|
+
sizing: {},
|
|
280
|
+
spacing: {},
|
|
281
|
+
stateLayerOpacity: {},
|
|
282
|
+
typography: {}
|
|
283
|
+
};
|
|
284
|
+
for (const [property, value] of declared) {
|
|
285
|
+
const classified = classify(property);
|
|
286
|
+
if (classified === void 0) {
|
|
287
|
+
problems.push({
|
|
288
|
+
property,
|
|
289
|
+
reason: "is not a Kanso UI token",
|
|
290
|
+
value
|
|
291
|
+
});
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const result = settle(property);
|
|
295
|
+
if (result !== null) Object.assign(theme[classified.group], { [classified.key]: result.value });
|
|
296
|
+
}
|
|
297
|
+
for (const [property, value] of replaced) if (!declared.has(property)) problems.push({
|
|
298
|
+
property,
|
|
299
|
+
reason: "is not a Kanso UI token",
|
|
300
|
+
value
|
|
301
|
+
});
|
|
302
|
+
return {
|
|
303
|
+
problems,
|
|
304
|
+
theme
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
//#endregion
|
|
308
|
+
export { resolveTheme };
|
|
309
|
+
|
|
310
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","names":["Attempt","Settled","reason","Easing","x1","y1","x2","y2","Group","ResolvedGroups","Groups","key","G","Kind","colors","Readonly","Record","motion","radii","shadows","sizing","spacing","stateLayerOpacity","typography","raw","value","Value","ThemeProblem","property","ThemeProperties","Partial","CATEGORIES","EASING_KEYWORDS","ease","linear","FONT_WEIGHT_KEYWORDS","bold","normal","UNSUPPORTED_FUNCTION","NUMBER","String","camelCase","kebab","replaceAll","_","letter","toUpperCase","classify","group","kind","startsWith","undefined","name","slice","length","match","find","category","rest","kindOf","closingParenthesis","from","depth","index","typographyKindOf","parseColor","drawable","test","parseDuration","exec","duration","Number","parseEasing","keyword","points","split","map","point","trim","every","RegExp","x","parseFontFamily","families","splitTopLevel","family","replace","includes","parseFontWeight","weight","parseLength","parseOpacity","opacity","Math","min","max","parseShadow","parts","quote","start","character","push","endsWith","measured","some","suffix","PARSERS","color","easing","fontFamily","fontWeight","shadow","declarations","properties","Map","Object","entries","filter","entry","resolveTheme","defaults","overrides","problems","theme","declared","replaced","settled","settling","cyclic","Set","substitute","indexOf","end","fallback","join","referenced","has","settle","replacement","evaluate","unsupported","read","parsed","attempt","result","known","get","classified","member","add","pop","set","assign"],"sources":["../../src/theme/resolve.ts"],"sourcesContent":["type Attempt = Settled | { readonly reason: string }\n\n/** A cubic Bézier curve's control points, as `cubic-bezier()` lists them. */\ntype Easing = readonly [x1: number, y1: number, x2: number, y2: number]\n\ntype Group = keyof ResolvedGroups\n\n// The resolved theme with its groups writable, for the resolver to fill in.\ntype Groups = {\n -readonly [G in Group]: { [key: string]: ResolvedGroups[G][string] }\n}\n\ntype Kind =\n | 'color'\n | 'duration'\n | 'easing'\n | 'fontFamily'\n | 'fontWeight'\n | 'length'\n | 'opacity'\n | 'shadow'\n\n/**\n * The tokens in the values React Native styles take, grouped and keyed as the\n * web library's StyleX tokens are: `--kui-color-primary` is `colors.primary`\n * and `--kui-typography-body-large-size` is `typography.bodyLargeSize`. Any\n * set of properties resolves to this shape; `ResolvedTheme` in ./scope.ts is\n * the same groups with the keys the library's own tokens give them.\n */\ntype ResolvedGroups = {\n readonly colors: Readonly<Record<string, string>>\n readonly motion: Readonly<Record<string, Easing | number>>\n readonly radii: Readonly<Record<string, number>>\n readonly shadows: Readonly<Record<string, string>>\n readonly sizing: Readonly<Record<string, number>>\n readonly spacing: Readonly<Record<string, number>>\n readonly stateLayerOpacity: Readonly<Record<string, number>>\n readonly typography: Readonly<Record<string, number | readonly string[]>>\n}\n\ntype Settled = { readonly raw: string; readonly value: Value }\n\n/** A property whose value could not be used, and why. */\ntype ThemeProblem = {\n readonly property: string\n readonly reason: string\n readonly value: string\n}\n\n/**\n * A set of `--kui-*` custom properties and their values, written in CSS syntax\n * exactly as kanso-ui's `tokens.css` declares them, so one theme serves both\n * libraries.\n */\ntype ThemeProperties = Readonly<Partial<Record<`--kui-${string}`, string>>>\n\ntype Value = Easing | number | readonly string[] | string\n\n// The segment each group takes in a property's name, as kanso-ui's\n// build-tokens.mjs writes it: singular where the group's own name is plural.\nconst CATEGORIES: readonly (readonly [string, Group])[] = [\n ['color', 'colors'],\n ['motion', 'motion'],\n ['radius', 'radii'],\n ['shadow', 'shadows'],\n ['sizing', 'sizing'],\n ['spacing', 'spacing'],\n ['state-layer-opacity', 'stateLayerOpacity'],\n ['typography', 'typography'],\n]\n\n// The CSS keywords a timing function may be written as, as the curves the\n// CSS Easing specification defines them to be.\nconst EASING_KEYWORDS: Readonly<Partial<Record<string, Easing>>> = {\n ease: [0.25, 0.1, 0.25, 1],\n 'ease-in': [0.42, 0, 1, 1],\n 'ease-in-out': [0.42, 0, 0.58, 1],\n 'ease-out': [0, 0, 0.58, 1],\n linear: [0, 0, 1, 1],\n}\n\nconst FONT_WEIGHT_KEYWORDS: Readonly<Partial<Record<string, number>>> = {\n bold: 700,\n normal: 400,\n}\n\n// Functions a browser evaluates and React Native does not. `var()` is the one\n// CSS function the resolver evaluates itself, so it is not among them.\nconst UNSUPPORTED_FUNCTION =\n /\\b(attr|calc|clamp|color-mix|env|light-dark|max|min)\\(/u\n\nconst NUMBER = String.raw`-?(?:\\d+\\.?\\d*|\\.\\d+)`\n\nfunction camelCase(kebab: string): string {\n return kebab.replaceAll(/-([a-z0-9])/gu, (_, letter: string) =>\n letter.toUpperCase(),\n )\n}\n\n/** The group a property belongs to, the key it takes there, and its kind. */\nfunction classify(\n property: string,\n): undefined | { group: Group; key: string; kind: Kind } {\n if (!property.startsWith('--kui-')) {\n return undefined\n }\n\n const name = property.slice('--kui-'.length)\n const match = CATEGORIES.find(([category]) => name.startsWith(`${category}-`))\n\n if (match === undefined) {\n return undefined\n }\n\n const [category, group] = match\n const rest = name.slice(category.length + 1)\n const kind = kindOf(category, rest)\n\n return kind === undefined ? undefined : { group, key: camelCase(rest), kind }\n}\n\n/** The index of the parenthesis closing the one just before `from`. */\nfunction closingParenthesis(value: string, from: number): number {\n let depth = 1\n\n for (let index = from; index < value.length; index++) {\n if (value[index] === '(') {\n depth++\n } else if (value[index] === ')') {\n depth--\n\n if (depth === 0) {\n return index\n }\n }\n }\n\n return -1\n}\n\nfunction kindOf(category: string, rest: string): Kind | undefined {\n switch (category) {\n case 'color': {\n return 'color'\n }\n case 'motion': {\n if (rest.startsWith('duration-')) {\n return 'duration'\n }\n\n return rest.startsWith('easing-') ? 'easing' : undefined\n }\n case 'radius':\n case 'sizing':\n case 'spacing': {\n return 'length'\n }\n case 'shadow': {\n return 'shadow'\n }\n case 'state-layer-opacity': {\n return 'opacity'\n }\n default: {\n return typographyKindOf(rest)\n }\n }\n}\n\nfunction parseColor(value: string): string | undefined {\n const drawable =\n /^#([\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$/iu.test(value) ||\n /^(rgba?|hsla?|hwb)\\([^()]*\\)$/iu.test(value) ||\n /^[a-z]+$/iu.test(value)\n\n return drawable ? value : undefined\n}\n\nfunction parseDuration(value: string): number | undefined {\n const match = /^(\\d+\\.?\\d*|\\.\\d+)(ms|s)$/u.exec(value)\n\n if (match === null) {\n return undefined\n }\n\n const duration = Number(match[1])\n\n return match[2] === 's' ? duration * 1000 : duration\n}\n\nfunction parseEasing(value: string): Easing | undefined {\n const keyword = EASING_KEYWORDS[value]\n\n if (keyword !== undefined) {\n return keyword\n }\n\n const match = /^cubic-bezier\\(([^()]*)\\)$/u.exec(value)\n\n if (match === null) {\n return undefined\n }\n\n const points = match[1].split(',').map((point) => point.trim())\n\n if (\n points.length !== 4 ||\n !points.every((point) => new RegExp(`^${NUMBER}$`, 'u').test(point))\n ) {\n return undefined\n }\n\n const [x1, y1, x2, y2] = points.map(Number)\n\n // The curve has to stay a function of time, which is what holds both x\n // coordinates inside the unit interval.\n return [x1, x2].every((x) => x >= 0 && x <= 1) ? [x1, y1, x2, y2] : undefined\n}\n\nfunction parseFontFamily(value: string): readonly string[] | undefined {\n const families = splitTopLevel(value).map((family) =>\n family.trim().replace(/^([\"'])(.*)\\1$/u, '$2'),\n )\n\n return families.every((family) => family !== '' && !family.includes('('))\n ? families\n : undefined\n}\n\nfunction parseFontWeight(value: string): number | undefined {\n const keyword = FONT_WEIGHT_KEYWORDS[value]\n\n if (keyword !== undefined) {\n return keyword\n }\n\n if (!/^\\d+(\\.\\d+)?$/u.test(value)) {\n return undefined\n }\n\n const weight = Number(value)\n\n return weight >= 1 && weight <= 1000 ? weight : undefined\n}\n\nfunction parseLength(value: string): number | undefined {\n const match = new RegExp(`^(${NUMBER})(px)?$`, 'u').exec(value)\n\n if (match === null) {\n return undefined\n }\n\n const length = Number(match[1])\n\n // CSS takes a bare number as a length only when it is zero.\n return match[2] !== 'px' && length !== 0 ? undefined : length\n}\n\nfunction parseOpacity(value: string): number | undefined {\n const match = new RegExp(`^(${NUMBER})(%)?$`, 'u').exec(value)\n\n if (match === null) {\n return undefined\n }\n\n const opacity = Number(match[1]) / (match[2] === '%' ? 100 : 1)\n\n // CSS clamps an opacity to the unit interval rather than rejecting it.\n return Math.min(Math.max(opacity, 0), 1)\n}\n\nfunction parseShadow(value: string): string | undefined {\n return value === '' ? undefined : value\n}\n\n/** Splits a value at its top-level commas, leaving any inside parentheses. */\nfunction splitTopLevel(value: string): string[] {\n const parts: string[] = []\n let depth = 0\n let quote: string | undefined\n let start = 0\n\n for (let index = 0; index < value.length; index++) {\n const character = value[index]\n\n if (quote !== undefined) {\n quote = character === quote ? undefined : quote\n } else if (character === '\"' || character === \"'\") {\n quote = character\n } else if (character === '(') {\n depth++\n } else if (character === ')') {\n depth--\n } else if (character === ',' && depth === 0) {\n parts.push(value.slice(start, index))\n start = index + 1\n }\n }\n\n parts.push(value.slice(start))\n\n return parts\n}\n\nfunction typographyKindOf(rest: string): Kind | undefined {\n if (rest.startsWith('font-family-') || rest.endsWith('-font')) {\n return 'fontFamily'\n }\n\n if (rest.startsWith('weight-') || rest.endsWith('-weight')) {\n return 'fontWeight'\n }\n\n const measured = ['-line-height', '-size', '-tracking'].some((suffix) =>\n rest.endsWith(suffix),\n )\n\n return measured ? 'length' : undefined\n}\n\n// How each kind of value is read, and what a value that cannot be read is\n// reported as.\nconst PARSERS: Readonly<\n Record<Kind, readonly [(value: string) => undefined | Value, string]>\n> = {\n color: [parseColor, 'is not a colour React Native can draw'],\n duration: [parseDuration, 'is not a duration in ms or s'],\n easing: [parseEasing, 'is not a cubic-bezier() curve or an easing keyword'],\n fontFamily: [parseFontFamily, 'is not a font family list'],\n fontWeight: [parseFontWeight, 'is not a font weight from 1 to 1000'],\n length: [parseLength, 'is not a length in px'],\n opacity: [parseOpacity, 'is not a number or a percentage'],\n shadow: [parseShadow, 'is empty'],\n}\n\n/** The properties a theme declares, leaving out any it lists as undefined. */\nfunction declarations(properties: ThemeProperties): Map<string, string> {\n return new Map(\n Object.entries(properties).filter(\n (entry): entry is [string, string] => entry[1] !== undefined,\n ),\n )\n}\n\n/**\n * Turns a theme in CSS syntax into the values React Native styles take.\n *\n * `defaults` is the whole contract, and `overrides` replace any of it, as a\n * consumer's declarations replace the library's on web. A `var()` reference\n * resolves against both, with its fallback where the property it names has no\n * usable value, so a role pointing at a family or a weight follows an override\n * of that family or weight as it does in a browser.\n *\n * A value native cannot use, such as a `calc()` or a `color-mix()`, leaves its\n * property at the default instead of dropping it, and a property the defaults\n * do not declare is ignored. Each is listed in `problems` by name, for the\n * caller to report.\n */\nfunction resolveTheme(\n defaults: ThemeProperties,\n overrides: ThemeProperties = {},\n): { problems: readonly ThemeProblem[]; theme: ResolvedGroups } {\n const declared = declarations(defaults)\n const replaced = declarations(overrides)\n const problems: ThemeProblem[] = []\n const settled = new Map<string, null | Settled>()\n const settling: string[] = []\n const cyclic = new Set<string>()\n\n function substitute(value: string): string | { reason: string } {\n const start = value.indexOf('var(')\n\n if (start === -1) {\n return value\n }\n\n const end = closingParenthesis(value, start + 'var('.length)\n\n if (end === -1) {\n return { reason: 'has a var() that is never closed' }\n }\n\n const [name, ...rest] = splitTopLevel(\n value.slice(start + 'var('.length, end),\n )\n const property = name.trim()\n const fallback = rest.length === 0 ? undefined : rest.join(',').trim()\n const referenced = declared.has(property) ? settle(property) : null\n\n let replacement: string | { reason: string }\n\n if (referenced !== null) {\n replacement = referenced.raw\n } else if (fallback === undefined) {\n replacement = {\n reason: `refers to ${property}, which has no usable value`,\n }\n } else {\n replacement = substitute(fallback)\n }\n\n if (typeof replacement !== 'string') {\n return replacement\n }\n\n return substitute(\n `${value.slice(0, start)}${replacement}${value.slice(end + 1)}`,\n )\n }\n\n function evaluate(kind: Kind, value: string): Attempt {\n const raw = substitute(value.trim())\n\n if (typeof raw !== 'string') {\n return raw\n }\n\n const unsupported = UNSUPPORTED_FUNCTION.exec(raw)\n\n if (unsupported !== null) {\n return {\n reason: `uses ${unsupported[1]}(), which native cannot evaluate`,\n }\n }\n\n const [read, reason] = PARSERS[kind]\n const parsed = read(raw)\n\n return parsed === undefined ? { reason } : { raw, value: parsed }\n }\n\n /** Evaluates one declaration, listing it as a problem if it fails. */\n function attempt(\n property: string,\n kind: Kind,\n value: string | undefined,\n ): null | Settled {\n if (value === undefined) {\n return null\n }\n\n const result = evaluate(kind, value)\n\n if ('reason' in result) {\n problems.push({ property, reason: result.reason, value })\n\n return null\n }\n\n return result\n }\n\n function settle(property: string): null | Settled {\n const known = settled.get(property)\n\n if (known !== undefined) {\n return known\n }\n\n const classified = classify(property)\n\n if (classified === undefined) {\n return null\n }\n\n // A reference leading back to a property still being settled closes a\n // cycle, and every property on it is invalid, as in CSS. The defaults\n // hold no cycle, so the members fall back to theirs.\n const index = settling.indexOf(property)\n\n if (index !== -1) {\n for (const member of settling.slice(index)) {\n cyclic.add(member)\n }\n\n return null\n }\n\n settling.push(property)\n\n const value = replaced.get(property)\n let result = attempt(property, classified.kind, value)\n\n if (result !== null && value !== undefined && cyclic.has(property)) {\n problems.push({\n property,\n reason: 'refers back to itself through var()',\n value,\n })\n result = null\n }\n\n result ??= attempt(property, classified.kind, declared.get(property))\n\n settling.pop()\n settled.set(property, result)\n\n return result\n }\n\n const theme: Groups = {\n colors: {},\n motion: {},\n radii: {},\n shadows: {},\n sizing: {},\n spacing: {},\n stateLayerOpacity: {},\n typography: {},\n }\n\n for (const [property, value] of declared) {\n const classified = classify(property)\n\n if (classified === undefined) {\n problems.push({ property, reason: 'is not a Kanso UI token', value })\n\n continue\n }\n\n const result = settle(property)\n\n // classify() pairs every group with the kinds whose values it holds, which\n // is the one thing the type of a group's record cannot see.\n if (result !== null) {\n Object.assign(theme[classified.group], { [classified.key]: result.value })\n }\n }\n\n for (const [property, value] of replaced) {\n if (!declared.has(property)) {\n problems.push({ property, reason: 'is not a Kanso UI token', value })\n }\n }\n\n return { problems, theme }\n}\n\nexport type { Easing, ResolvedGroups, ThemeProblem, ThemeProperties }\nexport { resolveTheme }\n"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { KuiTheme } from "../tokens/defaults.js";
|
|
2
|
+
import { Easing } from "./resolve.js";
|
|
3
|
+
//#region src/theme/scope.d.ts
|
|
4
|
+
type CamelCase<Name extends string> = Name extends `${infer Head}-${infer Tail}` ? `${Head}${Capitalize<CamelCase<Tail>>}` : Name;
|
|
5
|
+
type KeysOf<Segment extends string, Property extends string = keyof KuiTheme> = Property extends `--kui-${Segment}-${infer Rest}` ? CamelCase<Rest> : never;
|
|
6
|
+
/**
|
|
7
|
+
* The tokens in the values React Native styles take, grouped and keyed as the
|
|
8
|
+
* web library's StyleX token modules are: `var(--kui-color-primary)` on web is
|
|
9
|
+
* `colors.primary` here, and `--kui-typography-body-large-size` is
|
|
10
|
+
* `typography.bodyLargeSize`.
|
|
11
|
+
*
|
|
12
|
+
* Lengths are in dp and durations in milliseconds. An easing is the four
|
|
13
|
+
* control points `Easing.bezier()` takes, a font family the list of families
|
|
14
|
+
* to try in order, and a shadow the `boxShadow` value as CSS writes it.
|
|
15
|
+
*/
|
|
16
|
+
type ResolvedTheme = {
|
|
17
|
+
readonly colors: { readonly [Key in KeysOf<'color'>]: string; };
|
|
18
|
+
readonly motion: { readonly [Key in KeysOf<'motion'>]: Key extends `easing${string}` ? Easing : number; };
|
|
19
|
+
readonly radii: { readonly [Key in KeysOf<'radius'>]: number; };
|
|
20
|
+
readonly shadows: { readonly [Key in KeysOf<'shadow'>]: string; };
|
|
21
|
+
readonly sizing: { readonly [Key in KeysOf<'sizing'>]: number; };
|
|
22
|
+
readonly spacing: { readonly [Key in KeysOf<'spacing'>]: number; };
|
|
23
|
+
readonly stateLayerOpacity: { readonly [Key in KeysOf<'state-layer-opacity'>]: number; };
|
|
24
|
+
readonly typography: { readonly [Key in KeysOf<'typography'>]: Key extends `${string}Font` | `fontFamily${string}` ? readonly string[] : number; };
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The theme around the caller, resolved into the values React Native styles
|
|
28
|
+
* take, with the keys of the web library's token modules.
|
|
29
|
+
*
|
|
30
|
+
* ```tsx
|
|
31
|
+
* const { colors, spacing } = useTheme()
|
|
32
|
+
*
|
|
33
|
+
* <View style={{ backgroundColor: colors.surface, padding: spacing.md }} />
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* The same theme is the same object, so a stylesheet built from it can be
|
|
37
|
+
* kept for as long as the theme is.
|
|
38
|
+
*/
|
|
39
|
+
declare function useTheme(): ResolvedTheme;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { type ResolvedTheme, useTheme };
|
|
42
|
+
//# sourceMappingURL=scope.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { dark, light } from "../tokens/defaults.js";
|
|
2
|
+
import { resolveTheme } from "./resolve.js";
|
|
3
|
+
import { c } from "react/compiler-runtime";
|
|
4
|
+
import { useColorScheme } from "react-native";
|
|
5
|
+
import { createContext, use } from "react";
|
|
6
|
+
//#region src/theme/scope.ts
|
|
7
|
+
const DEFAULTS = {
|
|
8
|
+
dark,
|
|
9
|
+
light
|
|
10
|
+
};
|
|
11
|
+
const NO_OVERRIDES = {};
|
|
12
|
+
const ScopeContext = createContext(null);
|
|
13
|
+
const scopes = /* @__PURE__ */ new WeakMap();
|
|
14
|
+
/** The overrides of a scope: its own map over those of the scopes around it. */
|
|
15
|
+
function merged(parent, own) {
|
|
16
|
+
if (own === void 0 || own === parent) return parent;
|
|
17
|
+
return parent === NO_OVERRIDES ? own : {
|
|
18
|
+
...parent,
|
|
19
|
+
...own
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function report(problems) {
|
|
23
|
+
if (!__DEV__ || problems.length === 0) return;
|
|
24
|
+
console.warn(["ThemeScope kept these properties at their defaults:", ...problems.map(({ property, reason, value }) => ` ${property}: ${value} ${reason}`)].join("\n"));
|
|
25
|
+
}
|
|
26
|
+
function scopeFor(scheme, overrides) {
|
|
27
|
+
const known = scopes.get(overrides);
|
|
28
|
+
const cached = known?.[scheme];
|
|
29
|
+
if (cached !== void 0) return cached;
|
|
30
|
+
const { problems, theme } = resolveTheme(DEFAULTS[scheme], overrides);
|
|
31
|
+
report(problems);
|
|
32
|
+
const scope = {
|
|
33
|
+
overrides,
|
|
34
|
+
scheme,
|
|
35
|
+
theme
|
|
36
|
+
};
|
|
37
|
+
scopes.set(overrides, {
|
|
38
|
+
...known,
|
|
39
|
+
[scheme]: scope
|
|
40
|
+
});
|
|
41
|
+
return scope;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The scope around the caller. Outside every `ThemeScope`, that is the
|
|
45
|
+
* library's own tokens in the scheme the device's appearance asks for, as the
|
|
46
|
+
* tokens on `:root` follow `prefers-color-scheme` on web.
|
|
47
|
+
*/
|
|
48
|
+
function useScope() {
|
|
49
|
+
const $ = c(3);
|
|
50
|
+
const parent = use(ScopeContext);
|
|
51
|
+
const system = useColorScheme();
|
|
52
|
+
let t0;
|
|
53
|
+
if ($[0] !== parent || $[1] !== system) {
|
|
54
|
+
t0 = parent ?? scopeFor(system === "dark" ? "dark" : "light", NO_OVERRIDES);
|
|
55
|
+
$[0] = parent;
|
|
56
|
+
$[1] = system;
|
|
57
|
+
$[2] = t0;
|
|
58
|
+
} else t0 = $[2];
|
|
59
|
+
return t0;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The theme around the caller, resolved into the values React Native styles
|
|
63
|
+
* take, with the keys of the web library's token modules.
|
|
64
|
+
*
|
|
65
|
+
* ```tsx
|
|
66
|
+
* const { colors, spacing } = useTheme()
|
|
67
|
+
*
|
|
68
|
+
* <View style={{ backgroundColor: colors.surface, padding: spacing.md }} />
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* The same theme is the same object, so a stylesheet built from it can be
|
|
72
|
+
* kept for as long as the theme is.
|
|
73
|
+
*/
|
|
74
|
+
function useTheme() {
|
|
75
|
+
return useScope().theme;
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { ScopeContext, merged, scopeFor, useScope, useTheme };
|
|
79
|
+
|
|
80
|
+
//# sourceMappingURL=scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope.js","names":["createContext","use","useColorScheme","KuiTheme","Easing","ThemeProblem","dark","light","resolveTheme","CamelCase","Name","Head","Capitalize","Tail","KeysOf","Property","Segment","Rest","ResolvedTheme","colors","motion","Key","radii","shadows","sizing","spacing","stateLayerOpacity","typography","Scheme","Scope","overrides","scheme","theme","DEFAULTS","Readonly","Record","NO_OVERRIDES","ScopeContext","scopes","WeakMap","Partial","merged","parent","own","undefined","report","problems","__DEV__","length","console","warn","map","property","reason","value","join","scopeFor","known","get","cached","scope","set","useScope","$","_c","system","t0","useTheme","_c"],"sources":["../../src/theme/scope.ts"],"sourcesContent":["import { createContext, use } from 'react'\nimport { useColorScheme } from 'react-native'\n\nimport type { KuiTheme } from '../tokens/defaults'\nimport type { Easing, ThemeProblem } from './resolve'\n\nimport { dark, light } from '../tokens/defaults'\nimport { resolveTheme } from './resolve'\n\n// A property's name in camel case, as the resolver keys it:\n// `body-large-size` is `bodyLargeSize`.\ntype CamelCase<Name extends string> = Name extends `${infer Head}-${infer Tail}`\n ? `${Head}${Capitalize<CamelCase<Tail>>}`\n : Name\n\n// The keys a group takes: the rest of the name of every property that starts\n// with the group's segment.\ntype KeysOf<\n Segment extends string,\n Property extends string = keyof KuiTheme,\n> = Property extends `--kui-${Segment}-${infer Rest}` ? CamelCase<Rest> : never\n\n/**\n * The tokens in the values React Native styles take, grouped and keyed as the\n * web library's StyleX token modules are: `var(--kui-color-primary)` on web is\n * `colors.primary` here, and `--kui-typography-body-large-size` is\n * `typography.bodyLargeSize`.\n *\n * Lengths are in dp and durations in milliseconds. An easing is the four\n * control points `Easing.bezier()` takes, a font family the list of families\n * to try in order, and a shadow the `boxShadow` value as CSS writes it.\n */\ntype ResolvedTheme = {\n readonly colors: { readonly [Key in KeysOf<'color'>]: string }\n readonly motion: {\n readonly [Key in KeysOf<'motion'>]: Key extends `easing${string}`\n ? Easing\n : number\n }\n readonly radii: { readonly [Key in KeysOf<'radius'>]: number }\n readonly shadows: { readonly [Key in KeysOf<'shadow'>]: string }\n readonly sizing: { readonly [Key in KeysOf<'sizing'>]: number }\n readonly spacing: { readonly [Key in KeysOf<'spacing'>]: number }\n readonly stateLayerOpacity: {\n readonly [Key in KeysOf<'state-layer-opacity'>]: number\n }\n readonly typography: {\n readonly [Key in KeysOf<'typography'>]: Key extends\n | `${string}Font`\n | `fontFamily${string}`\n ? readonly string[]\n : number\n }\n}\n\ntype Scheme = 'dark' | 'light'\n\n/** What a scope hands everything inside it. */\ntype Scope = {\n /** Every override this scope and the scopes around it declare, merged. */\n readonly overrides: KuiTheme\n readonly scheme: Scheme\n readonly theme: ResolvedTheme\n}\n\nconst DEFAULTS: Readonly<Record<Scheme, KuiTheme>> = { dark, light }\n\n// What a scope declaring no overrides of its own hands on, shared so that\n// every such scope resolves to the same theme.\nconst NO_OVERRIDES: KuiTheme = {}\n\nconst ScopeContext = createContext<null | Scope>(null)\n\n// Each map resolves once per scheme, keyed on the map itself. A map kept as a\n// constant, or merged from constants, therefore never resolves twice, and every\n// component reading it is handed the same object, which is what lets a\n// component keep one stylesheet per theme.\nconst scopes = new WeakMap<KuiTheme, Partial<Record<Scheme, Scope>>>()\n\n/** The overrides of a scope: its own map over those of the scopes around it. */\nfunction merged(parent: KuiTheme, own: KuiTheme | undefined): KuiTheme {\n if (own === undefined || own === parent) {\n return parent\n }\n\n return parent === NO_OVERRIDES ? own : { ...parent, ...own }\n}\n\n// A value native cannot use keeps its property at the default, which a reader\n// of the app would otherwise never learn, so it is named in development.\nfunction report(problems: readonly ThemeProblem[]) {\n if (!__DEV__ || problems.length === 0) {\n return\n }\n\n console.warn(\n [\n 'ThemeScope kept these properties at their defaults:',\n ...problems.map(\n ({ property, reason, value }) => ` ${property}: ${value} ${reason}`,\n ),\n ].join('\\n'),\n )\n}\n\nfunction scopeFor(scheme: Scheme, overrides: KuiTheme): Scope {\n const known = scopes.get(overrides)\n const cached = known?.[scheme]\n\n if (cached !== undefined) {\n return cached\n }\n\n const { problems, theme } = resolveTheme(DEFAULTS[scheme], overrides)\n\n report(problems)\n\n const scope: Scope = {\n overrides,\n scheme,\n // The defaults declare every property and the resolver keeps a property\n // at its default when an override fails, so every key the type names is\n // there; src/tokens/defaults.test.ts pins that the defaults resolve whole.\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- see above\n theme: theme as ResolvedTheme,\n }\n\n scopes.set(overrides, { ...known, [scheme]: scope })\n\n return scope\n}\n\n/**\n * The scope around the caller. Outside every `ThemeScope`, that is the\n * library's own tokens in the scheme the device's appearance asks for, as the\n * tokens on `:root` follow `prefers-color-scheme` on web.\n */\nfunction useScope(): Scope {\n const parent = use(ScopeContext)\n const system = useColorScheme()\n\n return parent ?? scopeFor(system === 'dark' ? 'dark' : 'light', NO_OVERRIDES)\n}\n\n/**\n * The theme around the caller, resolved into the values React Native styles\n * take, with the keys of the web library's token modules.\n *\n * ```tsx\n * const { colors, spacing } = useTheme()\n *\n * <View style={{ backgroundColor: colors.surface, padding: spacing.md }} />\n * ```\n *\n * The same theme is the same object, so a stylesheet built from it can be\n * kept for as long as the theme is.\n */\nfunction useTheme(): ResolvedTheme {\n return useScope().theme\n}\n\nexport { merged, ScopeContext, scopeFor, useScope, useTheme }\nexport type { ResolvedTheme, Scheme, Scope }\n"],"mappings":";;;;;;AAiEA,MAAMiC,WAA+C;CAAE3B;CAAMC;AAAM;AAInE,MAAM6B,eAAyB,CAAC;AAEhC,MAAMC,eAAerC,cAA4B,IAAI;AAMrD,MAAMsC,yBAAS,IAAIC,QAAkD;;AAGrE,SAASE,OAAOC,QAAkBC,KAAqC;CACrE,IAAIA,QAAQC,KAAAA,KAAaD,QAAQD,QAC/B,OAAOA;CAGT,OAAOA,WAAWN,eAAeO,MAAM;EAAE,GAAGD;EAAQ,GAAGC;CAAI;AAC7D;AAIA,SAASE,OAAOC,UAAmC;CACjD,IAAI,CAACC,WAAWD,SAASE,WAAW,GAClC;CAGFC,QAAQC,KACN,CACE,uDACA,GAAGJ,SAASK,KACT,EAAEC,UAAUC,QAAQC,YAAY,KAAKF,SAAQ,IAAKE,MAAK,GAAID,QAC9D,CAAC,CACF,CAACE,KAAK,IAAI,CACb;AACF;AAEA,SAASC,SAASzB,QAAgBD,WAA4B;CAC5D,MAAM2B,QAAQnB,OAAOoB,IAAI5B,SAAS;CAClC,MAAM6B,SAASF,QAAQ1B;CAEvB,IAAI4B,WAAWf,KAAAA,GACb,OAAOe;CAGT,MAAM,EAAEb,UAAUd,UAAUxB,aAAayB,SAASF,SAASD,SAAS;CAEpEe,OAAOC,QAAQ;CAEf,MAAMc,QAAe;EACnB9B;EACAC;EAKOC;CACT;CAEAM,OAAOuB,IAAI/B,WAAW;EAAE,GAAG2B;GAAQ1B,SAAS6B;CAAM,CAAC;CAEnD,OAAOA;AACT;;;;;;AAOA,SAAAE,WAAA;CAAA,MAAAC,IAAAC,EAAA,CAAA;CACE,MAAAtB,SAAezC,IAAIoC,YAAY;CAC/B,MAAA4B,SAAe/D,eAAe;CAAC,IAAAgE;CAAA,IAAAH,EAAA,OAAArB,UAAAqB,EAAA,OAAAE,QAAA;EAExBC,KAAAxB,UAAUc,SAASS,WAAW,SAAX,SAAA,SAAsC7B,YAAY;EAAC2B,EAAA,KAAArB;EAAAqB,EAAA,KAAAE;EAAAF,EAAA,KAAAG;CAAA,OAAAA,KAAAH,EAAA;CAAA,OAAtEG;AAAsE;;;;;;;;;;;;;;AAgB/E,SAAAC,WAAA;CAAA,OACSL,SAAS,CAAC,CAAA9B;AAAM"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/theme/styles.ts
|
|
2
|
+
/**
|
|
3
|
+
* Styles in the order they apply, each over the ones before it, as React
|
|
4
|
+
* Native's array form takes them. A call site's own `style` goes last.
|
|
5
|
+
*/
|
|
6
|
+
function composeStyles(...styles) {
|
|
7
|
+
return styles;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A stylesheet built from the theme, created once for each theme it is asked
|
|
11
|
+
* for. The same theme is the same object, so a component re-rendering under
|
|
12
|
+
* an unchanged scope is handed the stylesheet it already had, and one under
|
|
13
|
+
* another scope or scheme is handed that scope's.
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* const stylesOf = themedStyles(({ colors, spacing }) =>
|
|
17
|
+
* StyleSheet.create({ box: { backgroundColor: colors.surface, padding: spacing.md } }),
|
|
18
|
+
* )
|
|
19
|
+
*
|
|
20
|
+
* function Box() {
|
|
21
|
+
* const styles = stylesOf(useTheme())
|
|
22
|
+
* return <View style={styles.box} />
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
function themedStyles(create) {
|
|
27
|
+
const sheets = /* @__PURE__ */ new WeakMap();
|
|
28
|
+
return (theme) => {
|
|
29
|
+
const known = sheets.get(theme);
|
|
30
|
+
if (known !== void 0) return known;
|
|
31
|
+
const sheet = create(theme);
|
|
32
|
+
sheets.set(theme, sheet);
|
|
33
|
+
return sheet;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { composeStyles, themedStyles };
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=styles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.js","names":["StyleProp","ResolvedTheme","composeStyles","styles","Style","themedStyles","create","theme","Styles","sheets","WeakMap","known","get","undefined","sheet","set"],"sources":["../../src/theme/styles.ts"],"sourcesContent":["import type { StyleProp } from 'react-native'\n\nimport type { ResolvedTheme } from './scope'\n\n/**\n * Styles in the order they apply, each over the ones before it, as React\n * Native's array form takes them. A call site's own `style` goes last.\n */\nfunction composeStyles<Style>(...styles: StyleProp<Style>[]): StyleProp<Style> {\n return styles\n}\n\n/**\n * A stylesheet built from the theme, created once for each theme it is asked\n * for. The same theme is the same object, so a component re-rendering under\n * an unchanged scope is handed the stylesheet it already had, and one under\n * another scope or scheme is handed that scope's.\n *\n * ```tsx\n * const stylesOf = themedStyles(({ colors, spacing }) =>\n * StyleSheet.create({ box: { backgroundColor: colors.surface, padding: spacing.md } }),\n * )\n *\n * function Box() {\n * const styles = stylesOf(useTheme())\n * return <View style={styles.box} />\n * }\n * ```\n */\nfunction themedStyles<Styles>(\n create: (theme: ResolvedTheme) => Styles,\n): (theme: ResolvedTheme) => Styles {\n const sheets = new WeakMap<ResolvedTheme, Styles>()\n\n return (theme) => {\n const known = sheets.get(theme)\n\n if (known !== undefined) {\n return known\n }\n\n const sheet = create(theme)\n\n sheets.set(theme, sheet)\n\n return sheet\n }\n}\n\nexport { composeStyles, themedStyles }\n"],"mappings":";;;;;AAQA,SAASE,cAAqB,GAAGC,QAA8C;CAC7E,OAAOA;AACT;;;;;;;;;;;;;;;;;;AAmBA,SAASE,aACPC,QACkC;CAClC,MAAMG,yBAAS,IAAIC,QAA+B;CAElD,QAAQH,UAAU;EAChB,MAAMI,QAAQF,OAAOG,IAAIL,KAAK;EAE9B,IAAII,UAAUE,KAAAA,GACZ,OAAOF;EAGT,MAAMG,QAAQR,OAAOC,KAAK;EAE1BE,OAAOM,IAAIR,OAAOO,KAAK;EAEvB,OAAOA;CACT;AACF"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { KuiTheme } from "./tokens/defaults.js";
|
|
2
|
+
import { View, ViewProps } from "react-native";
|
|
3
|
+
import { ComponentRef, Ref } from "react";
|
|
4
|
+
//#region src/theme-scope.d.ts
|
|
5
|
+
type ThemeScopeProps = ViewProps & {
|
|
6
|
+
readonly ref?: Ref<ComponentRef<typeof View>>;
|
|
7
|
+
/**
|
|
8
|
+
* Pins the scope to the library's light or dark scheme, whatever the
|
|
9
|
+
* device's appearance asks for, and paints the scope's own surface in it.
|
|
10
|
+
* Left unset, the scope paints nothing and keeps the scheme around it: the
|
|
11
|
+
* one an enclosing scope pins, or the device's.
|
|
12
|
+
*/
|
|
13
|
+
readonly scheme?: 'dark' | 'light' | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* `--kui-*` overrides for everything inside, as a map of the custom
|
|
16
|
+
* properties kanso-ui's `tokens.css` lists to CSS values, the same map
|
|
17
|
+
* web's `ThemeScope` takes. A nested scope's map applies over its parent's.
|
|
18
|
+
*
|
|
19
|
+
* The values hold for both schemes. To give light and dark a value each,
|
|
20
|
+
* keep two maps and hand the scope the one for the scheme in use.
|
|
21
|
+
*
|
|
22
|
+
* A map is resolved once for each scheme it is used in, so keep one that
|
|
23
|
+
* does not change as a constant rather than writing it inline.
|
|
24
|
+
*/
|
|
25
|
+
readonly theme?: KuiTheme | undefined;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Themes what is inside it.
|
|
29
|
+
*
|
|
30
|
+
* Every component inside reads the scope's tokens: the library's defaults in
|
|
31
|
+
* the scope's scheme, with the `--kui-*` overrides of this scope and every
|
|
32
|
+
* scope around it applied over them. A value is read as a browser would read
|
|
33
|
+
* it, `var()` references included, and one native cannot evaluate, such as a
|
|
34
|
+
* `calc()`, keeps its property at the default and is named in a development
|
|
35
|
+
* warning. Dialogs and sheets follow the scope they open in, since React
|
|
36
|
+
* Native's `Modal` keeps React context.
|
|
37
|
+
*
|
|
38
|
+
* ```tsx
|
|
39
|
+
* const accent: KuiTheme = { '--kui-color-primary': '#7d5260' }
|
|
40
|
+
*
|
|
41
|
+
* <ThemeScope scheme="dark" style={{ flex: 1 }} theme={accent}>
|
|
42
|
+
* <Stack />
|
|
43
|
+
* </ThemeScope>
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* The scope is a `View`, so one wrapping a whole screen takes `flex: 1` to
|
|
47
|
+
* fill it, as any view would.
|
|
48
|
+
*
|
|
49
|
+
* The outermost scope also hosts what the library draws above the screen,
|
|
50
|
+
* over everything inside it, so it belongs at the root of the app and fills
|
|
51
|
+
* it. What draws there follows the scope it was opened in, not the root's.
|
|
52
|
+
*/
|
|
53
|
+
declare function ThemeScope({ children, scheme, style, theme, ...props }: ThemeScopeProps): import("react").JSX.Element;
|
|
54
|
+
//#endregion
|
|
55
|
+
export { type KuiTheme, ThemeScope, type ThemeScopeProps };
|
|
56
|
+
//# sourceMappingURL=theme-scope.d.ts.map
|