fractalstyler2 0.4.1 → 0.7.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 +114 -95
- package/dist/cli.js +105 -25
- package/dist/components/Accordion.svelte +44 -0
- package/dist/components/Accordion.svelte.d.ts +23 -0
- package/dist/components/AccordionItem.svelte +44 -0
- package/dist/components/AccordionItem.svelte.d.ts +21 -0
- package/dist/components/AppShell.svelte +86 -0
- package/dist/components/AppShell.svelte.d.ts +39 -0
- package/dist/components/ColorPicker.svelte +38 -0
- package/dist/components/ColorPicker.svelte.d.ts +6 -0
- package/dist/components/Hero.svelte +14 -0
- package/dist/components/Hero.svelte.d.ts +9 -0
- package/dist/components/LayoutPicker.svelte +34 -0
- package/dist/components/LayoutPicker.svelte.d.ts +6 -0
- package/dist/components/MotionPicker.svelte +34 -0
- package/dist/components/MotionPicker.svelte.d.ts +6 -0
- package/dist/components/PageShell.svelte +14 -0
- package/dist/components/PageShell.svelte.d.ts +9 -0
- package/dist/components/PageSplit.svelte +26 -0
- package/dist/components/PageSplit.svelte.d.ts +18 -0
- package/dist/components/ShapePicker.svelte +37 -0
- package/dist/components/ShapePicker.svelte.d.ts +6 -0
- package/dist/css/fractalstyler.css +10643 -0
- package/dist/css/fractalstyler.min.css +2 -0
- package/dist/index.d.ts +18 -6
- package/dist/index.js +30 -12
- package/dist/mcp/schemas/compile_fractals.json +3 -3
- package/dist/mcp/schemas/css_to_fractals.json +1 -1
- package/dist/mcp/schemas/generate_component.json +2 -2
- package/dist/mcp/schemas/instructions.md +41 -14
- package/dist/mcp/schemas/list_fractals.json +9 -7
- package/dist/mcp/schemas/validate_recipe.json +2 -2
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +320 -234
- package/dist/presets.core.d.ts +42 -0
- package/dist/presets.core.js +190 -0
- package/dist/presets.svelte.d.ts +6 -0
- package/dist/presets.svelte.js +21 -0
- package/dist/styles/_00_presets.sass +111 -0
- package/dist/styles/_00_themes.sass +1061 -0
- package/dist/styles/_00_tokens.sass +39 -17
- package/dist/styles/_01_config.sass +77 -70
- package/dist/styles/_02_dimensions.sass +174 -0
- package/dist/styles/_03_containers.sass +105 -0
- package/dist/styles/_04_layouts.sass +100 -0
- package/dist/styles/_05_shells.sass +374 -0
- package/dist/styles/_06_visuals.sass +236 -0
- package/dist/styles/_07_interactions.sass +112 -0
- package/dist/styles/_08_own.sass +4 -0
- package/dist/styles/canonical-markups.md +160 -0
- package/dist/styles/index.sass +11 -18
- package/dist/themes.d.ts +7 -0
- package/dist/themes.js +45 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/package.json +26 -15
- package/plugin.json +11 -10
- package/registry.json +2002 -0
- package/skills/fractal-styler/SKILL.md +137 -50
- package/skills/fractal-styler/references/fractals.md +416 -28
- package/skills/fractal-styler/references/tokens.md +110 -36
- package/dist/styles/_02_fonts.sass +0 -13
- package/dist/styles/_03_responsive.sass +0 -31
- package/dist/styles/_04_atoms.sass +0 -155
- package/dist/styles/_05_molecules.sass +0 -97
- package/dist/styles/_06_recipes.sass +0 -189
- package/dist/styles/_07_base.sass +0 -44
- package/dist/styles/_08_blocks.sass +0 -433
- package/dist/styles/_09_utilities.sass +0 -208
- package/dist/styles/_10_layouts.sass +0 -374
- package/dist/styles/_11_own.sass +0 -21
- package/dist/styles/_fractals.sass +0 -14
- package/skills/style-migration/SKILL.md +0 -45
- package/templates/_00_tokens.sass +0 -154
- package/templates/_01_config.sass +0 -66
- package/templates/_02_fonts.sass +0 -13
- package/templates/_03_responsive.sass +0 -31
- package/templates/_04_atoms.sass +0 -155
- package/templates/_05_molecules.sass +0 -97
- package/templates/_06_recipes.sass +0 -189
- package/templates/_07_base.sass +0 -44
- package/templates/_08_blocks.sass +0 -433
- package/templates/_09_utilities.sass +0 -207
- package/templates/_10_layouts.sass +0 -374
- package/templates/_11_own.sass +0 -21
- package/templates/_fractals.sass +0 -14
- package/templates/index.sass +0 -18
package/dist/mcp/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* fractalstyler2 MCP Server
|
|
4
|
-
* Model Context Protocol server exposing design tokens,
|
|
4
|
+
* Model Context Protocol server exposing design tokens, the class registry,
|
|
5
5
|
* token snapping, component generation, and linting for OpenDesign, Claude Desktop, Cursor, etc.
|
|
6
6
|
*/
|
|
7
7
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
@@ -11,16 +11,27 @@ import * as sass from 'sass';
|
|
|
11
11
|
import { existsSync, readFileSync } from 'node:fs';
|
|
12
12
|
import { dirname, join } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
|
-
const VERSION =
|
|
14
|
+
const VERSION = (() => {
|
|
15
|
+
// Read from package.json so this can never drift from the published version.
|
|
16
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
for (const c of [join(HERE, '..', '..', 'package.json'), join(HERE, '..', 'package.json')]) {
|
|
18
|
+
try {
|
|
19
|
+
if (existsSync(c))
|
|
20
|
+
return JSON.parse(readFileSync(c, 'utf8')).version;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
/* fall through */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return '0.0.0';
|
|
27
|
+
})();
|
|
15
28
|
// Resolve styles directory for SASS compiler loadPaths
|
|
16
29
|
function getStylesDir() {
|
|
17
30
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
18
31
|
const candidates = [
|
|
19
|
-
join(HERE, '..', 'styles'),
|
|
20
|
-
join(HERE, '..', '..', 'templates'),
|
|
32
|
+
join(HERE, '..', 'styles'), // dist/mcp -> dist/styles
|
|
21
33
|
join(HERE, 'styles'),
|
|
22
|
-
join(process.cwd(), 'src', 'lib', 'styles')
|
|
23
|
-
join(process.cwd(), 'templates')
|
|
34
|
+
join(process.cwd(), 'src', 'lib', 'styles')
|
|
24
35
|
];
|
|
25
36
|
for (const candidate of candidates) {
|
|
26
37
|
if (existsSync(candidate) && existsSync(join(candidate, '_00_tokens.sass'))) {
|
|
@@ -40,26 +51,25 @@ const DESIGN_TOKENS = {
|
|
|
40
51
|
xl: '1240px'
|
|
41
52
|
},
|
|
42
53
|
space: {
|
|
43
|
-
scale: ['3xs', '2xs', 'xs', '
|
|
54
|
+
scale: ['3xs', '2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl'],
|
|
44
55
|
clamp: {
|
|
45
56
|
'3xs': 'clamp(0.3125rem, 0.3125rem + 0vw, 0.3125rem)', // ~5px
|
|
46
57
|
'2xs': 'clamp(0.5625rem, 0.5369rem + 0.1136vw, 0.625rem)', // ~9-10px
|
|
47
58
|
xs: 'clamp(0.875rem, 0.8494rem + 0.1136vw, 0.9375rem)', // ~14-15px
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
sm: 'clamp(1.125rem, 1.0739rem + 0.2273vw, 1.25rem)', // ~18-20px
|
|
60
|
+
md: 'clamp(1.6875rem, 1.6108rem + 0.3409vw, 1.875rem)', // ~27-30px
|
|
61
|
+
lg: 'clamp(2.25rem, 2.1477rem + 0.4545vw, 2.5rem)', // ~36-40px
|
|
51
62
|
xl: 'clamp(3.375rem, 3.2216rem + 0.6818vw, 3.75rem)', // ~54-60px
|
|
52
63
|
'2xl': 'clamp(4.5rem, 4.2955rem + 0.9091vw, 5rem)', // ~72-80px
|
|
53
|
-
'3xl': 'clamp(6.75rem, 6.4432rem + 1.3636vw, 7.5rem)'
|
|
54
|
-
's-l': 'clamp(1.125rem, 0.5625rem + 2.5vw, 2.5rem)' // fluid s to l
|
|
64
|
+
'3xl': 'clamp(6.75rem, 6.4432rem + 1.3636vw, 7.5rem)' // ~108-120px
|
|
55
65
|
},
|
|
56
66
|
approxPx: {
|
|
57
67
|
'3xs': 5,
|
|
58
68
|
'2xs': 9,
|
|
59
69
|
xs: 14,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
sm: 18,
|
|
71
|
+
md: 28,
|
|
72
|
+
lg: 38,
|
|
63
73
|
xl: 56,
|
|
64
74
|
'2xl': 76,
|
|
65
75
|
'3xl': 114
|
|
@@ -144,8 +154,8 @@ const DESIGN_TOKENS = {
|
|
|
144
154
|
feedbackError: 'var(--feedback-error)'
|
|
145
155
|
},
|
|
146
156
|
brand: {
|
|
147
|
-
theme: 'var(--theme)',
|
|
148
|
-
themeHover: 'var(--theme-
|
|
157
|
+
theme: 'var(--theme-color)',
|
|
158
|
+
themeHover: 'var(--theme-color-alt)',
|
|
149
159
|
themeActive: 'var(--theme-active)',
|
|
150
160
|
ring: 'var(--ring)'
|
|
151
161
|
},
|
|
@@ -158,76 +168,57 @@ const DESIGN_TOKENS = {
|
|
|
158
168
|
}
|
|
159
169
|
};
|
|
160
170
|
// Catalog of Fractals (Mixins)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
{ name: 'stack', signature: '+stack($gap: xs, $x: null)', description: 'Vertical rhythm: flex column + gap.' },
|
|
197
|
-
{ name: 'cluster', signature: '+cluster($gap: xs, $x: start, $y: center)', description: 'Wrapping row for tags/buttons/chips.' },
|
|
198
|
-
{ name: 'center-column', signature: '+center-column($max: var(--measure, 60ch), $pad: s)', description: 'Bounded reading column with max-width measure.' },
|
|
199
|
-
{ name: 'cover', signature: '+cover($min: 100vh, $pad: s)', description: 'Full-height container with vertically centered focal child.' },
|
|
200
|
-
{ name: 'frame', signature: '+frame($ratio: 16 / 9)', description: 'Aspect-ratio container for media (images, video, iframe).' },
|
|
201
|
-
{ name: 'reel', signature: '+reel($gap: xs)', description: 'Horizontal scroll-snap rail.' },
|
|
202
|
-
{ name: 'with-sidebar', signature: '+with-sidebar($rail: 240px, $gap: s, $min: 60%)', description: 'Intrinsic sidebar and fluid main content.' },
|
|
203
|
-
{ name: 'surface', signature: '+surface($bg: surface, $pad: null, $radius: 6, $elevation: none)', description: 'All-in-one material fractal: skin, radius, pad, and elevation.' },
|
|
204
|
-
{ name: 'cols', signature: '+cols($map, $gap: s)', description: 'Responsive column grid mapped across breakpoints, e.g. (base: 1, sm: 2, lg: 3).' }
|
|
205
|
-
],
|
|
206
|
-
recipes: [
|
|
207
|
-
{ name: 'card', signature: '+card($bg: surface, $pad: null, $radius: 6, $elevation: none)', description: 'Vertical card container recipe with optional pad and elevation.' },
|
|
208
|
-
{ name: 'control', signature: '+control($size: md, $radius: 4)', description: 'Universal interactive control recipe (buttons, triggers, inputs).' },
|
|
209
|
-
{ name: 'select', signature: '+select($size: md, $radius: 4)', description: 'Select input recipe with embedded SVG chevron.' },
|
|
210
|
-
{ name: 'badge', signature: '+badge($radius: 4)', description: 'Compact status badge recipe.' }
|
|
211
|
-
],
|
|
212
|
-
layouts: [
|
|
213
|
-
{ name: 'grid-3', class: '.grid-3', description: 'Responsive 1 → 2 → 3 column reflow.' },
|
|
214
|
-
{ name: 'card-grid', class: '.card-grid', description: 'Intrinsic auto-fit grid for cards.' },
|
|
215
|
-
{ name: 'hero', class: '.hero', description: 'Full viewport cover with centered hero message.' },
|
|
216
|
-
{ name: 'holy-grail', class: '.holy-grail', description: 'Responsive header / (nav · main · aside) / footer.' },
|
|
217
|
-
{ name: 'docs', class: '.docs', description: 'Docs template with sidebar nav, center reading column, and right TOC.' },
|
|
218
|
-
{ name: 'app-shell', class: '.app-shell', description: 'Sticky header, fluid body, and footer application frame.' }
|
|
219
|
-
]
|
|
171
|
+
/**
|
|
172
|
+
* The class registry — the public API — read from registry.json.
|
|
173
|
+
*
|
|
174
|
+
* This was previously a hardcoded catalog of authoring mixins (+box, +stack,
|
|
175
|
+
* +surface...). Those never shipped in v2; the registry replaced them. It is
|
|
176
|
+
* loaded from the generated file now so this surface cannot describe an API
|
|
177
|
+
* the stylesheet does not have.
|
|
178
|
+
*/
|
|
179
|
+
function getRegistryPath() {
|
|
180
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
181
|
+
const candidates = [
|
|
182
|
+
join(HERE, '..', '..', 'registry.json'), // dist/mcp -> package root
|
|
183
|
+
join(HERE, '..', 'registry.json'),
|
|
184
|
+
join(process.cwd(), 'registry.json')
|
|
185
|
+
];
|
|
186
|
+
return candidates.find((c) => existsSync(c)) ?? null;
|
|
187
|
+
}
|
|
188
|
+
const REGISTRY = (() => {
|
|
189
|
+
const p = getRegistryPath();
|
|
190
|
+
if (!p)
|
|
191
|
+
return [];
|
|
192
|
+
try {
|
|
193
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
})();
|
|
199
|
+
const LAYER_NAMES = {
|
|
200
|
+
L0: 'Tokens',
|
|
201
|
+
L1: 'Dimensions',
|
|
202
|
+
L2: 'Containers',
|
|
203
|
+
L3: 'Layouts',
|
|
204
|
+
L4: 'Shells',
|
|
205
|
+
L5: 'Visuals & Interactions'
|
|
220
206
|
};
|
|
207
|
+
/** Registry grouped by layer, which is what list_fractals serves. */
|
|
208
|
+
const FRACTAL_CATALOG = REGISTRY.reduce((acc, entry) => {
|
|
209
|
+
(acc[entry.layer] ??= []).push(entry);
|
|
210
|
+
return acc;
|
|
211
|
+
}, {});
|
|
221
212
|
// Guidelines & Golden Rules
|
|
222
213
|
const GUIDELINES = `
|
|
223
214
|
# fractalstyler2 Design System Rules
|
|
224
215
|
|
|
225
|
-
1. Never hardcode a value that a token covers
|
|
226
|
-
2.
|
|
227
|
-
3.
|
|
228
|
-
4.
|
|
229
|
-
5. Mobile-first: define base
|
|
230
|
-
6.
|
|
216
|
+
1. Never hardcode a value that a token covers: reach for .gap-sm, .radius-md, .surface. Literals like .gap-16 are the sanctioned escape hatch when an exact pixel is load-bearing.
|
|
217
|
+
2. Classes are the ONLY public API (.gap-sm, .pad-x-sm, .box.xcenter, .button.primary). The system defines no authoring mixins and no SASS functions; +stack, +surface, space() and the like do not exist.
|
|
218
|
+
3. Visual toggles ride classes (.open, .active, .elevated); semantic state stays on native attributes ([disabled], [aria-expanded='true'], :focus-visible).
|
|
219
|
+
4. Do NOT invent class names. A composed string like class="row ycenter xbetween gap-sm pad-md surface border" is the finished state, not something to tidy into a semantic class. _08_own.sass is for third-party widget overrides only; check docs/13-cookbook.md before writing any custom declaration.
|
|
220
|
+
5. Mobile-first: define base classes first, then bind to a side of the 768px seam with the -mob / -desk suffixes (.pad-xs-mob .pad-lg-desk). Grids step on their own; .grid-6 goes 6 -> 3 -> 2 -> 1 and never strands a row.
|
|
221
|
+
6. Literal utilities come from a discrete ladder, not any integer: 0, 1, 2, 4, 6, 8, 12, then every multiple of 8 — to 64px for gap/pad/marg and radius, to 512px for w/h/square (.gap-16, .radius-4, .w-240). Negative margins use the -- infix (.marg--16).
|
|
231
222
|
`.trim();
|
|
232
223
|
// Snapping helper: find closest token
|
|
233
224
|
function findNearestToken(val, scaleMap) {
|
|
@@ -261,13 +252,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
261
252
|
tools: [
|
|
262
253
|
{
|
|
263
254
|
name: 'compile_fractals',
|
|
264
|
-
description: 'Compiles indented SASS
|
|
255
|
+
description: 'Compiles indented SASS into CSS to verify output. The system exposes no authoring mixins — compose in markup and use this for the rare custom declaration.',
|
|
265
256
|
inputSchema: {
|
|
266
257
|
type: 'object',
|
|
267
258
|
properties: {
|
|
268
259
|
sassCode: {
|
|
269
260
|
type: 'string',
|
|
270
|
-
description: 'Indented SASS code
|
|
261
|
+
description: 'Indented SASS code (plain declarations; the styles directory is on the load path).'
|
|
271
262
|
},
|
|
272
263
|
className: {
|
|
273
264
|
type: 'string',
|
|
@@ -306,7 +297,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
306
297
|
},
|
|
307
298
|
{
|
|
308
299
|
name: 'css_to_fractals',
|
|
309
|
-
description: 'Converts raw CSS declarations (from Figma/OpenDesign inspection) into
|
|
300
|
+
description: 'Converts raw CSS declarations (from Figma/OpenDesign inspection) into fractalstyler2 registry classes, composed in markup.',
|
|
310
301
|
inputSchema: {
|
|
311
302
|
type: 'object',
|
|
312
303
|
properties: {
|
|
@@ -320,7 +311,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
320
311
|
},
|
|
321
312
|
{
|
|
322
313
|
name: 'generate_component',
|
|
323
|
-
description: 'Generates a production-ready Svelte 5 component with runes and
|
|
314
|
+
description: 'Generates a production-ready Svelte 5 component with runes, composed entirely from registry classes and carrying no style block.',
|
|
324
315
|
inputSchema: {
|
|
325
316
|
type: 'object',
|
|
326
317
|
properties: {
|
|
@@ -342,25 +333,25 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
342
333
|
},
|
|
343
334
|
{
|
|
344
335
|
name: 'validate_recipe',
|
|
345
|
-
description: 'Lints
|
|
336
|
+
description: 'Lints markup against the golden rules: flags class names absent from the registry, component <style> blocks, and hardcoded pixel values.',
|
|
346
337
|
inputSchema: {
|
|
347
338
|
type: 'object',
|
|
348
339
|
properties: {
|
|
349
|
-
code: { type: 'string', description: 'The
|
|
340
|
+
code: { type: 'string', description: 'The Svelte or HTML markup to validate.' }
|
|
350
341
|
},
|
|
351
342
|
required: ['code']
|
|
352
343
|
}
|
|
353
344
|
},
|
|
354
345
|
{
|
|
355
346
|
name: 'list_fractals',
|
|
356
|
-
description: 'Returns the
|
|
347
|
+
description: 'Returns the class registry — the public API. Every composable class with the CSS it applies and its source file, grouped by layer. Query this before naming any class.',
|
|
357
348
|
inputSchema: {
|
|
358
349
|
type: 'object',
|
|
359
350
|
properties: {
|
|
360
|
-
|
|
351
|
+
layer: {
|
|
361
352
|
type: 'string',
|
|
362
|
-
enum: ['all', '
|
|
363
|
-
description: 'Filter by
|
|
353
|
+
enum: ['all', 'L0', 'L1', 'L2', 'L3', 'L4', 'L5'],
|
|
354
|
+
description: 'Filter by layer: L0 tokens, L1 dimensions, L2 containers, L3 layouts, L4 shells, L5 visuals & interactions.'
|
|
364
355
|
}
|
|
365
356
|
}
|
|
366
357
|
}
|
|
@@ -383,12 +374,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
383
374
|
.split('\n')
|
|
384
375
|
.map((line) => (line.trim() ? `\t${line}` : ''))
|
|
385
376
|
.join('\n');
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
.${className}
|
|
377
|
+
// No @use is injected: the system exposes no mixin module to import.
|
|
378
|
+
// loadPaths is set to the styles directory, so a caller that wants
|
|
379
|
+
// the token layer can `@use '00_tokens'` explicitly.
|
|
380
|
+
const fullSass = `.${className}
|
|
392
381
|
${indented}
|
|
393
382
|
`;
|
|
394
383
|
try {
|
|
@@ -437,7 +426,6 @@ ${indented}
|
|
|
437
426
|
inputPx: args.gap,
|
|
438
427
|
nearestToken: match.token,
|
|
439
428
|
cssVar: `var(--space-${match.token})`,
|
|
440
|
-
suggestedMixin: `+gap(${match.token})`,
|
|
441
429
|
utilityClass: `.gap-${match.token}`
|
|
442
430
|
};
|
|
443
431
|
}
|
|
@@ -447,7 +435,6 @@ ${indented}
|
|
|
447
435
|
inputPx: args.padding,
|
|
448
436
|
nearestToken: match.token,
|
|
449
437
|
cssVar: `var(--space-${match.token})`,
|
|
450
|
-
suggestedMixin: `+pad(${match.token})`,
|
|
451
438
|
utilityClass: `.pad-${match.token}`
|
|
452
439
|
};
|
|
453
440
|
}
|
|
@@ -458,7 +445,6 @@ ${indented}
|
|
|
458
445
|
inputPx: args.radius,
|
|
459
446
|
nearestToken: match.token,
|
|
460
447
|
cssVar: `var(--radius-${match.token})`,
|
|
461
|
-
suggestedMixin: `+radius(${match.token})`,
|
|
462
448
|
utilityClass: `.radius-${match.token}`
|
|
463
449
|
};
|
|
464
450
|
}
|
|
@@ -468,7 +454,6 @@ ${indented}
|
|
|
468
454
|
inputPx: args.fontSize,
|
|
469
455
|
nearestToken: match.token,
|
|
470
456
|
cssVar: `var(--text-${match.token})`,
|
|
471
|
-
suggestedMixin: `+type(${match.token})`,
|
|
472
457
|
utilityClass: `.text-${match.token}`
|
|
473
458
|
};
|
|
474
459
|
}
|
|
@@ -477,81 +462,197 @@ ${indented}
|
|
|
477
462
|
};
|
|
478
463
|
}
|
|
479
464
|
case 'css_to_fractals': {
|
|
465
|
+
// Emits registry CLASSES, not mixins. The system has no authoring
|
|
466
|
+
// mixins; earlier versions of this tool suggested +surface/+stack,
|
|
467
|
+
// which do not compile.
|
|
480
468
|
const rawCss = args.css || '';
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
let
|
|
488
|
-
let
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
|
|
469
|
+
const decls = rawCss
|
|
470
|
+
.split(/[;\n]/)
|
|
471
|
+
.map((l) => l.trim())
|
|
472
|
+
.filter(Boolean);
|
|
473
|
+
const classes = [];
|
|
474
|
+
const unmapped = [];
|
|
475
|
+
let display = null;
|
|
476
|
+
let direction = null;
|
|
477
|
+
const spaceStep = (n) => findNearestToken(n, DESIGN_TOKENS.space.approxPx).token;
|
|
478
|
+
const px = (v) => {
|
|
479
|
+
const n = parseInt(v, 10);
|
|
480
|
+
return isNaN(n) ? null : n;
|
|
481
|
+
};
|
|
482
|
+
for (const decl of decls) {
|
|
483
|
+
const idx = decl.indexOf(':');
|
|
484
|
+
if (idx < 0)
|
|
493
485
|
continue;
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
486
|
+
const prop = decl.slice(0, idx).trim();
|
|
487
|
+
const val = decl.slice(idx + 1).trim();
|
|
488
|
+
const n = px(val);
|
|
489
|
+
switch (prop) {
|
|
490
|
+
case 'display':
|
|
491
|
+
display = val;
|
|
492
|
+
break;
|
|
493
|
+
case 'flex-direction':
|
|
494
|
+
direction = val;
|
|
495
|
+
break;
|
|
496
|
+
case 'gap':
|
|
497
|
+
if (n !== null)
|
|
498
|
+
classes.push(`gap-${spaceStep(n)}`);
|
|
499
|
+
break;
|
|
500
|
+
case 'row-gap':
|
|
501
|
+
if (n !== null)
|
|
502
|
+
classes.push(`rgap-${spaceStep(n)}`);
|
|
503
|
+
break;
|
|
504
|
+
case 'column-gap':
|
|
505
|
+
if (n !== null)
|
|
506
|
+
classes.push(`cgap-${spaceStep(n)}`);
|
|
507
|
+
break;
|
|
508
|
+
case 'padding':
|
|
509
|
+
if (n !== null)
|
|
510
|
+
classes.push(`pad-${spaceStep(n)}`);
|
|
511
|
+
break;
|
|
512
|
+
case 'padding-inline':
|
|
513
|
+
if (n !== null)
|
|
514
|
+
classes.push(`pad-x-${spaceStep(n)}`);
|
|
515
|
+
break;
|
|
516
|
+
case 'padding-block':
|
|
517
|
+
if (n !== null)
|
|
518
|
+
classes.push(`pad-y-${spaceStep(n)}`);
|
|
519
|
+
break;
|
|
520
|
+
case 'margin':
|
|
521
|
+
if (n !== null)
|
|
522
|
+
classes.push(`marg-${spaceStep(n)}`);
|
|
523
|
+
break;
|
|
524
|
+
case 'border-radius':
|
|
525
|
+
if (n !== null)
|
|
526
|
+
classes.push(`radius-${n}`);
|
|
527
|
+
break;
|
|
528
|
+
case 'width':
|
|
529
|
+
if (val === '100%')
|
|
530
|
+
classes.push('wfull');
|
|
531
|
+
else if (n !== null)
|
|
532
|
+
classes.push(`w-${n}`);
|
|
533
|
+
break;
|
|
534
|
+
case 'height':
|
|
535
|
+
if (val === '100%')
|
|
536
|
+
classes.push('hfull');
|
|
537
|
+
else if (n !== null)
|
|
538
|
+
classes.push(`h-${n}`);
|
|
539
|
+
break;
|
|
540
|
+
case 'background':
|
|
541
|
+
case 'background-color':
|
|
542
|
+
if (val.includes('raised'))
|
|
543
|
+
classes.push('raised');
|
|
544
|
+
else if (val.includes('panel'))
|
|
545
|
+
classes.push('panel');
|
|
546
|
+
else if (val.includes('surface') || /^#(fff|ffffff)$/i.test(val))
|
|
547
|
+
classes.push('surface');
|
|
548
|
+
else
|
|
549
|
+
unmapped.push(`${prop}: ${val}`);
|
|
550
|
+
break;
|
|
551
|
+
case 'color':
|
|
552
|
+
if (val.includes('muted'))
|
|
553
|
+
classes.push('text-muted');
|
|
554
|
+
else if (val.includes('secondary'))
|
|
555
|
+
classes.push('text-secondary');
|
|
556
|
+
else if (val.includes('primary'))
|
|
557
|
+
classes.push('text-primary');
|
|
558
|
+
else
|
|
559
|
+
unmapped.push(`${prop}: ${val}`);
|
|
560
|
+
break;
|
|
561
|
+
case 'border':
|
|
562
|
+
classes.push('border');
|
|
563
|
+
break;
|
|
564
|
+
case 'border-bottom':
|
|
565
|
+
classes.push('border-bottom');
|
|
566
|
+
break;
|
|
567
|
+
case 'border-top':
|
|
568
|
+
classes.push('border-top');
|
|
569
|
+
break;
|
|
570
|
+
case 'box-shadow': {
|
|
571
|
+
// Depth is a three-step scale; pick by the blur radius.
|
|
572
|
+
const blur = parseInt(val.split(/\s+/)[3] ?? '0', 10);
|
|
573
|
+
classes.push(blur >= 24 ? 'shadow-lg' : blur >= 8 ? 'shadow-md' : 'shadow-sm');
|
|
574
|
+
break;
|
|
507
575
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
576
|
+
case 'flex-wrap':
|
|
577
|
+
if (val === 'wrap')
|
|
578
|
+
classes.push('wrap');
|
|
579
|
+
break;
|
|
580
|
+
case 'flex-grow':
|
|
581
|
+
if (val !== '0')
|
|
582
|
+
classes.push('grow');
|
|
583
|
+
break;
|
|
584
|
+
case 'flex-shrink':
|
|
585
|
+
if (val === '0')
|
|
586
|
+
classes.push('shrink-0');
|
|
587
|
+
break;
|
|
588
|
+
case 'min-width':
|
|
589
|
+
case 'min-height':
|
|
590
|
+
if (val === '0')
|
|
591
|
+
classes.push('min0');
|
|
592
|
+
break;
|
|
593
|
+
case 'position':
|
|
594
|
+
if (['relative', 'absolute', 'fixed', 'sticky'].includes(val))
|
|
595
|
+
classes.push(val);
|
|
596
|
+
break;
|
|
597
|
+
case 'text-overflow':
|
|
598
|
+
if (val === 'ellipsis')
|
|
599
|
+
classes.push('truncate');
|
|
600
|
+
break;
|
|
601
|
+
case 'font-weight':
|
|
602
|
+
if (['400', '500', '600', '700'].includes(val))
|
|
603
|
+
classes.push(`weight-${val}`);
|
|
604
|
+
else
|
|
605
|
+
unmapped.push(`${prop}: ${val}`);
|
|
606
|
+
break;
|
|
607
|
+
case 'justify-content':
|
|
608
|
+
case 'align-items':
|
|
609
|
+
// resolved below, once the container is known
|
|
610
|
+
unmapped.push(`${prop}: ${val}`);
|
|
611
|
+
break;
|
|
612
|
+
default:
|
|
613
|
+
unmapped.push(`${prop}: ${val}`);
|
|
531
614
|
}
|
|
532
615
|
}
|
|
533
|
-
//
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
616
|
+
// Container first, so alignment can be named on the physical axis.
|
|
617
|
+
const container = display === 'grid' ? 'grid' : display === 'flex' ? (direction === 'row' ? 'row' : 'box') : null;
|
|
618
|
+
if (container)
|
|
619
|
+
classes.unshift(container);
|
|
620
|
+
const ALIGN = {
|
|
621
|
+
'flex-start': 'left',
|
|
622
|
+
start: 'left',
|
|
623
|
+
center: 'center',
|
|
624
|
+
'flex-end': 'right',
|
|
625
|
+
end: 'right',
|
|
626
|
+
'space-between': 'between',
|
|
627
|
+
'space-evenly': 'evenly',
|
|
628
|
+
'space-around': 'around'
|
|
629
|
+
};
|
|
630
|
+
const resolved = [];
|
|
631
|
+
for (const u of unmapped.slice()) {
|
|
632
|
+
const m = /^(justify-content|align-items):\s*(.+)$/.exec(u);
|
|
633
|
+
if (!m || !container)
|
|
634
|
+
continue;
|
|
635
|
+
const word = ALIGN[m[2]];
|
|
636
|
+
if (!word)
|
|
637
|
+
continue;
|
|
638
|
+
// In a row, justify-content is the X axis; in a column it is Y.
|
|
639
|
+
const isX = container === 'row' ? m[1] === 'justify-content' : m[1] === 'align-items';
|
|
640
|
+
resolved.push(`${isX ? 'x' : 'y'}${word === 'left' && !isX ? 'top' : word === 'right' && !isX ? 'bot' : word}`);
|
|
641
|
+
unmapped.splice(unmapped.indexOf(u), 1);
|
|
542
642
|
}
|
|
543
|
-
|
|
544
|
-
|
|
643
|
+
classes.push(...resolved);
|
|
644
|
+
const attr = classes.length ? `<div class="${classes.join(' ')}">` : '';
|
|
645
|
+
let text = classes.length
|
|
646
|
+
? `Compose in markup:\n\n${attr}`
|
|
647
|
+
: 'No registry class matched. Check list_fractals for the full API.';
|
|
648
|
+
if (unmapped.length) {
|
|
649
|
+
text +=
|
|
650
|
+
`\n\nNot covered by the registry:\n` +
|
|
651
|
+
unmapped.map((u) => ` ${u}`).join('\n') +
|
|
652
|
+
`\n\nThese are candidates for a component <style> block or _08_own.sass — ` +
|
|
653
|
+
`but check docs/13-cookbook.md first; most patterns compose.`;
|
|
545
654
|
}
|
|
546
|
-
|
|
547
|
-
return {
|
|
548
|
-
content: [
|
|
549
|
-
{
|
|
550
|
-
type: 'text',
|
|
551
|
-
text: `Suggested fractalstyler2 recipe:\n\n${output}`
|
|
552
|
-
}
|
|
553
|
-
]
|
|
554
|
-
};
|
|
655
|
+
return { content: [{ type: 'text', text }] };
|
|
555
656
|
}
|
|
556
657
|
case 'generate_component': {
|
|
557
658
|
const compName = args.name || 'CustomCard';
|
|
@@ -566,89 +667,69 @@ ${indented}
|
|
|
566
667
|
let { title = 'Card Title', description = '${desc || 'Card summary text'}', children } = $props();
|
|
567
668
|
</script>
|
|
568
669
|
|
|
569
|
-
<article class="card"${elevation !== 'none' ? ' data-elevated' : ''}>
|
|
570
|
-
<div class="row ycenter xbetween">
|
|
571
|
-
<h3 class="text-lg">{title}</h3>
|
|
670
|
+
<article class="card box gap-sm pad-md surface border"${elevation !== 'none' ? ' data-elevated' : ''}>
|
|
671
|
+
<div class="row ycenter xbetween gap-sm">
|
|
672
|
+
<h3 class="text-lg weight-600 m-0">{title}</h3>
|
|
572
673
|
<span class="badge">Active</span>
|
|
573
674
|
</div>
|
|
574
|
-
<p class="
|
|
675
|
+
<p class="text-sm text-secondary">{description}</p>
|
|
575
676
|
{#if children}
|
|
576
677
|
{@render children()}
|
|
577
678
|
{/if}
|
|
578
679
|
</article>
|
|
579
|
-
|
|
580
|
-
<style lang="sass">
|
|
581
|
-
@use '$lib/styles/fractals' as *
|
|
582
|
-
</style>`;
|
|
680
|
+
`;
|
|
583
681
|
break;
|
|
584
682
|
case 'panel':
|
|
585
683
|
template = `<script lang="ts">
|
|
586
684
|
let { heading = 'Panel Heading', children } = $props();
|
|
587
685
|
</script>
|
|
588
686
|
|
|
589
|
-
<section class="panel">
|
|
590
|
-
<header class="row ycenter xbetween">
|
|
591
|
-
<h2 class="text-xl">{heading}</h2>
|
|
687
|
+
<section class="panel box gap-md pad-md">
|
|
688
|
+
<header class="row ycenter xbetween gap-sm">
|
|
689
|
+
<h2 class="text-xl weight-600 m-0">{heading}</h2>
|
|
592
690
|
</header>
|
|
593
|
-
<div class="box gap-
|
|
691
|
+
<div class="box gap-sm">
|
|
594
692
|
{#if children}
|
|
595
693
|
{@render children()}
|
|
596
694
|
{/if}
|
|
597
695
|
</div>
|
|
598
696
|
</section>
|
|
599
|
-
|
|
600
|
-
<style lang="sass">
|
|
601
|
-
@use '$lib/styles/fractals' as *
|
|
602
|
-
</style>`;
|
|
697
|
+
`;
|
|
603
698
|
break;
|
|
604
699
|
case 'button':
|
|
605
700
|
template = `<script lang="ts">
|
|
606
701
|
let { variant = 'primary', disabled = false, onclick, children } = $props();
|
|
607
702
|
</script>
|
|
608
703
|
|
|
609
|
-
<button class="button
|
|
704
|
+
<button class="button {variant}" {disabled} {onclick}>
|
|
610
705
|
{#if children}
|
|
611
706
|
{@render children()}
|
|
612
707
|
{:else}
|
|
613
708
|
Action
|
|
614
709
|
{/if}
|
|
615
710
|
</button>
|
|
616
|
-
|
|
617
|
-
<style lang="sass">
|
|
618
|
-
@use '$lib/styles/fractals' as *
|
|
619
|
-
</style>`;
|
|
711
|
+
`;
|
|
620
712
|
break;
|
|
621
713
|
case 'badge':
|
|
622
714
|
template = `<script lang="ts">
|
|
623
715
|
let { label = 'Badge', variant = 'default' } = $props();
|
|
624
716
|
</script>
|
|
625
717
|
|
|
626
|
-
<span class="badge" data-
|
|
718
|
+
<span class="badge" data-status={variant}>
|
|
627
719
|
{label}
|
|
628
720
|
</span>
|
|
629
|
-
|
|
630
|
-
<style lang="sass">
|
|
631
|
-
@use '$lib/styles/fractals' as *
|
|
632
|
-
</style>`;
|
|
721
|
+
`;
|
|
633
722
|
break;
|
|
634
723
|
default:
|
|
635
724
|
template = `<script lang="ts">
|
|
636
725
|
let { children } = $props();
|
|
637
726
|
</script>
|
|
638
727
|
|
|
639
|
-
<div class="
|
|
728
|
+
<div class="box gap-sm pad-md surface border radius-md">
|
|
640
729
|
{#if children}
|
|
641
730
|
{@render children()}
|
|
642
731
|
{/if}
|
|
643
|
-
</div
|
|
644
|
-
|
|
645
|
-
<style lang="sass">
|
|
646
|
-
@use '$lib/styles/fractals' as *
|
|
647
|
-
|
|
648
|
-
.custom-container
|
|
649
|
-
+surface(surface, m, 16)
|
|
650
|
-
+stack(s)
|
|
651
|
-
</style>`;
|
|
732
|
+
</div>`;
|
|
652
733
|
break;
|
|
653
734
|
}
|
|
654
735
|
return {
|
|
@@ -663,28 +744,34 @@ ${indented}
|
|
|
663
744
|
case 'validate_recipe': {
|
|
664
745
|
const code = args.code || '';
|
|
665
746
|
const diagnostics = [];
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
747
|
+
// The failure mode worth catching is an invented class, not a legacy one.
|
|
748
|
+
const INVENTED = /class="([^"]*)"/g;
|
|
749
|
+
let m;
|
|
750
|
+
while ((m = INVENTED.exec(code)) !== null) {
|
|
751
|
+
for (const cls of m[1].split(/\s+/).filter(Boolean)) {
|
|
752
|
+
if (cls.includes('{') || cls.includes('$'))
|
|
753
|
+
continue;
|
|
754
|
+
const known = REGISTRY.some((e) => e.class.split('.').filter(Boolean).includes(cls)) ||
|
|
755
|
+
/^(gap|rgap|cgap|pad|marg|radius|w|h|square|text|weight|clamp|grid|frame)-/.test(cls) ||
|
|
756
|
+
['open', 'active', 'checked', 'primary', 'ghost', 'is-icon'].includes(cls);
|
|
757
|
+
if (!known) {
|
|
758
|
+
diagnostics.push({
|
|
759
|
+
severity: 'error',
|
|
760
|
+
message: `Unknown class ".${cls}". It is not in the registry. Compose from existing classes — call list_fractals — or check docs/13-cookbook.md for the pattern you are building.`
|
|
761
|
+
});
|
|
762
|
+
}
|
|
682
763
|
}
|
|
683
764
|
}
|
|
765
|
+
if (/<style/.test(code)) {
|
|
766
|
+
diagnostics.push({
|
|
767
|
+
severity: 'error',
|
|
768
|
+
message: 'Component <style> block found. Compose in markup instead; the registry covers nearly every pattern (docs/13-cookbook.md).'
|
|
769
|
+
});
|
|
770
|
+
}
|
|
684
771
|
if (/padding:\s*\d+px/i.test(code) || /gap:\s*\d+px/i.test(code)) {
|
|
685
772
|
diagnostics.push({
|
|
686
773
|
severity: 'warning',
|
|
687
|
-
message: 'Hardcoded px values
|
|
774
|
+
message: 'Hardcoded px values in CSS. Compose .gap-sm / .pad-md in the markup instead — or a literal .gap-18 if the exact value is load-bearing.'
|
|
688
775
|
});
|
|
689
776
|
}
|
|
690
777
|
if (code.includes('.is-active') || code.includes('--active')) {
|
|
@@ -704,13 +791,12 @@ ${indented}
|
|
|
704
791
|
};
|
|
705
792
|
}
|
|
706
793
|
case 'list_fractals': {
|
|
707
|
-
const
|
|
708
|
-
if (
|
|
709
|
-
|
|
794
|
+
const layer = args.layer || 'all';
|
|
795
|
+
if (REGISTRY.length === 0) {
|
|
796
|
+
throw new Error('Class registry not found. registry.json ships with the package; run `npm run registry` if working from source.');
|
|
710
797
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
};
|
|
798
|
+
const payload = layer === 'all' ? FRACTAL_CATALOG : { [layer]: FRACTAL_CATALOG[layer] ?? [] };
|
|
799
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
714
800
|
}
|
|
715
801
|
default:
|
|
716
802
|
throw new Error(`Unknown tool: ${name}`);
|
|
@@ -730,8 +816,8 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
|
730
816
|
},
|
|
731
817
|
{
|
|
732
818
|
uri: 'fractalstyler2://fractals',
|
|
733
|
-
name: '
|
|
734
|
-
description: '
|
|
819
|
+
name: 'Class Registry',
|
|
820
|
+
description: 'The public API: every composable class, grouped by layer L0-L5, with the CSS it applies and its source file.',
|
|
735
821
|
mimeType: 'application/json'
|
|
736
822
|
},
|
|
737
823
|
{
|
|
@@ -791,7 +877,7 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
|
791
877
|
prompts: [
|
|
792
878
|
{
|
|
793
879
|
name: 'design_system_review',
|
|
794
|
-
description: 'Audit and refactor a component or screen to
|
|
880
|
+
description: 'Audit and refactor a component or screen to compose from the fractalstyler2 class registry.'
|
|
795
881
|
},
|
|
796
882
|
{
|
|
797
883
|
name: 'generate_ui',
|
|
@@ -813,7 +899,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
813
899
|
role: 'user',
|
|
814
900
|
content: {
|
|
815
901
|
type: 'text',
|
|
816
|
-
text: `
|
|
902
|
+
text: `Review the following code and refactor it to compose from the fractalstyler2 class registry in the markup. Remove stylesheet declarations the registry already covers; do not introduce new class names. Call list_fractals if unsure of a class:\n\n${GUIDELINES}`
|
|
817
903
|
}
|
|
818
904
|
}
|
|
819
905
|
]
|
|
@@ -827,7 +913,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
827
913
|
role: 'user',
|
|
828
914
|
content: {
|
|
829
915
|
type: 'text',
|
|
830
|
-
text: `Generate a responsive UI component using Svelte 5 runes ($props, $state) and
|
|
916
|
+
text: `Generate a responsive UI component using Svelte 5 runes ($props, $state), composed entirely from fractalstyler2 registry classes in the markup. Do not write a style block and do not invent class names — call list_fractals if unsure. Adhere to the golden rules:\n\n${GUIDELINES}`
|
|
831
917
|
}
|
|
832
918
|
}
|
|
833
919
|
]
|