@vsreact/core 0.0.10 → 0.0.12

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/src/tw.ts CHANGED
@@ -1,284 +1,288 @@
1
- import { resolveColor, setThemeColors, type ThemeColors } from "./theme";
2
-
3
- export type StyleValue = string | number;
4
- export type Style = Record<string, StyleValue>;
5
-
6
- export interface ResolvedClasses {
7
- style: Style;
8
- hoverStyle?: Style;
9
- activeStyle?: Style;
10
- focusStyle?: Style;
11
- }
12
-
13
- export function configureTheme(theme: { colors?: ThemeColors }): void {
14
- if (theme.colors) setThemeColors(theme.colors);
15
- cache.clear();
16
- }
17
-
18
- const staticClasses: Record<string, Style> = {
19
- flex: {},
20
- "flex-row": { flexDirection: "row" },
21
- "flex-col": { flexDirection: "column" },
22
- "flex-row-reverse": { flexDirection: "row-reverse" },
23
- "flex-col-reverse": { flexDirection: "column-reverse" },
24
- "flex-1": { flex: 1 },
25
- "flex-auto": { flexGrow: 1, flexShrink: 1 },
26
- "flex-none": { flexGrow: 0, flexShrink: 0 },
27
- grow: { flexGrow: 1 },
28
- "grow-0": { flexGrow: 0 },
29
- shrink: { flexShrink: 1 },
30
- "shrink-0": { flexShrink: 0 },
31
- "flex-wrap": { flexWrap: "wrap" },
32
- "flex-nowrap": { flexWrap: "nowrap" },
33
- "items-start": { alignItems: "flex-start" },
34
- "items-center": { alignItems: "center" },
35
- "items-end": { alignItems: "flex-end" },
36
- "items-stretch": { alignItems: "stretch" },
37
- "items-baseline": { alignItems: "baseline" },
38
- "justify-start": { justifyContent: "flex-start" },
39
- "justify-center": { justifyContent: "center" },
40
- "justify-end": { justifyContent: "flex-end" },
41
- "justify-between": { justifyContent: "space-between" },
42
- "justify-around": { justifyContent: "space-around" },
43
- "justify-evenly": { justifyContent: "space-evenly" },
44
- "self-start": { alignSelf: "flex-start" },
45
- "self-center": { alignSelf: "center" },
46
- "self-end": { alignSelf: "flex-end" },
47
- "self-stretch": { alignSelf: "stretch" },
48
- "self-auto": { alignSelf: "auto" },
49
- absolute: { position: "absolute" },
50
- relative: { position: "relative" },
51
- "overflow-hidden": { overflow: "hidden" },
52
- "overflow-visible": { overflow: "visible" },
53
- "overflow-y-scroll": { overflow: "scroll" },
54
- "overflow-scroll": { overflow: "scroll" },
55
- "w-full": { width: "100%" },
56
- "h-full": { height: "100%" },
57
- "text-left": { textAlign: "left" },
58
- "text-center": { textAlign: "center" },
59
- "text-right": { textAlign: "right" },
60
- "font-mono": { fontFamily: "monospace" },
61
- "font-normal": { fontWeight: 400 },
62
- "font-medium": { fontWeight: 500 },
63
- "font-semibold": { fontWeight: 600 },
64
- "font-bold": { fontWeight: 700 },
65
- "tracking-tighter": { letterSpacing: -0.8 },
66
- "tracking-tight": { letterSpacing: -0.4 },
67
- "tracking-normal": { letterSpacing: 0 },
68
- "tracking-wide": { letterSpacing: 0.4 },
69
- "tracking-wider": { letterSpacing: 0.8 },
70
- "tracking-widest": { letterSpacing: 1.6 },
71
- border: { borderWidth: 1 },
72
- "aspect-square": { aspectRatio: 1 },
73
- "aspect-video": { aspectRatio: 16 / 9 },
74
- "cursor-pointer": { cursor: "pointer" },
75
- "cursor-text": { cursor: "text" },
76
- "cursor-default": { cursor: "default" },
77
- shadow: { shadowColor: "#00000066", shadowRadius: 4, shadowOffsetY: 1 },
78
- "shadow-sm": { shadowColor: "#00000066", shadowRadius: 4, shadowOffsetY: 1 },
79
- "shadow-md": { shadowColor: "#00000066", shadowRadius: 8, shadowOffsetY: 2 },
80
- "shadow-lg": { shadowColor: "#00000066", shadowRadius: 12, shadowOffsetY: 4 },
81
- "shadow-xl": { shadowColor: "#00000066", shadowRadius: 20, shadowOffsetY: 8 },
82
- };
83
-
84
- const textSizes: Record<string, number> = {
85
- xs: 12, sm: 14, base: 16, lg: 18, xl: 20, "2xl": 24, "3xl": 30, "4xl": 36,
86
- "5xl": 48, "6xl": 60,
87
- };
88
-
89
- const radiusSizes: Record<string, number> = {
90
- "": 4, sm: 2, md: 6, lg: 8, xl: 12, "2xl": 16, "3xl": 24, full: 9999,
91
- };
92
-
93
- const radiusCorners: Record<string, string[]> = {
94
- "": ["borderTopLeftRadius", "borderTopRightRadius", "borderBottomLeftRadius", "borderBottomRightRadius"],
95
- t: ["borderTopLeftRadius", "borderTopRightRadius"],
96
- b: ["borderBottomLeftRadius", "borderBottomRightRadius"],
97
- l: ["borderTopLeftRadius", "borderBottomLeftRadius"],
98
- r: ["borderTopRightRadius", "borderBottomRightRadius"],
99
- tl: ["borderTopLeftRadius"],
100
- tr: ["borderTopRightRadius"],
101
- bl: ["borderBottomLeftRadius"],
102
- br: ["borderBottomRightRadius"],
103
- };
104
-
105
- // Utilities whose value is a length on the 4px spacing scale.
106
- const lengthKeys: Record<string, string[]> = {
107
- w: ["width"],
108
- h: ["height"],
109
- size: ["width", "height"],
110
- "min-w": ["minWidth"],
111
- "min-h": ["minHeight"],
112
- "max-w": ["maxWidth"],
113
- "max-h": ["maxHeight"],
114
- p: ["padding"],
115
- px: ["paddingLeft", "paddingRight"],
116
- py: ["paddingTop", "paddingBottom"],
117
- pt: ["paddingTop"],
118
- pr: ["paddingRight"],
119
- pb: ["paddingBottom"],
120
- pl: ["paddingLeft"],
121
- m: ["margin"],
122
- mx: ["marginLeft", "marginRight"],
123
- my: ["marginTop", "marginBottom"],
124
- mt: ["marginTop"],
125
- mr: ["marginRight"],
126
- mb: ["marginBottom"],
127
- ml: ["marginLeft"],
128
- gap: ["gap"],
129
- "gap-x": ["columnGap"],
130
- "gap-y": ["rowGap"],
131
- top: ["top"],
132
- right: ["right"],
133
- bottom: ["bottom"],
134
- left: ["left"],
135
- inset: ["left", "right", "top", "bottom"],
136
- "inset-x": ["left", "right"],
137
- "inset-y": ["top", "bottom"],
138
- basis: ["flexBasis"],
139
- };
140
-
141
- const warned = new Set<string>();
142
-
143
- function warnUnknown(cls: string): void {
144
- if (warned.has(cls)) return;
145
- warned.add(cls);
146
- console.warn(`[vsreact] unknown class "${cls}" ignored`);
147
- }
148
-
149
- /** "4"→16, "1.5"→6, "px"→1, "1/2"→"50%", "full"→"100%", "[123]"→123, "[45%]"→"45%" */
150
- function parseLength(raw: string): StyleValue | undefined {
151
- if (raw.startsWith("[") && raw.endsWith("]")) {
152
- const inner = raw.slice(1, -1);
153
- if (inner.endsWith("%")) return inner;
154
- const n = Number(inner.replace(/px$/, ""));
155
- return Number.isFinite(n) ? n : undefined;
156
- }
157
- if (raw === "px") return 1;
158
- if (raw === "full") return "100%";
159
- if (raw === "auto") return "auto";
160
- if (/^\d+\/\d+$/.test(raw)) {
161
- const [num, den] = raw.split("/").map(Number);
162
- return `${((num / den) * 100).toFixed(4).replace(/\.?0+$/, "")}%`;
163
- }
164
- const n = Number(raw);
165
- return Number.isFinite(n) ? n * 4 : undefined;
166
- }
167
-
168
- function resolveClass(cls: string): Style | undefined {
169
- const negative = cls.startsWith("-");
170
- const body = negative ? cls.slice(1) : cls;
171
-
172
- const negate = (v: StyleValue): StyleValue => {
173
- if (!negative) return v;
174
- if (typeof v === "number") return -v;
175
- if (typeof v === "string" && v.endsWith("%")) return `-${v}`;
176
- return v;
177
- };
178
-
179
- const known = staticClasses[body];
180
- if (known && !negative) return known;
181
-
182
- // rounded, rounded-lg, rounded-t-lg, rounded-br-full, rounded-[10]
183
- if (body === "rounded" || body.startsWith("rounded-")) {
184
- const rest = body === "rounded" ? "" : body.slice("rounded-".length);
185
- const parts = rest === "" ? [] : rest.split("-");
186
- let corner = "";
187
- let size = "";
188
- if (parts.length === 1) {
189
- if (parts[0] in radiusCorners) corner = parts[0];
190
- else size = parts[0];
191
- } else if (parts.length === 2) {
192
- [corner, size] = parts;
193
- }
194
- const corners = radiusCorners[corner];
195
- const radius =
196
- size.startsWith("[") ? (parseLength(size) as number | undefined) : radiusSizes[size];
197
- if (corners === undefined || radius === undefined) return undefined;
198
- if (corner === "") return { borderRadius: radius };
199
- const style: Style = {};
200
- for (const key of corners) style[key] = radius;
201
- return style;
202
- }
203
-
204
- const dash = body.indexOf("-");
205
- if (dash <= 0) return undefined;
206
-
207
- // Longest matching length-utility prefix (handles gap-x-4 vs gap-4, min-w-*).
208
- for (const prefix of Object.keys(lengthKeys).sort((a, b) => b.length - a.length)) {
209
- if (body.startsWith(prefix + "-")) {
210
- const value = parseLength(body.slice(prefix.length + 1));
211
- if (value === undefined) break;
212
- const style: Style = {};
213
- for (const key of lengthKeys[prefix]) style[key] = negate(value);
214
- return style;
215
- }
216
- }
217
-
218
- const prefix = body.slice(0, dash);
219
- const rest = body.slice(dash + 1);
220
-
221
- switch (prefix) {
222
- case "bg": {
223
- const color = resolveColor(rest);
224
- return color !== undefined ? { backgroundColor: color } : undefined;
225
- }
226
- case "text": {
227
- if (rest in textSizes) return { fontSize: textSizes[rest] };
228
- const color = resolveColor(rest);
229
- return color !== undefined ? { color } : undefined;
230
- }
231
- case "border": {
232
- const n = Number(rest);
233
- if (Number.isFinite(n)) return { borderWidth: n };
234
- const color = resolveColor(rest);
235
- return color !== undefined ? { borderColor: color } : undefined;
236
- }
237
- case "opacity": {
238
- const n = Number(rest);
239
- return Number.isFinite(n) ? { opacity: n / 100 } : undefined;
240
- }
241
- case "leading": {
242
- const n = Number(rest);
243
- return Number.isFinite(n) ? { lineHeight: n * 4 } : undefined;
244
- }
245
- case "flex": {
246
- const n = Number(rest);
247
- return Number.isFinite(n) ? { flex: n } : undefined;
248
- }
249
- default:
250
- return undefined;
251
- }
252
- }
253
-
254
- const cache = new Map<string, ResolvedClasses>();
255
-
256
- export function tw(classes: string): ResolvedClasses {
257
- const cached = cache.get(classes);
258
- if (cached) return cached;
259
-
260
- const result: ResolvedClasses = { style: {} };
261
-
262
- for (const cls of classes.split(/\s+/)) {
263
- if (cls === "") continue;
264
-
265
- let bucket: "style" | "hoverStyle" | "activeStyle" | "focusStyle" = "style";
266
- let body = cls;
267
-
268
- if (body.startsWith("hover:")) [bucket, body] = ["hoverStyle", body.slice(6)];
269
- else if (body.startsWith("active:")) [bucket, body] = ["activeStyle", body.slice(7)];
270
- else if (body.startsWith("focus:")) [bucket, body] = ["focusStyle", body.slice(6)];
271
-
272
- const resolved = resolveClass(body);
273
-
274
- if (resolved === undefined) {
275
- warnUnknown(cls);
276
- continue;
277
- }
278
-
279
- result[bucket] = Object.assign(result[bucket] ?? {}, resolved);
280
- }
281
-
282
- cache.set(classes, result);
283
- return result;
284
- }
1
+ import { resolveColor, setThemeColors, type ThemeColors } from "./theme";
2
+
3
+ export type StyleValue = string | number;
4
+ export type Style = Record<string, StyleValue>;
5
+
6
+ export interface ResolvedClasses {
7
+ style: Style;
8
+ hoverStyle?: Style;
9
+ activeStyle?: Style;
10
+ focusStyle?: Style;
11
+ }
12
+
13
+ export function configureTheme(theme: { colors?: ThemeColors }): void {
14
+ if (theme.colors) setThemeColors(theme.colors);
15
+ cache.clear();
16
+ }
17
+
18
+ const staticClasses: Record<string, Style> = {
19
+ flex: {},
20
+ "flex-row": { flexDirection: "row" },
21
+ "flex-col": { flexDirection: "column" },
22
+ "flex-row-reverse": { flexDirection: "row-reverse" },
23
+ "flex-col-reverse": { flexDirection: "column-reverse" },
24
+ "flex-1": { flex: 1 },
25
+ "flex-auto": { flexGrow: 1, flexShrink: 1 },
26
+ "flex-none": { flexGrow: 0, flexShrink: 0 },
27
+ grow: { flexGrow: 1 },
28
+ "grow-0": { flexGrow: 0 },
29
+ shrink: { flexShrink: 1 },
30
+ "shrink-0": { flexShrink: 0 },
31
+ "flex-wrap": { flexWrap: "wrap" },
32
+ "flex-nowrap": { flexWrap: "nowrap" },
33
+ "items-start": { alignItems: "flex-start" },
34
+ "items-center": { alignItems: "center" },
35
+ "items-end": { alignItems: "flex-end" },
36
+ "items-stretch": { alignItems: "stretch" },
37
+ "items-baseline": { alignItems: "baseline" },
38
+ "justify-start": { justifyContent: "flex-start" },
39
+ "justify-center": { justifyContent: "center" },
40
+ "justify-end": { justifyContent: "flex-end" },
41
+ "justify-between": { justifyContent: "space-between" },
42
+ "justify-around": { justifyContent: "space-around" },
43
+ "justify-evenly": { justifyContent: "space-evenly" },
44
+ "self-start": { alignSelf: "flex-start" },
45
+ "self-center": { alignSelf: "center" },
46
+ "self-end": { alignSelf: "flex-end" },
47
+ "self-stretch": { alignSelf: "stretch" },
48
+ "self-auto": { alignSelf: "auto" },
49
+ absolute: { position: "absolute" },
50
+ relative: { position: "relative" },
51
+ "overflow-hidden": { overflow: "hidden" },
52
+ "overflow-visible": { overflow: "visible" },
53
+ "overflow-y-scroll": { overflow: "scroll" },
54
+ "overflow-scroll": { overflow: "scroll" },
55
+ "w-full": { width: "100%" },
56
+ "h-full": { height: "100%" },
57
+ "text-left": { textAlign: "left" },
58
+ "text-center": { textAlign: "center" },
59
+ "text-right": { textAlign: "right" },
60
+ "font-mono": { fontFamily: "monospace" },
61
+ "font-normal": { fontWeight: 400 },
62
+ "font-medium": { fontWeight: 500 },
63
+ "font-semibold": { fontWeight: 600 },
64
+ "font-bold": { fontWeight: 700 },
65
+ "tracking-tighter": { letterSpacing: -0.8 },
66
+ "tracking-tight": { letterSpacing: -0.4 },
67
+ "tracking-normal": { letterSpacing: 0 },
68
+ "tracking-wide": { letterSpacing: 0.4 },
69
+ "tracking-wider": { letterSpacing: 0.8 },
70
+ "tracking-widest": { letterSpacing: 1.6 },
71
+ border: { borderWidth: 1 },
72
+ "aspect-square": { aspectRatio: 1 },
73
+ "aspect-video": { aspectRatio: 16 / 9 },
74
+ "cursor-pointer": { cursor: "pointer" },
75
+ "cursor-text": { cursor: "text" },
76
+ "cursor-default": { cursor: "default" },
77
+ shadow: { shadowColor: "#00000066", shadowRadius: 4, shadowOffsetY: 1 },
78
+ "shadow-sm": { shadowColor: "#00000066", shadowRadius: 4, shadowOffsetY: 1 },
79
+ "shadow-md": { shadowColor: "#00000066", shadowRadius: 8, shadowOffsetY: 2 },
80
+ "shadow-lg": { shadowColor: "#00000066", shadowRadius: 12, shadowOffsetY: 4 },
81
+ "shadow-xl": { shadowColor: "#00000066", shadowRadius: 20, shadowOffsetY: 8 },
82
+ };
83
+
84
+ const textSizes: Record<string, number> = {
85
+ xs: 12, sm: 14, base: 16, lg: 18, xl: 20, "2xl": 24, "3xl": 30, "4xl": 36,
86
+ "5xl": 48, "6xl": 60,
87
+ };
88
+
89
+ const radiusSizes: Record<string, number> = {
90
+ "": 4, sm: 2, md: 6, lg: 8, xl: 12, "2xl": 16, "3xl": 24, full: 9999,
91
+ };
92
+
93
+ const radiusCorners: Record<string, string[]> = {
94
+ "": ["borderTopLeftRadius", "borderTopRightRadius", "borderBottomLeftRadius", "borderBottomRightRadius"],
95
+ t: ["borderTopLeftRadius", "borderTopRightRadius"],
96
+ b: ["borderBottomLeftRadius", "borderBottomRightRadius"],
97
+ l: ["borderTopLeftRadius", "borderBottomLeftRadius"],
98
+ r: ["borderTopRightRadius", "borderBottomRightRadius"],
99
+ tl: ["borderTopLeftRadius"],
100
+ tr: ["borderTopRightRadius"],
101
+ bl: ["borderBottomLeftRadius"],
102
+ br: ["borderBottomRightRadius"],
103
+ };
104
+
105
+ // Utilities whose value is a length on the 4px spacing scale.
106
+ const lengthKeys: Record<string, string[]> = {
107
+ w: ["width"],
108
+ h: ["height"],
109
+ size: ["width", "height"],
110
+ "min-w": ["minWidth"],
111
+ "min-h": ["minHeight"],
112
+ "max-w": ["maxWidth"],
113
+ "max-h": ["maxHeight"],
114
+ p: ["padding"],
115
+ px: ["paddingLeft", "paddingRight"],
116
+ py: ["paddingTop", "paddingBottom"],
117
+ pt: ["paddingTop"],
118
+ pr: ["paddingRight"],
119
+ pb: ["paddingBottom"],
120
+ pl: ["paddingLeft"],
121
+ m: ["margin"],
122
+ mx: ["marginLeft", "marginRight"],
123
+ my: ["marginTop", "marginBottom"],
124
+ mt: ["marginTop"],
125
+ mr: ["marginRight"],
126
+ mb: ["marginBottom"],
127
+ ml: ["marginLeft"],
128
+ gap: ["gap"],
129
+ "gap-x": ["columnGap"],
130
+ "gap-y": ["rowGap"],
131
+ top: ["top"],
132
+ right: ["right"],
133
+ bottom: ["bottom"],
134
+ left: ["left"],
135
+ inset: ["left", "right", "top", "bottom"],
136
+ "inset-x": ["left", "right"],
137
+ "inset-y": ["top", "bottom"],
138
+ basis: ["flexBasis"],
139
+ };
140
+
141
+ const warned = new Set<string>();
142
+
143
+ function warnUnknown(cls: string): void {
144
+ if (warned.has(cls)) return;
145
+ warned.add(cls);
146
+ console.warn(`[vsreact] unknown class "${cls}" ignored`);
147
+ }
148
+
149
+ /** "4"→16, "1.5"→6, "px"→1, "1/2"→"50%", "full"→"100%", "[123]"→123, "[45%]"→"45%" */
150
+ function parseLength(raw: string): StyleValue | undefined {
151
+ if (raw.startsWith("[") && raw.endsWith("]")) {
152
+ const inner = raw.slice(1, -1);
153
+ if (inner.endsWith("%")) return inner;
154
+ const n = Number(inner.replace(/px$/, ""));
155
+ return Number.isFinite(n) ? n : undefined;
156
+ }
157
+ if (raw === "px") return 1;
158
+ if (raw === "full") return "100%";
159
+ if (raw === "auto") return "auto";
160
+ if (/^\d+\/\d+$/.test(raw)) {
161
+ const [num, den] = raw.split("/").map(Number);
162
+ return `${((num / den) * 100).toFixed(4).replace(/\.?0+$/, "")}%`;
163
+ }
164
+ const n = Number(raw);
165
+ return Number.isFinite(n) ? n * 4 : undefined;
166
+ }
167
+
168
+ function resolveClass(cls: string): Style | undefined {
169
+ const negative = cls.startsWith("-");
170
+ const body = negative ? cls.slice(1) : cls;
171
+
172
+ const negate = (v: StyleValue): StyleValue => {
173
+ if (!negative) return v;
174
+ if (typeof v === "number") return -v;
175
+ if (typeof v === "string" && v.endsWith("%")) return `-${v}`;
176
+ return v;
177
+ };
178
+
179
+ const known = staticClasses[body];
180
+ if (known && !negative) return known;
181
+
182
+ // rounded, rounded-lg, rounded-t-lg, rounded-br-full, rounded-[10]
183
+ if (body === "rounded" || body.startsWith("rounded-")) {
184
+ const rest = body === "rounded" ? "" : body.slice("rounded-".length);
185
+ const parts = rest === "" ? [] : rest.split("-");
186
+ let corner = "";
187
+ let size = "";
188
+ if (parts.length === 1) {
189
+ if (parts[0] in radiusCorners) corner = parts[0];
190
+ else size = parts[0];
191
+ } else if (parts.length === 2) {
192
+ [corner, size] = parts;
193
+ }
194
+ const corners = radiusCorners[corner];
195
+ const radius =
196
+ size.startsWith("[") ? (parseLength(size) as number | undefined) : radiusSizes[size];
197
+ if (corners === undefined || radius === undefined) return undefined;
198
+ if (corner === "") return { borderRadius: radius };
199
+ const style: Style = {};
200
+ for (const key of corners) style[key] = radius;
201
+ return style;
202
+ }
203
+
204
+ const dash = body.indexOf("-");
205
+ if (dash <= 0) return undefined;
206
+
207
+ // Longest matching length-utility prefix (handles gap-x-4 vs gap-4, min-w-*).
208
+ for (const prefix of Object.keys(lengthKeys).sort((a, b) => b.length - a.length)) {
209
+ if (body.startsWith(prefix + "-")) {
210
+ const value = parseLength(body.slice(prefix.length + 1));
211
+ if (value === undefined) break;
212
+ const style: Style = {};
213
+ for (const key of lengthKeys[prefix]) style[key] = negate(value);
214
+ return style;
215
+ }
216
+ }
217
+
218
+ const prefix = body.slice(0, dash);
219
+ const rest = body.slice(dash + 1);
220
+
221
+ switch (prefix) {
222
+ case "bg": {
223
+ const color = resolveColor(rest);
224
+ return color !== undefined ? { backgroundColor: color } : undefined;
225
+ }
226
+ case "text": {
227
+ if (rest in textSizes) return { fontSize: textSizes[rest] };
228
+ if (rest.startsWith("[") && !rest.startsWith("[#")) {
229
+ const size = parseLength(rest);
230
+ if (typeof size === "number") return { fontSize: size };
231
+ }
232
+ const color = resolveColor(rest);
233
+ return color !== undefined ? { color } : undefined;
234
+ }
235
+ case "border": {
236
+ const n = Number(rest);
237
+ if (Number.isFinite(n)) return { borderWidth: n };
238
+ const color = resolveColor(rest);
239
+ return color !== undefined ? { borderColor: color } : undefined;
240
+ }
241
+ case "opacity": {
242
+ const n = Number(rest);
243
+ return Number.isFinite(n) ? { opacity: n / 100 } : undefined;
244
+ }
245
+ case "leading": {
246
+ const n = Number(rest);
247
+ return Number.isFinite(n) ? { lineHeight: n * 4 } : undefined;
248
+ }
249
+ case "flex": {
250
+ const n = Number(rest);
251
+ return Number.isFinite(n) ? { flex: n } : undefined;
252
+ }
253
+ default:
254
+ return undefined;
255
+ }
256
+ }
257
+
258
+ const cache = new Map<string, ResolvedClasses>();
259
+
260
+ export function tw(classes: string): ResolvedClasses {
261
+ const cached = cache.get(classes);
262
+ if (cached) return cached;
263
+
264
+ const result: ResolvedClasses = { style: {} };
265
+
266
+ for (const cls of classes.split(/\s+/)) {
267
+ if (cls === "") continue;
268
+
269
+ let bucket: "style" | "hoverStyle" | "activeStyle" | "focusStyle" = "style";
270
+ let body = cls;
271
+
272
+ if (body.startsWith("hover:")) [bucket, body] = ["hoverStyle", body.slice(6)];
273
+ else if (body.startsWith("active:")) [bucket, body] = ["activeStyle", body.slice(7)];
274
+ else if (body.startsWith("focus:")) [bucket, body] = ["focusStyle", body.slice(6)];
275
+
276
+ const resolved = resolveClass(body);
277
+
278
+ if (resolved === undefined) {
279
+ warnUnknown(cls);
280
+ continue;
281
+ }
282
+
283
+ result[bucket] = Object.assign(result[bucket] ?? {}, resolved);
284
+ }
285
+
286
+ cache.set(classes, result);
287
+ return result;
288
+ }