justskill-design 0.0.1 → 3.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.
Files changed (67) hide show
  1. package/README.en.md +235 -0
  2. package/README.md +184 -11
  3. package/SKILL.md +435 -0
  4. package/canvas/app.mjs +2870 -0
  5. package/canvas/groups.mjs +204 -0
  6. package/canvas/index.html +141 -0
  7. package/canvas/styles.css +1032 -0
  8. package/canvas/tree.mjs +249 -0
  9. package/cordis.patch.yml +8 -0
  10. package/design.mjs +172 -0
  11. package/docs/dsh-workbench.png +0 -0
  12. package/dsh/client.js +864 -0
  13. package/dsh/index.js +354 -0
  14. package/dsh/scan.js +139 -0
  15. package/dsh/skill.js +41 -0
  16. package/lib/base-css.mjs +92 -0
  17. package/lib/detect.mjs +123 -0
  18. package/lib/diagnostics.mjs +382 -0
  19. package/lib/export-html.mjs +309 -0
  20. package/lib/export-svg.mjs +111 -0
  21. package/lib/fragment.mjs +702 -0
  22. package/lib/geometry.mjs +827 -0
  23. package/lib/icons.mjs +106 -0
  24. package/lib/levenshtein.mjs +133 -0
  25. package/lib/node/cli.mjs +387 -0
  26. package/lib/node/export-svg.mjs +75 -0
  27. package/lib/node/fs.mjs +309 -0
  28. package/lib/node/op-json.mjs +75 -0
  29. package/lib/node/raster-run.mjs +115 -0
  30. package/lib/node/server.mjs +1264 -0
  31. package/lib/node/static.mjs +206 -0
  32. package/lib/node/verbs/add.mjs +220 -0
  33. package/lib/node/verbs/export.mjs +161 -0
  34. package/lib/node/verbs/format.mjs +106 -0
  35. package/lib/node/verbs/import.mjs +126 -0
  36. package/lib/node/verbs/ls.mjs +98 -0
  37. package/lib/node/verbs/migrate.mjs +236 -0
  38. package/lib/node/verbs/move.mjs +76 -0
  39. package/lib/node/verbs/new.mjs +284 -0
  40. package/lib/node/verbs/remove.mjs +41 -0
  41. package/lib/node/verbs/rename.mjs +87 -0
  42. package/lib/node/verbs/serve.mjs +164 -0
  43. package/lib/node/verbs/set.mjs +150 -0
  44. package/lib/node/verbs/show.mjs +122 -0
  45. package/lib/node/verbs/tokens.mjs +167 -0
  46. package/lib/node/verbs/validate.mjs +130 -0
  47. package/lib/node/write-verbs.mjs +215 -0
  48. package/lib/normalize.mjs +288 -0
  49. package/lib/ops.mjs +2063 -0
  50. package/lib/parse.mjs +997 -0
  51. package/lib/raster.mjs +186 -0
  52. package/lib/readers/pen.mjs +1684 -0
  53. package/lib/receipt.mjs +203 -0
  54. package/lib/render.mjs +503 -0
  55. package/lib/serialize.mjs +441 -0
  56. package/lib/spans.mjs +177 -0
  57. package/lib/svg-path.mjs +483 -0
  58. package/lib/table.mjs +1047 -0
  59. package/lib/tokens.mjs +295 -0
  60. package/lib/validate.mjs +929 -0
  61. package/package.json +68 -6
  62. package/references/cookbook.md +156 -0
  63. package/references/images.md +171 -0
  64. package/references/product-flow.md +408 -0
  65. package/references/schema.md +90 -0
  66. package/references/styling.md +181 -0
  67. package/scripts/install.mjs +95 -0
