@buttonschool/create-wireframe 0.1.2 → 0.2.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@buttonschool/create-wireframe",
3
- "version": "0.1.2",
4
- "description": "Scaffold a Vite-based wireframe prototype with tokens, Comic Neue, and Lucide",
3
+ "version": "0.2.0",
4
+ "description": "Scaffold a Vite-based wireframe prototype with tokens, components, Comic Neue, and Lucide",
5
5
  "type": "module",
6
6
  "bin": "index.js",
7
7
  "files": [
@@ -2,16 +2,16 @@
2
2
 
3
3
  This is a **wireframe prototype** scaffold. Stack: **Vite** (vanilla JS), design tokens, **Comic Neue** font, and **Lucide** icons. Keep structure minimal; suitable for code-gen and rapid iteration.
4
4
 
5
- The project starts with **two HTML pages**: **Start** (`index.html`) has a small word game (Spelling Bee–style); **Tokens** (`tokens.html`) showcases the design tokens. Both share a nav linking to each other. Styles are linked from each page’s `<head>` via `<link rel="stylesheet" href="/src/styles/main.css">`. Prefer writing markup in HTML files and using JS only for behavior. To add a page: add a new `.html` file at the project root and add a link to it in the nav; no need to edit Vite config (every root-level HTML file is automatically a page).
5
+ The project starts with **three HTML pages**: **Start** (`index.html`) has a small word game (Spelling Bee–style); **Tokens** (`tokens.html`) showcases the design tokens; **Components** (`components.html`) showcases the wireframe kit controls. All share a nav linking to each other. The nav is a single **Nunjucks partial**: `_partials/nav.html` (uses `site.nav.*` from `src/strings.json` and `current` for `aria-current="page"`). Each HTML page includes it and receives a `current` value via `vite.config.js` so the active link is marked. To add a page: add the `.html` file, add a link in `_partials/nav.html`, add a `variables` entry in `vite.config.js` with `{ ...strings, current: "yourpage" }`, and add a link in `main.css` if you add page-specific styles. Styles are linked from each page’s `<head>` via `<link rel="stylesheet" href="/src/styles/main.css">`. Prefer writing markup in HTML files and using JS only for behavior. To add a page: add a new `.html` file at the project root and add a link to it in the nav; no need to edit Vite config (every root-level HTML file is automatically a page).
6
6
 
7
7
  ## Styles
8
8
 
9
- Styles are organized under `src/styles/`: `main.css` (orchestration), `base.css`, `layout.css`, `components/` (e.g. nav, button), and `pages/` (home, tokens-showcase). To remove or add a page or component, edit the corresponding file and the `@import` list in `styles/main.css`.
9
+ Styles are organized under `src/styles/`: `main.css` (orchestration), `base.css`, `layout.css`, `components/` (e.g. nav), and `pages/` (home, tokens-showcase, components-showcase). Reusable controls live under **`src/kit/components/`** — one CSS file per component (button, input, textarea, checkbox, radio, select, label, field), using `--wire-*` tokens and `.wire-*` class names (e.g. `.wire-button`, `.wire-input`). Use `.wire-field` to wrap a label and its control for consistent spacing. The select can be wrapped in `.wire-select-wrap` for a CSS-only chevron (pseudo-element, no icon element or JS). To remove or add a page or component, edit the corresponding file and the `@import` list in `styles/main.css`.
10
10
 
11
11
  ## Tokens
12
12
 
13
13
  - **Location:** `src/kit/tokens.css`
14
- - **Usage:** CSS custom properties on `:root` with `--wire-*` prefix: colors (grayscale + semantic aliases), spacing (`--wire-space-*`), radius (`--wire-radius-*`), typography (`--wire-font-family`, `--wire-line-height`), font sizes (`--wire-text-xs` through `--wire-text-2xl`, plus semantic aliases like `--wire-text-body`, `--wire-text-heading`, `--wire-text-subheading`, `--wire-text-caption`, `--wire-text-label`), and sizing (`--wire-size-icon-sm`, `--wire-size-icon-md`, `--wire-size-icon-lg`, `--wire-size-avatar-sm`, `--wire-size-avatar-md`, `--wire-size-avatar-lg`). Use them in your CSS, e.g. `color: var(--wire-text-primary);`, `padding: var(--wire-space-md);`, `font-size: var(--wire-text-body);`, `width: var(--wire-size-icon-md); height: var(--wire-size-icon-md);`.
14
+ - **Usage:** CSS custom properties on `:root` with `--wire-*` prefix: colors (grayscale + semantic aliases), spacing (`--wire-space-*`), radius (`--wire-radius-*`), border width (`--wire-border-width`, default 2px for controls and strokes), typography (`--wire-font-family`, `--wire-line-height`), font sizes (`--wire-text-xs` through `--wire-text-2xl`, plus semantic aliases like `--wire-text-body`, `--wire-text-heading`, `--wire-text-subheading`, `--wire-text-caption`, `--wire-text-label`), and sizing (`--wire-size-icon-sm`, `--wire-size-icon-md`, `--wire-size-icon-lg`, `--wire-size-avatar-sm`, `--wire-size-avatar-md`, `--wire-size-avatar-lg`). Use them in your CSS, e.g. `color: var(--wire-text-primary);`, `padding: var(--wire-space-md);`, `font-size: var(--wire-text-body);`, `width: var(--wire-size-icon-md); height: var(--wire-size-icon-md);`.
15
15
 
16
16
  ## Dev server
17
17
 
@@ -0,0 +1,5 @@
1
+ <nav class="site-nav">
2
+ <a href="/" {% if current == "start" %}aria-current="page"{% endif %}>{{ site.nav.start }}</a>
3
+ <a href="/tokens.html" {% if current == "tokens" %}aria-current="page"{% endif %}>{{ site.nav.tokens }}</a>
4
+ <a href="/components.html" {% if current == "components" %}aria-current="page"{% endif %}>{{ site.nav.components }}</a>
5
+ </nav>
@@ -0,0 +1,122 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="stylesheet" href="/src/styles/main.css" />
7
+ <title>Components – Wireframe</title>
8
+ </head>
9
+ <body>
10
+ {% include "_partials/nav.html" %}
11
+ <main class="components-showcase">
12
+ <h1>Components</h1>
13
+ <p class="components-showcase-intro">Reusable controls from the wireframe kit. Use classes <code>.wire-button</code>, <code>.wire-input</code>, and the rest in your HTML.</p>
14
+
15
+ <section class="showcase-section">
16
+ <h2>Buttons</h2>
17
+ <div class="component-demo-row">
18
+ <button type="button" class="wire-button">Default</button>
19
+ <button type="button" class="wire-button wire-button--primary">Primary</button>
20
+ <button type="button" class="wire-button" disabled>Disabled</button>
21
+ </div>
22
+ </section>
23
+
24
+ <section class="showcase-section">
25
+ <h2>Text input</h2>
26
+ <div class="component-demo-block">
27
+ <div class="wire-field">
28
+ <label class="wire-label" for="demo-input">Label</label>
29
+ <input type="text" id="demo-input" class="wire-input" placeholder="Placeholder text" />
30
+ </div>
31
+ </div>
32
+ <div class="component-demo-block">
33
+ <input type="text" class="wire-input" placeholder="No label" disabled />
34
+ </div>
35
+ </section>
36
+
37
+ <section class="showcase-section">
38
+ <h2>Textarea</h2>
39
+ <div class="component-demo-block">
40
+ <div class="wire-field">
41
+ <label class="wire-label" for="demo-textarea">Message</label>
42
+ <textarea id="demo-textarea" class="wire-textarea" rows="4" placeholder="Enter text…"></textarea>
43
+ </div>
44
+ </div>
45
+ </section>
46
+
47
+ <section class="showcase-section">
48
+ <h2>Checkbox</h2>
49
+ <div class="component-demo-stack">
50
+ <label class="wire-checkbox">
51
+ <input type="checkbox" name="demo-cb" />
52
+ <span>Option one</span>
53
+ </label>
54
+ <label class="wire-checkbox">
55
+ <input type="checkbox" name="demo-cb" checked />
56
+ <span>Option two (checked)</span>
57
+ </label>
58
+ <label class="wire-checkbox">
59
+ <input type="checkbox" name="demo-cb" disabled />
60
+ <span>Option three (disabled)</span>
61
+ </label>
62
+ </div>
63
+ </section>
64
+
65
+ <section class="showcase-section">
66
+ <h2>Radio</h2>
67
+ <div class="component-demo-stack">
68
+ <label class="wire-radio">
69
+ <input type="radio" name="demo-radio" value="a" />
70
+ <span>Choice A</span>
71
+ </label>
72
+ <label class="wire-radio">
73
+ <input type="radio" name="demo-radio" value="b" checked />
74
+ <span>Choice B</span>
75
+ </label>
76
+ <label class="wire-radio">
77
+ <input type="radio" name="demo-radio" value="c" disabled />
78
+ <span>Choice C (disabled)</span>
79
+ </label>
80
+ </div>
81
+ </section>
82
+
83
+ <section class="showcase-section">
84
+ <h2>Select</h2>
85
+ <div class="component-demo-block">
86
+ <div class="wire-field">
87
+ <label class="wire-label" for="demo-select">Choose one</label>
88
+ <span class="wire-select-wrap">
89
+ <select id="demo-select">
90
+ <option value="">Select…</option>
91
+ <option value="1">Option 1</option>
92
+ <option value="2">Option 2</option>
93
+ <option value="3">Option 3</option>
94
+ </select>
95
+ </span>
96
+ </div>
97
+ </div>
98
+ <div class="component-demo-block">
99
+ <span class="wire-select-wrap">
100
+ <select disabled>
101
+ <option>Disabled select</option>
102
+ </select>
103
+ </span>
104
+ </div>
105
+ </section>
106
+
107
+ <section class="showcase-section">
108
+ <h2>Label</h2>
109
+ <div class="component-demo-stack">
110
+ <div class="wire-field">
111
+ <label class="wire-label" for="demo-label-plain">Plain label</label>
112
+ <input type="text" id="demo-label-plain" class="wire-input" />
113
+ </div>
114
+ <div class="wire-field">
115
+ <label class="wire-label wire-label--required" for="demo-label-required">Required field</label>
116
+ <input type="text" id="demo-label-required" class="wire-input" />
117
+ </div>
118
+ </div>
119
+ </section>
120
+ </main>
121
+ </body>
122
+ </html>
@@ -4,31 +4,30 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="stylesheet" href="/src/styles/main.css" />
7
- <title>Wireframe</title>
7
+ <title>{{ site.title }}</title>
8
8
  </head>
9
9
  <body>
10
- <nav class="site-nav">
11
- <a href="/" aria-current="page">Start</a>
12
- <a href="/tokens.html">Tokens</a>
13
- </nav>
10
+ {% include "_partials/nav.html" %}
14
11
  <main class="game-area">
15
- <p class="bee-score" aria-live="polite"><i data-lucide="crown" class="bee-score-icon" aria-hidden="true"></i><span class="bee-score-value">0</span><span class="bee-score-label visually-hidden"> points</span></p>
16
- <div class="bee-honeycomb" aria-hidden="true"></div>
17
- <div class="bee-input-row">
18
- <label for="bee-word" class="visually-hidden">Your word</label>
12
+ <p class="bee-score" aria-live="polite"><i data-lucide="crown" class="bee-score-icon" aria-hidden="true"></i><span class="bee-score-value">0</span><span class="bee-score-label visually-hidden">{{ game.scoreLabel }}</span></p>
13
+ <div class="bee-focus-region" tabindex="-1">
14
+ <div class="bee-honeycomb" aria-hidden="true"></div>
15
+ <div class="bee-input-row">
16
+ <label for="bee-word" class="visually-hidden">{{ game.inputLabel }}</label>
19
17
  <input
20
18
  type="text"
21
19
  id="bee-word"
22
20
  class="bee-input"
23
- placeholder="Enter a word"
21
+ placeholder="{{ game.inputPlaceholder }}"
24
22
  autocomplete="off"
25
23
  autocapitalize="off"
26
24
  maxlength="15"
27
25
  />
28
- <button type="button" class="bee-submit">Guess</button>
26
+ <button type="button" class="bee-submit">{{ game.submitButton }}</button>
27
+ </div>
29
28
  </div>
30
29
  <p class="bee-feedback" aria-live="polite"></p>
31
- <h2 class="bee-found-heading">Found words</h2>
30
+ <h2 class="bee-found-heading">{{ game.foundHeading }}</h2>
32
31
  <ul class="bee-found-list" aria-live="polite"></ul>
33
32
  </main>
34
33
  <script type="module" src="/src/main.js"></script>
@@ -13,6 +13,8 @@
13
13
  },
