@domandigital/craft 0.14.0 → 0.16.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@domandigital/craft",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "private": false,
5
5
  "description": "The Doman Digital house craft standard as numbers: OKLCh colour ramps that snap to existing anchors, derived semantic tokens with measured WCAG and APCA contrast, one shared motion vocabulary for CSS and JS, and a base stylesheet. Zero dependencies.",
6
6
  "license": "Apache-2.0",
@@ -1,295 +0,0 @@
1
- import {
2
- REFLEX_FONTS_1,
3
- REFLEX_FONTS_2,
4
- deltaEOk,
5
- isAiViolet,
6
- isCream,
7
- normaliseFamily,
8
- parseColour
9
- } from "./chunk-GQVEYTBY.js";
10
-
11
- // src/direction/licences.ts
12
- var LICENCES = [
13
- {
14
- name: "Basteleur",
15
- subject: "face",
16
- licence: "OFL-1.1",
17
- multiClient: "yes",
18
- checked: "2026-09-24",
19
- source: "https://velvetyne.fr/fonts/basteleur/",
20
- note: "Velvetyne, designed by Keussel. Keep the OFL notice with the font files."
21
- },
22
- {
23
- name: "Gulax",
24
- subject: "face",
25
- licence: "OFL-1.1",
26
- multiClient: "yes",
27
- checked: "2026-09-24",
28
- source: "https://velvetyne.fr/fonts/gulax/",
29
- note: "Velvetyne. Keep the OFL notice with the font files."
30
- },
31
- {
32
- name: "Le Murmure",
33
- subject: "face",
34
- licence: "OFL-1.1",
35
- multiClient: "yes",
36
- checked: "2026-09-24",
37
- source: "https://velvetyne.fr/fonts/le-murmure/",
38
- note: "Velvetyne. Keep the OFL notice with the font files."
39
- },
40
- {
41
- name: "Blaze Type",
42
- subject: "foundry",
43
- licence: "commercial",
44
- multiClient: "per-site",
45
- cap: 10,
46
- checked: "2026-09-24",
47
- source: "https://blazetype.eu/eula",
48
- note: "The EULA sells a professional licence for 1 to 10 websites, and lets an agency buy in a client's name at the listed price. Its /license page says one licence covers every domain; the EULA is the contract, so buy per client."
49
- },
50
- {
51
- name: "Power Type",
52
- subject: "foundry",
53
- licence: "commercial",
54
- multiClient: "unverified",
55
- checked: "2026-09-24",
56
- source: "https://power-type.com/eula",
57
- note: "Research on 2026-09-11 reported a Super License capped at 10 self-hosted domains. The EULA page did not load when checked; read it before relying on the cap."
58
- },
59
- {
60
- name: "Grilli Type",
61
- subject: "foundry",
62
- licence: "commercial",
63
- multiClient: "per-site",
64
- checked: "2026-09-24",
65
- source: "https://www.grillitype.com/information",
66
- note: "Web licences cover any number of domains controlled by the licensee, priced by monthly visitors, so a client site needs the client's own licence. Unlimited tiers start at $10,000 per style."
67
- },
68
- {
69
- name: "Phosphor",
70
- subject: "icons",
71
- licence: "MIT",
72
- multiClient: "yes",
73
- checked: "2026-09-24",
74
- source: "https://github.com/phosphor-icons/core/blob/main/LICENSE",
75
- note: "Six weights. The icon-tile tells treat it exactly as they treat Lucide: the library is not the tell, the tinted tile is."
76
- },
77
- {
78
- name: "Tabler Icons",
79
- subject: "icons",
80
- licence: "MIT",
81
- multiClient: "yes",
82
- checked: "2026-09-24",
83
- source: "https://github.com/tabler/tabler-icons/blob/main/LICENSE",
84
- note: "One stroke grid across the set."
85
- },
86
- {
87
- name: "Iconoir",
88
- subject: "icons",
89
- licence: "MIT",
90
- multiClient: "yes",
91
- checked: "2026-09-24",
92
- source: "https://github.com/iconoir-icons/iconoir/blob/main/LICENSE",
93
- note: "Smaller set, no paid tier."
94
- },
95
- {
96
- name: "Unicons",
97
- subject: "icons",
98
- licence: "IconScout Simple License",
99
- multiClient: "yes",
100
- checked: "2026-09-24",
101
- source: "https://github.com/Iconscout/unicons/blob/master/LICENSE",
102
- note: "IconScout's free set, personal and commercial use, attribution optional."
103
- },
104
- {
105
- name: "IconScout premium",
106
- subject: "icons",
107
- licence: "commercial",
108
- multiClient: "per-site",
109
- checked: "2026-09-24",
110
- source: "https://iconscout.com/licenses",
111
- note: "Each licence covers a single end product. Never in a tool, template or shared kit (create-site, craft, a house component library), even modified. Record the assets each site uses. Assets come from different contributors, so take one site's set from one contributor."
112
- }
113
- ];
114
- function faceLicence(family) {
115
- const name = family.trim().toLowerCase();
116
- return LICENCES.find((l) => l.subject === "face" && l.name.toLowerCase() === name);
117
- }
118
-
119
- // src/direction/types.ts
120
- var DIRECTION_VERSION = 1;
121
- var SOURCE_KINDS = [
122
- "livery",
123
- "shopfront",
124
- "signage",
125
- "packaging",
126
- "product",
127
- "uniform",
128
- "interior",
129
- "place",
130
- "material",
131
- "trade",
132
- "print",
133
- "photo",
134
- "founder"
135
- ];
136
- var CHOICE_KEYS = ["accent", "ground", "display", "body", "shape", "motif", "signature"];
137
-
138
- // src/direction/validate.ts
139
- var PREFERENCE = /\b(?:client (?:likes|liked|wants|wanted|asked|chose|prefers?)|(?:we|they|i) (?:like|liked|love|loved|prefer)|on[- ]trend|trendy|popular|what (?:everyone|competitors) (?:uses?|does)|brand (?:colou?r|font|guidelines?) (?:is|are|says?))\b/i;
140
- var MOOD = /\b(?:modern|clean|fresh|professional|trustworthy|premium|elegant|sleek|timeless|minimal(?:ist)?|bold|vibrant|friendly|approachable|luxur(?:y|ious)|sophisticated|contemporary|stylish|warm|inviting|high[- ]end)\b/gi;
141
- var VALUE_WORDS = /* @__PURE__ */ new Set(["green", "blue", "navy", "black", "white", "cream", "grey", "gray", "gold", "yellow", "orange", "purple", "violet", "pink", "brown", "teal", "bottle", "dark", "light", "deep", "pale", "bright", "colour", "color", "font", "face", "serif", "sans", "type", "lettering"]);
142
- var STOP = /* @__PURE__ */ new Set(["with", "that", "this", "from", "their", "they", "have", "been", "were", "which", "what", "when", "where", "there", "about", "into", "onto", "over", "same", "each", "every"]);
143
- function words(text) {
144
- return new Set((text.toLowerCase().match(/[a-z][a-z'-]{3,}/g) ?? []).map((w) => w.replace(/'s$/, "")).filter((w) => !STOP.has(w) && !VALUE_WORDS.has(w)));
145
- }
146
- function tellFor(key, value) {
147
- if (key === "accent") {
148
- const c = parseColour(value);
149
- return c && isAiViolet(c.oklch) ? "ai-violet" : null;
150
- }
151
- if (key === "ground") {
152
- const c = parseColour(value);
153
- return c && isCream(c.oklch) ? "cream-palette" : null;
154
- }
155
- if (key === "display" || key === "body") {
156
- const name = normaliseFamily(value).toLowerCase();
157
- if (REFLEX_FONTS_1.some((f) => f.toLowerCase() === name)) return "reflex-font";
158
- if (REFLEX_FONTS_2.some((f) => f.toLowerCase() === name)) return "reflex-font-2";
159
- }
160
- return null;
161
- }
162
- function validateDirection(input, ctx = {}) {
163
- const problems = [];
164
- const err = (at, message) => problems.push({ severity: "error", at, message });
165
- const warn = (at, message) => problems.push({ severity: "warn", at, message });
166
- if (!input || typeof input !== "object" || Array.isArray(input)) {
167
- err("", "art-direction.json must be a JSON object");
168
- return { valid: false, problems, decided: 0 };
169
- }
170
- const d = input;
171
- const legacy = d.version === void 0 && d.choices === void 0 && Array.isArray(d.exceptions);
172
- if (legacy) {
173
- warn("", "exceptions only: run craft direction init to record the choices and their reasons");
174
- return { valid: true, problems, decided: 0 };
175
- }
176
- if (d.version !== DIRECTION_VERSION) err("version", `version must be ${DIRECTION_VERSION}`);
177
- if (typeof d.client !== "string" || !d.client.trim()) err("client", "name the client");
178
- if (typeof d.brief !== "string" || d.brief.trim().split(/\s+/).length < 8) err("brief", "the brief needs a sentence: who, where, and what they do");
179
- const sources = Array.isArray(d.sources) ? d.sources : [];
180
- if (!Array.isArray(d.sources)) err("sources", "sources must be a list");
181
- if (sources.length === 0) err("sources", "no sources: a reason needs something in the client's world to point at");
182
- const ids = /* @__PURE__ */ new Set();
183
- sources.forEach((s, i) => {
184
- const at = `sources[${i}]`;
185
- if (!s || typeof s !== "object") return err(at, "a source must be an object");
186
- if (typeof s.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(s.id)) err(`${at}.id`, "id must be short kebab-case");
187
- else if (ids.has(s.id)) err(`${at}.id`, `"${s.id}" is used twice`);
188
- else ids.add(s.id);
189
- if (!SOURCE_KINDS.includes(s.kind)) err(`${at}.kind`, `kind must be one of: ${SOURCE_KINDS.join(", ")}`);
190
- if (typeof s.note !== "string" || s.note.trim().split(/\s+/).length < 3) err(`${at}.note`, "say what it is and where");
191
- if (s.path && ctx.pathExists && !/^https?:/.test(s.path) && !ctx.pathExists(s.path)) err(`${at}.path`, `${s.path} does not exist`);
192
- for (const [j, c] of (s.colours ?? []).entries()) if (!parseColour(c)) err(`${at}.colours[${j}]`, `"${c}" is not a colour`);
193
- });
194
- const choices = d.choices && typeof d.choices === "object" ? d.choices : {};
195
- if (!d.choices || typeof d.choices !== "object") err("choices", "choices must be an object");
196
- const exceptions = Array.isArray(d.exceptions) ? d.exceptions : [];
197
- let decided = 0;
198
- for (const key of Object.keys(choices)) {
199
- if (!CHOICE_KEYS.includes(key)) err(`choices.${key}`, `not a choice craft knows. Choices are: ${CHOICE_KEYS.join(", ")}. Layout and navigation stay conventional.`);
200
- }
201
- for (const key of CHOICE_KEYS) {
202
- const c = choices[key];
203
- const at = `choices.${key}`;
204
- if (!c) {
205
- warn(at, "not decided yet");
206
- continue;
207
- }
208
- let ok = true;
209
- if (typeof c.value !== "string" || !c.value.trim()) {
210
- err(`${at}.value`, "no value");
211
- ok = false;
212
- } else if ((key === "accent" || key === "ground") && !parseColour(c.value)) {
213
- err(`${at}.value`, `"${c.value}" is not a colour`);
214
- ok = false;
215
- }
216
- const because = typeof c.because === "string" ? c.because.trim() : "";
217
- const evidence = Array.isArray(c.evidence) ? c.evidence : [];
218
- if (because.split(/\s+/).filter(Boolean).length < 8) {
219
- err(`${at}.because`, "a reason is at least a sentence. Without one this is a default, however good it looks.");
220
- ok = false;
221
- } else if (because.startsWith("PROPOSED:")) {
222
- err(`${at}.because`, "a proposal, not yet a decision. Check it against the source, then rewrite the reason in your own words.");
223
- ok = false;
224
- } else {
225
- if (PREFERENCE.test(because)) {
226
- err(`${at}.because`, "that is a preference, not a reason. Say what in the client's world this comes from.");
227
- ok = false;
228
- }
229
- const moods = because.match(MOOD) ?? [];
230
- if (moods.length >= 2) {
231
- err(`${at}.because`, `"${moods.join(", ")}" describes a mood, not a source. Name the thing it comes from.`);
232
- ok = false;
233
- }
234
- }
235
- if (evidence.length === 0) {
236
- err(`${at}.evidence`, "cite at least one source");
237
- ok = false;
238
- }
239
- for (const id of evidence) {
240
- if (!ids.has(id)) {
241
- err(`${at}.evidence`, `no source called "${id}"`);
242
- ok = false;
243
- }
244
- }
245
- if (ok) {
246
- const cited = sources.filter((s) => evidence.includes(s.id));
247
- const reasonWords = words(because);
248
- const tied = cited.some((s) => reasonWords.has(s.id) || [...words(`${s.note} ${s.lettering ?? ""}`)].some((w) => reasonWords.has(w)));
249
- if (!tied) {
250
- err(`${at}.because`, `the reason does not mention what its evidence shows (${evidence.join(", ")}). Say what on it this comes from.`);
251
- ok = false;
252
- }
253
- }
254
- if (typeof c.value === "string") {
255
- const tell = tellFor(key, c.value);
256
- if (tell && !exceptions.some((e) => e.tell === tell)) {
257
- err(`${at}.value`, `${c.value} is on the tell catalogue (${tell}). Keep it only with an exception for ${tell}, carrying the same reason.`);
258
- ok = false;
259
- }
260
- }
261
- if ((key === "display" || key === "body") && typeof c.value === "string" && c.value.trim()) {
262
- const family = normaliseFamily(c.value);
263
- const entry = faceLicence(family);
264
- if (!entry) {
265
- warn(`${at}.value`, `${family}: licence not in craft's register. Read the licensor's own terms for use on this client's site, then add it to src/direction/licences.ts.`);
266
- } else if (entry.multiClient === "unverified") {
267
- warn(`${at}.value`, `${family}: licence unverified (${entry.note})`);
268
- } else if (entry.multiClient === "capped" || entry.multiClient === "per-site") {
269
- const scope = entry.multiClient === "capped" ? `covers up to ${entry.cap} sites` : "needs a licence for this site, in the client's name";
270
- warn(`${at}.value`, `${family}: ${entry.licence} licence ${scope}. Record the purchase before launch.`);
271
- }
272
- }
273
- const fp = ctx.fingerprint;
274
- if (fp && typeof c.value === "string") {
275
- if ((key === "accent" || key === "ground") && parseColour(c.value)) {
276
- const declared = parseColour(c.value).oklch;
277
- const measured = key === "accent" ? fp.accent : fp.ground;
278
- if (measured && deltaEOk(declared, measured) > 0.08) {
279
- warn(`${at}.value`, `the page does not show it: its ${key} measures oklch(${measured.l.toFixed(2)} ${measured.c.toFixed(3)} ${measured.h.toFixed(0)})`);
280
- }
281
- }
282
- if (key === "display" || key === "body") {
283
- const measured = key === "display" ? fp.display.family : fp.body.family;
284
- if (normaliseFamily(c.value).toLowerCase() !== measured.toLowerCase()) warn(`${at}.value`, `the page sets ${measured}, not ${c.value}`);
285
- }
286
- }
287
- if (ok) decided += 1;
288
- }
289
- return { valid: problems.every((p) => p.severity !== "error"), problems, decided };
290
- }
291
-
292
- export {
293
- DIRECTION_VERSION,
294
- validateDirection
295
- };