@fractaldesign/fractalstyler 0.0.0-stage → 0.9.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/LICENSE +19 -0
- package/README.md +123 -2
- package/cli/main.mjs +141 -0
- package/cli/scaffold.mjs +406 -0
- package/data/recipes.json +61 -0
- package/dist/asset.d.ts +3 -0
- package/dist/css/fractalstyler.css +2453 -0
- package/dist/css/fractalstyler.min.css +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/styles/_00_config.sass +29 -0
- package/dist/styles/_00_fonts.sass +58 -0
- package/dist/styles/_00_tokens.sass +211 -0
- package/dist/styles/_01_base.sass +58 -0
- package/dist/styles/_02_dimensions.sass +70 -0
- package/dist/styles/_03_typography.sass +170 -0
- package/dist/styles/_04_containers.sass +173 -0
- package/dist/styles/_05_layouts.sass +104 -0
- package/dist/styles/_06_shells.sass +135 -0
- package/dist/styles/_07_interactions.sass +212 -0
- package/dist/styles/_08_visuals.sass +136 -0
- package/dist/styles/_09_own.sass +174 -0
- package/dist/styles/colorpacks.sass +119 -0
- package/dist/styles/index.sass +14 -0
- package/dist/styles/themeplates.sass +159 -0
- package/docs/REGISTRY.api.md +477 -0
- package/docs/REGISTRY.md +14 -0
- package/docs/references/configurations-api.md +220 -0
- package/docs/references/configurations.md +258 -0
- package/lint/browser.mjs +79 -0
- package/lint/cli.mjs +212 -0
- package/lint/lib/agent-reporter.mjs +51 -0
- package/lint/lib/fuzzy.mjs +45 -0
- package/lint/lib/registry.mjs +107 -0
- package/lint/lib/sass-linter.mjs +71 -0
- package/lint/lib/svelte-linter.mjs +116 -0
- package/lint/lib/token-linter.mjs +97 -0
- package/package.json +112 -5
- package/registry.json +3999 -0
- package/scripts/class-vocab.js +197 -0
- package/scripts/update-registry.js +934 -0
- package/skills/fractal-styler/references/fractals.md +630 -0
- package/skills/fractal-styler/references/tokens.md +226 -0
- package/skills/fractalstyler/SKILL.md +59 -0
- package/skills/fractalstyler/references/fractals.md +630 -0
- package/skills/fractalstyler/references/tokens.md +226 -0
package/cli/scaffold.mjs
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// scaffold — the init/eject engine behind cli/main.mjs.
|
|
3
|
+
//
|
|
4
|
+
// Two ways a project can hold the styles:
|
|
5
|
+
// track — the package stays in node_modules; the stylesheet is imported
|
|
6
|
+
// from it; upgrades come from npm. Only docs (and optional VS Code
|
|
7
|
+
// settings) are placed into the project.
|
|
8
|
+
// eject — the style layers, the palette modules and the registry builder are
|
|
9
|
+
// copied into the project. Nothing is ever overwritten: on a name
|
|
10
|
+
// conflict the existing file is kept and reported.
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import readline from 'node:readline/promises';
|
|
16
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
17
|
+
|
|
18
|
+
// --- shared constants --------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const DOCS_FILES = [
|
|
21
|
+
'REGISTRY.md',
|
|
22
|
+
'REGISTRY.api.md',
|
|
23
|
+
path.join('references', 'configurations.md'),
|
|
24
|
+
path.join('references', 'configurations-api.md')
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const PALETTES = ['colorpacks.sass', 'themeplates.sass'];
|
|
28
|
+
|
|
29
|
+
const SEED_OWN = `// ============================================================================
|
|
30
|
+
// own — the sanctioned extension point (_09_own.sass).
|
|
31
|
+
//
|
|
32
|
+
// Project-specific styling that cannot be expressed via the canonical layers
|
|
33
|
+
// lives here. Document a class with a trailing \`// …\` comment on its selector
|
|
34
|
+
// line — scripts/build-registry.mjs lifts it into the registry description.
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
// Example seed — delete freely:
|
|
38
|
+
.your-class // Describe what this class is for
|
|
39
|
+
color: var(--text-primary)
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const BUILD_REGISTRY = `#!/usr/bin/env node
|
|
43
|
+
// Regenerates registry.json, REGISTRY.md, docs/REGISTRY.api.md and the agent
|
|
44
|
+
// skill references from src/lib/styles. Run it after any edit to the layers.
|
|
45
|
+
// Node only — no Sass toolchain needed.
|
|
46
|
+
import { spawnSync } from 'node:child_process';
|
|
47
|
+
import { dirname, join } from 'node:path';
|
|
48
|
+
import { fileURLToPath } from 'node:url';
|
|
49
|
+
|
|
50
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
51
|
+
for (const script of ['update-registry.mjs', 'class-vocab.mjs']) {
|
|
52
|
+
const result = spawnSync(process.execPath, [join(here, script)], { stdio: 'inherit' });
|
|
53
|
+
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
// --- small helpers -----------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
const say = (text = '') => console.log(text);
|
|
60
|
+
|
|
61
|
+
function copyNoOverwrite(from, to) {
|
|
62
|
+
if (fs.existsSync(to)) return { file: path.basename(to), status: 'kept' };
|
|
63
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
64
|
+
fs.copyFileSync(from, to);
|
|
65
|
+
return { file: path.basename(to), status: 'placed' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function writeFileNoOverwrite(to, content) {
|
|
69
|
+
if (fs.existsSync(to)) return { file: path.basename(to), status: 'kept' };
|
|
70
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
71
|
+
fs.writeFileSync(to, content, 'utf8');
|
|
72
|
+
return { file: path.basename(to), status: 'placed' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function confirm(rl, message, defaultValue) {
|
|
76
|
+
const answer = (await rl.question(`${message} ${defaultValue ? '(Y/n)' : '(y/N)'} `)).trim().toLowerCase();
|
|
77
|
+
if (!answer) return defaultValue;
|
|
78
|
+
return answer.startsWith('y');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function select(rl, message, options, defaultValue) {
|
|
82
|
+
say(message);
|
|
83
|
+
options.forEach((option, index) => {
|
|
84
|
+
const mark = option.value === defaultValue ? ' (default)' : '';
|
|
85
|
+
say(` ${index + 1}. ${option.title}${mark}`);
|
|
86
|
+
});
|
|
87
|
+
for (;;) {
|
|
88
|
+
const answer = (await rl.question('> ')).trim();
|
|
89
|
+
if (!answer) return defaultValue;
|
|
90
|
+
const number = Number(answer);
|
|
91
|
+
if (Number.isInteger(number) && number >= 1 && number <= options.length) {
|
|
92
|
+
return options[number - 1].value;
|
|
93
|
+
}
|
|
94
|
+
const byTitle = options.find((o) => o.title.toLowerCase() === answer.toLowerCase());
|
|
95
|
+
if (byTitle) return byTitle.value;
|
|
96
|
+
say(`Pick 1-${options.length}, or Enter for the default.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function maybePrompt() {
|
|
101
|
+
return process.stdin.isTTY && process.stdout.isTTY;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- docs --------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Copy the registry + configurations docs from the package into the project.
|
|
108
|
+
* These are generated snapshots — within the target dir they are overwritten
|
|
109
|
+
* (they carry their own "Generated" banners). Only the default dedicated
|
|
110
|
+
* folder is safe for that; a custom --docs-dir of '.' gets a warning.
|
|
111
|
+
*/
|
|
112
|
+
function placeDocs(pkgRoot, docsDir) {
|
|
113
|
+
const placed = [];
|
|
114
|
+
const kept = [];
|
|
115
|
+
for (const rel of DOCS_FILES) {
|
|
116
|
+
const from = path.join(pkgRoot, 'docs', rel);
|
|
117
|
+
if (!fs.existsSync(from)) continue;
|
|
118
|
+
const to = path.join(docsDir, ...rel.split(path.sep));
|
|
119
|
+
const existing = fs.existsSync(to);
|
|
120
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
121
|
+
fs.copyFileSync(from, to);
|
|
122
|
+
if (existing) kept.push(rel);
|
|
123
|
+
else placed.push(rel);
|
|
124
|
+
}
|
|
125
|
+
return { placed, kept };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- intellisense -------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
const EXTENSION_ID = 'fractalmandala.styles-intellisense';
|
|
131
|
+
const EXTENSION_REPO = 'https://github.com/fractalmandala/styles-intellisense';
|
|
132
|
+
|
|
133
|
+
function mergeWorkspaceSettings(settingsFile, patch) {
|
|
134
|
+
// Tolerate JSONC-ish leftovers only enough to refuse corrupting the file:
|
|
135
|
+
// if it does not parse as plain JSON, we back off and tell the user.
|
|
136
|
+
let settings = {};
|
|
137
|
+
if (fs.existsSync(settingsFile)) {
|
|
138
|
+
try {
|
|
139
|
+
settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
140
|
+
} catch {
|
|
141
|
+
return { ok: false, reason: 'unparseable' };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const changed = [];
|
|
145
|
+
for (const [key, values] of Object.entries(patch)) {
|
|
146
|
+
const list = Array.isArray(settings[key]) ? settings[key] : [];
|
|
147
|
+
const merged = [...list];
|
|
148
|
+
for (const value of values) if (!merged.includes(value)) merged.push(value);
|
|
149
|
+
if (merged.length !== list.length) {
|
|
150
|
+
settings[key] = merged;
|
|
151
|
+
changed.push(key);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
fs.mkdirSync(path.dirname(settingsFile), { recursive: true });
|
|
155
|
+
fs.writeFileSync(settingsFile, `${JSON.stringify(settings, null, '\t')}\n`, 'utf8');
|
|
156
|
+
return { ok: true, changed };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function codeCli() {
|
|
160
|
+
for (const candidate of ['code', 'code-insiders']) {
|
|
161
|
+
try {
|
|
162
|
+
execFileSync(candidate, ['--version'], { stdio: 'ignore' });
|
|
163
|
+
return candidate;
|
|
164
|
+
} catch {
|
|
165
|
+
/* try the next */
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function extensionInstalled(code) {
|
|
172
|
+
try {
|
|
173
|
+
const list = execFileSync(code, ['--list-extensions', '--show-versions'], { encoding: 'utf8' });
|
|
174
|
+
return list
|
|
175
|
+
.split('\n')
|
|
176
|
+
.some((line) => line.trim().toLowerCase().startsWith(`${EXTENSION_ID.toLowerCase()}@`));
|
|
177
|
+
} catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function tryInstallExtension(code) {
|
|
183
|
+
const result = spawnSync(code, ['--install-extension', EXTENSION_ID], { stdio: 'pipe', encoding: 'utf8' });
|
|
184
|
+
return result.status === 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Configure the Styles IntelliSense VS Code extension: write the workspace
|
|
189
|
+
* settings it reads (sources, description files) and make sure the extension
|
|
190
|
+
* itself is installed. Never fatal — worst case it prints instructions.
|
|
191
|
+
*/
|
|
192
|
+
function setupIntellisense(projectRoot, opts, packageStylesPath) {
|
|
193
|
+
const settingsFile = path.join(projectRoot, '.vscode', 'settings.json');
|
|
194
|
+
const patch = {};
|
|
195
|
+
// In track mode the styles live inside node_modules — point the extension
|
|
196
|
+
// straight at the shipped partials so classes still resolve and complete.
|
|
197
|
+
patch['stylesIntellisense.sources'] = [packageStylesPath];
|
|
198
|
+
if (opts.docs !== false) {
|
|
199
|
+
patch['stylesIntellisense.descriptionFiles'] = [path.posix.join(opts.docsDir, 'REGISTRY.api.md')];
|
|
200
|
+
}
|
|
201
|
+
const merged = mergeWorkspaceSettings(settingsFile, patch);
|
|
202
|
+
if (!merged.ok) {
|
|
203
|
+
say(` ⚠ kept .vscode/settings.json untouched — it is not plain JSON. Add manually:`);
|
|
204
|
+
for (const [key, values] of Object.entries(patch)) say(` "${key}": ${JSON.stringify(values)}`);
|
|
205
|
+
} else {
|
|
206
|
+
say(` ✓ .vscode/settings.json — ${merged.changed.join(', ') || 'already configured'}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const code = codeCli();
|
|
210
|
+
if (!code) {
|
|
211
|
+
say(` ℹ no VS Code CLI found. Install the extension from ${EXTENSION_REPO} (dist/*.vsix)`);
|
|
212
|
+
say(' via Extensions: Install from VSIX… — the settings above are already in place.');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (extensionInstalled(code)) {
|
|
216
|
+
say(' ✓ Styles IntelliSense extension already installed');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
say(' ℹ extension not installed yet — trying the marketplace…');
|
|
220
|
+
if (tryInstallExtension(code)) {
|
|
221
|
+
say(` ✓ installed ${EXTENSION_ID} — reload the window`);
|
|
222
|
+
} else {
|
|
223
|
+
say(` ⚠ not on the marketplace (or the CLI is offline). Install the VSIX manually:`);
|
|
224
|
+
say(` ${EXTENSION_REPO} → dist/ → Extensions: Install from VSIX…`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- init / eject --------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
export async function runInit(opts, pkgRoot) {
|
|
231
|
+
const interactive = maybePrompt() && !opts.yes;
|
|
232
|
+
const rl = interactive ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
233
|
+
try {
|
|
234
|
+
if (opts.mode === null) {
|
|
235
|
+
opts.mode = interactive
|
|
236
|
+
? await select(rl, 'How should the styles be active in this project?', [
|
|
237
|
+
{ title: 'Track the package — imported from node_modules, upgraded by npm (advised)', value: 'track' },
|
|
238
|
+
{ title: 'Eject — copy the style sources into the project; they become yours', value: 'eject' }
|
|
239
|
+
], 'track')
|
|
240
|
+
: 'track';
|
|
241
|
+
}
|
|
242
|
+
if (opts.docs === null) opts.docs = interactive ? await confirm(rl, 'Copy the registry + configurations docs into the project?', true) : true;
|
|
243
|
+
if (opts.intellisense === null) {
|
|
244
|
+
opts.intellisense = interactive
|
|
245
|
+
? await confirm(rl, 'Configure the Styles IntelliSense VS Code extension?', true)
|
|
246
|
+
: true;
|
|
247
|
+
}
|
|
248
|
+
} finally {
|
|
249
|
+
rl?.close();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (opts.mode === 'eject') {
|
|
253
|
+
await runEject(opts, pkgRoot);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
await runTrack(opts, pkgRoot);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function runTrack(opts, pkgRoot) {
|
|
260
|
+
const projectRoot = process.cwd();
|
|
261
|
+
say();
|
|
262
|
+
say('fractalstyler — track mode');
|
|
263
|
+
say('──────────────────────────');
|
|
264
|
+
say('The stylesheet stays in node_modules; upgrades come from npm.');
|
|
265
|
+
say();
|
|
266
|
+
say('Install and use:');
|
|
267
|
+
say(' pnpm add -D @fractaldesign/fractalstyler');
|
|
268
|
+
say(" @use '@fractaldesign/fractalstyler/styles' as *");
|
|
269
|
+
say();
|
|
270
|
+
say('Plain CSS consumers (no SASS toolchain):');
|
|
271
|
+
say(" import '@fractaldesign/fractalstyler/css' // dist/css/fractalstyler.css");
|
|
272
|
+
say(" import '@fractaldesign/fractalstyler/css/min' // dist/css/fractalstyler.min.css");
|
|
273
|
+
|
|
274
|
+
if (opts.docs !== false) {
|
|
275
|
+
say();
|
|
276
|
+
say(`Docs → ${opts.docsDir}/`);
|
|
277
|
+
if (opts.docsDir === '.') say(' ⚠ docs-dir is the project root — existing files there are overwritten.');
|
|
278
|
+
const { placed, kept } = placeDocs(pkgRoot, path.join(process.cwd(), opts.docsDir));
|
|
279
|
+
for (const rel of placed) say(` + ${rel}`);
|
|
280
|
+
for (const rel of kept) say(` ↻ ${rel} (refreshed)`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (opts.intellisense !== false) {
|
|
284
|
+
say();
|
|
285
|
+
say('Styles IntelliSense (VS Code):');
|
|
286
|
+
setupIntellisense(projectRoot, opts, 'node_modules/@fractaldesign/fractalstyler/dist/styles');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
say();
|
|
290
|
+
const registryFile = path.join(pkgRoot, 'registry.json');
|
|
291
|
+
const count = fs.existsSync(registryFile)
|
|
292
|
+
? JSON.parse(fs.readFileSync(registryFile, 'utf8')).length
|
|
293
|
+
: null;
|
|
294
|
+
say(`Done. ${count ? `${count} classes are ` : ''}active once the import is in place.`);
|
|
295
|
+
say('Grep the registry: REGISTRY.md · docs/REGISTRY.api.md · references/configurations.md');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function runEject(opts, pkgRoot) {
|
|
299
|
+
const projectRoot = process.cwd();
|
|
300
|
+
const stylesSource = resolveStylesSource(pkgRoot);
|
|
301
|
+
|
|
302
|
+
const stylesDir = path.join(projectRoot, opts.stylesDir);
|
|
303
|
+
const paletteDir = path.join(projectRoot, opts.paletteDir);
|
|
304
|
+
const scriptsDir = path.join(projectRoot, opts.scriptsDir);
|
|
305
|
+
|
|
306
|
+
say();
|
|
307
|
+
say('fractalstyler — eject');
|
|
308
|
+
say('─────────────────────');
|
|
309
|
+
say(`Source: ${path.relative(projectRoot, stylesSource) || stylesSource}`);
|
|
310
|
+
say('Nothing existing is overwritten — conflicts are kept and reported.');
|
|
311
|
+
say();
|
|
312
|
+
|
|
313
|
+
// 1. The style layers (+ index). _09_own.sass is seeded, not copied: this
|
|
314
|
+
// project's own mascot/hero content is not a consumer's starting point.
|
|
315
|
+
const layers = fs
|
|
316
|
+
.readdirSync(stylesSource)
|
|
317
|
+
.filter((file) => file.endsWith('.sass') && !PALETTES.includes(file) && file !== '_09_own.sass')
|
|
318
|
+
.sort();
|
|
319
|
+
const styleResults = layers.map((file) => copyNoOverwrite(path.join(stylesSource, file), path.join(stylesDir, file)));
|
|
320
|
+
const ownResult = writeFileNoOverwrite(path.join(stylesDir, '_09_own.sass'), SEED_OWN);
|
|
321
|
+
|
|
322
|
+
// 2. The palette modules.
|
|
323
|
+
const paletteResults = PALETTES.map((file) => {
|
|
324
|
+
const from = path.join(stylesSource, file);
|
|
325
|
+
return fs.existsSync(from)
|
|
326
|
+
? copyNoOverwrite(from, path.join(paletteDir, file))
|
|
327
|
+
: { file, status: 'missing' };
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// 3. The registry builder — the two scripts plus the wrapper that runs both.
|
|
331
|
+
// Copied as .mjs so they run as ESM whatever the project's package type.
|
|
332
|
+
const builderFiles = [
|
|
333
|
+
['update-registry.js', 'update-registry.mjs'],
|
|
334
|
+
['class-vocab.js', 'class-vocab.mjs']
|
|
335
|
+
];
|
|
336
|
+
const builderResults = builderFiles.map(([fromFile, toFile]) => {
|
|
337
|
+
const from = path.join(pkgRoot, 'scripts', fromFile);
|
|
338
|
+
return fs.existsSync(from)
|
|
339
|
+
? copyNoOverwrite(from, path.join(scriptsDir, toFile))
|
|
340
|
+
: { file: toFile, status: 'missing' };
|
|
341
|
+
});
|
|
342
|
+
const wrapperResult = writeFileNoOverwrite(path.join(scriptsDir, 'build-registry.mjs'), BUILD_REGISTRY);
|
|
343
|
+
|
|
344
|
+
report(`${opts.stylesDir}/`, styleResults);
|
|
345
|
+
say(` ${ownResult.status === 'placed' ? '+' : '↷'} _09_own.sass ${ownResult.status === 'placed' ? '(seeded — the sanctioned extension point)' : '(kept yours)'}`);
|
|
346
|
+
report(`${opts.paletteDir}/`, paletteResults);
|
|
347
|
+
report(`${opts.scriptsDir}/`, builderResults);
|
|
348
|
+
say(` ${wrapperResult.status === 'placed' ? '+' : '↷'} build-registry.mjs ${wrapperResult.status === 'placed' ? '(regenerates your registry)' : '(kept yours)'}`);
|
|
349
|
+
|
|
350
|
+
if (opts.stylesDir !== 'src/lib/styles') {
|
|
351
|
+
say();
|
|
352
|
+
say(` ⚠ scripts/build-registry.mjs reads src/lib/styles — with --styles-dir ${opts.stylesDir}`);
|
|
353
|
+
say(' adjust the stylesDir constant in the copied update-registry.js.');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (opts.docs !== false) {
|
|
357
|
+
say();
|
|
358
|
+
say(`Docs → ${opts.docsDir}/`);
|
|
359
|
+
if (opts.docsDir === '.') say(' ⚠ docs-dir is the project root — existing files there are overwritten.');
|
|
360
|
+
const { placed, kept } = placeDocs(pkgRoot, path.join(projectRoot, opts.docsDir));
|
|
361
|
+
for (const rel of placed) say(` + ${rel}`);
|
|
362
|
+
for (const rel of kept) say(` ↻ ${rel} (refreshed)`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (opts.intellisense !== false) {
|
|
366
|
+
say();
|
|
367
|
+
say('Styles IntelliSense (VS Code):');
|
|
368
|
+
setupIntellisense(projectRoot, opts, opts.stylesDir);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
say();
|
|
372
|
+
say('The files are yours now. After every edit, regenerate your registry:');
|
|
373
|
+
say(' node scripts/build-registry.mjs');
|
|
374
|
+
say('and keep the contract enforced against your copy:');
|
|
375
|
+
say(' npx @fractaldesign/fractalstyler lint src/');
|
|
376
|
+
say();
|
|
377
|
+
say('⚠ Drift warning: an ejected copy no longer tracks the package. Diff');
|
|
378
|
+
say(' against a fresh eject when you want to see how far you have walked.');
|
|
379
|
+
say(' To re-place a kept file: delete it and run eject again.');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function report(title, results) {
|
|
383
|
+
for (const result of results) {
|
|
384
|
+
if (result.status === 'placed') say(` + ${result.file}`);
|
|
385
|
+
else if (result.status === 'kept') say(` ↷ ${result.file} (kept yours)`);
|
|
386
|
+
else if (result.status === 'missing') say(` ? ${result.file} (not in this package version)`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Where the shipped .sass layers live. The published tarball carries
|
|
392
|
+
* dist/styles (copied by the pack build); a checkout of this repository also
|
|
393
|
+
* has the live src/lib/styles — prefer it when present so ejected content
|
|
394
|
+
* always matches what the registry was generated from.
|
|
395
|
+
*/
|
|
396
|
+
function resolveStylesSource(pkgRoot) {
|
|
397
|
+
const candidates = [
|
|
398
|
+
path.join(pkgRoot, 'src', 'lib', 'styles'),
|
|
399
|
+
path.join(pkgRoot, 'dist', 'styles')
|
|
400
|
+
];
|
|
401
|
+
for (const candidate of candidates) {
|
|
402
|
+
if (fs.existsSync(path.join(candidate, 'index.sass'))) return candidate;
|
|
403
|
+
}
|
|
404
|
+
console.error('✕ no styles source found in this package (expected src/lib/styles or dist/styles).');
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0.0",
|
|
3
|
+
"recipes": [
|
|
4
|
+
{
|
|
5
|
+
"id": "workspace-segmented-control",
|
|
6
|
+
"name": "Segmented control",
|
|
7
|
+
"intents": ["segmented control", "mode switcher", "view switcher", "tab selector"],
|
|
8
|
+
"markup": "<div class=\"row ycenter shrink-0 pad-2 raised border radius-sm\">\n\t<button class=\"button ghost text-xs active\">View</button>\n\t<button class=\"button ghost text-xs\">Split</button>\n</div>",
|
|
9
|
+
"notes": "Use active on the currently selected button and expose selection state with native ARIA."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "search-input-bar",
|
|
13
|
+
"name": "Search input bar",
|
|
14
|
+
"intents": ["search", "command search", "filter input", "omnibar"],
|
|
15
|
+
"markup": "<div class=\"row ycenter gap-xs pad-x-xs pad-y-2xs surface border radius-sm grow min0\">\n\t<span class=\"text-muted text-xs\">⌕</span>\n\t<input class=\"input grow min0 text-sm\" type=\"search\" placeholder=\"Search entries…\" />\n\t<kbd class=\"kbd text-xs\">⌘K</kbd>\n</div>",
|
|
16
|
+
"notes": "Provide an accessible label when no visible label is present."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "responsive-metrics-grid",
|
|
20
|
+
"name": "Responsive metrics grid",
|
|
21
|
+
"intents": ["metrics", "kpi", "analytics", "dashboard cards", "stat grid"],
|
|
22
|
+
"markup": "<div class=\"grid-4 gap-sm\">\n\t<div class=\"card box ycenter xcenter gap-2xs pad-sm surface border\">\n\t\t<span class=\"text-xl weight-600 text-primary\">99.98%</span>\n\t\t<span class=\"text-xs text-muted\">Availability</span>\n\t</div>\n</div>",
|
|
23
|
+
"notes": "Repeat the card child for each metric; grid-4 owns responsive column stepping."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "profile-action-row",
|
|
27
|
+
"name": "Profile action row",
|
|
28
|
+
"intents": ["profile row", "user list", "avatar actions", "user card"],
|
|
29
|
+
"markup": "<div class=\"row ycenter xbetween gap-sm pad-sm surface border radius-md\">\n\t<div class=\"row ycenter gap-sm min0\">\n\t\t<div class=\"avatar shrink-0\"><img src=\"/user.jpg\" alt=\"\" /></div>\n\t\t<div class=\"box min0 gap-2xs\"><span class=\"text-sm weight-600 truncate\">Alex Vance</span><span class=\"text-xs text-muted truncate\">alex@example.com</span></div>\n\t</div>\n\t<button class=\"button is-icon text-muted\" aria-label=\"More actions\">⋯</button>\n</div>",
|
|
30
|
+
"notes": "Give the image meaningful alt text when it conveys information not already present in adjacent text."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "settings-switch-row",
|
|
34
|
+
"name": "Settings switch row",
|
|
35
|
+
"intents": ["settings", "toggle", "switch", "preference row"],
|
|
36
|
+
"markup": "<div class=\"row ycenter xbetween gap-md pad-y-sm border-bottom\">\n\t<div class=\"box gap-2xs\"><span class=\"text-sm weight-500\">Enable telemetry</span><span class=\"text-xs text-muted\">Collect anonymized traces.</span></div>\n\t<button type=\"button\" role=\"switch\" aria-checked=\"true\" class=\"switch-track checked shrink-0\"><span class=\"switch-thumb\"></span></button>\n</div>",
|
|
37
|
+
"notes": "Keep aria-checked synchronized with the visual checked modifier."
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "action-dialog",
|
|
41
|
+
"name": "Action dialog",
|
|
42
|
+
"intents": ["dialog", "modal", "confirmation", "alert modal"],
|
|
43
|
+
"markup": "<dialog class=\"dialog pad-0 surface border radius-md\">\n\t<header class=\"row ycenter xbetween pad-x-md pad-y-sm border-bottom\"><h3 class=\"text-sm weight-600 marg-0\">Confirm action</h3><button class=\"button is-icon text-muted\" aria-label=\"Close\">×</button></header>\n\t<div class=\"box gap-sm pad-md\"><p class=\"text-sm text-secondary marg-0\">Review this action before continuing.</p></div>\n\t<footer class=\"row ycenter xright gap-xs pad-x-md pad-y-sm border-top raised\"><button class=\"button ghost text-xs\">Cancel</button><button class=\"button primary text-xs\">Continue</button></footer>\n</dialog>",
|
|
44
|
+
"notes": "Use the native dialog API and return focus to the invoking control."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "dropdown-action-menu",
|
|
48
|
+
"name": "Dropdown action menu",
|
|
49
|
+
"intents": ["dropdown", "action menu", "popover menu", "context menu"],
|
|
50
|
+
"markup": "<div class=\"relative\">\n\t<button class=\"button ghost\" aria-expanded=\"true\">Actions</button>\n\t<div class=\"popover open box gap-2xs pad-2xs surface border shadow-md radius-sm\">\n\t\t<button class=\"button ghost text-xs wfull xleft\">Duplicate</button>\n\t\t<button class=\"button ghost text-xs wfull xleft text-danger\">Delete</button>\n\t</div>\n</div>",
|
|
51
|
+
"notes": "Synchronize open and aria-expanded; implement keyboard navigation for menu semantics."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "status-bar",
|
|
55
|
+
"name": "Application status bar",
|
|
56
|
+
"intents": ["status bar", "footer status", "connection state", "app footer"],
|
|
57
|
+
"markup": "<footer class=\"app-footer row ycenter xbetween pad-x-sm text-xs text-muted border-top surface\">\n\t<div class=\"row ycenter gap-xs\"><span class=\"square-6 radius-full bg-success\"></span><span>Connected</span></div>\n\t<div class=\"row ycenter gap-sm\"><span>Ln 42, Col 18</span><span>UTF-8</span></div>\n</footer>",
|
|
58
|
+
"notes": "Do not rely on color alone for connection state."
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
package/dist/asset.d.ts
ADDED