14
14
  "devDependencies": {
15
15
  "@fontsource/comic-neue": "^5.1.0",
16
- "vite": "^6.0.0"
16
+ "@fontsource/monaspace-argon": "^5.1.0",
17
+ "vite": "^6.0.0",
18
+ "vite-plugin-nunjucks": "^0.2.0"
17
19
  }
18
20
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { createIcons, Crown } from "lucide";
7
+ import strings from "./strings.json";
7
8
 
8
9
  const LETTERS = ["W", "I", "R", "E", "F", "A", "M"];
9
10
  const CENTER_LETTER = "E";
@@ -164,10 +165,10 @@ function scoreWord(word) {
164
165
  function validate(word) {
165
166
  const w = word.trim().toLowerCase();
166
167
  if (!w) return { ok: false, message: "" };
167
- if (w.length < MIN_LENGTH) return { ok: false, message: "Too short." };
168
- if (!hasCenterLetter(w)) return { ok: false, message: "Missing center letter." };
169
- if (!usesOnlyLetters(w)) return { ok: false, message: "Invalid letters." };
170
- if (!WORD_LIST.has(w)) return { ok: false, message: "Not in word list." };
168
+ if (w.length < MIN_LENGTH) return { ok: false, message: strings.game.feedback.tooShort };
169
+ if (!hasCenterLetter(w)) return { ok: false, message: strings.game.feedback.missingCenter };
170
+ if (!usesOnlyLetters(w)) return { ok: false, message: strings.game.feedback.invalidLetters };
171
+ if (!WORD_LIST.has(w)) return { ok: false, message: strings.game.feedback.notInList };
171
172
  return { ok: true, word: w };
172
173
  }
173
174
 
@@ -177,6 +178,7 @@ function init() {
177
178
  const area = document.querySelector(".game-area");
178
179
  if (!area) return;
179
180
 
181
+ const focusRegion = area.querySelector(".bee-focus-region");
180
182
  const scoreEl = area.querySelector(".bee-score");
181
183
  const honeycombEl = area.querySelector(".bee-honeycomb");
182
184
  const inputEl = area.querySelector(".bee-input");
@@ -184,7 +186,7 @@ function init() {
184
186
  const feedbackEl = area.querySelector(".bee-feedback");
185
187
  const foundListEl = area.querySelector(".bee-found-list");
186
188
 
187
- if (!scoreEl || !honeycombEl || !inputEl || !submitBtn || !feedbackEl || !foundListEl) return;
189
+ if (!focusRegion || !scoreEl || !honeycombEl || !inputEl || !submitBtn || !feedbackEl || !foundListEl) return;
188
190
 
189
191
  const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
190
192
  const loaded = Array.isArray(saved)
@@ -224,19 +226,21 @@ function init() {
224
226
  const raw = inputEl.value;
225
227
  const result = validate(raw);
226
228
  if (!result.ok) {
227
- setFeedback(result.message || "Try again.", false, true);
229
+ setFeedback(result.message || strings.game.feedback.tryAgain, false, true);
230
+ inputEl.value = "";
228
231
  return;
229
232
  }
230
233
  const { word } = result;
231
234
  if (found.has(word)) {
232
- setFeedback("Already found.", false, true);
235
+ setFeedback(strings.game.feedback.alreadyFound, false, true);
236
+ inputEl.value = "";
233
237
  return;
234
238
  }
235
239
  foundOrdered.push(word);
236
240
  found.add(word);
237
241
  totalScore += scoreWord(word);
238
242
  saveFound();
239
- setFeedback(isPangram(word) ? "Pangram!" : "Good!", true, false);
243
+ setFeedback(isPangram(word) ? strings.game.feedback.pangram : strings.game.feedback.good, true, false);
240
244
  inputEl.value = "";
241
245
  inputEl.focus();
242
246
  updateScore();
@@ -246,26 +250,53 @@ function init() {
246
250
  submitBtn.addEventListener("click", submitWord);
247
251
  inputEl.addEventListener("keydown", (e) => {
248
252
  if (e.key === "Enter") submitWord();
253
+ else if (!e.ctrlKey && !e.metaKey && !e.altKey) {
254
+ const upper = (e.key || "").toUpperCase();
255
+ if (LETTERS.includes(upper)) {
256
+ const span = honeycombEl.querySelector(`[data-letter="${upper}"]`);
257
+ if (span) span.classList.add("bee-letter--typed-press");
258
+ }
259
+ }
260
+ });
261
+ inputEl.addEventListener("keyup", (e) => {
262
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
263
+ const upper = (e.key || "").toUpperCase();
264
+ if (LETTERS.includes(upper)) {
265
+ const span = honeycombEl.querySelector(`[data-letter="${upper}"]`);
266
+ if (span) span.classList.remove("bee-letter--typed-press");
267
+ }
249
268
  });
250
269
 
270
+ function appendLetter(letter) {
271
+ if (inputEl.value.length >= 15) return;
272
+ inputEl.value += letter.toLowerCase();
273
+ inputEl.focus();
274
+ }
275
+
251
276
  honeycombEl.innerHTML = "";
252
277
  const outer = LETTERS.filter((l) => l !== CENTER_LETTER);
253
278
  const centerNode = document.createElement("span");
254
279
  centerNode.className = "bee-letter bee-letter--center";
255
280
  centerNode.textContent = CENTER_LETTER;
256
281
  centerNode.setAttribute("aria-hidden", "true");
282
+ centerNode.setAttribute("data-letter", CENTER_LETTER);
283
+ centerNode.addEventListener("mousedown", () => focusRegion.focus());
284
+ centerNode.addEventListener("click", () => appendLetter(CENTER_LETTER));
257
285
  honeycombEl.appendChild(centerNode);
258
286
  outer.forEach((letter) => {
259
287
  const span = document.createElement("span");
260
288
  span.className = "bee-letter";
261
289
  span.textContent = letter;
262
290
  span.setAttribute("aria-hidden", "true");
291
+ span.setAttribute("data-letter", letter);
292
+ span.addEventListener("mousedown", () => focusRegion.focus());
293
+ span.addEventListener("click", () => appendLetter(letter));
263
294
  honeycombEl.appendChild(span);
264
295
  });
265
296
 
266
297
  updateScore();
267
298
  renderFoundWords();
268
- setFeedback("Make words with the letters. Use the center letter in every word.");
299
+ setFeedback(strings.game.hint);
269
300
  }
270
301
 
271
302
  init();
@@ -0,0 +1,53 @@
1
+ /* Wireframe Kit – button component. Uses --wire-* tokens only. */
2
+
3
+ .wire-button {
4
+ padding: var(--wire-space-md) var(--wire-space-xl);
5
+ font-family: inherit;
6
+ font-size: var(--wire-text-body);
7
+ font-weight: var(--wire-font-weight-bold);
8
+ color: var(--wire-text-primary);
9
+ background-color: var(--wire-surface);
10
+ border: var(--wire-border-width) solid var(--wire-border);
11
+ border-radius: var(--wire-radius-lg);
12
+ cursor: pointer;
13
+ }
14
+
15
+ .wire-button:hover {
16
+ background-color: var(--wire-surface-alt);
17
+ border-color: var(--wire-dark);
18
+ }
19
+
20
+ .wire-button:focus {
21
+ outline: none;
22
+ border-color: var(--wire-dark);
23
+ }
24
+
25
+ .wire-button:disabled {
26
+ color: var(--wire-text-muted);
27
+ background-color: var(--wire-surface-alt);
28
+ border-width: var(--wire-border-width);
29
+ border-color: var(--wire-border-light);
30
+ cursor: not-allowed;
31
+ }
32
+
33
+ /* Primary: use sparingly (e.g. one per view). */
34
+ .wire-button--primary {
35
+ color: var(--wire-white);
36
+ background-color: var(--wire-dark);
37
+ border-color: var(--wire-dark);
38
+ }
39
+
40
+ .wire-button--primary:hover {
41
+ background-color: var(--wire-black);
42
+ border-color: var(--wire-black);
43
+ }
44
+
45
+ .wire-button--primary:focus {
46
+ border-color: var(--wire-black);
47
+ }
48
+
49
+ .wire-button--primary:disabled {
50
+ color: var(--wire-mid);
51
+ background-color: var(--wire-lighter);
52
+ border-color: var(--wire-lighter);
53
+ }
@@ -0,0 +1,26 @@
1
+ /* Wireframe Kit – checkbox component. Uses --wire-* tokens only. */
2
+
3
+ .wire-checkbox {
4
+ display: flex;
5
+ align-items: center;
6
+ gap: var(--wire-space-sm);
7
+ cursor: pointer;
8
+ font-size: var(--wire-text-body);
9
+ color: var(--wire-text-primary);
10
+ }
11
+
12
+ .wire-checkbox input {
13
+ width: var(--wire-size-icon-md);
14
+ height: var(--wire-size-icon-md);
15
+ accent-color: var(--wire-dark);
16
+ cursor: pointer;
17
+ }
18
+
19
+ .wire-checkbox:has(input:disabled) {
20
+ color: var(--wire-text-muted);
21
+ cursor: not-allowed;
22
+ }
23
+
24
+ .wire-checkbox:has(input:disabled) input {
25
+ cursor: not-allowed;
26
+ }
@@ -0,0 +1,11 @@
1
+ /* Wireframe Kit – field component. Wraps a label and control for consistent spacing. */
2
+
3
+ .wire-field {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: var(--wire-space-2xs);
7
+ }
8
+
9
+ .wire-field .wire-label {
10
+ margin-block-end: 0;
11
+ }
@@ -0,0 +1,31 @@
1
+ /* Wireframe Kit – text input component. Uses --wire-* tokens only. */
2
+
3
+ .wire-input {
4
+ display: block;
5
+ width: 100%;
6
+ min-inline-size: 0;
7
+ padding: var(--wire-space-md) var(--wire-space-lg);
8
+ font-family: inherit;
9
+ font-size: var(--wire-text-body);
10
+ color: var(--wire-text-primary);
11
+ background-color: var(--wire-surface);
12
+ border: var(--wire-border-width) solid var(--wire-border);
13
+ border-radius: var(--wire-radius-lg);
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ .wire-input::placeholder {
18
+ color: var(--wire-text-placeholder);
19
+ }
20
+
21
+ .wire-input:focus {
22
+ outline: none;
23
+ border-color: var(--wire-dark);
24
+ }
25
+
26
+ .wire-input:disabled {
27
+ color: var(--wire-text-muted);
28
+ background-color: var(--wire-surface-alt);
29
+ border-color: var(--wire-border-light);
30
+ cursor: not-allowed;
31
+ }
@@ -0,0 +1,14 @@
1
+ /* Wireframe Kit – label component. Uses --wire-* tokens only. */
2
+
3
+ .wire-label {
4
+ display: block;
5
+ font-size: var(--wire-text-label);
6
+ font-weight: var(--wire-font-weight-bold);
7
+ color: var(--wire-text-secondary);
8
+ margin-block-end: var(--wire-space-2xs);
9
+ }
10
+
11
+ .wire-label--required::after {
12
+ content: " *";
13
+ color: var(--wire-text-muted);
14
+ }
@@ -0,0 +1,26 @@
1
+ /* Wireframe Kit – radio component. Uses --wire-* tokens only. */
2
+
3
+ .wire-radio {
4
+ display: flex;
5
+ align-items: center;
6
+ gap: var(--wire-space-sm);
7
+ cursor: pointer;
8
+ font-size: var(--wire-text-body);
9
+ color: var(--wire-text-primary);
10
+ }
11
+
12
+ .wire-radio input {
13
+ width: var(--wire-size-icon-md);
14
+ height: var(--wire-size-icon-md);
15
+ accent-color: var(--wire-dark);
16
+ cursor: pointer;
17
+ }
18
+
19
+ .wire-radio:has(input:disabled) {
20
+ color: var(--wire-text-muted);
21
+ cursor: not-allowed;
22
+ }
23
+
24
+ .wire-radio:has(input:disabled) input {
25
+ cursor: not-allowed;
26
+ }
@@ -0,0 +1,69 @@
1
+ /* Wireframe Kit – select component. Uses --wire-* tokens only. */
2
+
3
+ .wire-select,
4
+ .wire-select-wrap select {
5
+ display: block;
6
+ width: 100%;
7
+ min-inline-size: 0;
8
+ padding: var(--wire-space-md) var(--wire-space-lg);
9
+ font-family: inherit;
10
+ font-size: var(--wire-text-body);
11
+ color: var(--wire-text-primary);
12
+ background-color: var(--wire-surface);
13
+ border: var(--wire-border-width) solid var(--wire-border);
14
+ border-radius: var(--wire-radius-lg);
15
+ box-sizing: border-box;
16
+ cursor: pointer;
17
+ appearance: auto;
18
+ }
19
+
20
+ .wire-select:focus,
21
+ .wire-select-wrap select:focus {
22
+ outline: none;
23
+ border-color: var(--wire-dark);
24
+ }
25
+
26
+ .wire-select:disabled,
27
+ .wire-select-wrap select:disabled {
28
+ color: var(--wire-text-muted);
29
+ background-color: var(--wire-surface-alt);
30
+ border-color: var(--wire-border-light);
31
+ cursor: not-allowed;
32
+ }
33
+
34
+ /* Wrapper: hides native arrow and adds chevron via CSS (no icon element). */
35
+ .wire-select-wrap {
36
+ position: relative;
37
+ display: block;
38
+ }
39
+
40
+ .wire-select-wrap select {
41
+ appearance: none;
42
+ -webkit-appearance: none;
43
+ padding-inline-end: calc(var(--wire-space-lg) + var(--wire-size-icon-sm) + var(--wire-space-sm));
44
+ }
45
+
46
+ /* Chevron (Lucide-style) as pseudo-element so no JS or extra HTML is needed. */
47
+ .wire-select-wrap::after {
48
+ content: "";
49
+ position: absolute;
50
+ inset-inline-end: var(--wire-space-md);
51
+ top: 50%;
52
+ transform: translateY(-50%);
53
+ width: var(--wire-size-icon-sm);
54
+ height: var(--wire-size-icon-sm);
55
+ background-color: currentColor;
56
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") no-repeat center;
57
+ mask-size: contain;
58
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") no-repeat center;
59
+ -webkit-mask-size: contain;
60
+ pointer-events: none;
61
+ }
62
+
63
+ .wire-select-wrap {
64
+ color: var(--wire-text-muted);
65
+ }
66
+
67
+ .wire-select-wrap:has(select:disabled) {
68
+ color: var(--wire-text-placeholder);
69
+ }
@@ -0,0 +1,33 @@
1
+ /* Wireframe Kit – textarea component. Uses --wire-* tokens only. */
2
+
3
+ .wire-textarea {
4
+ display: block;
5
+ width: 100%;
6
+ min-inline-size: 0;
7
+ padding: var(--wire-space-md) var(--wire-space-lg);
8
+ font-family: inherit;
9
+ font-size: var(--wire-text-body);
10
+ line-height: var(--wire-line-height);
11
+ color: var(--wire-text-primary);
12
+ background-color: var(--wire-surface);
13
+ border: var(--wire-border-width) solid var(--wire-border);
14
+ border-radius: var(--wire-radius-lg);
15
+ box-sizing: border-box;
16
+ resize: vertical;
17
+ }
18
+
19
+ .wire-textarea::placeholder {
20
+ color: var(--wire-text-placeholder);
21
+ }
22
+
23
+ .wire-textarea:focus {
24
+ outline: none;
25
+ border-color: var(--wire-dark);
26
+ }
27
+
28
+ .wire-textarea:disabled {
29
+ color: var(--wire-text-muted);
30
+ background-color: var(--wire-surface-alt);
31
+ border-color: var(--wire-border-light);
32
+ cursor: not-allowed;
33
+ }
@@ -41,7 +41,7 @@
41
41
  /* Semantic type aliases */
42
42
  --wire-text-body: var(--wire-text-sm);
43
43
  --wire-text-caption: var(--wire-text-xs);
44
- --wire-text-label: var(--wire-text-2xs);
44
+ --wire-text-label: var(--wire-text-sm);
45
45
  --wire-text-heading: var(--wire-text-2xl);
46
46
  --wire-text-subheading: var(--wire-text-xl);
47
47
 
@@ -55,6 +55,9 @@
55
55
  --wire-space-2xl: 24px;
56
56
  --wire-space-3xl: 32px;
57
57
 
58
+ /* Border width – controls and strokes */
59
+ --wire-border-width: 2px;
60
+
58
61
  /* Border radius */
59
62
  --wire-radius-sm: 4px;
60
63
  --wire-radius-md: 6px;
@@ -0,0 +1,24 @@
1
+ {
2
+ "site": {
3
+ "title": "Wireframe",
4
+ "nav": { "start": "Start", "tokens": "Tokens", "components": "Components" }
5
+ },
6
+ "game": {
7
+ "scoreLabel": " points",
8
+ "inputLabel": "Your word",
9
+ "inputPlaceholder": "Enter a word",
10
+ "submitButton": "Guess",
11
+ "foundHeading": "Found words",
12
+ "hint": "Make words with the letters. Use the center letter in every word.",
13
+ "feedback": {
14
+ "tryAgain": "Try again.",
15
+ "tooShort": "Too short.",
16
+ "missingCenter": "Missing center letter.",
17
+ "invalidLetters": "Invalid letters.",
18
+ "notInList": "Not in word list.",
19
+ "alreadyFound": "Already found.",
20
+ "pangram": "Pangram!",
21
+ "good": "Good!"
22
+ }
23
+ }
24
+ }
@@ -1,13 +1,16 @@
1
1
  .site-nav {
2
2
  display: flex;
3
- gap: var(--wire-space-lg);
4
- padding: var(--wire-space-md) var(--wire-space-xl);
5
- border-bottom: 2px solid var(--wire-border-light);
3
+ gap: 0;
4
+ padding: 0 var(--wire-space-lg);
5
+ border-bottom: var(--wire-border-width) solid var(--wire-border-light);
6
6
  background-color: var(--wire-surface);
7
7
  }
8
8
 
9
9
  .site-nav a {
10
10
  color: var(--wire-text-secondary);
11
+ display: block;
12
+ font-weight: var(--wire-font-weight-bold);
13
+ padding: var(--wire-space-sm) var(--wire-space-md);
11
14
  text-decoration: none;
12
15
  }
13
16
 
@@ -17,5 +20,17 @@
17
20
 
18
21
  .site-nav a[aria-current="page"] {
19
22
  color: var(--wire-text-primary);
20
- font-weight: var(--wire-font-weight-bold);
23
+ position: relative;
21
24
  }
25
+
26
+ .site-nav a[aria-current="page"]::after {
27
+ content: "";
28
+ display: block;
29
+ height: var(--wire-border-width);
30
+ background-color: var(--wire-light);
31
+ border-radius: var(--wire-radius-sm) var(--wire-radius-sm) 0 0;
32
+ position: absolute;
33
+ bottom: 0;
34
+ left: 0;
35
+ right: 0;
36
+ }
@@ -1,6 +1,15 @@
1
1
  @import "../kit/tokens.css";
2
+ @import "../kit/components/button.css";
3
+ @import "../kit/components/input.css";
4
+ @import "../kit/components/textarea.css";
5
+ @import "../kit/components/checkbox.css";
6
+ @import "../kit/components/radio.css";
7
+ @import "../kit/components/select.css";
8
+ @import "../kit/components/label.css";
9
+ @import "../kit/components/field.css";
2
10
  @import "base.css";
3
11
  @import "layout.css";
4
12
  @import "components/nav.css";
5
13
  @import "pages/start.css";
6
14
  @import "pages/tokens-showcase.css";
15
+ @import "pages/components-showcase.css";
@@ -0,0 +1,38 @@
1
+ /* Components showcase page. Reuses .showcase-section from tokens-showcase. */
2
+
3
+ .components-showcase-intro {
4
+ margin-block-end: var(--wire-space-2xl);
5
+ color: var(--wire-text-muted);
6
+ }
7
+
8
+ .components-showcase code {
9
+ padding: var(--wire-space-2xs) 0;
10
+ background-color: var(--wire-surface-alt);
11
+ border-radius: var(--wire-radius-sm);
12
+ font-family: "Monaspace Argon", monospace;
13
+ font-size: var(--wire-text-xs);
14
+ color: #c41;
15
+ white-space: nowrap;
16
+ }
17
+
18
+ .component-demo-row {
19
+ display: flex;
20
+ flex-wrap: wrap;
21
+ gap: var(--wire-space-md);
22
+ align-items: center;
23
+ }
24
+
25
+ .component-demo-block {
26
+ max-inline-size: 20rem;
27
+ margin-block-end: var(--wire-space-lg);
28
+ }
29
+
30
+ .component-demo-block:last-child {
31
+ margin-block-end: 0;
32
+ }
33
+
34
+ .component-demo-stack {
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: var(--wire-space-md);
38
+ }
@@ -48,47 +48,53 @@
48
48
  --width: 3.25em;
49
49
  --height: calc(var(--width) * 0.8660254);
50
50
  --gap: var(--wire-space-xs);
51
+ box-sizing: content-box;
51
52
  position: relative;
52
53
  height: calc(3 * var(--height) + 2 * var(--gap));
53
54
  width: calc(2.5 * var(--width) + 2 * 1.1547 * var(--gap));
54
55
  margin-block-end: var(--wire-space-2xl);
55
56
  margin-inline: auto;
57
+ padding: calc(var(--height) / 2 - var(--gap)) calc(var(--width) / 2 - var(--gap));
56
58
  }
57
59
 
58
60
  /* DOM order: 1=E(center), 2=W, 3=I, 4=R, 5=F, 6=A, 7=M */
61
+ .bee-honeycomb .bee-letter:nth-child(1) {
62
+ transform: scale(var(--bee-scale, 1));
63
+ }
59
64
  .bee-honeycomb .bee-letter:nth-child(2) {
60
- transform: translate(0, calc(-1 * var(--height) - var(--gap)));
65
+ transform: translate(0, calc(-1 * var(--height) - var(--gap))) scale(var(--bee-scale, 1));
61
66
  }
62
67
  .bee-honeycomb .bee-letter:nth-child(3) {
63
68
  transform: translate(
64
69
  calc(0.75 * var(--width) + 1.1547 * var(--gap)),
65
70
  calc(-0.5 * var(--height) - 0.5 * var(--gap))
66
- );
71
+ ) scale(var(--bee-scale, 1));
67
72
  }
68
73
  .bee-honeycomb .bee-letter:nth-child(4) {
69
74
  transform: translate(
70
75
  calc(0.75 * var(--width) + 1.1547 * var(--gap)),
71
76
  calc(0.5 * var(--height) + 0.5 * var(--gap))
72
- );
77
+ ) scale(var(--bee-scale, 1));
73
78
  }
74
79
  .bee-honeycomb .bee-letter:nth-child(5) {
75
80
  transform: translate(
76
81
  calc(-0.75 * var(--width) - 1.1547 * var(--gap)),
77
82
  calc(-0.5 * var(--height) - 0.5 * var(--gap))
78
- );
83
+ ) scale(var(--bee-scale, 1));
79
84
  }
80
85
  .bee-honeycomb .bee-letter:nth-child(6) {
81
86
  transform: translate(
82
87
  calc(-0.75 * var(--width) - 1.1547 * var(--gap)),
83
88
  calc(0.5 * var(--height) + 0.5 * var(--gap))
84
- );
89
+ ) scale(var(--bee-scale, 1));
85
90
  }
