@decocms/start 0.31.1 → 0.32.1

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.
@@ -0,0 +1,518 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Tailwind Lint Script — @decocms/start
4
+ *
5
+ * Detects and auto-fixes Tailwind v3 → v4 migration issues:
6
+ *
7
+ * 1. Responsive classes in wrong order (v4 CSS cascade issue)
8
+ * 2. Arbitrary values with native Tailwind equivalents (px-[16px] → px-4)
9
+ * 3. Deprecated/renamed classes (v3→v4 + DaisyUI v4→v5)
10
+ *
11
+ * Usage (from site root):
12
+ * npx tsx node_modules/@decocms/start/scripts/tailwind-lint.ts # scan src/
13
+ * npx tsx node_modules/@decocms/start/scripts/tailwind-lint.ts --fix # auto-fix
14
+ * npx tsx node_modules/@decocms/start/scripts/tailwind-lint.ts src/sections # scan specific dir
15
+ *
16
+ * Also works on pre-migration code (detects class= in addition to className=)
17
+ */
18
+
19
+ import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
20
+ import { join, relative } from "node:path";
21
+
22
+ // ── Breakpoint order (mobile-first) ─────────────────────────────
23
+ const BREAKPOINT_ORDER = ["sm", "md", "lg", "xl", "2xl"] as const;
24
+ const BP_INDEX: Record<string, number> = {};
25
+ BREAKPOINT_ORDER.forEach((bp, i) => {
26
+ BP_INDEX[bp] = i + 1;
27
+ });
28
+
29
+ // ── Tailwind v3 → v4 class renames ──────────────────────────────
30
+ const CLASS_RENAMES: Record<string, string> = {
31
+ "flex-grow-0": "grow-0",
32
+ "flex-grow": "grow",
33
+ "flex-shrink-0": "shrink-0",
34
+ "flex-shrink": "shrink",
35
+ "overflow-ellipsis": "text-ellipsis",
36
+ "decoration-clone": "box-decoration-clone",
37
+ "decoration-slice": "box-decoration-slice",
38
+ "transform": "",
39
+ "transform-gpu": "",
40
+ "filter": "",
41
+ "backdrop-filter": "",
42
+ "ring": "ring-3",
43
+ };
44
+
45
+ // ── DaisyUI v4 → v5 class renames ──────────────────────────────
46
+ const DAISYUI_RENAMES: Record<string, string> = {
47
+ "badge-ghost": "badge-soft",
48
+ "card-compact": "card-sm",
49
+ };
50
+
51
+ // ── Spacing scale ───────────────────────────────────────────────
52
+ const PX_TO_SPACING: Record<number, string> = {};
53
+ for (let i = 0; i <= 96; i++) {
54
+ PX_TO_SPACING[i * 4] = String(i);
55
+ }
56
+ PX_TO_SPACING[2] = "0.5";
57
+ PX_TO_SPACING[6] = "1.5";
58
+ PX_TO_SPACING[10] = "2.5";
59
+ PX_TO_SPACING[14] = "3.5";
60
+
61
+ const TEXT_SIZE_MAP: Record<string, string> = {
62
+ "12": "xs", "14": "sm", "16": "base", "18": "lg", "20": "xl",
63
+ "24": "2xl", "30": "3xl", "36": "4xl", "48": "5xl", "60": "6xl",
64
+ "72": "7xl", "96": "8xl", "128": "9xl",
65
+ };
66
+
67
+ const SPACING_PROPS = new Set([
68
+ "p", "px", "py", "pt", "pb", "pl", "pr",
69
+ "m", "mx", "my", "mt", "mb", "ml", "mr",
70
+ "gap", "gap-x", "gap-y", "space-x", "space-y",
71
+ "w", "h", "min-w", "min-h", "max-w", "max-h",
72
+ "top", "right", "bottom", "left", "inset", "inset-x", "inset-y",
73
+ "rounded", "rounded-t", "rounded-b", "rounded-l", "rounded-r",
74
+ "rounded-tl", "rounded-tr", "rounded-bl", "rounded-br",
75
+ "border", "border-t", "border-b", "border-l", "border-r",
76
+ "text",
77
+ ]);
78
+
79
+ // ── CSS category ────────────────────────────────────────────────
80
+ const TEXT_SIZE_VALUES = new Set([
81
+ "xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl",
82
+ "7xl", "8xl", "9xl",
83
+ ]);
84
+ const TEXT_ALIGN_VALUES = new Set([
85
+ "left", "center", "right", "justify", "start", "end",
86
+ ]);
87
+
88
+ function getCssCategory(prop: string, value: string): string {
89
+ if (prop === "text" || prop === "-text") {
90
+ if (TEXT_SIZE_VALUES.has(value) || /^\[\d/.test(value)) return "text-size";
91
+ if (TEXT_ALIGN_VALUES.has(value)) return "text-align";
92
+ return "text-color";
93
+ }
94
+ if (prop === "flex") {
95
+ if (value === "") return "display";
96
+ if (["1", "auto", "initial", "none"].includes(value)) return "flex-grow";
97
+ if (["row", "col", "row-reverse", "col-reverse"].includes(value)) return "flex-direction";
98
+ if (["wrap", "nowrap", "wrap-reverse"].includes(value)) return "flex-wrap";
99
+ return "flex";
100
+ }
101
+ if (prop === "font") {
102
+ if (["bold", "semibold", "medium", "normal", "light", "thin", "extrabold", "black", "extralight"].includes(value)) return "font-weight";
103
+ return "font-family";
104
+ }
105
+ return prop;
106
+ }
107
+
108
+ // ── Types ────────────────────────────────────────────────────────
109
+ interface Issue {
110
+ file: string;
111
+ line: number;
112
+ type: "order" | "arbitrary" | "rename";
113
+ message: string;
114
+ original: string;
115
+ suggestion?: string;
116
+ }
117
+
118
+ interface ParsedClass {
119
+ raw: string;
120
+ modifiers: string[];
121
+ bpIndex: number;
122
+ property: string;
123
+ value: string;
124
+ cssCategory: string;
125
+ }
126
+
127
+ function parseClass(cls: string): ParsedClass {
128
+ const parts = cls.split(":");
129
+ const utility = parts.pop()!;
130
+ const modifiers = parts;
131
+ let bpIndex = 0;
132
+ for (const mod of modifiers) {
133
+ if (BP_INDEX[mod] !== undefined && BP_INDEX[mod] > bpIndex) {
134
+ bpIndex = BP_INDEX[mod];
135
+ }
136
+ }
137
+ const negMatch = utility.match(/^(-?)(.+)-(.+)$/);
138
+ let property = utility;
139
+ let value = "";
140
+ if (negMatch) {
141
+ property = negMatch[1] + negMatch[2];
142
+ value = negMatch[3];
143
+ }
144
+ return { raw: cls, modifiers, bpIndex, property, value, cssCategory: getCssCategory(property, value) };
145
+ }
146
+
147
+ function extractClassStrings(source: string): { classes: string; line: number }[] {
148
+ const results: { classes: string; line: number }[] = [];
149
+ const patterns = [
150
+ /className\s*=\s*"([^"]+)"/g,
151
+ /className\s*=\s*{`([^`]+)`}/g,
152
+ /className\s*=\s*{\s*"([^"]+)"\s*}/g,
153
+ /class\s*=\s*"([^"]+)"/g,
154
+ ];
155
+ const lines = source.split("\n");
156
+ for (const pattern of patterns) {
157
+ let match: RegExpExecArray | null;
158
+ while ((match = pattern.exec(source)) !== null) {
159
+ const offset = match.index;
160
+ let line = 1;
161
+ let counted = 0;
162
+ for (let i = 0; i < lines.length; i++) {
163
+ counted += lines[i].length + 1;
164
+ if (counted > offset) { line = i + 1; break; }
165
+ }
166
+ results.push({ classes: match[1], line });
167
+ }
168
+ }
169
+ return results;
170
+ }
171
+
172
+ function checkResponsiveOrder(classes: string): Issue[] {
173
+ const issues: Issue[] = [];
174
+ const classList = classes.split(/\s+/).filter(Boolean);
175
+ const propGroups: Record<string, (ParsedClass & { idx: number })[]> = {};
176
+ for (let i = 0; i < classList.length; i++) {
177
+ const parsed = parseClass(classList[i]);
178
+ const key = parsed.cssCategory;
179
+ if (!propGroups[key]) propGroups[key] = [];
180
+ propGroups[key].push({ ...parsed, idx: i });
181
+ }
182
+ for (const group of Object.values(propGroups)) {
183
+ if (group.length < 2) continue;
184
+ for (let i = 0; i < group.length; i++) {
185
+ for (let j = i + 1; j < group.length; j++) {
186
+ const a = group[i];
187
+ const b = group[j];
188
+ if (a.idx < b.idx && a.bpIndex > b.bpIndex) {
189
+ issues.push({
190
+ file: "", line: 0, type: "order",
191
+ message: `Order: \`${a.raw}\` (bp=${a.bpIndex}) before \`${b.raw}\` (bp=${b.bpIndex}) — ${b.raw} will override in v4`,
192
+ original: classes,
193
+ });
194
+ }
195
+ if (b.idx < a.idx && b.bpIndex > a.bpIndex) {
196
+ issues.push({
197
+ file: "", line: 0, type: "order",
198
+ message: `Order: \`${b.raw}\` (bp=${b.bpIndex}) before \`${a.raw}\` (bp=${a.bpIndex}) — ${a.raw} will override in v4`,
199
+ original: classes,
200
+ });
201
+ }
202
+ }
203
+ }
204
+ }
205
+ return issues;
206
+ }
207
+
208
+ function scanFile(filePath: string): Issue[] {
209
+ const issues: Issue[] = [];
210
+ const source = readFileSync(filePath, "utf-8");
211
+ const classStrings = extractClassStrings(source);
212
+ for (const { classes, line } of classStrings) {
213
+ const classList = classes.split(/\s+/).filter(Boolean);
214
+
215
+ const orderIssues = checkResponsiveOrder(classes);
216
+ for (const oi of orderIssues) {
217
+ issues.push({ ...oi, file: filePath, line });
218
+ }
219
+
220
+ for (const cls of classList) {
221
+ if (!cls.includes("[")) continue;
222
+ const parsed = parseClass(cls);
223
+ const arbMatch = parsed.value.match(/^\[(-?\d+(?:\.\d+)?)(px|rem|%)?\]$/);
224
+ if (!arbMatch) continue;
225
+ const num = parseFloat(arbMatch[1]);
226
+ const unit = arbMatch[2] || "px";
227
+ const baseProp = parsed.property.replace(/^-/, "");
228
+
229
+ let suggestion: string | null = null;
230
+ if (baseProp === "text" && unit === "px" && TEXT_SIZE_MAP[String(num)]) {
231
+ suggestion = `text-${TEXT_SIZE_MAP[String(num)]}`;
232
+ } else if (SPACING_PROPS.has(baseProp)) {
233
+ const pxVal = unit === "px" ? num : unit === "rem" ? num * 16 : null;
234
+ if (pxVal !== null && PX_TO_SPACING[pxVal] !== undefined) {
235
+ suggestion = `${baseProp}-${PX_TO_SPACING[pxVal]}`;
236
+ }
237
+ }
238
+ if (suggestion) {
239
+ issues.push({
240
+ file: filePath, line, type: "arbitrary",
241
+ message: `${cls} → ${suggestion}`,
242
+ original: cls, suggestion,
243
+ });
244
+ }
245
+ }
246
+
247
+ for (const cls of classList) {
248
+ const parts = cls.split(":");
249
+ const utility = parts[parts.length - 1];
250
+ if (CLASS_RENAMES[utility] !== undefined) {
251
+ const renamed = CLASS_RENAMES[utility];
252
+ issues.push({
253
+ file: filePath, line, type: "rename",
254
+ message: renamed === "" ? `Remove deprecated: ${cls}` : `Rename: ${cls} → ${renamed}`,
255
+ original: cls, suggestion: renamed || undefined,
256
+ });
257
+ }
258
+ if (DAISYUI_RENAMES[utility] && DAISYUI_RENAMES[utility] !== utility) {
259
+ issues.push({
260
+ file: filePath, line, type: "rename",
261
+ message: `DaisyUI: ${cls} → ${DAISYUI_RENAMES[utility]}`,
262
+ original: cls, suggestion: DAISYUI_RENAMES[utility],
263
+ });
264
+ }
265
+ }
266
+ }
267
+ return issues;
268
+ }
269
+
270
+ // ── Fix functions ───────────────────────────────────────────────
271
+ function fixClassOrder(classes: string): string {
272
+ const classList = classes.split(/\s+/).filter(Boolean);
273
+ const parsed = classList.map((cls, i) => ({ ...parseClass(cls), idx: i }));
274
+ const groups: Record<string, typeof parsed> = {};
275
+ for (const p of parsed) {
276
+ if (!groups[p.cssCategory]) groups[p.cssCategory] = [];
277
+ groups[p.cssCategory].push(p);
278
+ }
279
+ const result = [...classList];
280
+ for (const group of Object.values(groups)) {
281
+ if (group.length < 2) continue;
282
+ const positions = group.map((g) => g.idx).sort((a, b) => a - b);
283
+ const sorted = [...group].sort((a, b) => a.bpIndex - b.bpIndex);
284
+ for (let i = 0; i < positions.length; i++) {
285
+ result[positions[i]] = sorted[i].raw;
286
+ }
287
+ }
288
+ return result.join(" ");
289
+ }
290
+
291
+ function fixClassName(classes: string): string {
292
+ let classList = classes.split(/\s+/).filter(Boolean);
293
+
294
+ classList = classList.map((cls) => {
295
+ const parts = cls.split(":");
296
+ const utility = parts.pop()!;
297
+ if (CLASS_RENAMES[utility] !== undefined) {
298
+ const renamed = CLASS_RENAMES[utility];
299
+ if (renamed === "") return "";
300
+ parts.push(renamed);
301
+ return parts.join(":");
302
+ }
303
+ if (DAISYUI_RENAMES[utility] && DAISYUI_RENAMES[utility] !== utility) {
304
+ parts.push(DAISYUI_RENAMES[utility]);
305
+ return parts.join(":");
306
+ }
307
+ return cls;
308
+ }).filter(Boolean);
309
+
310
+ classList = classList.map((cls) => {
311
+ if (!cls.includes("[")) return cls;
312
+ const parsed = parseClass(cls);
313
+ const arbMatch = parsed.value.match(/^\[(-?\d+(?:\.\d+)?)(px|rem|%)?\]$/);
314
+ if (!arbMatch) {
315
+ if (parsed.value === "[100%]" && (parsed.property === "w" || parsed.property === "h")) {
316
+ const prefix = parsed.modifiers.length ? parsed.modifiers.join(":") + ":" : "";
317
+ return `${prefix}${parsed.property}-full`;
318
+ }
319
+ if (parsed.value === "[auto]" && (parsed.property === "w" || parsed.property === "h")) {
320
+ const prefix = parsed.modifiers.length ? parsed.modifiers.join(":") + ":" : "";
321
+ return `${prefix}${parsed.property}-auto`;
322
+ }
323
+ return cls;
324
+ }
325
+ const num = parseFloat(arbMatch[1]);
326
+ const unit = arbMatch[2] || "px";
327
+ const baseProp = parsed.property.replace(/^-/, "");
328
+ const isNeg = parsed.property.startsWith("-");
329
+ const prefix = parsed.modifiers.length ? parsed.modifiers.join(":") + ":" : "";
330
+ const negPrefix = isNeg ? "-" : "";
331
+
332
+ if (baseProp === "text" && unit === "px" && TEXT_SIZE_MAP[String(num)]) {
333
+ return `${prefix}text-${TEXT_SIZE_MAP[String(num)]}`;
334
+ }
335
+ if (SPACING_PROPS.has(baseProp)) {
336
+ const pxVal = unit === "px" ? num : unit === "rem" ? num * 16 : null;
337
+ if (pxVal !== null && PX_TO_SPACING[pxVal] !== undefined) {
338
+ return `${prefix}${negPrefix}${baseProp}-${PX_TO_SPACING[pxVal]}`;
339
+ }
340
+ }
341
+ return cls;
342
+ });
343
+
344
+ return fixClassOrder(classList.join(" "));
345
+ }
346
+
347
+ function fixFile(filePath: string): { changed: boolean; fixes: number } {
348
+ let source = readFileSync(filePath, "utf-8");
349
+ let fixes = 0;
350
+ const patterns = [
351
+ /(?<=className\s*=\s*")([^"]+)(?=")/g,
352
+ /(?<=className\s*=\s*{`)([^`]+)(?=`})/g,
353
+ /(?<=className\s*=\s*{\s*")([^"]+)(?="\s*})/g,
354
+ /(?<=class\s*=\s*")([^"]+)(?=")/g,
355
+ ];
356
+ for (const pattern of patterns) {
357
+ source = source.replace(pattern, (match) => {
358
+ if (match.includes("\n")) {
359
+ const lines = match.split("\n");
360
+ const fixedLines = lines.map((line) => {
361
+ const trimmed = line.trim();
362
+ if (!trimmed) return line;
363
+ const indent = line.match(/^(\s*)/)?.[1] ?? "";
364
+ const fixed = fixClassName(trimmed);
365
+ if (fixed !== trimmed) fixes++;
366
+ return indent + fixed;
367
+ });
368
+ return fixedLines.join("\n");
369
+ }
370
+ const fixed = fixClassName(match);
371
+ if (fixed !== match) fixes++;
372
+ return fixed;
373
+ });
374
+ }
375
+ if (fixes > 0) writeFileSync(filePath, source, "utf-8");
376
+ return { changed: fixes > 0, fixes };
377
+ }
378
+
379
+ function walkDir(dir: string): string[] {
380
+ const files: string[] = [];
381
+ for (const entry of readdirSync(dir)) {
382
+ if (entry.startsWith(".") || entry === "node_modules") continue;
383
+ const full = join(dir, entry);
384
+ const stat = statSync(full);
385
+ if (stat.isDirectory()) files.push(...walkDir(full));
386
+ else if (/\.(tsx|jsx|ts|js)$/.test(full)) files.push(full);
387
+ }
388
+ return files;
389
+ }
390
+
391
+ // ── Main ─────────────────────────────────────────────────────────
392
+ const args = process.argv.slice(2);
393
+ const doFix = args.includes("--fix");
394
+ const scanPaths = args.filter((a) => !a.startsWith("--"));
395
+ const root = process.cwd();
396
+
397
+ const dirs = scanPaths.length > 0
398
+ ? scanPaths.map((p) => join(root, p))
399
+ : [join(root, "src"), join(root, "sections"), join(root, "islands"), join(root, "components")];
400
+
401
+ const allFiles: string[] = [];
402
+ for (const dir of dirs) {
403
+ try {
404
+ const stat = statSync(dir);
405
+ if (stat.isDirectory()) allFiles.push(...walkDir(dir));
406
+ else allFiles.push(dir);
407
+ } catch {
408
+ // dir doesn't exist, skip
409
+ }
410
+ }
411
+
412
+ if (allFiles.length === 0) {
413
+ console.log("No files found to scan.");
414
+ process.exit(0);
415
+ }
416
+
417
+ if (doFix) {
418
+ let totalFixes = 0;
419
+ let totalFiles = 0;
420
+ for (const file of allFiles) {
421
+ const { changed, fixes } = fixFile(file);
422
+ if (changed) {
423
+ totalFiles++;
424
+ totalFixes += fixes;
425
+ console.log(` ✅ ${relative(root, file)} (${fixes} fixes)`);
426
+ }
427
+ }
428
+ if (totalFixes === 0) {
429
+ console.log("\n✅ Nothing to fix!");
430
+ } else {
431
+ console.log(`\n🔧 Fixed ${totalFixes} classNames across ${totalFiles} files`);
432
+ console.log(" Run without --fix to verify.\n");
433
+ }
434
+ process.exit(0);
435
+ }
436
+
437
+ let allIssues: Issue[] = [];
438
+ for (const file of allFiles) {
439
+ allIssues.push(...scanFile(file));
440
+ }
441
+
442
+ if (allIssues.length === 0) {
443
+ console.log("✅ No Tailwind issues found!");
444
+ process.exit(0);
445
+ }
446
+
447
+ const orderIssues = allIssues.filter((i) => i.type === "order");
448
+ const arbIssues = allIssues.filter((i) => i.type === "arbitrary");
449
+ const renameIssues = allIssues.filter((i) => i.type === "rename");
450
+
451
+ console.log(`\n🔍 Found ${allIssues.length} issues (${orderIssues.length} order, ${arbIssues.length} arbitrary, ${renameIssues.length} rename)\n`);
452
+
453
+ if (orderIssues.length > 0) {
454
+ console.log("━".repeat(80));
455
+ console.log("📐 RESPONSIVE ORDER ISSUES (will cause wrong CSS in Tailwind v4)");
456
+ console.log("━".repeat(80));
457
+ console.log(" In v4, base classes MUST come before responsive modifiers:");
458
+ console.log(" ✅ px-4 md:px-6 xl:px-0");
459
+ console.log(" ❌ md:px-6 px-4 (px-4 will override md:px-6)\n");
460
+ const seen = new Set<string>();
461
+ for (const issue of orderIssues) {
462
+ const key = `${issue.file}:${issue.line}:${issue.message}`;
463
+ if (seen.has(key)) continue;
464
+ seen.add(key);
465
+ console.log(` ${relative(root, issue.file)}:${issue.line}`);
466
+ console.log(` ❌ ${issue.message}\n`);
467
+ }
468
+ }
469
+
470
+ if (renameIssues.length > 0) {
471
+ console.log("━".repeat(80));
472
+ console.log("🔄 RENAMED/DEPRECATED CLASSES (v3→v4 + DaisyUI v4→v5)");
473
+ console.log("━".repeat(80) + "\n");
474
+ const seen = new Set<string>();
475
+ for (const issue of renameIssues) {
476
+ const key = `${issue.file}:${issue.line}:${issue.original}`;
477
+ if (seen.has(key)) continue;
478
+ seen.add(key);
479
+ console.log(` ${relative(root, issue.file)}:${issue.line}`);
480
+ console.log(` 🔄 ${issue.message}\n`);
481
+ }
482
+ }
483
+
484
+ if (arbIssues.length > 0) {
485
+ console.log("━".repeat(80));
486
+ console.log("💡 ARBITRARY VALUES WITH NATIVE EQUIVALENTS");
487
+ console.log("━".repeat(80) + "\n");
488
+ const seen = new Set<string>();
489
+ for (const issue of arbIssues) {
490
+ const key = `${issue.file}:${issue.line}:${issue.original}`;
491
+ if (seen.has(key)) continue;
492
+ seen.add(key);
493
+ console.log(` ${relative(root, issue.file)}:${issue.line}`);
494
+ console.log(` 💡 ${issue.message}\n`);
495
+ }
496
+ }
497
+
498
+ console.log("━".repeat(80));
499
+ console.log("📊 SUMMARY");
500
+ console.log("━".repeat(80));
501
+ const fileStats: Record<string, { order: number; arbitrary: number; rename: number }> = {};
502
+ for (const issue of allIssues) {
503
+ const rel = relative(root, issue.file);
504
+ if (!fileStats[rel]) fileStats[rel] = { order: 0, arbitrary: 0, rename: 0 };
505
+ fileStats[rel][issue.type]++;
506
+ }
507
+ console.log(`\n ${"File".padEnd(50)} ${"Order".padStart(6)} ${"Arb.".padStart(6)} ${"Rename".padStart(7)}`);
508
+ console.log(` ${"─".repeat(50)} ${"─".repeat(6)} ${"─".repeat(6)} ${"─".repeat(7)}`);
509
+ for (const [file, stats] of Object.entries(fileStats).sort()) {
510
+ const o = stats.order > 0 ? `${stats.order}` : "-";
511
+ const a = stats.arbitrary > 0 ? `${stats.arbitrary}` : "-";
512
+ const r = stats.rename > 0 ? `${stats.rename}` : "-";
513
+ console.log(` ${file.padEnd(50)} ${o.padStart(6)} ${a.padStart(6)} ${r.padStart(7)}`);
514
+ }
515
+ console.log(`\n Total: ${orderIssues.length} order, ${arbIssues.length} arbitrary, ${renameIssues.length} rename`);
516
+ console.log(` Run with --fix to auto-fix all issues.\n`);
517
+
518
+ process.exit(1);
@@ -11,3 +11,4 @@ export type RichText = string;
11
11
  export type Secret = string;
12
12
  export type Color = string;
13
13
  export type ButtonWidget = string;
14
+ export type TextArea = string;