flet-box 1.0.8 → 1.1.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 +3 -0
- package/bin/commands/checkUi.js +53 -3
- package/bin/commands/create.js +44 -19
- package/bin/utils/skills.js +59 -0
- package/bin/utils/templates.js +143 -511
- package/docs/README.md +3 -1
- package/docs/cli/README.md +1 -1
- package/docs/guides/README.md +3 -1
- package/docs/guides/clean-ui.md +35 -17
- package/docs/guides/mobile-and-platforms.md +31 -0
- package/docs/tools/theme.md +1 -0
- package/docs/widget/README.md +2 -0
- package/package.json +3 -2
- package/skills/fletbox-design/SKILL.md +573 -0
- package/skills/fletbox-memory/SKILL.md +134 -0
- package/skills/fletbox-motion/SKILL.md +168 -0
- package/skills/fletbox-rules/SKILL.md +154 -0
- package/src/utils/themes.js +64 -4
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Real DOM · No Virtual DOM · No build config to learn.
|
|
|
23
23
|
<br><br>
|
|
24
24
|
|
|
25
25
|
<a href="https://kuko53348.github.io/flet-box-docs/">📖 Docs</a> •
|
|
26
|
+
<a href="https://kuko53348.github.io/flet-box-widgets-exemples/">🧪 Widget gallery</a> •
|
|
26
27
|
<a href="#quick-start">Quick start</a> •
|
|
27
28
|
<a href="https://www.youtube.com/@flet-box">▶ YouTube</a> •
|
|
28
29
|
<a href="#support--sponsorship">❤️ Sponsor</a>
|
|
@@ -157,6 +158,8 @@ Read the docs online: **[The FletBox Book — kuko53348.github.io/flet-box-docs]
|
|
|
157
158
|
|
|
158
159
|
They are **one continuous book** — the cover maps every page: [The FletBox Book source](docs/README.md).
|
|
159
160
|
|
|
161
|
+
Rather than read, click: **[the live widget gallery](https://kuko53348.github.io/flet-box-widgets-exemples/)** gives every widget its own page, renders each example in the browser it was written for, and prints the exact call underneath it. The theme toggle in the corner shows the same screen in light and dark, which is the honest way to judge a default color.
|
|
162
|
+
|
|
160
163
|
- [Your first app](docs/guides/first-app.md) — build a working todo app in 20 minutes
|
|
161
164
|
- [The widget book](docs/widget/README.md) — every widget with property tables and examples
|
|
162
165
|
- [State, Router & Services](docs/guides/state.md) — shared state, routing, storage, and HTTP
|
package/bin/commands/checkUi.js
CHANGED
|
@@ -109,13 +109,54 @@ export const WARN_CODES = new Set([
|
|
|
109
109
|
"UNCONSTRAINED_FORM_WIDTH",
|
|
110
110
|
]);
|
|
111
111
|
|
|
112
|
-
const INFO_CODES = new Set([
|
|
112
|
+
export const INFO_CODES = new Set([
|
|
113
113
|
"SMALL_PRESET",
|
|
114
114
|
"CROWDED_FORM",
|
|
115
115
|
"LIST_WITHOUT_STATE",
|
|
116
116
|
"MULTIPLE_PRIMARY_ACTIONS",
|
|
117
117
|
]);
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Which design rule each code proves, and the number the finding prints.
|
|
121
|
+
*
|
|
122
|
+
* The gate is the machine half of a contract whose human half is
|
|
123
|
+
* `skills/fletbox-design/SKILL.md`: 25 numbered rules, read by the agent that
|
|
124
|
+
* builds the screen. A finding that says `OFF_SCALE_SPACING` tells the reader the
|
|
125
|
+
* value is wrong; a finding that says `OFF_SCALE_SPACING — rule 07` tells them to
|
|
126
|
+
* open rule 07 and learn *why*, which is the only way the next screen is right
|
|
127
|
+
* without being told twice. `scripts/check-skills.mjs` fails if a code is missing
|
|
128
|
+
* here, if a number here is not a heading in the skill, or if the skill does not
|
|
129
|
+
* name that code inside the rule it is filed under — so this table and that file
|
|
130
|
+
* cannot drift apart.
|
|
131
|
+
*/
|
|
132
|
+
export const CODE_RULE = {
|
|
133
|
+
RAW_COLOR: "03",
|
|
134
|
+
OFF_SCALE_SPACING: "07",
|
|
135
|
+
OFF_SCALE_RADIUS: "03",
|
|
136
|
+
OFF_SCALE_TYPE: "08",
|
|
137
|
+
SMALL_TAP_TARGET: "03",
|
|
138
|
+
LOW_CONTRAST: "11",
|
|
139
|
+
UNLABELED_FIELD: "13",
|
|
140
|
+
UNLABELED_TOGGLE: "11",
|
|
141
|
+
DESTRUCTIVE_COLOR: "14",
|
|
142
|
+
UNREADABLE: "00",
|
|
143
|
+
LOW_CONTRAST_SOFT: "11",
|
|
144
|
+
LOW_CONTRAST_UI: "11",
|
|
145
|
+
LOW_CONTRAST_ON_SURFACE: "11",
|
|
146
|
+
HAND_MIXED_COLOR: "03",
|
|
147
|
+
DEAD_ELEVATION: "10",
|
|
148
|
+
IMAGE_NO_ALT: "11",
|
|
149
|
+
ICON_ONLY_CONTROL: "11",
|
|
150
|
+
CENTERED_BODY_TEXT: "08",
|
|
151
|
+
GENERIC_ACTION_LABEL: "14",
|
|
152
|
+
CONFIRM_ORDER: "14",
|
|
153
|
+
UNCONSTRAINED_FORM_WIDTH: "13",
|
|
154
|
+
SMALL_PRESET: "03",
|
|
155
|
+
CROWDED_FORM: "06",
|
|
156
|
+
LIST_WITHOUT_STATE: "12",
|
|
157
|
+
MULTIPLE_PRIMARY_ACTIONS: "04",
|
|
158
|
+
};
|
|
159
|
+
|
|
119
160
|
/**
|
|
120
161
|
* The legal home for a hex value is the app's own token file. Anything else in
|
|
121
162
|
* `src/` that types a color by hand is the drift this rule exists to catch.
|
|
@@ -1319,7 +1360,15 @@ export const checkUi = async (argv = [], io = {}) => {
|
|
|
1319
1360
|
|
|
1320
1361
|
if (asJson) {
|
|
1321
1362
|
print(
|
|
1322
|
-
JSON.stringify(
|
|
1363
|
+
JSON.stringify(
|
|
1364
|
+
{
|
|
1365
|
+
package: api.pkg.version,
|
|
1366
|
+
scanned: files.length,
|
|
1367
|
+
findings: findings.map((item) => ({ rule: CODE_RULE[item.code] || null, ...item })),
|
|
1368
|
+
},
|
|
1369
|
+
null,
|
|
1370
|
+
2,
|
|
1371
|
+
),
|
|
1323
1372
|
);
|
|
1324
1373
|
} else {
|
|
1325
1374
|
print(
|
|
@@ -1338,10 +1387,11 @@ export const checkUi = async (argv = [], io = {}) => {
|
|
|
1338
1387
|
byCode.get(item.code).push(item);
|
|
1339
1388
|
}
|
|
1340
1389
|
for (const [code, list] of byCode) {
|
|
1390
|
+
const rule = CODE_RULE[code];
|
|
1341
1391
|
print(
|
|
1342
1392
|
section(
|
|
1343
1393
|
"•",
|
|
1344
|
-
`${code} — ${list.length} finding(s)`,
|
|
1394
|
+
`${code}${rule ? ` — rule ${rule}` : ""} — ${list.length} finding(s)`,
|
|
1345
1395
|
ERROR_CODES.has(code) ? "#f87171" : INFO_CODES.has(code) ? "#94a3b8" : "#fbbf24",
|
|
1346
1396
|
),
|
|
1347
1397
|
);
|
package/bin/commands/create.js
CHANGED
|
@@ -19,8 +19,16 @@ import * as templates from "../utils/templates.js";
|
|
|
19
19
|
import { parseGithubRepo } from "../utils/bundleConfig.js";
|
|
20
20
|
import { checkApi } from "../commands/checkApi.js";
|
|
21
21
|
import { unpack } from "../commands/unpack.js";
|
|
22
|
+
import { skillNames, readSkill } from "../utils/skills.js";
|
|
22
23
|
import { c, banner, gradient } from "../utils/colors.js";
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Skills that every screen needs, whatever the task. Their Cursor twin rule loads
|
|
27
|
+
* with the file instead of waiting to be picked by description; the rest are
|
|
28
|
+
* read when their description matches what the agent is about to do.
|
|
29
|
+
*/
|
|
30
|
+
const ALWAYS_ON_SKILLS = new Set(["fletbox-rules", "fletbox-design"]);
|
|
31
|
+
|
|
24
32
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
33
|
const __dirname = path.dirname(__filename);
|
|
26
34
|
|
|
@@ -216,8 +224,8 @@ export const createProject = async (projectName, rawArgs = []) => {
|
|
|
216
224
|
".fletbox/api",
|
|
217
225
|
".fletbox/memory/notes",
|
|
218
226
|
".claude",
|
|
219
|
-
".claude/skills
|
|
220
|
-
".qoder/skills
|
|
227
|
+
".claude/skills",
|
|
228
|
+
".qoder/skills",
|
|
221
229
|
".cursor/rules",
|
|
222
230
|
];
|
|
223
231
|
const extraDirs =
|
|
@@ -437,23 +445,36 @@ export const createProject = async (projectName, rawArgs = []) => {
|
|
|
437
445
|
templates.cursorRules(),
|
|
438
446
|
);
|
|
439
447
|
|
|
440
|
-
// The
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
);
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
448
|
+
// The agent skills: `skills/` at the package root is the only source, one
|
|
449
|
+
// kebab-case folder per subject (rules, design, motion, memory). The app gets
|
|
450
|
+
// the same text twice over: once as `skills/` at its own root — the folder any
|
|
451
|
+
// assistant or person reads first, and the one AGENTS.md §0 indexes — and once
|
|
452
|
+
// mirrored into `.claude/skills/` and `.qoder/skills/`, because Claude Code and
|
|
453
|
+
// Qoder load a skill by frontmatter description from those two roots and offer it
|
|
454
|
+
// without being told. Cursor has no skills folder, so each skill also ships as a
|
|
455
|
+
// pointer rule in `.cursor/rules/`. One text, three loaders, never three edits.
|
|
456
|
+
const skillList = skillNames();
|
|
457
|
+
if (skillList.length === 0) {
|
|
458
|
+
console.log(
|
|
459
|
+
c(
|
|
460
|
+
"yellow",
|
|
461
|
+
"⚠️ This flet-box package ships no skills/ folder — the app will have no design or motion contract.",
|
|
462
|
+
),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
for (const name of skillList) {
|
|
466
|
+
const text = readSkill(name);
|
|
467
|
+
createFile(path.join(projectPath, "skills", name, "SKILL.md"), text);
|
|
468
|
+
createFile(
|
|
469
|
+
path.join(projectPath, ".claude/skills", name, "SKILL.md"),
|
|
470
|
+
text,
|
|
471
|
+
);
|
|
472
|
+
createFile(path.join(projectPath, ".qoder/skills", name, "SKILL.md"), text);
|
|
473
|
+
createFile(
|
|
474
|
+
path.join(projectPath, ".cursor/rules", `${name}.mdc`),
|
|
475
|
+
templates.skillCursorRule(name, { alwaysApply: ALWAYS_ON_SKILLS.has(name) }),
|
|
476
|
+
);
|
|
477
|
+
}
|
|
457
478
|
createFile(
|
|
458
479
|
path.join(projectPath, "scripts/check-ui.mjs"),
|
|
459
480
|
templates.checkUiScript(finalName),
|
|
@@ -467,6 +488,10 @@ export const createProject = async (projectName, rawArgs = []) => {
|
|
|
467
488
|
path.join(projectPath, ".fletbox/memory/notes/baseline.md"),
|
|
468
489
|
templates.memoryBaselineMd(finalName, template, pkgVersion),
|
|
469
490
|
);
|
|
491
|
+
createFile(
|
|
492
|
+
path.join(projectPath, ".fletbox/memory/notes/current-status.md"),
|
|
493
|
+
templates.memoryStatusMd(finalName, template),
|
|
494
|
+
);
|
|
470
495
|
createFile(
|
|
471
496
|
path.join(projectPath, "scripts/check-app.mjs"),
|
|
472
497
|
templates.checkAppScript(finalName),
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// bin/utils/skills.js
|
|
2
|
+
//
|
|
3
|
+
// The `skills/` folder at the package root is the ONLY source for the agent
|
|
4
|
+
// skills. Claude Code and Qoder load `.claude/skills/<name>/SKILL.md` and
|
|
5
|
+
// `.qoder/skills/<name>/SKILL.md` by frontmatter description, so `flet-box create`
|
|
6
|
+
// copies each file into both roots byte-for-byte. Keeping the text in a real
|
|
7
|
+
// markdown file (instead of a 450-line array of strings in templates.js) means an
|
|
8
|
+
// editor, a diff and `scripts/check-skills.mjs` can all read the same bytes the
|
|
9
|
+
// agent will.
|
|
10
|
+
//
|
|
11
|
+
// Node core only: `bin/` may not import a devDependency at module scope.
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const packageDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
18
|
+
|
|
19
|
+
/** Absolute path of the folder that holds every shipped skill. */
|
|
20
|
+
export const skillRoot = () => path.join(packageDir, "skills");
|
|
21
|
+
|
|
22
|
+
/** Kebab-case folder names, sorted, skipping anything without a SKILL.md. */
|
|
23
|
+
export const skillNames = () => {
|
|
24
|
+
let entries = [];
|
|
25
|
+
try {
|
|
26
|
+
entries = fs.readdirSync(skillRoot(), { withFileTypes: true });
|
|
27
|
+
} catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return entries
|
|
31
|
+
.filter((entry) => entry.isDirectory())
|
|
32
|
+
.map((entry) => entry.name)
|
|
33
|
+
.filter((name) => fs.existsSync(path.join(skillRoot(), name, "SKILL.md")))
|
|
34
|
+
.sort();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The full skill text, frontmatter included.
|
|
39
|
+
* @throws when the skill does not exist — a missing skill is a build error, not
|
|
40
|
+
* a silent skip, or an app ships with half the contract and no one notices.
|
|
41
|
+
*/
|
|
42
|
+
export const readSkill = (name) =>
|
|
43
|
+
fs.readFileSync(path.join(skillRoot(), name, "SKILL.md"), "utf8");
|
|
44
|
+
|
|
45
|
+
/** The `key: value` lines between the two `---` fences. */
|
|
46
|
+
export const readFrontmatter = (name) => {
|
|
47
|
+
const text = readSkill(name);
|
|
48
|
+
const match = /^---\n([\s\S]*?)\n---/.exec(text);
|
|
49
|
+
if (!match) throw new Error(`skill "${name}" has no frontmatter block`);
|
|
50
|
+
const fields = {};
|
|
51
|
+
for (const line of match[1].split("\n")) {
|
|
52
|
+
const pair = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/.exec(line);
|
|
53
|
+
if (pair) fields[pair[1]] = pair[2];
|
|
54
|
+
}
|
|
55
|
+
return fields;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** One short sentence for a Cursor rule or an index: the description field. */
|
|
59
|
+
export const skillDescription = (name) => readFrontmatter(name).description || name;
|