86
91
  .bee-honeycomb .bee-letter:nth-child(7) {
87
- transform: translate(0, calc(var(--height) + var(--gap)));
92
+ transform: translate(0, calc(var(--height) + var(--gap))) scale(var(--bee-scale, 1));
88
93
  }
89
94
 
90
95
  .bee-letter {
91
96
  --bee-hex: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
97
+ --bee-scale: 1;
92
98
  position: absolute;
93
99
  left: 50%;
94
100
  top: 50%;
@@ -103,6 +109,12 @@
103
109
  font-size: var(--wire-text-xl);
104
110
  font-weight: var(--wire-font-weight-bold);
105
111
  color: var(--wire-text-primary);
112
+ cursor: pointer;
113
+ transition: transform 50ms ease-out;
114
+ }
115
+ .bee-letter:active,
116
+ .bee-letter.bee-letter--typed-press {
117
+ --bee-scale: 0.92;
106
118
  }
107
119
 
108
120
  .bee-letter::before {
@@ -134,6 +146,10 @@
134
146
  background-color: var(--wire-dark);
135
147
  }
136
148
 
149
+ .bee-focus-region:focus {
150
+ outline: none;
151
+ }
152
+
137
153
  /* Word input row */
138
154
  .bee-input-row {
139
155
  display: flex;
@@ -149,7 +165,7 @@
149
165
  font-size: var(--wire-text-body);
150
166
  color: var(--wire-text-primary);
151
167
  background-color: var(--wire-surface);
152
- border: 2px solid var(--wire-border);
168
+ border: var(--wire-border-width) solid var(--wire-border);
153
169
  border-radius: var(--wire-radius-lg);
154
170
  }
