Emilio Cantú commited on
Commit
2e97c7c
·
1 Parent(s): fa7b256
Files changed (3) hide show
  1. README.md +2 -2
  2. index.html +630 -16
  3. style.css +0 -28
README.md CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
  title: Sudoku Visualizer
3
- emoji: 🔥
4
  colorFrom: green
5
  colorTo: indigo
6
  sdk: static
@@ -9,4 +9,4 @@ license: mit
9
  short_description: str to sudoku viz
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
  title: Sudoku Visualizer
3
+ emoji: 🔢
4
  colorFrom: green
5
  colorTo: indigo
6
  sdk: static
 
9
  short_description: str to sudoku viz
10
  ---
11
 
12
+ A quick vibe-coded str-to-sudoku visualizer
index.html CHANGED
@@ -1,19 +1,633 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
 
16
  </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <title>Sudoku Visualizer</title>
8
+ <style>
9
+ /* --- Dark theme only, but with mid-contrast so it isn't "all dark" --- */
10
+ :root {
11
+ --bg: #0b0e14;
12
+ /* page background */
13
+ --panel: #121826;
14
+ /* cards */
15
+ --panel-2: #0f1522;
16
+ /* slight contrast */
17
+ --text: #d2d7e3;
18
+ /* body text */
19
+ --muted: #8e98ad;
20
+ /* secondary text */
21
+ --accent: #7aa2ff;
22
+ /* focus color */
23
+
24
+ /* Board palette (muted, not bright white) */
25
+ --board-bg: #131824;
26
+ /* board fill */
27
+ --line-thin: #3a455d;
28
+ /* inner 1px */
29
+ --line-thick: #a7b0c4;
30
+ /* 2px group lines (soft light gray-blue) */
31
+ --line-outer: #b9c1d3;
32
+ /* 2px outer frame (slightly brighter than group) */
33
+ --num: #d6dbe6;
34
+ /* filled numbers */
35
+ --num-empty: #7b8498;
36
+ /* empty placeholders */
37
+ }
38
+
39
+ * {
40
+ box-sizing: border-box;
41
+ }
42
+
43
+ html,
44
+ body {
45
+ height: 100%;
46
+ }
47
+
48
+ body {
49
+ margin: 0;
50
+ background: var(--bg);
51
+ color: var(--text);
52
+ font: 16px/1.45 system-ui, -apple-system, Segoe UI, Roboto, Inter, "Helvetica Neue", Arial, sans-serif;
53
+ }
54
+
55
+ .app {
56
+ max-width: 1100px;
57
+ margin: 24px auto;
58
+ padding: 0 16px;
59
+ display: grid;
60
+ gap: 18px;
61
+ grid-template-columns: 1.1fr 1fr;
62
+ }
63
+
64
+ @media (max-width: 980px) {
65
+ .app {
66
+ grid-template-columns: 1fr;
67
+ }
68
+ }
69
+
70
+ .card {
71
+ background: linear-gradient(180deg, var(--panel) 0%, var(--panel-2) 100%);
72
+ border: 1px solid #1d2740;
73
+ }
74
+
75
+ .app__header {
76
+ grid-column: 1/-1;
77
+ padding: 20px;
78
+ }
79
+
80
+ .app__header h1 {
81
+ margin: 0 0 6px;
82
+ font-weight: 800;
83
+ font-size: clamp(22px, 2.4vw, 30px);
84
+ }
85
+
86
+ .app__header p {
87
+ margin: 6px 0 0;
88
+ color: var(--muted);
89
+ }
90
+
91
+ code {
92
+ background: #0f1422;
93
+ padding: 2px 6px;
94
+ border-radius: 4px;
95
+ color: #c4ccda;
96
+ }
97
+
98
+ .input-panel {
99
+ padding: 16px;
100
+ display: grid;
101
+ gap: 12px;
102
+ align-content: start;
103
+ }
104
+
105
+ .input-panel__label {
106
+ margin: 6px 0;
107
+ font-weight: 600;
108
+ }
109
+
110
+ textarea,
111
+ input[type="text"] {
112
+ width: 100%;
113
+ color: var(--text);
114
+ background: #0f1524;
115
+ border: 1px solid #27324a;
116
+ padding: 10px 12px;
117
+ outline: none;
118
+ resize: vertical;
119
+ min-height: 110px;
120
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
121
+ }
122
+
123
+ input[type="text"] {
124
+ min-height: unset;
125
+ }
126
+
127
+ textarea:focus,
128
+ input[type="text"]:focus {
129
+ border-color: var(--accent);
130
+ box-shadow: 0 0 0 2px rgba(122, 162, 255, .18);
131
+ }
132
+
133
+ .is-invalid {
134
+ border-color: #ff6b6b !important;
135
+ box-shadow: 0 0 0 2px rgba(255, 107, 107, .18) !important;
136
+ }
137
+
138
+ .input-panel__extras {
139
+ display: grid;
140
+ gap: 12px;
141
+ grid-template-columns: 1fr 1fr;
142
+ }
143
+
144
+ @media (max-width: 640px) {
145
+ .input-panel__extras {
146
+ grid-template-columns: 1fr;
147
+ }
148
+ }
149
+
150
+ .field__hint {
151
+ margin: 6px 2px 0;
152
+ color: var(--muted);
153
+ font-size: .9rem;
154
+ }
155
+
156
+ .input-panel__hint {
157
+ margin: 0;
158
+ color: var(--muted);
159
+ padding: 0 6px 8px;
160
+ }
161
+
162
+ .input-panel__controls {
163
+ display: flex;
164
+ gap: 10px;
165
+ flex-wrap: wrap;
166
+ padding-top: 4px;
167
+ align-items: center;
168
+ }
169
+
170
+ button {
171
+ appearance: none;
172
+ border: 1px solid #27324a;
173
+ background: #151d32;
174
+ color: var(--text);
175
+ padding: 10px 14px;
176
+ font-weight: 700;
177
+ cursor: pointer;
178
+ }
179
+
180
+ button:hover {
181
+ filter: brightness(1.06);
182
+ }
183
+
184
+ button.secondary {
185
+ background: #101727;
186
+ }
187
+
188
+ .grid-panel {
189
+ padding: 16px;
190
+ display: grid;
191
+ gap: 12px;
192
+ align-content: start;
193
+ }
194
+
195
+ .grid-head {
196
+ display: flex;
197
+ justify-content: space-between;
198
+ align-items: center;
199
+ gap: 12px;
200
+ color: var(--muted);
201
+ }
202
+
203
+ .badge {
204
+ font-size: .9rem;
205
+ padding: 4px 8px;
206
+ border: 1px solid #27324a;
207
+ color: var(--muted);
208
+ }
209
+
210
+ /* --- Classic Sudoku board (no rounded corners, mid-contrast lines) --- */
211
+ .sudoku-grid {
212
+ display: grid;
213
+ grid-template-columns: repeat(9, 1fr);
214
+ width: 100%;
215
+ max-width: 560px;
216
+ aspect-ratio: 1/1;
217
+ background: var(--board-bg);
218
+ border: 2px solid var(--line-outer);
219
+ }
220
+
221
+ .sudoku-cell {
222
+ display: grid;
223
+ place-items: center;
224
+ font-weight: 600;
225
+ font-size: clamp(16px, 2.2vw, 22px);
226
+ border: 1px solid var(--line-thin);
227
+ min-height: 48px;
228
+ user-select: none;
229
+ color: var(--num);
230
+ }
231
+
232
+ .sudoku-cell--empty {
233
+ color: var(--num-empty);
234
+ }
235
+
236
+ .sudoku-cell--conflict {
237
+ background: rgba(220, 38, 38, .16);
238
+ border-color: #dc2626 !important;
239
+ color: #ffd7d7;
240
+ }
241
+
242
+ /* 3×3 separators */
243
+ .border-right-group {
244
+ border-right: 2px solid var(--line-thick) !important;
245
+ }
246
+
247
+ .border-bottom-group {
248
+ border-bottom: 2px solid var(--line-thick) !important;
249
+ }
250
+
251
+ /* Outer edges */
252
+ .border-left-strong {
253
+ border-left: 2px solid var(--line-outer) !important;
254
+ }
255
+
256
+ .border-right-strong {
257
+ border-right: 2px solid var(--line-outer) !important;
258
+ }
259
+
260
+ .border-top-strong {
261
+ border-top: 2px solid var(--line-outer) !important;
262
+ }
263
+
264
+ .border-bottom-strong {
265
+ border-bottom: 2px solid var(--line-outer) !important;
266
+ }
267
+
268
+ /* Toast / status */
269
+ .status-wrap {
270
+ position: fixed;
271
+ right: 18px;
272
+ bottom: 18px;
273
+ z-index: 999;
274
+ display: grid;
275
+ gap: 10px;
276
+ width: min(420px, calc(100vw - 36px));
277
+ }
278
+
279
+ .status-message {
280
+ padding: 12px 14px;
281
+ border: 1px solid #27324a;
282
+ background: #0f1526;
283
+ color: var(--text);
284
+ }
285
+
286
+ .status-message[data-tone="success"] {
287
+ border-color: #1f3227;
288
+ background: #0f1a15;
289
+ }
290
+
291
+ .status-message[data-tone="warning"] {
292
+ border-color: #4b3a13;
293
+ background: #17140e;
294
+ }
295
+
296
+ .status-message[data-tone="error"] {
297
+ border-color: #4a1717;
298
+ background: #160f0f;
299
+ }
300
+
301
+ .status-message[data-tone="info"] {
302
+ border-color: #27324a;
303
+ background: #0f1526;
304
+ }
305
+
306
+ .kbd {
307
+ font: 12px/1.2 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
308
+ background: #10162a;
309
+ border: 1px solid #27324a;
310
+ padding: 3px 6px;
311
+ border-radius: 4px;
312
+ color: #c4ccda;
313
+ }
314
+
315
+ .sr-only {
316
+ position: absolute;
317
+ width: 1px;
318
+ height: 1px;
319
+ padding: 0;
320
+ margin: -1px;
321
+ overflow: hidden;
322
+ clip: rect(0, 0, 0, 0);
323
+ border: 0;
324
+ }
325
+ </style>
326
+ </head>
327
+
328
+ <body>
329
+ <main class="app">
330
+ <header class="app__header card">
331
+ <h1>Sudoku Visualizer</h1>
332
  <p>