@@ -0,0 +1,204 @@
1
+ /**
2
+ * canvas/groups.mjs — the inspector's sections, rows and editors.
3
+ *
4
+ * Pure data and pure decisions, no DOM: which section a property belongs to, in
5
+ * what order the sections are drawn, which two properties share a row, and which
6
+ * kind of control a property gets. `app.mjs` draws what this returns — which is
7
+ * why every claim here can be checked by `node --test` instead of by looking at
8
+ * a panel and counting.
9
+ *
10
+ * Two rules make this more than a hand-written list:
11
+ *
12
+ * 1. The property set is `CSS_ALLOWED` from `lib/table.mjs` — the same table
13
+ * the validator uses. A property missing from these sections is not lost:
14
+ * it falls into `Other`, and a test holds every allowed property to a
15
+ * section, so the fallback stays a safety net instead of a dumping ground.
16
+ * 2. The control is chosen from the *declared grammar*, not from the spelling
17
+ * of the current value. The first inspector tested the value with a regular
18
+ * expression — `#rrggbb` meant a colour picker, `-?\d+(px|%)?` meant a
19
+ * scrubber — which is a guess that breaks the moment a length is written
20
+ * `calc(...)` or a colour arrives as `var(--color-ink)`.
21
+ */
22
+
23
+ import { CSS_ORDER, specFor } from "../lib/table.mjs";
24
+
25
+ /**
26
+ * The sections, in the order they are drawn.
27
+ *
28
+ * `members` is ordered by the format's own property order (`CSS_ORDER`), not by
29
+ * preference: the panel then reads in the same direction as the file it edits,
30
+ * and a property that moves in the format moves here too.
31
+ */
32
+ export const GROUPS = [
33
+ { id: "layout", label: "Layout", members: ["display", "flex-direction", "flex-wrap", "align-items", "justify-content", "align-self", "flex", "flex-shrink", "gap"] },
34
+ { id: "position", label: "Position", members: ["position", "left", "top"] },
35
+ { id: "size", label: "Size", members: ["width", "height", "min-width", "min-height", "max-width", "max-height"] },
36
+ { id: "spacing", label: "Spacing", members: ["padding", "margin"] },
37
+ { id: "appearance", label: "Appearance", members: ["opacity", "border-radius", "overflow"] },
38
+ { id: "fill", label: "Fill", members: ["background-color", "background-image", "background-size", "background-position"] },
39
+ { id: "stroke", label: "Stroke", members: ["border", "border-width", "border-style", "border-color", "outline", "outline-offset"] },
40
+ { id: "effects", label: "Effects", members: ["box-shadow", "backdrop-filter", "transform"] },
41
+ { id: "text", label: "Text", members: ["font-family", "font-size", "font-weight", "font-style", "line-height", "letter-spacing", "color", "text-align", "text-transform", "white-space"] },
42
+ { id: "image", label: "Image", members: ["object-fit"] },
43
+ ];
44
+
45
+ /** The section an unknown property lands in. Never a section anyone aims for. */
46
+ export const OTHER = { id: "other", label: "Other", members: [] };
47
+
48
+ /**
49
+ * Two properties that are read as a pair, and are therefore drawn on one row.
50
+ *
51
+ * Only pairs whose members both appear: a lone `height` is a row of its own, not
52
+ * half a row with an empty box beside it.
53
+ */
54
+ export const PAIRS = [
55
+ ["width", "height"],
56
+ ["min-width", "min-height"],
57
+ ["max-width", "max-height"],
58
+ ["left", "top"],
59
+ ];
60
+
61
+ /**
62
+ * The rows to draw for the properties an element actually has.
63
+ *
64
+ * @param {Iterable<string>} properties
65
+ * @returns {Array<{ id: string, label: string, rows: string[][] }>}
66
+ */
67
+ export function groupedFields(properties) {
68
+ const present = new Set(properties);
69
+ const claimed = new Set();
70
+ const sections = [];
71
+
72
+ for (const group of GROUPS) {
73
+ const members = group.members.filter((property) => present.has(property));
74
+ if (members.length === 0) continue;
75
+ for (const property of members) claimed.add(property);
76
+ sections.push({ id: group.id, label: group.label, rows: pairUp(members) });
77
+ }
78
+
79
+ const orphans = [...present].filter((property) => !claimed.has(property));
80
+ if (orphans.length > 0) {
81
+ // Anything the format allows and these sections do not name — a property
82
+ // added to `CSS_ALLOWED` after this table was written. It is drawn last, and
83
+ // it is drawn: a property the inspector cannot reach is a property a person
84
+ // has to edit by hand.
85
+ sections.push({ id: OTHER.id, label: OTHER.label, rows: pairUp(sortByFormat(orphans)) });
86
+ }
87
+ return sections;
88
+ }
89
+
90
+ function pairUp(members) {
91
+ const rows = [];
92
+ const used = new Set();
93
+ for (const property of members) {
94
+ if (used.has(property)) continue;
95
+ const partner = partnerOf(property, members);
96
+ if (partner) {
97
+ rows.push([property, partner]);
98
+ used.add(partner);
99
+ } else {
100
+ rows.push([property]);
101
+ }
102
+ used.add(property);
103
+ }
104
+ return rows;
105
+ }
106
+
107
+ function partnerOf(property, members) {
108
+ for (const [a, b] of PAIRS) {
109
+ if (a === property && members.includes(b)) return b;
110
+ if (b === property && members.includes(a)) return a;
111
+ }
112
+ return null;
113
+ }
114
+
115
+ /** `CSS_ORDER` is the format's canonical order; unknown names go last, sorted. */
116
+ function sortByFormat(properties) {
117
+ return [...properties].sort((a, b) => {
118
+ const ia = CSS_ORDER.indexOf(a);
119
+ const ib = CSS_ORDER.indexOf(b);
120
+ if (ia === -1 && ib === -1) return a < b ? -1 : 1;
121
+ if (ia === -1) return 1;
122
+ if (ib === -1) return -1;
123
+ return ia - ib;
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Which control a property gets.
129
+ *
130
+ * segmented — an enum of three values or fewer, drawn as buttons: two or three
131
+ * short alternatives are faster to hit than a dropdown
132
+ * select — a longer enum, where buttons would be a wall of text
133
+ * length — a number with a unit, draggable
134
+ * number — a bare number, draggable
135
+ * color — a colour, with a swatch
136
+ * text — everything with structure inside it (`border`, `box-shadow`,
137
+ * `flex`, `font-family`, …), where a free-text field is the only
138
+ * editor that cannot lose a value it does not understand
139
+ */
140
+ export function controlFor(property) {
141
+ const spec = specFor(property);
142
+ if (!spec) return "text";
143
+ switch (spec.kind) {
144
+ case "enum":
145
+ case "keyword-set":
146
+ return spec.values.length <= 3 ? "segmented" : "select";
147
+ case "length":
148
+ return "length";
149
+ case "number":
150
+ return "number";
151
+ case "color":
152
+ return "color";
153
+ default:
154
+ return "text";
155
+ }
156
+ }
157
+
158
+ /** The declared values of an enum property, or an empty list. */
159
+ export function valuesFor(property) {
160
+ const spec = specFor(property);
161
+ return Array.isArray(spec?.values) ? [...spec.values] : [];
162
+ }
163
+
164
+ /**
165
+ * A value the scrubber can read: a number, and nothing else.
166
+ *
167
+ * The first version of this decided by property name, so every `length` field
168
+ * got a scrub handle — including `gap: var(--space-md)`, `width: fit-content`
169
+ * and `padding: 0 0 24px 0`. Dragging one of those by a pixel wrote `1`, and the
170
+ * token, the keyword or the four-value shorthand was gone: a silent loss with a
171
+ * gesture that looked like a nudge. A scrubber only makes sense on a value it
172
+ * can add a number to, and this is what that means.
173
+ */
174
+ const SCRUBBABLE = /^-?\d+(\.\d+)?(px|%|em|rem|vh|vw)?$/;
175
+
176
+ /** The kinds whose values are numbers, so dragging one is meaningful. */
177
+ const NUMERIC_KINDS = new Set(["length", "number", "font-weight", "line-height"]);
178
+
179
+ /** Is this a bare number with an optional unit? */
180
+ export function isNumericValue(value) {
181
+ return SCRUBBABLE.test(String(value).trim());
182
+ }
183
+
184
+ /** Should this property's current value get a drag handle? */
185
+ export function scrubFor(property, value) {
186
+ const spec = specFor(property);
187
+ return Boolean(spec) && NUMERIC_KINDS.has(spec.kind) && isNumericValue(value);
188
+ }
189
+
190
+ /** What the format says this property may hold, for a placeholder or a tooltip. */
191
+ export function hintFor(property) {
192
+ return specFor(property)?.hint ?? "";
193
+ }
194
+
195
+ /**
196
+ * The bounds a numeric property declares, if it declares any.
197
+ *
198
+ * `opacity` is `0…1` in the table, and a field that lets you type `4` and then
199
+ * reports an error is a field that already knew. The bounds go on the input.
200
+ */
201
+ export function rangeFor(property) {
202
+ const spec = specFor(property);
203
+ return { min: spec?.min, max: spec?.max };
204
+ }
@@ -0,0 +1,141 @@
1
+ <!doctype html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>JustSkill Design canvas</title>
7
+ <link rel="stylesheet" href="/canvas/styles.css">
8
+ </head>
9
+ <body tabindex="-1">
10
+
11
+ <header class="bar">
12
+ <div class="bar-left">
13
+ <span class="brand">JustSkill Design</span>
14
+ <span class="file" id="file-name">—</span>
15
+ <span class="pill" id="revision">—</span>
16
+ <span class="pill warn" id="readonly" hidden>read only</span>
17
+ </div>
18
+ <div class="bar-right">
19
+ <span class="zoom" id="zoom-label">100%</span>
20
+ <button type="button" id="zoom-out" title="Zoom out">−</button>
21
+ <button type="button" id="zoom-in" title="Zoom in">+</button>
22
+ <button type="button" id="fit" title="Fit every page">Fit</button>
23
+ <span class="sep"></span>
24
+ <button type="button" id="undo" title="Undo">Undo</button>
25
+ <button type="button" id="redo" title="Redo">Redo</button>
26
+ <button type="button" id="toggle-problems" title="Problems"><span class="problems-label">Problems</span><span id="problem-count" class="count">0</span></button>
27
+ <span class="sep export-sep"></span>
28
+ <button type="button" id="export-page" title="Write the selected page as a standalone HTML file">Export</button>
29
+ <button type="button" id="export-all" title="Write every page">Export all</button>
30
+ </div>
31
+ </header>
32
+
33
+ <main id="stage">
34
+ <aside class="panel left" id="panel-left">
35
+ <div class="panel-head">
36
+ <!-- The fold control sits on the panel's window edge, and the tree
37
+ controls sit on its inner edge: folding a panel keeps its button in
38
+ the same place on screen, which is the whole point of putting it
39
+ here. The right panel does the mirror of this. -->
40
+ <span class="head-title">
41
+ <button type="button" class="head-button" id="toggle-left" aria-pressed="true" aria-controls="panel-left" title="Hide the layers panel (⌘\)" aria-label="Hide the layers panel">
42
+ <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
43
+ <rect x="1.5" y="2.5" width="13" height="11" rx="2.5" fill="none" stroke="currentColor"/>
44
+ <rect x="3.4" y="4.2" width="3.6" height="7.6" rx="1" fill="currentColor"/>
45
+ </svg>
46
+ </button>
47
+ <span class="label">Layers</span>
48
+ </span>
49
+ <span class="head-actions">
50
+ <button type="button" class="head-button" id="tree-expand" title="Expand every page and element" aria-label="Expand every page and element">
51
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
52
+ <path d="m7 6 5 5 5-5" />
53
+ <path d="m7 13 5 5 5-5" />
54
+ </svg>
55
+ </button>
56
+ <button type="button" class="head-button" id="tree-collapse" title="Collapse everything" aria-label="Collapse everything">
57
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
58
+ <path d="m17 11-5-5-5 5" />
59
+ <path d="m17 18-5-5-5 5" />
60
+ </svg>
61
+ </button>
62
+ </span>
63
+ </div>
64
+ <div class="panel-scroll">
65
+ <ul class="tree" id="layer-tree" role="tree" aria-label="Pages and their elements"></ul>
66
+ </div>
67
+ </aside>
68
+
69
+ <div class="divider" id="divider-left" role="separator" aria-orientation="vertical" aria-label="Resize the layers panel" tabindex="0"></div>
70
+
71
+ <div class="viewport" id="viewport">
72
+ <div class="canvas" id="canvas"></div>
73
+ <div class="overlay" id="overlay"></div>
74
+ <!--
75
+ One control per panel, and the control survives its panel.
76
+
77
+ The open/close button lives in the panel's own header, where the thing it
78
+ folds is. When the panel is folded away the header goes with it, so the
79
+ same control reappears as a tab on the canvas edge — that tab is hidden
80
+ whenever its panel is open, so there is exactly one button at a time.
81
+ -->
82
+ <button type="button" class="edge-toggle left" id="edge-left" aria-controls="panel-left" title="Show the layers panel (⌘\)" hidden>
83
+ <svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true">
84
+ <rect x="1.5" y="2.5" width="13" height="11" rx="2.5" fill="none" stroke="currentColor"/>
85
+ <rect x="3.4" y="4.2" width="3.6" height="7.6" rx="1" fill="currentColor"/>
86
+ </svg>
87
+ </button>
88
+ <button type="button" class="edge-toggle right" id="edge-right" aria-controls="panel-right" title="Show the inspector (⇧⌘\)" hidden>
89
+ <svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true">
90
+ <rect x="1.5" y="2.5" width="13" height="11" rx="2.5" fill="none" stroke="currentColor"/>
91
+ <rect x="9" y="4.2" width="3.6" height="7.6" rx="1" fill="currentColor"/>
92
+ </svg>
93
+ </button>
94
+ </div>
95
+
96
+ <div class="divider" id="divider-right" role="separator" aria-orientation="vertical" aria-label="Resize the inspector" tabindex="0"></div>
97
+
98
+ <aside class="panel right" id="panel-right">
99
+ <div class="panel-head">
100
+ <span id="selection-name">Nothing selected</span>
101
+ <span class="head-actions">
102
+ <button type="button" class="head-button" id="toggle-right" aria-pressed="true" aria-controls="panel-right" title="Hide the inspector (⇧⌘\)" aria-label="Hide the inspector">
103
+ <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
104
+ <rect x="1.5" y="2.5" width="13" height="11" rx="2.5" fill="none" stroke="currentColor"/>
105
+ <rect x="9" y="4.2" width="3.6" height="7.6" rx="1" fill="currentColor"/>
106
+ </svg>
107
+ </button>
108
+ </span>
109
+ </div>
110
+ <div class="panel-scroll">
111
+ <div class="panel-body" id="inspector">
112
+ <p class="empty">Click an element on the canvas, or a row in the layer tree.</p>
113
+ </div>
114
+ </div>
115
+ </aside>
116
+ </main>
117
+
118
+ <section class="drawer" id="problems" hidden>
119
+ <div class="drawer-head">
120
+ <span class="drawer-title">
121
+ <span class="eyebrow">Problems</span>
122
+ <span class="drawer-summary" id="problem-summary"></span>
123
+ </span>
124
+ <span class="drawer-actions">
125
+ <button type="button" id="fix-text" hidden>Fix text…</button>
126
+ <button type="button" id="fix-save" hidden>Save and reload</button>
127
+ <button type="button" class="drawer-close" id="close-problems">Close</button>
128
+ </span>
129
+ </div>
130
+ <div class="drawer-body" id="problem-list"></div>
131
+ <div class="drawer-fix" id="fix-pane" hidden>
132
+ <p class="drawer-hint" id="fix-hint"></p>
133
+ <textarea id="fix-textarea" spellcheck="false" wrap="off"></textarea>
134
+ </div>
135
+ </section>
136
+
137
+ <div class="toast" id="toast" hidden></div>
138
+
139
+ <script type="module" src="/canvas/app.mjs"></script>
140
+ </body>
141
+ </html>