155
171
 
@@ -157,7 +173,8 @@
157
173
  color: var(--wire-text-placeholder);
158
174
  }
159
175
 
160
- .bee-input:focus {
176
+ .bee-input:focus,
177
+ .game-area:focus-within .bee-input {
161
178
  outline: none;
162
179
  border-color: var(--wire-dark);
163
180
  }
@@ -169,7 +186,7 @@
169
186
  font-weight: var(--wire-font-weight-bold);
170
187
  color: var(--wire-white);
171
188
  background-color: var(--wire-dark);
172
- border: 2px solid var(--wire-dark);
189
+ border: var(--wire-border-width) solid var(--wire-dark);
173
190
  border-radius: var(--wire-radius-lg);
174
191
  cursor: pointer;
175
192
  }
@@ -1,13 +1,19 @@
1
+ /* Code font/color for this page only – not part of the wireframe kit. */
2
+ @import "@fontsource/monaspace-argon/400.css";
3
+
1
4
  .token-showcase-intro {
2
5
  margin-block-end: var(--wire-space-2xl);
3
6
  color: var(--wire-text-muted);
4
7
  }
5
8
 
6
9
  .token-showcase code {
7
- padding: var(--wire-space-2xs) var(--wire-space-xs);
10
+ padding: var(--wire-space-2xs) 0;
8
11
  background-color: var(--wire-surface-alt);
9
12
  border-radius: var(--wire-radius-sm);
10
- font-size: var(--wire-text-sm);
13
+ font-family: "Monaspace Argon", monospace;
14
+ font-size: var(--wire-text-xs);
15
+ color: #c41;
16
+ white-space: nowrap;
11
17
  }