333
+ Paste <strong>81 values</strong> separated by commas, spaces, or newlines. Use <code>.</code>,
334
+ <code>_</code>, or <code>0</code> for empty cells. Multi-digit entries (e.g. <code>10</code>) are
335
+ supported.
336
  </p>
337
+ </header>
338
+
339
+ <section class="input-panel card" aria-label="Sudoku input">
340
+ <form id="sudoku-form">
341
+ <label class="input-panel__label" for="grid-input">Grid values</label>
342
+ <textarea id="grid-input" name="grid-input" rows="6"
343
+ placeholder="Example: 5,3,.,.,7,.,.,.,.; 6,.,.,1,9,5,.,.,." spellcheck="false"></textarea>
344
+
345
+ <div class="input-panel__extras">
346
+ <div class="field">
347
+ <label class="input-panel__label" for="separator-input">Separator (optional)</label>
348
+ <input id="separator-input" name="separator-input" type="text"
349
+ placeholder="Defaults to comma, space or newline (leave empty)" maxlength="10" />
350
+ <p class="field__hint">Use <code>\n</code> for newline or <code>\s</code> / <code>space</code>
351
+ for whitespace.</p>
352
+ </div>
353
+
354
+ <div class="field">
355
+ <label class="input-panel__label" for="empty-input">Empty token(s) (optional)</label>
356
+ <input id="empty-input" name="empty-input" type="text"
357
+ placeholder="e.g. . , _ , 0 or blank (comma/space separated)" maxlength="50" />
358
+ <p class="field__hint">Additional tokens to treat as empty. Defaults include <code>.</code>,
359
+ <code>_</code>, <code>0</code>, <code>x</code>.</p>
360
+ </div>
361
+ </div>
362
+
363
+ <div class="input-panel__controls">
364
+ <button type="submit" title="Visualize (Ctrl/⌘ + Enter)">Visualize</button>
365
+ <button type="button" id="clear-button" class="secondary">Clear</button>
366
+ <button type="button" id="sample-button" class="secondary">Load sample</button>
367
+ <span class="badge" id="count-badge" aria-live="polite">—</span>
368
+ </div>
369
+ </form>
370
+ <p class="input-panel__hint">The first 81 recognizable values will be used. Extra values are ignored so you
371
+ can paste entire rows at once.</p>
372
+ </section>
373
+
374
+ <section class="grid-panel card" aria-live="polite" aria-label="Rendered Sudoku grid">
375
+ <div class="grid-head">
376
+ <strong>Grid</strong>
377
+ </div>
378
+ <div id="sudoku-grid" class="sudoku-grid"></div>
379
+ </section>
380
+ </main>
381
+
382
+ <div class="status-wrap">
383
+ <div id="status-message" class="status-message" role="status" aria-live="polite"></div>
384
+ </div>
385
+
386
+ <script>
387
+ (function () {
388
+ const form = document.getElementById("sudoku-form");
389
+ const input = document.getElementById("grid-input");
390
+ const separatorInput = document.getElementById("separator-input");
391
+ const emptyInput = document.getElementById("empty-input");
392
+ const grid = document.getElementById("sudoku-grid");
393
+ const status = document.getElementById("status-message");
394
+ const countBadge = document.getElementById("count-badge");
395
+ const clearButton = document.getElementById("clear-button");
396
+ const sampleButton = document.getElementById("sample-button");
397
+
398
+ const samplePuzzle =
399
+ "3, 10, 4, 8, 7, 6, 9, 2, 5,\n" +
400
+ "6, 7, 9, 2, 5, 3, 10, 4, 8,\n" +
401
+ "2, 8, 5, 9, 10, 4, 3, 6, 7,\n" +
402
+ "4, 2, 10, 5, 9, 8, 7, 3, 6,\n" +
403
+ "7, 9, 6, 3, 4, 10, 5, 8, 2,\n" +
404
+ "8, 5, 3, 7, 6, 2, 4, 10, 9,\n" +
405
+ "5, 6, 8, 4, 3, 7, 2, 9, 10,\n" +
406
+ "9, 4, 2, 10, 8, 5, 6, 7, 3,\n" +
407
+ "10, 3, 7, 6, 2, 9, 8, 5, 4";
408
+
409
+ // Live token counter and autosize
410
+ input.addEventListener("input", () => {
411
+ updateCount();
412
+ autosize(input);
413
+ });
414
+
415
+ function updateCount() {
416
+ const { values } = quickTokenize(input.value, separatorInput.value);
417
+ countBadge.textContent = values.length ? `${values.length} token${values.length === 1 ? "" : "s"}` : "—";
418
+ }
419
+ function autosize(el) {
420
+ el.style.height = "auto";
421
+ el.style.height = Math.min(el.scrollHeight, 400) + "px";
422
+ }
423
+
424
+ form.addEventListener("submit", (event) => {
425
+ event.preventDefault();
426
+ const raw = input.value;
427
+ const sep = separatorInput.value;
428
+ const emptySpec = emptyInput.value;
429
+ const { cells, error, warning } = parseInput(raw, sep, emptySpec);
430
+
431
+ input.classList.toggle("is-invalid", Boolean(error));
432
+
433
+ if (error) {
434
+ updateStatus(error, "error");
435
+ hideGrid();
436
+ return;
437
+ }
438
+
439
+ const conflictCount = renderGrid(cells);
440
+ const filledCount = cells.filter(Boolean).length;
441
+ const conflictNote = conflictCount ? ` • <strong>${conflictCount}</strong> conflict${conflictCount === 1 ? "" : "s"}` : "";
442
+
443
+ if (warning) {
444
+ updateStatus(`${warning} Showing ${filledCount} filled cell${filledCount === 1 ? "" : "s"}.${conflictNote}`, conflictCount ? "warning" : "warning");
445
+ } else {
446
+ updateStatus(`Showing ${filledCount} filled cell${filledCount === 1 ? "" : "s"}.${conflictNote}`, conflictCount ? "error" : "success");
447
+ }
448
+ });
449
+
450
+ // Keyboard shortcuts
451
+ document.addEventListener("keydown", (e) => {
452
+ const isMetaEnter = (e.ctrlKey || e.metaKey) && e.key === "Enter";
453
+ if (isMetaEnter) {
454
+ form.requestSubmit();
455
+ } else if (e.key === "Escape") {
456
+ clearButton.click();
457
+ }
458
+ });
459
+
460
+ clearButton.addEventListener("click", () => {
461
+ input.value = "";
462
+ input.classList.remove("is-invalid");
463
+ hideGrid();
464
+ updateStatus("Cleared input. Paste a puzzle to get started.", "info");
465
+ updateCount();
466
+ input.focus();
467
+ });
468
+
469
+ sampleButton.addEventListener("click", () => {
470
+ input.value = samplePuzzle;
471
+ updateStatus("Loaded sample puzzle. Press Visualize or <span class=\"kbd\">Ctrl</span>/<span class=\"kbd\">⌘</span> + <span class=\"kbd\">Enter</span>.", "info");
472
+ updateCount();
473
+ input.focus();
474
+ });
475
+
476
+ function quickTokenize(raw, separator) {
477
+ let values = [];
478
+ if (separator && separator.trim()) {
479
+ let pattern;
480
+ if (separator === "\\n") pattern = /\n/; else if (separator === "\\s" || separator.toLowerCase() === "space") pattern = /\s+/; else pattern = new RegExp(escapeRegExp(separator));
481
+ values = raw.split(pattern).map((s) => (s || "").trim()).filter(Boolean);
482
+ } else {
483
+ values = (raw.match(/([+-]?\d+|[A-Za-z]+|\.|_)/g) || []).filter(Boolean);
484
+ if (values.length === 0) {
485
+ const compact = raw.replace(/\s+/g, "");
486
+ if (/^[0-9._]+$/.test(compact)) values = compact.split("");
487
+ }
488
+ }
489
+ return { values };
490
+ }
491
+
492
+ function parseInput(raw, separator, emptySpec) {
493
+ const result = { cells: [], error: null, warning: null };
494
+
495
+ if (!raw || !raw.trim()) {
496
+ result.error = "Please paste or type 81 values first.";
497
+ return result;
498
+ }
499
+
500
+ let values = quickTokenize(raw, separator).values;
501
+
502
+ if (values.length === 0) {
503
+ result.error = "No recognizable numbers were found.";
504
+ return result;
505
+ }
506
+
507
+ if (values.length < 81) {
508
+ result.error = `Need 81 values, found ${values.length}.`;
509
+ return result;
510
+ }
511
+
512
+ if (values.length > 81) {
513
+ result.warning = `Found ${values.length} values; using the first 81.`;
514
+ values = values.slice(0, 81);
515
+ }
516
+
517
+ const defaults = [".", "_", "0", "x", "blank", "empty"];
518
+ const emptySet = new Set(defaults.map((s) => s.toLowerCase()));
519
+ if (emptySpec && emptySpec.trim()) {
520
+ const extras = emptySpec.split(/[\s,]+/).map((s) => s.trim().toLowerCase()).filter(Boolean);
521
+ extras.forEach((t) => emptySet.add(t));
522
+ }
523
+
524
+ result.cells = values.map((t) => normalizeToken(t, emptySet));
525
+ return result;
526
+ }
527
+
528
+ function escapeRegExp(string) {
529
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
530
+ }
531
+
532
+ function normalizeToken(token, emptySet) {
533
+ const trimmed = (token || "").toString().trim();
534
+ if (!trimmed) return "";
535
+ const lowered = trimmed.toLowerCase();
536
+ if (emptySet && emptySet.has(lowered)) return "";
537
+ if (/^[A-Za-z]+$/.test(trimmed)) return "";
538
+ if (/^[+-]?\d+$/.test(trimmed)) {
539
+ const numeric = Number.parseInt(trimmed, 10);
540
+ if (!Number.isFinite(numeric) || numeric <= 0) return "";
541
+ return String(numeric);
542
+ }
543
+ return trimmed;
544
+ }
545
+
546
+ function renderGrid(cells) {
547
+ grid.innerHTML = "";
548
+ if (!cells || cells.length !== 81) { hideGrid(); return 0; }
549
+
550
+ const conflicts = findConflicts(cells);
551
+
552
+ cells.forEach((value, index) => {
553
+ const cell = document.createElement("div");
554
+ cell.className = "sudoku-cell";
555
+ const row = Math.floor(index / 9);
556
+ const col = index % 9;
557
+ if (!value) cell.classList.add("sudoku-cell--empty");
558
+ if (conflicts.has(index)) cell.classList.add("sudoku-cell--conflict");
559
+ if (col === 0) cell.classList.add("border-left-strong");
560
+ if (col === 8) cell.classList.add("border-right-strong");
561
+ if (col === 2 || col === 5) cell.classList.add("border-right-group");
562
+ if (row === 0) cell.classList.add("border-top-strong");
563
+ if (row === 8) cell.classList.add("border-bottom-strong");
564
+ if (row === 2 || row === 5) cell.classList.add("border-bottom-group");
565
+ cell.textContent = value;
566
+ grid.appendChild(cell);
567
+ });
568
+ grid.dataset.ready = "true";
569
+ return conflicts.size;
570
+ }
571
+
572
+ function findConflicts(cells) {
573
+ const bad = new Set();
574
+ const rows = Array.from({ length: 9 }, () => new Map());
575
+ const cols = Array.from({ length: 9 }, () => new Map());
576
+ const boxes = Array.from({ length: 9 }, () => new Map());
577
+
578
+ for (let i = 0; i < 81; i++) {
579
+ const v = cells[i];
580
+ if (!v) continue; // ignore empties
581
+ const r = Math.floor(i / 9);
582
+ const c = i % 9;
583
+ const b = Math.floor(r / 3) * 3 + Math.floor(c / 3);
584
+
585
+ if (!rows[r].has(v)) rows[r].set(v, []);
586
+ if (!cols[c].has(v)) cols[c].set(v, []);
587
+ if (!boxes[b].has(v)) boxes[b].set(v, []);
588
+
589
+ rows[r].get(v).push(i);
590
+ cols[c].get(v).push(i);
591
+ boxes[b].get(v).push(i);
592
+ }
593
+
594
+ const markDupes = (maps) => {
595
+ for (const m of maps) {
596
+ for (const [_val, idxs] of m) {
597
+ if (idxs.length > 1) idxs.forEach((ix) => bad.add(ix));
598
+ }
599
+ }
600
+ };
601
+
602
+ markDupes(rows);
603
+ markDupes(cols);
604
+ markDupes(boxes);
605
+ return bad;
606
+ }
607
+
608
+ function hideGrid() {
609
+ // Keep board visible; just clear contents if needed
610
+ if (!grid.children.length) {
611
+ renderGrid(new Array(81).fill(""));
612
+ return;
613
+ }
614
+ [...grid.children].forEach((c) => (c.textContent = ""));
615
+ grid.dataset.ready = "";
616
+ }
617
+
618
+ function updateStatus(message, tone) {
619
+ status.innerHTML = message; // allow small inline markup for kbd hints
620
+ status.dataset.tone = tone || "";
621
+ }
622
+
623
+ // Initial UI state
624
+ updateStatus("Paste values and click <strong>Visualize</strong> to see the grid.", "info");
625
+ renderGrid(new Array(81).fill("")); // show board immediately
626
+ updateCount();
627
+ (function autosizeOnLoad() { const el = input; el.style.height = "auto"; el.style.height = Math.min(el.scrollHeight, 400) + "px"; })();
628
+ input.focus();
629
+ })();
630
+ </script>
631
+ </body>
632
+
633
+ </html>
style.css DELETED
@@ -1,28 +0,0 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
28
- }