@buttonschool/create-wireframe 0.1.1 → 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/README.md +4 -0
- package/index.js +15 -2
- package/package.json +2 -2
- package/template/.editorconfig +8 -0
- package/template/AGENTS.md +12 -2
- package/template/_partials/nav.html +5 -0
- package/template/components.html +122 -0
- package/template/index.html +25 -2
- package/template/package.json +3 -1
- package/template/src/game.js +302 -0
- package/template/src/kit/components/button.css +53 -0
- package/template/src/kit/components/checkbox.css +26 -0
- package/template/src/kit/components/field.css +11 -0
- package/template/src/kit/components/input.css +31 -0
- package/template/src/kit/components/label.css +14 -0
- package/template/src/kit/components/radio.css +26 -0
- package/template/src/kit/components/select.css +69 -0
- package/template/src/kit/components/textarea.css +33 -0
- package/template/src/kit/tokens.css +28 -0
- package/template/src/main.js +1 -6
- package/template/src/strings.json +24 -0
- package/template/src/{style.css → styles/base.css} +7 -7
- package/template/src/styles/components/nav.css +36 -0
- package/template/src/styles/layout.css +5 -0
- package/template/src/styles/main.css +15 -0
- package/template/src/styles/pages/components-showcase.css +38 -0
- package/template/src/styles/pages/start.css +243 -0
- package/template/src/styles/pages/tokens-showcase.css +233 -0
- package/template/tokens.html +240 -0
- package/template/vite.config.js +29 -1
package/README.md
CHANGED
|
@@ -10,13 +10,17 @@ cd my-app
|
|
|
10
10
|
pnpm dev
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
Or with npm or yarn: `npm create @buttonschool/wireframe my-app` / `yarn create @buttonschool/wireframe my-app`. The CLI runs the matching install command (pnpm, npm, or yarn). Then `pnpm build` and `pnpm preview` (or `npm run build` / `npm run preview`) to build and preview production.
|
|
14
|
+
|
|
13
15
|
## What’s included
|
|
14
16
|
|
|
15
17
|
- **Vite** – dev server with HMR, `pnpm dev` / `pnpm build` / `pnpm preview`
|
|
18
|
+
- **Two-page starter** – Start (small word game, Spelling Bee–style) and token showcase; CSS is linked from HTML (`<link>` in each page’s head); every root-level `.html` file is automatically a page (add a file and a nav link, no config change)
|
|
16
19
|
- **Design tokens** – `src/kit/tokens.css`: grayscale palette, semantic color aliases, spacing scale, border radius, typography (Comic Neue)
|
|
17
20
|
- **Comic Neue** – via [@fontsource/comic-neue](https://fontsource.org/fonts/comic-neue) (OFL-1.1)
|
|
18
21
|
- **Lucide** – dependency only; [Lucide icons](https://lucide.dev) – import and render as SVG as needed
|
|
19
22
|
- **AGENTS.md** – short instructions for AI/code-gen (tokens location, dev server, icons)
|
|
23
|
+
- **Accessibility** – starter includes basic a11y: live regions, current-page marking on nav, and visually hidden labels where needed; extend as you build.
|
|
20
24
|
|
|
21
25
|
First version: tokens + font + icons only, no component CSS. Not opinionated; add your own structure.
|
|
22
26
|
|
package/index.js
CHANGED
|
@@ -8,6 +8,14 @@ import { spawn } from "child_process";
|
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const templateDir = join(__dirname, "template");
|
|
10
10
|
|
|
11
|
+
function getPackageManager() {
|
|
12
|
+
const execPath = process.env.npm_execpath || "";
|
|
13
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
|
14
|
+
if (execPath.includes("pnpm") || userAgent.startsWith("pnpm/")) return "pnpm";
|
|
15
|
+
if (execPath.includes("yarn") || userAgent.startsWith("yarn/")) return "yarn";
|
|
16
|
+
return "npm";
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
const projectName = process.argv[2];
|
|
12
20
|
if (!projectName) {
|
|
13
21
|
console.error("Usage: pnpm create @buttonschool/wireframe <project-name>");
|
|
@@ -27,12 +35,17 @@ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
|
27
35
|
pkg.name = projectName;
|
|
28
36
|
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
29
37
|
|
|
30
|
-
const
|
|
38
|
+
const pm = getPackageManager();
|
|
39
|
+
const installCmd = pm === "pnpm" ? "pnpm" : pm === "yarn" ? "yarn" : "npm";
|
|
40
|
+
const installArgs = pm === "npm" ? ["install"] : ["install"];
|
|
41
|
+
const devCmd = pm === "pnpm" ? "pnpm dev" : pm === "yarn" ? "yarn dev" : "npm run dev";
|
|
42
|
+
|
|
43
|
+
const child = spawn(installCmd, installArgs, {
|
|
31
44
|
cwd: targetDir,
|
|
32
45
|
stdio: "inherit",
|
|
33
46
|
shell: true,
|
|
34
47
|
});
|
|
35
48
|
child.on("close", (code) => {
|
|
36
49
|
if (code !== 0) process.exit(code);
|
|
37
|
-
console.log(`\nDone. Run:\n cd ${projectName}\n
|
|
50
|
+
console.log(`\nDone. Run:\n cd ${projectName}\n ${devCmd}\n`);
|
|
38
51
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buttonschool/create-wireframe",
|
|
3
|
-
"version": "0.
|
|
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": [
|
package/template/AGENTS.md
CHANGED
|
@@ -2,10 +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 **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
|
+
|
|
7
|
+
## Styles
|
|
8
|
+
|
|
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
|
+
|
|
5
11
|
## Tokens
|
|
6
12
|
|
|
7
13
|
- **Location:** `src/kit/tokens.css`
|
|
8
|
-
- **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`). Use them in your CSS, e.g. `color: var(--wire-text-primary);`, `padding: var(--wire-space-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);`.
|
|
9
15
|
|
|
10
16
|
## Dev server
|
|
11
17
|
|
|
@@ -13,4 +19,8 @@ This is a **wireframe prototype** scaffold. Stack: **Vite** (vanilla JS), design
|
|
|
13
19
|
|
|
14
20
|
## Icons
|
|
15
21
|
|
|
16
|
-
- **Lucide** is installed. Import icons and render as SVG (e.g. use the `lucide` package API or copy SVGs from [Lucide](https://lucide.dev)). Prefer semantic icon names; keep icons consistent with wireframe style.
|
|
22
|
+
- **Lucide** is installed. Import icons and render as SVG (e.g. use the `lucide` package API or copy SVGs from [Lucide](https://lucide.dev)). Prefer semantic icon names; keep icons consistent with wireframe style. Example: the Start page uses `data-lucide="crown"` in the HTML and `createIcons({ icons: { Crown } })` from `lucide` in JS to render the crown icon.
|
|
23
|
+
|
|
24
|
+
## Accessibility
|
|
25
|
+
|
|
26
|
+
The starter includes basic a11y: `aria-live` for dynamic feedback, `aria-current="page"` on the nav, and `.visually-hidden` for labels. Extend with focus management, landmarks, and ARIA as needed.
|
|
@@ -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>
|
package/template/index.html
CHANGED
|
@@ -3,10 +3,33 @@
|
|
|
3
3
|
<head>
|
|
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>{{ site.title }}</title>
|
|
7
8
|
</head>
|
|
8
9
|
<body>
|
|
9
|
-
|
|
10
|
+
{% include "_partials/nav.html" %}
|
|
11
|
+
<main class="game-area">
|
|
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>
|
|
17
|
+
<input
|
|
18
|
+
type="text"
|
|
19
|
+
id="bee-word"
|
|
20
|
+
class="bee-input"
|
|
21
|
+
placeholder="{{ game.inputPlaceholder }}"
|
|
22
|
+
autocomplete="off"
|
|
23
|
+
autocapitalize="off"
|
|
24
|
+
maxlength="15"
|
|
25
|
+
/>
|
|
26
|
+
<button type="button" class="bee-submit">{{ game.submitButton }}</button>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<p class="bee-feedback" aria-live="polite"></p>
|
|
30
|
+
<h2 class="bee-found-heading">{{ game.foundHeading }}</h2>
|
|
31
|
+
<ul class="bee-found-list" aria-live="polite"></ul>
|
|
32
|
+
</main>
|
|
10
33
|
<script type="module" src="/src/main.js"></script>
|
|
11
34
|
</body>
|
|
12
35
|
</html>
|
package/template/package.json
CHANGED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spelling Bee clone – pangram WIREFRAME.
|
|
3
|
+
* Letters: W, I, R, E, F, A, M. Center letter E (required in every word).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createIcons, Crown } from "lucide";
|
|
7
|
+
import strings from "./strings.json";
|
|
8
|
+
|
|
9
|
+
const LETTERS = ["W", "I", "R", "E", "F", "A", "M"];
|
|
10
|
+
const CENTER_LETTER = "E";
|
|
11
|
+
const MIN_LENGTH = 4;
|
|
12
|
+
const STORAGE_KEY = "wireframe-bee-found";
|
|
13
|
+
|
|
14
|
+
const WORD_LIST = new Set([
|
|
15
|
+
"aerie",
|
|
16
|
+
"aerier",
|
|
17
|
+
"afar",
|
|
18
|
+
"afear",
|
|
19
|
+
"affair",
|
|
20
|
+
"affirm",
|
|
21
|
+
"affirmer",
|
|
22
|
+
"afire",
|
|
23
|
+
"aimer",
|
|
24
|
+
"airer",
|
|
25
|
+
"airfare",
|
|
26
|
+
"airframe",
|
|
27
|
+
"airier",
|
|
28
|
+
"area",
|
|
29
|
+
"arear",
|
|
30
|
+
"aria",
|
|
31
|
+
"arrear",
|
|
32
|
+
"aware",
|
|
33
|
+
"awarer",
|
|
34
|
+
"eerie",
|
|
35
|
+
"eerier",
|
|
36
|
+
"emir",
|
|
37
|
+
"ewer",
|
|
38
|
+
"faerie",
|
|
39
|
+
"fair",
|
|
40
|
+
"fairer",
|
|
41
|
+
"fame",
|
|
42
|
+
"fare",
|
|
43
|
+
"farer",
|
|
44
|
+
"farm",
|
|
45
|
+
"farmer",
|
|
46
|
+
"farmwife",
|
|
47
|
+
"farrier",
|
|
48
|
+
"fear",
|
|
49
|
+
"fearer",
|
|
50
|
+
"femme",
|
|
51
|
+
"ferm",
|
|
52
|
+
"fermi",
|
|
53
|
+
"fewer",
|
|
54
|
+
"fief",
|
|
55
|
+
"fierier",
|
|
56
|
+
"fife",
|
|
57
|
+
"fifer",
|
|
58
|
+
"fire",
|
|
59
|
+
"firearm",
|
|
60
|
+
"firer",
|
|
61
|
+
"firm",
|
|
62
|
+
"firmer",
|
|
63
|
+
"firmware",
|
|
64
|
+
"frame",
|
|
65
|
+
"framer",
|
|
66
|
+
"free",
|
|
67
|
+
"freer",
|
|
68
|
+
"freeware",
|
|
69
|
+
"friar",
|
|
70
|
+
"frier",
|
|
71
|
+
"iffier",
|
|
72
|
+
"imam",
|
|
73
|
+
"mafia",
|
|
74
|
+
"maim",
|
|
75
|
+
"maimer",
|
|
76
|
+
"mama",
|
|
77
|
+
"mamma",
|
|
78
|
+
"mare",
|
|
79
|
+
"marm",
|
|
80
|
+
"marrer",
|
|
81
|
+
"marrier",
|
|
82
|
+
"meme",
|
|
83
|
+
"mere",
|
|
84
|
+
"merer",
|
|
85
|
+
"merrier",
|
|
86
|
+
"miff",
|
|
87
|
+
"miffier",
|
|
88
|
+
"mime",
|
|
89
|
+
"mimer",
|
|
90
|
+
"mire",
|
|
91
|
+
"raffia",
|
|
92
|
+
"rammer",
|
|
93
|
+
"rare",
|
|
94
|
+
"rarefier",
|
|
95
|
+
"rarer",
|
|
96
|
+
"rawer",
|
|
97
|
+
"reaffirm",
|
|
98
|
+
"ream",
|
|
99
|
+
"reamer",
|
|
100
|
+
"rear",
|
|
101
|
+
"rearer",
|
|
102
|
+
"rearm",
|
|
103
|
+
"reef",
|
|
104
|
+
"reefer",
|
|
105
|
+
"refer",
|
|
106
|
+
"referee",
|
|
107
|
+
"referrer",
|
|
108
|
+
"refire",
|
|
109
|
+
"reframe",
|
|
110
|
+
"reifier",
|
|
111
|
+
"rewarm",
|
|
112
|
+
"rewear",
|
|
113
|
+
"rewire",
|
|
114
|
+
"rife",
|
|
115
|
+
"riff",
|
|
116
|
+
"riffraff",
|
|
117
|
+
"rime",
|
|
118
|
+
"rimer",
|
|
119
|
+
"rimfire",
|
|
120
|
+
"rimmer",
|
|
121
|
+
"wafer",
|
|
122
|
+
"waif",
|
|
123
|
+
"ware",
|
|
124
|
+
"warfare",
|
|
125
|
+
"warfarer",
|
|
126
|
+
"warier",
|
|
127
|
+
"warm",
|
|
128
|
+
"warmer",
|
|
129
|
+
"wear",
|
|
130
|
+
"wearer",
|
|
131
|
+
"wearier",
|
|
132
|
+
"weer",
|
|
133
|
+
"weir",
|
|
134
|
+
"were",
|
|
135
|
+
"wife",
|
|
136
|
+
"wire",
|
|
137
|
+
"wireframe",
|
|
138
|
+
"wirer",
|
|
139
|
+
"wirier",
|
|
140
|
+
"wrier",
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
function isPangram(word) {
|
|
144
|
+
const lower = word.toLowerCase();
|
|
145
|
+
return LETTERS.every((letter) => lower.includes(letter.toLowerCase()));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function usesOnlyLetters(word) {
|
|
149
|
+
const allowed = new Set(LETTERS.map((l) => l.toLowerCase()));
|
|
150
|
+
return [...word.toLowerCase()].every((ch) => allowed.has(ch));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hasCenterLetter(word) {
|
|
154
|
+
return word.toLowerCase().includes(CENTER_LETTER.toLowerCase());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function scoreWord(word) {
|
|
158
|
+
const len = word.length;
|
|
159
|
+
if (len < MIN_LENGTH) return 0;
|
|
160
|
+
let pts = len === 4 ? 1 : len === 5 ? 2 : len === 6 ? 3 : 4;
|
|
161
|
+
if (isPangram(word)) pts += 7;
|
|
162
|
+
return pts;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function validate(word) {
|
|
166
|
+
const w = word.trim().toLowerCase();
|
|
167
|
+
if (!w) return { ok: false, message: "" };
|
|
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 };
|
|
172
|
+
return { ok: true, word: w };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function init() {
|
|
176
|
+
createIcons({ icons: { Crown } });
|
|
177
|
+
|
|
178
|
+
const area = document.querySelector(".game-area");
|
|
179
|
+
if (!area) return;
|
|
180
|
+
|
|
181
|
+
const focusRegion = area.querySelector(".bee-focus-region");
|
|
182
|
+
const scoreEl = area.querySelector(".bee-score");
|
|
183
|
+
const honeycombEl = area.querySelector(".bee-honeycomb");
|
|
184
|
+
const inputEl = area.querySelector(".bee-input");
|
|
185
|
+
const submitBtn = area.querySelector(".bee-submit");
|
|
186
|
+
const feedbackEl = area.querySelector(".bee-feedback");
|
|
187
|
+
const foundListEl = area.querySelector(".bee-found-list");
|
|
188
|
+
|
|
189
|
+
if (!focusRegion || !scoreEl || !honeycombEl || !inputEl || !submitBtn || !feedbackEl || !foundListEl) return;
|
|
190
|
+
|
|
191
|
+
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
|
|
192
|
+
const loaded = Array.isArray(saved)
|
|
193
|
+
? saved.filter((w) => typeof w === "string" && WORD_LIST.has(w.toLowerCase()))
|
|
194
|
+
: [];
|
|
195
|
+
const foundOrdered = [...new Set(loaded)];
|
|
196
|
+
const found = new Set(foundOrdered);
|
|
197
|
+
let totalScore = foundOrdered.reduce((sum, w) => sum + scoreWord(w), 0);
|
|
198
|
+
|
|
199
|
+
function setFeedback(text, success = false, error = false) {
|
|
200
|
+
feedbackEl.textContent = text;
|
|
201
|
+
feedbackEl.classList.toggle("bee-feedback--success", success);
|
|
202
|
+
feedbackEl.classList.toggle("bee-feedback--error", error);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function updateScore() {
|
|
206
|
+
const val = scoreEl.querySelector(".bee-score-value");
|
|
207
|
+
if (val) val.textContent = totalScore;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function saveFound() {
|
|
211
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(foundOrdered));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function renderFoundWords() {
|
|
215
|
+
foundListEl.innerHTML = "";
|
|
216
|
+
const sorted = [...foundOrdered].sort((a, b) => a.localeCompare(b));
|
|
217
|
+
sorted.forEach((word) => {
|
|
218
|
+
const li = document.createElement("li");
|
|
219
|
+
li.textContent = word;
|
|
220
|
+
if (isPangram(word)) li.classList.add("bee-pangram");
|
|
221
|
+
foundListEl.appendChild(li);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function submitWord() {
|
|
226
|
+
const raw = inputEl.value;
|
|
227
|
+
const result = validate(raw);
|
|
228
|
+
if (!result.ok) {
|
|
229
|
+
setFeedback(result.message || strings.game.feedback.tryAgain, false, true);
|
|
230
|
+
inputEl.value = "";
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const { word } = result;
|
|
234
|
+
if (found.has(word)) {
|
|
235
|
+
setFeedback(strings.game.feedback.alreadyFound, false, true);
|
|
236
|
+
inputEl.value = "";
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
foundOrdered.push(word);
|
|
240
|
+
found.add(word);
|
|
241
|
+
totalScore += scoreWord(word);
|
|
242
|
+
saveFound();
|
|
243
|
+
setFeedback(isPangram(word) ? strings.game.feedback.pangram : strings.game.feedback.good, true, false);
|
|
244
|
+
inputEl.value = "";
|
|
245
|
+
inputEl.focus();
|
|
246
|
+
updateScore();
|
|
247
|
+
renderFoundWords();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
submitBtn.addEventListener("click", submitWord);
|
|
251
|
+
inputEl.addEventListener("keydown", (e) => {
|
|
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
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
function appendLetter(letter) {
|
|
271
|
+
if (inputEl.value.length >= 15) return;
|
|
272
|
+
inputEl.value += letter.toLowerCase();
|
|
273
|
+
inputEl.focus();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
honeycombEl.innerHTML = "";
|
|
277
|
+
const outer = LETTERS.filter((l) => l !== CENTER_LETTER);
|
|
278
|
+
const centerNode = document.createElement("span");
|
|
279
|
+
centerNode.className = "bee-letter bee-letter--center";
|
|
280
|
+
centerNode.textContent = CENTER_LETTER;
|
|
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));
|
|
285
|
+
honeycombEl.appendChild(centerNode);
|
|
286
|
+
outer.forEach((letter) => {
|
|
287
|
+
const span = document.createElement("span");
|
|
288
|
+
span.className = "bee-letter";
|
|
289
|
+
span.textContent = letter;
|
|
290
|
+
span.setAttribute("aria-hidden", "true");
|
|
291
|
+
span.setAttribute("data-letter", letter);
|
|
292
|
+
span.addEventListener("mousedown", () => focusRegion.focus());
|
|
293
|
+
span.addEventListener("click", () => appendLetter(letter));
|
|
294
|
+
honeycombEl.appendChild(span);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
updateScore();
|
|
298
|
+
renderFoundWords();
|
|
299
|
+
setFeedback(strings.game.hint);
|
|
300
|
+
}
|
|
301
|
+
|
|
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
|
+
}
|