@ttsc/playground 0.18.4 → 0.19.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.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/lib/src/compiler/createWorkerCompiler.js +7 -191
  3. package/lib/src/compiler/createWorkerCompiler.js.map +1 -1
  4. package/lib/src/compiler/internal/createWorkerCompilerService.d.ts +18 -0
  5. package/lib/src/compiler/internal/createWorkerCompilerService.js +272 -0
  6. package/lib/src/compiler/internal/createWorkerCompilerService.js.map +1 -0
  7. package/lib/src/npm/collectExternalPackageNames.js +253 -9
  8. package/lib/src/npm/collectExternalPackageNames.js.map +1 -1
  9. package/lib/src/react/ConsoleViewer.js +17 -17
  10. package/lib/src/react/ConsoleViewer.js.map +1 -1
  11. package/lib/src/react/DependencyProgressModal.js +1 -1
  12. package/lib/src/react/DependencyProgressModal.js.map +1 -1
  13. package/lib/src/react/DiagnosticsPanel.js +2 -2
  14. package/lib/src/react/DiagnosticsPanel.js.map +1 -1
  15. package/lib/src/react/ExamplePicker.js +2 -2
  16. package/lib/src/react/ExamplePicker.js.map +1 -1
  17. package/lib/src/react/LintPane.js +3 -3
  18. package/lib/src/react/LintPane.js.map +1 -1
  19. package/lib/src/react/OptionsPanel.js +1 -1
  20. package/lib/src/react/OptionsPanel.js.map +1 -1
  21. package/lib/src/react/PlaygroundShell.js +14 -13
  22. package/lib/src/react/PlaygroundShell.js.map +1 -1
  23. package/lib/src/react/ResultViewer.d.ts +2 -2
  24. package/lib/src/react/ResultViewer.js +3 -3
  25. package/lib/src/react/ResultViewer.js.map +1 -1
  26. package/lib/src/react/SourceEditor.js +1 -1
  27. package/lib/src/react/SourceEditor.js.map +1 -1
  28. package/lib/src/sandbox/createSandboxRequire.js +34 -9
  29. package/lib/src/sandbox/createSandboxRequire.js.map +1 -1
  30. package/package.json +3 -3
  31. package/src/compiler/createWorkerCompiler.ts +8 -236
  32. package/src/compiler/internal/createWorkerCompilerService.ts +358 -0
  33. package/src/npm/collectExternalPackageNames.ts +262 -9
  34. package/src/react/ConsoleViewer.tsx +18 -18
  35. package/src/react/DependencyProgressModal.tsx +11 -11
  36. package/src/react/DiagnosticsPanel.tsx +11 -11
  37. package/src/react/ExamplePicker.tsx +7 -7
  38. package/src/react/LintPane.tsx +5 -5
  39. package/src/react/OptionsPanel.tsx +10 -10
  40. package/src/react/PlaygroundShell.tsx +64 -74
  41. package/src/react/ResultViewer.tsx +4 -4
  42. package/src/react/SourceEditor.tsx +2 -3
  43. package/src/sandbox/createSandboxRequire.ts +32 -9
@@ -1,9 +1,6 @@
1
1
  import { BUILT_IN_PLAYGROUND_PACKAGES } from "./BUILT_IN_PLAYGROUND_PACKAGES";
2
2
  import { packageNameFromSpecifier } from "./packageNameFromSpecifier";
3
3
 