12
18
 
13
19
  .showcase-section {
@@ -69,7 +75,7 @@
69
75
  aspect-ratio: 1;
70
76
  border-radius: var(--wire-radius-md);
71
77
  flex-shrink: 0;
72
- border: 1px solid var(--wire-dark);
78
+ border: var(--wire-border-width) solid var(--wire-dark);
73
79
  }
74
80
 
75
81
  .swatch figcaption {
@@ -220,3 +226,8 @@
220
226
  .type-scale-line:last-of-type {
221
227
  margin-block-end: 0;
222
228
  }
229
+
230
+ .type-scale--semantic .type-scale-line code {
231
+ display: block;
232
+ margin-block-start: var(--wire-space-2xs);
233
+ }
@@ -7,10 +7,7 @@
7
7
  <title>Tokens – Wireframe</title>
8
8
  </head>
9
9
  <body>
10
- <nav class="site-nav">
11
- <a href="/">Start</a>
12
- <a href="/tokens.html" aria-current="page">Tokens</a>
13
- </nav>
10
+ {% include "_partials/nav.html" %}
14
11
  <main class="token-showcase">
15
12
  <h1>Design tokens</h1>
16
13
  <p class="token-showcase-intro">This page shows the wireframe kit tokens. Use them in your CSS with <code>var(--wire-*)</code>.</p>
@@ -154,24 +151,24 @@
154
151
  <div class="typography-two-col">
155
152
  <div>
156
153
  <h3 class="type-scale-heading">Type scale</h3>
157
- <div class="type-scale">
158
- <p class="type-scale-line" style="font-size: var(--wire-text-2xs);">Sample text <code>--wire-text-2xs</code></p>
159
- <p class="type-scale-line" style="font-size: var(--wire-text-xs);">Sample text <code>--wire-text-xs</code></p>
160
- <p class="type-scale-line" style="font-size: var(--wire-text-sm);">Sample text <code>--wire-text-sm</code></p>
161
- <p class="type-scale-line" style="font-size: var(--wire-text-base);">Sample text <code>--wire-text-base</code></p>
162
- <p class="type-scale-line" style="font-size: var(--wire-text-lg);">Sample text <code>--wire-text-lg</code></p>
163
- <p class="type-scale-line" style="font-size: var(--wire-text-xl);">Sample text <code>--wire-text-xl</code></p>
164
- <p class="type-scale-line" style="font-size: var(--wire-text-2xl);">Sample text <code>--wire-text-2xl</code></p>
154
+ <div class="type-scale type-scale--semantic">
155
+ <p class="type-scale-line" style="font-size: var(--wire-text-2xs);">Sample text<code>--wire-text-2xs</code></p>
156
+ <p class="type-scale-line" style="font-size: var(--wire-text-xs);">Sample text<code>--wire-text-xs</code></p>
157
+ <p class="type-scale-line" style="font-size: var(--wire-text-sm);">Sample text<code>--wire-text-sm</code></p>
158
+ <p class="type-scale-line" style="font-size: var(--wire-text-base);">Sample text<code>--wire-text-base</code></p>
159
+ <p class="type-scale-line" style="font-size: var(--wire-text-lg);">Sample text<code>--wire-text-lg</code></p>
160
+ <p class="type-scale-line" style="font-size: var(--wire-text-xl);">Sample text<code>--wire-text-xl</code></p>
161
+ <p class="type-scale-line" style="font-size: var(--wire-text-2xl);">Sample text<code>--wire-text-2xl</code></p>
165
162
  </div>
166
163
  </div>
167
164
  <div>
168
165
  <h3 class="type-scale-heading">Semantic type</h3>
169
- <div class="type-scale">
170
- <p class="type-scale-line" style="font-size: var(--wire-text-body);">Body text <code>--wire-text-body</code></p>
171
- <p class="type-scale-line" style="font-size: var(--wire-text-heading);">Heading <code>--wire-text-heading</code></p>
172
- <p class="type-scale-line" style="font-size: var(--wire-text-subheading);">Subheading <code>--wire-text-subheading</code></p>
173
- <p class="type-scale-line" style="font-size: var(--wire-text-caption);">Caption <code>--wire-text-caption</code></p>
174
- <p class="type-scale-line" style="font-size: var(--wire-text-label);">Label <code>--wire-text-label</code></p>
166
+ <div class="type-scale type-scale--semantic">
167
+ <p class="type-scale-line" style="font-size: var(--wire-text-body);">Body text<code>--wire-text-body</code></p>
168
+ <p class="type-scale-line" style="font-size: var(--wire-text-heading);">Heading<code>--wire-text-heading</code></p>
169
+ <p class="type-scale-line" style="font-size: var(--wire-text-subheading);">Subheading<code>--wire-text-subheading</code></p>
170
+ <p class="type-scale-line" style="font-size: var(--wire-text-caption);">Caption<code>--wire-text-caption</code></p>
171
+ <p class="type-scale-line" style="font-size: var(--wire-text-label);">Label<code>--wire-text-label</code></p>
175
172
  </div>
176
173
  </div>
177
174
  </div>
@@ -1,14 +1,30 @@
1
- import { readdirSync } from "fs";
2
- import { resolve } from "path";
1
+ import { readFileSync, readdirSync } from "fs";
2
+ import { dirname, resolve } from "path";
3
+ import { fileURLToPath } from "url";
3
4
  import { defineConfig } from "vite";
5
+ import nunjucks from "vite-plugin-nunjucks";
4
6
 
5
- const root = __dirname;
7
+ const root = dirname(fileURLToPath(import.meta.url));
6
8
  const htmlFiles = readdirSync(root).filter((f) => f.endsWith(".html"));
7
9
  const input = Object.fromEntries(
8
10
  htmlFiles.map((f) => [f.replace(/\.html$/, ""), resolve(root, f)])
9
11
  );
10
12
 
13
+ const strings = JSON.parse(
14
+ readFileSync(resolve(root, "src/strings.json"), "utf-8")
15
+ );
16
+
11
17
  export default defineConfig({
18
+ plugins: [
19
+ nunjucks({
20
+ templatesDir: root,
21
+ variables: {
22
+ "index.html": { ...strings, current: "start" },
23
+ "tokens.html": { ...strings, current: "tokens" },
24
+ "components.html": { ...strings, current: "components" },
25
+ },
26
+ }),
27
+ ],
12
28
  build: {
13
29
  rollupOptions: { input },
14
30
  },