4
- const MODULE_SPECIFIER_REGEXP =
5
- /\b(?:import|export)\s+(?:type\s+)?(?:[^"'()]*?\s+from\s*)?["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)|require\s*\(\s*["']([^"']+)["']\s*\)/g;
6
-
7
4
  /**
8
5
  * Scan `source` for `import` / `require` specifiers and return the unique
9
6
  * sorted list of bare npm package names that are not in `ignoredPackages`.
@@ -21,14 +18,270 @@ export function collectExternalPackageNames(
21
18
  return [...found].sort();
22
19
  }
23
20
 
21
+ /**
22
+ * Collect the string specifiers of every executable module-loading construct in
23
+ * `source`: `import`/`export ... from`, side-effect `import "x"`, dynamic
24
+ * `import("x")`, and `require("x")` calls.
25
+ *
26
+ * The scan tokenizes `source` first so import/export/require lookalikes that
27
+ * live inside comments, string or template contents, or regular-expression
28
+ * literals never become package requests — only real code-level string
29
+ * arguments are returned. Specifiers that cannot be resolved statically (a
30
+ * template-literal or computed argument) are intentionally skipped so inert
31
+ * text cannot drive a network install.
32
+ */
24
33
  function collectModuleSpecifiers(source: string): string[] {
34
+ const tokens = tokenize(source);
25
35
  const out: string[] = [];
26
- MODULE_SPECIFIER_REGEXP.lastIndex = 0;
27
- for (;;) {
28
- const match = MODULE_SPECIFIER_REGEXP.exec(source);
29
- if (!match) break;
30
- const specifier = match[1] ?? match[2] ?? match[3];
31
- if (specifier) out.push(specifier);
36
+ const asString = (token: Token | undefined): string | null =>
37
+ token && token.kind === "string" ? token.value : null;
38
+ const isOpenParen = (token: Token | undefined): boolean =>
39
+ token !== undefined && token.kind === "punct" && token.value === "(";
40
+ const isMemberAccess = (token: Token | undefined): boolean =>
41
+ token !== undefined && token.kind === "punct" && token.value === ".";
42
+
43
+ for (let i = 0; i < tokens.length; i++) {
44
+ const token = tokens[i];
45
+ if (!token || token.kind !== "word") continue;
46
+
47
+ if (token.value === "require") {
48
+ // `obj.require(...)` is an unrelated method call, not CommonJS require.
49
+ if (isMemberAccess(tokens[i - 1])) continue;
50
+ if (isOpenParen(tokens[i + 1])) {
51
+ const spec = asString(tokens[i + 2]);
52
+ if (spec !== null) out.push(spec);
53
+ }
54
+ continue;
55
+ }
56
+
57
+ if (token.value === "import" || token.value === "export") {
58
+ // `foo.import(...)` / `import.meta` are not module-loading imports.
59
+ if (token.value === "import" && isMemberAccess(tokens[i - 1])) continue;
60
+ // Dynamic `import("x")`.
61
+ if (token.value === "import" && isOpenParen(tokens[i + 1])) {
62
+ const spec = asString(tokens[i + 2]);
63
+ if (spec !== null) out.push(spec);
64
+ continue;
65
+ }
66
+ // Side-effect `import "x"`.
67
+ if (token.value === "import") {
68
+ const bare = asString(tokens[i + 1]);
69
+ if (bare !== null) {
70
+ out.push(bare);
71
+ continue;
72
+ }
73
+ }
74
+ // `import ... from "x"` / `export ... from "x"`.
75
+ const spec = findFromSpecifier(tokens, i + 1);
76
+ if (spec !== null) out.push(spec);
77
+ }
32
78
  }
33
79
  return out;
34
80
  }
81
+
82
+ /**
83
+ * From token index `start`, find the specifier of a `... from "x"` clause,
84
+ * bounded to the current statement. Stops at a `;` terminator or the start of
85
+ * another `import`/`export` so a local `export const x = ...` never borrows a
86
+ * later statement's `from`.
87
+ */
88
+ function findFromSpecifier(tokens: Token[], start: number): string | null {
89
+ for (let i = start; i < tokens.length; i++) {
90
+ const token = tokens[i];
91
+ if (!token) break;
92
+ if (token.kind === "punct" && token.value === ";") return null;
93
+ if (
94
+ token.kind === "word" &&
95
+ (token.value === "import" || token.value === "export")
96
+ )
97
+ return null;
98
+ if (token.kind === "word" && token.value === "from") {
99
+ const next = tokens[i + 1];
100
+ return next && next.kind === "string" ? next.value : null;
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+
106
+ type Token =
107
+ // An identifier or keyword.
108
+ | { kind: "word"; value: string }
109
+ // A single- or double-quoted string literal, with escapes decoded to their
110
+ // literal characters so a specifier survives unchanged.
111
+ | { kind: "string"; value: string }
112
+ // A single punctuation character.
113
+ | { kind: "punct"; value: string }
114
+ // An opaque value token — number, template literal, or regular-expression
115
+ // literal — whose contents can never be a static specifier.
116
+ | { kind: "other" };
117
+
118
+ // Keywords after which a `/` begins a regular-expression literal rather than a
119
+ // division operator. After any other word (an identifier or value keyword such
120
+ // as `this`), `/` is division.
121
+ const REGEX_PRECEDING_KEYWORDS = new Set([
122
+ "return",
123
+ "typeof",
124
+ "instanceof",
125
+ "in",
126
+ "of",
127
+ "new",
128
+ "delete",
129
+ "void",
130
+ "do",
131
+ "else",
132
+ "yield",
133
+ "await",
134
+ "case",
135
+ "throw",
136
+ ]);
137
+
138
+ /**
139
+ * Lexically tokenize `source` into the coarse token stream the specifier
140
+ * collector needs. Comments are dropped; strings, templates, regex literals,
141
+ * and numbers become single tokens so their contents cannot leak into the
142
+ * grammar match.
143
+ */
144
+ function tokenize(source: string): Token[] {
145
+ const tokens: Token[] = [];
146
+ const n = source.length;
147
+ const isIdStart = (c: string): boolean =>
148
+ (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || c === "_" || c === "$";
149
+ const isIdPart = (c: string): boolean =>
150
+ isIdStart(c) || (c >= "0" && c <= "9");
151
+ const isDigit = (c: string): boolean => c >= "0" && c <= "9";
152
+
153
+ // A `/` opens a regex only in operator/statement position — never right after
154
+ // a value (identifier, number, string, template, regex, `)` or `]`).
155
+ const regexAllowed = (): boolean => {
156
+ const prev = tokens[tokens.length - 1];
157
+ if (!prev) return true;
158
+ if (prev.kind === "string" || prev.kind === "other") return false;
159
+ if (prev.kind === "word") return REGEX_PRECEDING_KEYWORDS.has(prev.value);
160
+ return prev.value !== ")" && prev.value !== "]";
161
+ };
162
+
163
+ let i = 0;
164
+ while (i < n) {
165
+ const c = source[i]!;
166
+ // Whitespace.
167
+ if (
168
+ c === " " ||
169
+ c === "\t" ||
170
+ c === "\r" ||
171
+ c === "\n" ||
172
+ c === "\f" ||
173
+ c === "\v"
174
+ ) {
175
+ i++;
176
+ continue;
177
+ }
178
+ // Line comment.
179
+ if (c === "/" && source[i + 1] === "/") {
180
+ i += 2;
181
+ while (i < n && source[i] !== "\n") i++;
182
+ continue;
183
+ }
184
+ // Block comment.
185
+ if (c === "/" && source[i + 1] === "*") {
186
+ i += 2;
187
+ while (i < n && !(source[i] === "*" && source[i + 1] === "/")) i++;
188
+ i += 2;
189
+ continue;
190
+ }
191
+ // Regular-expression literal.
192
+ if (c === "/" && regexAllowed()) {
193
+ i++;
194
+ let inClass = false;
195
+ while (i < n) {
196
+ const d = source[i];
197
+ if (d === "\\") {
198
+ i += 2;
199
+ continue;
200
+ }
201
+ if (d === "\n") break;
202
+ if (d === "[") inClass = true;
203
+ else if (d === "]") inClass = false;
204
+ else if (d === "/" && !inClass) {
205
+ i++;
206
+ break;
207
+ }
208
+ i++;
209
+ }
210
+ while (i < n && isIdPart(source[i]!)) i++; // flags
211
+ tokens.push({ kind: "other" });
212
+ continue;
213
+ }
214
+ // String literal.
215
+ if (c === '"' || c === "'") {
216
+ i++;
217
+ let value = "";
218
+ while (i < n) {
219
+ const d = source[i]!;
220
+ if (d === "\\") {
221
+ value += source[i + 1] ?? "";
222
+ i += 2;
223
+ continue;
224
+ }
225
+ if (d === c) {
226
+ i++;
227
+ break;
228
+ }
229
+ if (d === "\n") break; // unterminated single-line string
230
+ value += d;
231
+ i++;
232
+ }
233
+ tokens.push({ kind: "string", value });
234
+ continue;
235
+ }
236
+ // Template literal. Contents (including `${...}` expressions) are treated as
237
+ // opaque: a template specifier is not statically resolvable.
238
+ if (c === "`") {
239
+ i++;
240
+ let depth = 0;
241
+ while (i < n) {
242
+ const d = source[i];
243
+ if (d === "\\") {
244
+ i += 2;
245
+ continue;
246
+ }
247
+ if (depth === 0 && d === "`") {
248
+ i++;
249
+ break;
250
+ }
251
+ if (d === "$" && source[i + 1] === "{") {
252
+ depth++;
253
+ i += 2;
254
+ continue;
255
+ }
256
+ if (depth > 0 && d === "}") {
257
+ depth--;
258
+ i++;
259
+ continue;
260
+ }
261
+ i++;
262
+ }
263
+ tokens.push({ kind: "other" });
264
+ continue;
265
+ }
266
+ // Identifier / keyword.
267
+ if (isIdStart(c)) {
268
+ let j = i + 1;
269
+ while (j < n && isIdPart(source[j]!)) j++;
270
+ tokens.push({ kind: "word", value: source.slice(i, j) });
271
+ i = j;
272
+ continue;
273
+ }
274
+ // Numeric literal.
275
+ if (isDigit(c) || (c === "." && isDigit(source[i + 1] ?? ""))) {
276
+ let j = i + 1;
277
+ while (j < n && /[0-9a-fA-FxXoObBeE._]/.test(source[j]!)) j++;
278
+ tokens.push({ kind: "other" });
279
+ i = j;
280
+ continue;
281
+ }
282
+ // Single punctuation character.
283
+ tokens.push({ kind: "punct", value: c });
284
+ i++;
285
+ }
286
+ return tokens;
287
+ }
@@ -15,7 +15,7 @@ export function ConsoleViewer({
15
15
  }: ConsoleViewerProps) {
16
16
  if (messages.length === 0)
17
17
  return (
18
- <div className="h-full w-full flex items-center justify-center text-neutral-600 font-mono text-[11px] px-4 text-center">
18
+ <div className="flex h-full w-full items-center justify-center px-4 text-center font-mono text-[11px] text-slate-400">
19
19
  {empty}
20
20
  </div>
21
21
  );
@@ -24,7 +24,7 @@ export function ConsoleViewer({
24
24
  {messages.map((msg, i) => (
25
25
  <div
26
26
  key={i}
27
- className="py-1 border-b border-neutral-900/70 last:border-b-0 flex gap-2"
27
+ className="flex gap-2 border-b border-[#d8e7f4] py-1 last:border-b-0"
28
28
  >
29
29
  <span
30
30
  className={`shrink-0 text-[10px] uppercase tracking-wider w-12 ${typeColor(
@@ -33,7 +33,7 @@ export function ConsoleViewer({
33
33
  >
34
34
  {msg.type}
35
35
  </span>
36
- <span className="flex-1 break-words whitespace-pre-wrap text-neutral-200">
36
+ <span className="flex-1 whitespace-pre-wrap break-words text-slate-700">
37
37
  {(Array.isArray(msg.value) ? msg.value : [msg.value]).map(
38
38
  (arg, idx) => (
39
39
  <span key={idx}>
@@ -52,33 +52,33 @@ export function ConsoleViewer({
52
52
  function typeColor(type: IConsoleMessage["type"]): string {
53
53
  switch (type) {
54
54
  case "error":
55
- return "text-red-400";
55
+ return "text-red-600";
56
56
  case "warn":
57
- return "text-yellow-400";
57
+ return "text-amber-600";
58
58
  case "info":
59
- return "text-sky-400";
59
+ return "text-sky-700";
60
60
  case "debug":
61
- return "text-fuchsia-400";
61
+ return "text-fuchsia-700";
62
62
  case "dir":
63
63
  case "table":
64
- return "text-cyan-400";
64
+ return "text-cyan-700";
65
65
  default:
66
- return "text-emerald-400";
66
+ return "text-emerald-700";
67
67
  }
68
68
  }
69
69
 
70
70
  function formatValue(value: unknown, depth = 0): JSX.Element {
71
71
  if (typeof value === "string")
72
- return <span className="text-amber-200">{JSON.stringify(value)}</span>;
72
+ return <span className="text-amber-700">{JSON.stringify(value)}</span>;
73
73
  if (typeof value === "number")
74
- return <span className="text-purple-300">{String(value)}</span>;
74
+ return <span className="text-purple-700">{String(value)}</span>;
75
75
  if (typeof value === "boolean")
76
- return <span className="text-sky-300">{String(value)}</span>;
77
- if (value === null) return <span className="text-neutral-500">null</span>;
76
+ return <span className="text-sky-700">{String(value)}</span>;
77
+ if (value === null) return <span className="text-slate-500">null</span>;
78
78
  if (value === undefined)
79
- return <span className="text-neutral-500">undefined</span>;
79
+ return <span className="text-slate-500">undefined</span>;
80
80
  if (typeof value === "function")
81
- return <span className="text-neutral-500">[Function]</span>;
81
+ return <span className="text-slate-500">[Function]</span>;
82
82
  if (Array.isArray(value))
83
83
  return (
84
84
  <span>
@@ -94,21 +94,21 @@ function formatValue(value: unknown, depth = 0): JSX.Element {
94
94
  );
95
95
  if (value instanceof Error)
96
96
  return (
97
- <span className="text-red-300">
97
+ <span className="text-red-700">
98
98
  {value.name}: {value.message}
99
99
  </span>
100
100
  );
101
101
  try {
102
102
  const entries = Object.entries(value as Record<string, unknown>);
103
103
  if (entries.length === 0) return <span>{"{}"}</span>;
104
- if (depth > 4) return <span className="text-neutral-500">[...]</span>;
104
+ if (depth > 4) return <span className="text-slate-500">[...]</span>;
105
105
  return (
106
106
  <span>
107
107
  {"{"}
108
108
  {entries.map(([k, v], idx) => (
109
109
  <span key={k}>
110
110
  {idx > 0 ? ", " : " "}
111
- <span className="text-blue-300">{k}</span>:{" "}
111
+ <span className="text-[#3178c6]">{k}</span>:{" "}
112
112
  {formatValue(v, depth + 1)}
113
113
  </span>
114
114
  ))}
@@ -20,34 +20,34 @@ export function DependencyProgressModal({
20
20
  : progress.packageName;
21
21
 
22
22
  return (
23
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 px-4">
24
- <div className="w-full max-w-md rounded-lg border border-neutral-700 bg-neutral-950 p-5 shadow-2xl">
23
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-[#102a43]/35 px-4 backdrop-blur-sm">
24
+ <div className="w-full max-w-md rounded-2xl border border-[#b9d5ee] bg-white p-5 shadow-[0_28px_70px_rgba(35,90,151,0.25)]">
25
25
  <div className="flex items-start justify-between gap-4">
26
26
  <div>
27
- <div className="text-[11px] font-mono uppercase text-blue-300">
27
+ <div className="font-mono text-[11px] uppercase text-[#3178c6]">
28
28
  Dependencies
29
29
  </div>
30
- <h2 className="mt-1 text-base font-mono text-white">
30
+ <h2 className="mt-1 font-mono text-base text-[#102a43]">
31
31
  Installing npm packages
32
32
  </h2>
33
33
  </div>
34
- <div className="text-[11px] font-mono text-neutral-500">
34
+ <div className="font-mono text-[11px] text-slate-500">
35
35
  {progress.completed}/{total}
36
36
  </div>
37
37
  </div>
38
38
 
39
- <div className="mt-4 h-2 overflow-hidden rounded-full bg-neutral-800">
39
+ <div className="mt-4 h-2 overflow-hidden rounded-full bg-[#e7f0f8]">
40
40
  <div
41
- className="h-full bg-blue-400 transition-[width]"
41
+ className="h-full bg-[#3178c6] transition-[width]"
42
42
  style={{ width: `${Math.max(8, ratio * 100)}%` }}
43
43
  />
44
44
  </div>
45
45
 
46
46
  <div className="mt-4 space-y-1 font-mono">
47
47
  {activePackage && (
48
- <div className="text-[12px] text-neutral-200">{activePackage}</div>
48
+ <div className="text-[12px] text-slate-700">{activePackage}</div>
49
49
  )}
50
- <div className="text-[11px] text-neutral-400">{progress.message}</div>
50
+ <div className="text-[11px] text-slate-500">{progress.message}</div>
51
51
  </div>
52
52
 
53
53
  {packages.length > 0 && (
@@ -55,13 +55,13 @@ export function DependencyProgressModal({
55
55
  {packages.slice(0, 8).map((name) => (
56
56
  <span
57
57
  key={name}
58
- className="rounded border border-neutral-800 bg-neutral-900 px-2 py-1 text-[10px] font-mono text-neutral-300"
58
+ className="rounded border border-[#d2e4f4] bg-[#f7fbff] px-2 py-1 font-mono text-[10px] text-slate-600"
59
59
  >
60
60
  {name}
61
61
  </span>
62
62
  ))}
63
63
  {packages.length > 8 && (
64
- <span className="rounded border border-neutral-800 bg-neutral-900 px-2 py-1 text-[10px] font-mono text-neutral-500">
64
+ <span className="rounded border border-[#d2e4f4] bg-[#f7fbff] px-2 py-1 font-mono text-[10px] text-slate-500">
65
65
  +{packages.length - 8}
66
66
  </span>
67
67
  )}
@@ -15,19 +15,19 @@ export function DiagnosticsPanel({
15
15
 
16
16
  if (diagnostics.length === 0)
17
17
  return (
18
- <div className="shrink-0 px-4 py-2 border-t border-neutral-800/70 bg-neutral-950 flex items-center gap-3">
18
+ <div className="flex shrink-0 items-center gap-3 border-t border-[#c7dff4] bg-[#eef6ff] px-4 py-2">
19
19
  <span className="text-emerald-400 text-xs">●</span>
20
- <span className="text-[12px] font-mono text-neutral-400">
20
+ <span className="font-mono text-[12px] text-slate-600">
21
21
  0 errors · 0 warnings
22
22
  </span>
23
23
  </div>
24
24
  );
25
25
 
26
26
  return (
27
- <div className="shrink-0 border-t border-neutral-800/70 bg-neutral-950">
27
+ <div className="shrink-0 border-t border-[#c7dff4] bg-[#eef6ff]">
28
28
  <button
29
29
  onClick={() => setExpanded((v) => !v)}
30
- className="w-full px-4 py-2 flex items-center gap-3 hover:bg-neutral-900/50 transition-colors text-left"
30
+ className="flex w-full items-center gap-3 px-4 py-2 text-left transition-colors hover:bg-[#e1effc]"
31
31
  >
32
32
  <span
33
33
  className={`text-xs ${
@@ -36,20 +36,20 @@ export function DiagnosticsPanel({
36
36
  >
37
37
 
38
38
  </span>
39
- <span className="text-[12px] font-mono text-neutral-300">
39
+ <span className="font-mono text-[12px] text-slate-700">
40
40
  {errorCount} error{errorCount === 1 ? "" : "s"} · {warnCount} warning
41
41
  {warnCount === 1 ? "" : "s"}
42
42
  </span>
43
- <span className="ml-auto text-[10px] font-mono text-neutral-600">
43
+ <span className="ml-auto font-mono text-[10px] text-slate-400">
44
44
  {expanded ? "▲ collapse" : "▼ expand"}
45
45
  </span>
46
46
  </button>
47
47
  {expanded && (
48
- <div className="border-t border-neutral-800/70 max-h-48 overflow-auto">
48
+ <div className="max-h-48 overflow-auto border-t border-[#c7dff4]">
49
49
  {diagnostics.map((d, i) => (
50
50
  <div
51
51
  key={i}
52
- className="px-4 py-2 flex gap-3 text-[12px] font-mono border-b border-neutral-900 last:border-b-0"
52
+ className="flex gap-3 border-b border-[#d8e7f4] px-4 py-2 font-mono text-[12px] last:border-b-0"
53
53
  >
54
54
  <span
55
55
  className={`shrink-0 ${
@@ -58,13 +58,13 @@ export function DiagnosticsPanel({
58
58
  >
59
59
  {d.severity === "error" ? "✗" : "!"}
60
60
  </span>
61
- <span className="shrink-0 text-neutral-500 w-16">
61
+ <span className="w-16 shrink-0 text-slate-500">
62
62
  {d.line}:{d.column}
63
63
  </span>
64
- <span className="shrink-0 text-neutral-600 w-16">
64
+ <span className="w-16 shrink-0 text-slate-400">
65
65
  {d.code ?? ""}
66
66
  </span>
67
- <span className="text-neutral-200">{d.message}</span>
67
+ <span className="text-slate-700">{d.message}</span>
68
68
  </div>
69
69
  ))}
70
70
  </div>
@@ -52,19 +52,19 @@ export function ExamplePicker({
52
52
  <button
53
53
  data-playground-examples-toggle
54
54
  onClick={() => setOpen((v) => !v)}
55
- className="px-3 py-1.5 text-xs font-mono text-neutral-300 border border-neutral-800 rounded-md hover:border-neutral-600 hover:bg-neutral-900 transition-colors"
55
+ className="rounded-md border border-[#b9d5ee] bg-white px-3 py-1.5 font-mono text-xs text-[#235a97] transition-colors hover:border-[#3178c6] hover:bg-[#eaf4ff]"
56
56
  title="Cmd/Ctrl+K"
57
57
  >
58
58
  Examples ▾
59
59
  </button>
60
60
  {open && (
61
- <div className="absolute right-0 top-full mt-2 w-80 rounded-lg border border-neutral-800 bg-neutral-950 shadow-[0_10px_40px_rgba(0,0,0,0.6)] z-10 overflow-hidden">
61
+ <div className="absolute right-0 top-full z-10 mt-2 w-80 overflow-hidden rounded-xl border border-[#b9d5ee] bg-white shadow-[0_14px_42px_rgba(49,120,198,0.18)]">
62
62
  {Object.entries(grouped).map(([group, items]) => (
63
63
  <div
64
64
  key={group}
65
- className="border-b border-neutral-900 last:border-b-0"
65
+ className="border-b border-[#d8e7f4] last:border-b-0"
66
66
  >
67
- <div className="px-3 py-1.5 text-[10px] font-mono uppercase tracking-wider text-neutral-600">
67
+ <div className="bg-[#f7fbff] px-3 py-1.5 font-mono text-[10px] uppercase tracking-wider text-slate-500">
68
68
  {groupLabels?.[group] ?? group}
69
69
  </div>
70
70
  {items.map((item) => (
@@ -74,12 +74,12 @@ export function ExamplePicker({
74
74
  onPick(item.id);
75
75
  setOpen(false);
76
76
  }}
77
- className="w-full text-left px-3 py-2 hover:bg-neutral-900 transition-colors"
77
+ className="w-full px-3 py-2 text-left transition-colors hover:bg-[#eaf4ff]"
78
78
  >
79
- <div className="text-[12px] font-mono text-neutral-100">
79
+ <div className="font-mono text-[12px] text-[#102a43]">
80
80
  {item.title}
81
81
  </div>
82
- <div className="text-[10px] text-neutral-500 mt-0.5 leading-snug">
82
+ <div className="mt-0.5 text-[10px] leading-snug text-slate-500">
83
83
  {item.description}
84
84
  </div>
85
85
  </button>
@@ -15,10 +15,10 @@ export function LintPane({
15
15
  }) {
16
16
  if (diagnostics.length === 0)
17
17
  return (
18
- <div className="flex flex-col items-center justify-center h-full text-neutral-500 font-mono text-sm gap-2">
18
+ <div className="flex h-full flex-col items-center justify-center gap-2 font-mono text-sm text-slate-500">
19
19
  <span className="text-emerald-400 text-xl">✓</span>
20
20
  <span>No lint diagnostics.</span>
21
- <span className="text-[10px] text-neutral-600 max-w-xs text-center">
21
+ <span className="max-w-xs text-center text-[10px] text-slate-400">
22
22
  {emptyHint}
23
23
  </span>
24
24
  </div>
@@ -28,7 +28,7 @@ export function LintPane({
28
28
  {diagnostics.map((d, i) => (
29
29
  <div
30
30
  key={i}
31
- className="flex gap-3 p-3 rounded-md bg-neutral-900/60 border border-neutral-800/80"
31
+ className="flex gap-3 rounded-lg border border-[#d2e4f4] bg-[#f7fbff] p-3"
32
32
  >
33
33
  <span
34
34
  className={`mt-0.5 text-[10px] font-mono px-1.5 py-0.5 rounded shrink-0 ${
@@ -40,14 +40,14 @@ export function LintPane({
40
40
  {d.severity}
41
41
  </span>
42
42
  <div className="flex-1 min-w-0">
43
- <div className="flex items-center gap-2 text-[11px] font-mono text-neutral-500 mb-1">
43
+ <div className="mb-1 flex items-center gap-2 font-mono text-[11px] text-slate-500">
44
44
  <span>{d.code}</span>
45
45
  <span>·</span>
46
46
  <span>
47
47
  {d.line}:{d.column}
48
48
  </span>
49
49
  </div>
50
- <div className="text-[13px] text-neutral-200 font-mono">
50
+ <div className="font-mono text-[13px] text-slate-700">
51
51
  {d.message}
52
52
  </div>
53
53
  </div>
@@ -62,7 +62,7 @@ export function OptionsPanel({
62
62
 
63
63
  return (
64
64
  <div
65
- className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center"
65
+ className="fixed inset-0 z-50 flex items-center justify-center bg-[#102a43]/35 backdrop-blur-sm"
66
66
  onClick={onClose}
67
67
  >
68
68
  <div
@@ -70,19 +70,19 @@ export function OptionsPanel({
70
70
  role="dialog"
71
71
  aria-modal="true"
72
72
  aria-labelledby="playground-options-title"
73
- className="w-[480px] max-w-[90vw] rounded-2xl border border-neutral-800 bg-neutral-950 shadow-[0_30px_80px_rgba(0,0,0,0.7)]"
73
+ className="w-[480px] max-w-[90vw] rounded-2xl border border-[#b9d5ee] bg-white shadow-[0_30px_80px_rgba(35,90,151,0.25)]"
74
74
  onClick={(e) => e.stopPropagation()}
75
75
  >
76
- <div className="flex items-center justify-between px-6 py-4 border-b border-neutral-800/70">
76
+ <div className="flex items-center justify-between border-b border-[#c7dff4] bg-[#f7fbff] px-6 py-4">
77
77
  <h2
78
78
  id="playground-options-title"
79
- className="text-base font-semibold text-white"
79
+ className="text-base font-semibold text-[#102a43]"
80
80
  >
81
81
  {title}
82
82
  </h2>
83
83
  <button
84
84
  onClick={onClose}
85
- className="text-neutral-500 hover:text-white transition-colors"
85
+ className="text-slate-400 transition-colors hover:text-[#235a97]"
86
86
  aria-label="Close options"
87
87
  >
88
88
 
@@ -90,7 +90,7 @@ export function OptionsPanel({
90
90
  </div>
91
91
  <div className="p-6 space-y-6">
92
92
  <div>
93
- <div className="text-[10px] font-mono uppercase tracking-wider text-neutral-600 mb-3">
93
+ <div className="mb-3 font-mono text-[10px] uppercase tracking-wider text-slate-500">
94
94
  Plugins
95
95
  </div>
96
96
  <div className="space-y-3">
@@ -106,10 +106,10 @@ export function OptionsPanel({
106
106
  className="mt-1 w-4 h-4 accent-blue-500"
107
107
  />
108
108
  <div className="flex-1">
109
- <div className="text-sm font-mono text-neutral-100 group-hover:text-white transition-colors">
109
+ <div className="font-mono text-sm text-slate-800 transition-colors group-hover:text-[#235a97]">
110
110
  {t.label}
111
111
  </div>
112
- <div className="text-[11px] text-neutral-500 leading-snug">
112
+ <div className="text-[11px] leading-snug text-slate-500">
113
113
  {t.description}
114
114
  </div>
115
115
  </div>
@@ -118,10 +118,10 @@ export function OptionsPanel({
118
118
  </div>
119
119
  </div>
120
120
  </div>
121
- <div className="px-6 py-4 border-t border-neutral-800/70 flex justify-end">
121
+ <div className="flex justify-end border-t border-[#c7dff4] bg-[#f7fbff] px-6 py-4">
122
122
  <button
123
123
  onClick={onClose}
124
- className="px-4 py-2 text-xs font-mono text-neutral-900 bg-white rounded-md hover:shadow-[0_0_30px_rgba(255,255,255,0.2)] transition-shadow"
124
+ className="rounded-md bg-[#3178c6] px-4 py-2 font-mono text-xs text-white transition-colors hover:bg-[#235a97]"
125
125
  >
126
126
  Done
127
127
  </button>