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.
Files changed (87) hide show
  1. package/README.md +114 -95
  2. package/dist/cli.js +105 -25
  3. package/dist/components/Accordion.svelte +44 -0
  4. package/dist/components/Accordion.svelte.d.ts +23 -0
  5. package/dist/components/AccordionItem.svelte +44 -0
  6. package/dist/components/AccordionItem.svelte.d.ts +21 -0
  7. package/dist/components/AppShell.svelte +86 -0
  8. package/dist/components/AppShell.svelte.d.ts +39 -0
  9. package/dist/components/ColorPicker.svelte +38 -0
  10. package/dist/components/ColorPicker.svelte.d.ts +6 -0
  11. package/dist/components/Hero.svelte +14 -0
  12. package/dist/components/Hero.svelte.d.ts +9 -0
  13. package/dist/components/LayoutPicker.svelte +34 -0
  14. package/dist/components/LayoutPicker.svelte.d.ts +6 -0
  15. package/dist/components/MotionPicker.svelte +34 -0
  16. package/dist/components/MotionPicker.svelte.d.ts +6 -0
  17. package/dist/components/PageShell.svelte +14 -0
  18. package/dist/components/PageShell.svelte.d.ts +9 -0
  19. package/dist/components/PageSplit.svelte +26 -0
  20. package/dist/components/PageSplit.svelte.d.ts +18 -0
  21. package/dist/components/ShapePicker.svelte +37 -0
  22. package/dist/components/ShapePicker.svelte.d.ts +6 -0
  23. package/dist/css/fractalstyler.css +10643 -0
  24. package/dist/css/fractalstyler.min.css +2 -0
  25. package/dist/index.d.ts +18 -6
  26. package/dist/index.js +30 -12
  27. package/dist/mcp/schemas/compile_fractals.json +3 -3
  28. package/dist/mcp/schemas/css_to_fractals.json +1 -1
  29. package/dist/mcp/schemas/generate_component.json +2 -2
  30. package/dist/mcp/schemas/instructions.md +41 -14
  31. package/dist/mcp/schemas/list_fractals.json +9 -7
  32. package/dist/mcp/schemas/validate_recipe.json +2 -2
  33. package/dist/mcp/server.d.ts +1 -1
  34. package/dist/mcp/server.js +320 -234
  35. package/dist/presets.core.d.ts +42 -0
  36. package/dist/presets.core.js +190 -0
  37. package/dist/presets.svelte.d.ts +6 -0
  38. package/dist/presets.svelte.js +21 -0
  39. package/dist/styles/_00_presets.sass +111 -0
  40. package/dist/styles/_00_themes.sass +1061 -0
  41. package/dist/styles/_00_tokens.sass +39 -17
  42. package/dist/styles/_01_config.sass +77 -70
  43. package/dist/styles/_02_dimensions.sass +174 -0
  44. package/dist/styles/_03_containers.sass +105 -0
  45. package/dist/styles/_04_layouts.sass +100 -0
  46. package/dist/styles/_05_shells.sass +374 -0
  47. package/dist/styles/_06_visuals.sass +236 -0
  48. package/dist/styles/_07_interactions.sass +112 -0
  49. package/dist/styles/_08_own.sass +4 -0
  50. package/dist/styles/canonical-markups.md +160 -0
  51. package/dist/styles/index.sass +11 -18
  52. package/dist/themes.d.ts +7 -0
  53. package/dist/themes.js +45 -0
  54. package/dist/version.d.ts +1 -0
  55. package/dist/version.js +2 -0
  56. package/package.json +26 -15
  57. package/plugin.json +11 -10
  58. package/registry.json +2002 -0
  59. package/skills/fractal-styler/SKILL.md +137 -50
  60. package/skills/fractal-styler/references/fractals.md +416 -28
  61. package/skills/fractal-styler/references/tokens.md +110 -36
  62. package/dist/styles/_02_fonts.sass +0 -13
  63. package/dist/styles/_03_responsive.sass +0 -31
  64. package/dist/styles/_04_atoms.sass +0 -155
  65. package/dist/styles/_05_molecules.sass +0 -97
  66. package/dist/styles/_06_recipes.sass +0 -189
  67. package/dist/styles/_07_base.sass +0 -44
  68. package/dist/styles/_08_blocks.sass +0 -433
  69. package/dist/styles/_09_utilities.sass +0 -208
  70. package/dist/styles/_10_layouts.sass +0 -374
  71. package/dist/styles/_11_own.sass +0 -21
  72. package/dist/styles/_fractals.sass +0 -14
  73. package/skills/style-migration/SKILL.md +0 -45
  74. package/templates/_00_tokens.sass +0 -154
  75. package/templates/_01_config.sass +0 -66
  76. package/templates/_02_fonts.sass +0 -13
  77. package/templates/_03_responsive.sass +0 -31
  78. package/templates/_04_atoms.sass +0 -155
  79. package/templates/_05_molecules.sass +0 -97
  80. package/templates/_06_recipes.sass +0 -189
  81. package/templates/_07_base.sass +0 -44
  82. package/templates/_08_blocks.sass +0 -433
  83. package/templates/_09_utilities.sass +0 -207
  84. package/templates/_10_layouts.sass +0 -374
  85. package/templates/_11_own.sass +0 -21
  86. package/templates/_fractals.sass +0 -14
  87. package/templates/index.sass +0 -18
@@ -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, SASS mixin compilation,
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 = '0.4.0';
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', 's', 'm', 'l', 'xl', '2xl', '3xl', 's-l'],
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
- s: 'clamp(1.125rem, 1.0739rem + 0.2273vw, 1.25rem)', // ~18-20px
49
- m: 'clamp(1.6875rem, 1.6108rem + 0.3409vw, 1.875rem)', // ~27-30px
50
- l: 'clamp(2.25rem, 2.1477rem + 0.4545vw, 2.5rem)', // ~36-40px
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)', // ~108-120px
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
- s: 18,
61
- m: 28,
62
- l: 38,
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-hover)',
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
- const FRACTAL_CATALOG = {
162
- atoms: [
163
- { name: 'box', signature: '+box($x: null, $y: null)', description: 'Flex column layout with optional cross/main axis alignment.' },
164
- { name: 'row', signature: '+row($x: null, $y: null)', description: 'Flex row layout with optional main/cross axis alignment.' },
165
- { name: 'wrap', signature: '+wrap', description: 'Enables flex-wrap: wrap on container.' },
166
- { name: 'grid', signature: '+grid($cols: 1)', description: 'CSS grid with fixed column count repeat($cols, minmax(0, 1fr)).' },
167
- { name: 'auto-grid', signature: '+auto-grid($min: 15rem, $gap: s)', description: 'Intrinsic auto-fit CSS grid without breakpoints.' },
168
- { name: 'center', signature: '+center', description: 'Dead-center anything using display: grid; place-items: center.' },
169
- { name: 'gap', signature: '+gap($v: s)', description: 'Applies gap from space token or raw px value.' },
170
- { name: 'pad', signature: '+pad($v: s)', description: 'Applies padding on all sides from space token or raw px.' },
171
- { name: 'px', signature: '+px($v: s)', description: 'Applies inline padding (left/right) from space token or raw px.' },
172
- { name: 'py', signature: '+py($v: s)', description: 'Applies block padding (top/bottom) from space token or raw px.' },
173
- { name: 'mx-auto', signature: '+mx-auto', description: 'Sets margin-inline: auto for horizontal centering.' },
174
- { name: 'my-auto', signature: '+my-auto', description: 'Sets margin-block: auto for vertical centering.' },
175
- { name: 'w', signature: '+w($v: 100%)', description: 'Sets width.' },
176
- { name: 'h', signature: '+h($v: 100%)', description: 'Sets height.' },
177
- { name: 'full', signature: '+full', description: 'Sets width: 100% and height: 100%.' },
178
- { name: 'square', signature: '+square($v)', description: 'Sets equal width and height.' },
179
- { name: 'grow', signature: '+grow($n: 1)', description: 'Sets flex-grow.' },
180
- { name: 'shrink', signature: '+shrink($n: 0)', description: 'Sets flex-shrink.' },
181
- { name: 'min0', signature: '+min0', description: 'Sets min-width: 0 and min-height: 0 to prevent overflow.' },
182
- { name: 'bg', signature: '+bg($role: surface)', description: 'Sets background-color to any of the 21 surface tokens.' },
183
- { name: 'ink', signature: '+ink($role: primary)', description: 'Sets text color to primary, secondary, muted, inverse, theme-color, theme-color-alt.' },
184
- { name: 'border', signature: '+border($side: all, $color: var(--border))', description: 'Applies 1px solid border on all sides or a specific side.' },
185
- { name: 'radius', signature: '+radius($v: 6)', description: 'Applies border-radius from radius token or raw px.' },
186
- { name: 'shadow', signature: '+shadow($v: md)', description: 'Applies box-shadow from shadow scale (sm, md, lg).' },
187
- { name: 'type', signature: '+type($v)', description: 'Applies font-size from fluid type scale (xs..4xl).' },
188
- { name: 'weight', signature: '+weight($w: 500)', description: 'Sets font-weight.' },
189
- { name: 'leading', signature: '+leading($lh: 1.5)', description: 'Sets line-height.' },
190
- { name: 'truncate', signature: '+truncate', description: 'Single-line text truncation with ellipsis.' },
191
- { name: 'clamp-lines', signature: '+clamp-lines($n: 2)', description: 'Multi-line clamp using -webkit-line-clamp.' },
192
- { name: 'transition', signature: '+transition($props: all, $dur: 150ms, $ease: ease)', description: 'Smooth CSS transition.' },
193
- { name: 'ring', signature: '+ring($color: var(--ring))', description: 'Focus outline ring with 1px offset.' }
194
- ],
195
- molecules: [
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 (+gap(m), +radius(6), +bg(surface)).
226
- 2. Compose fractals (+surface, +stack, +cluster, +card); write raw CSS only for genuinely unique lines.
227
- 3. Express component state on data-* / aria-* attributes, never modifier classes (e.g. &[data-elevated], &[data-variant='ghost']).
228
- 4. Markup stays thin and semantic: prefer clean tags (<article class="card">) over utility class soup.
229
- 5. Mobile-first: define base styles first, then grow with +at(md/lg/xl) or +cols().
230
- 6. No legacy v1 classes (e.g. no gap8, pad16, w100, .stack as markup class).
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 fractal mixins into CSS. Useful for live preview in OpenDesign or web apps.',
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. Can use any fractal mixin (+surface, +stack, +gap, etc.).'
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 idiomatic fractalstyler2 SASS mixins.',
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 scoped SASS fractal mixins.',
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 a SASS snippet or Svelte component against fractalstyler2 golden rules (flags legacy classes, unmapped pixels, etc.).',
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 SASS or Svelte code to validate.' }
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 catalog of all available atom & molecule mixins with their signatures and descriptions.',
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
- tier: {
351
+ layer: {
361
352
  type: 'string',
362
- enum: ['all', 'atoms', 'molecules', 'recipes', 'layouts'],
363
- description: 'Filter by fractal tier.'
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
- const fullSass = `
387
- @use 'tokens'
388
- @use 'base'
389
- @use 'fractals' as *
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 lines = rawCss.split(/[;\n]/).map((l) => l.trim()).filter(Boolean);
482
- const mixins = [];
483
- let hasFlexCol = false;
484
- let hasFlexRow = false;
485
- let gapVal = null;
486
- let padVal = null;
487
- let radiusVal = null;
488
- let bgVal = null;
489
- let elevationVal = null;
490
- for (const line of lines) {
491
- const [prop, val] = line.split(':').map((s) => s?.trim());
492
- if (!prop || !val)
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
- if (prop === 'display' && val === 'flex') {
495
- // wait for flex-direction
496
- }
497
- else if (prop === 'flex-direction' && val === 'column') {
498
- hasFlexCol = true;
499
- }
500
- else if (prop === 'flex-direction' && val === 'row') {
501
- hasFlexRow = true;
502
- }
503
- else if (prop === 'gap') {
504
- const num = parseInt(val, 10);
505
- if (!isNaN(num)) {
506
- gapVal = findNearestToken(num, DESIGN_TOKENS.space.approxPx).token;
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
- else if (prop === 'padding') {
510
- const num = parseInt(val, 10);
511
- if (!isNaN(num)) {
512
- padVal = findNearestToken(num, DESIGN_TOKENS.space.approxPx).token;
513
- }
514
- }
515
- else if (prop === 'border-radius') {
516
- const num = parseInt(val, 10);
517
- if (!isNaN(num)) {
518
- radiusVal = String(num);
519
- }
520
- }
521
- else if (prop === 'background-color' || prop === 'background') {
522
- if (val.includes('raised'))
523
- bgVal = 'raised';
524
- else if (val.includes('surface') || val === '#ffffff' || val === '#fff')
525
- bgVal = 'surface';
526
- else
527
- bgVal = 'bg';
528
- }
529
- else if (prop === 'box-shadow') {
530
- elevationVal = 'md';
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
- // If surface combination
534
- if (bgVal || padVal || radiusVal) {
535
- mixins.push(`+surface(${bgVal || 'surface'}, ${padVal || 's'}, ${radiusVal || '12'}${elevationVal ? `, ${elevationVal}` : ''})`);
536
- }
537
- if (hasFlexCol) {
538
- mixins.push(`+stack(${gapVal || 's'})`);
539
- }
540
- else if (hasFlexRow) {
541
- mixins.push(`+cluster(${gapVal || 'xs'})`);
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
- else if (gapVal && !hasFlexCol && !hasFlexRow) {
544
- mixins.push(`+gap(${gapVal})`);
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
- const output = mixins.length > 0 ? mixins.join('\n') : '// No direct fractal match; use atoms:\n+box\n+gap(s)';
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="body muted">{description}</p>
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-s">
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" data-variant={variant} {disabled} {onclick}>
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-variant={variant}>
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="custom-container">
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
- const legacyClassPatterns = [
667
- { regex: /\bgap\d+\b/g, name: 'gapN (e.g. gap8)', replacement: '.gap-xs / .gap-s or +gap(N)' },
668
- { regex: /\bpad\d+\b/g, name: 'padN (e.g. pad16)', replacement: '.pad-s or +pad(N)' },
669
- { regex: /\bw100\b/g, name: 'w100', replacement: '.wfull or +w(100%)' },
670
- { regex: /\bh100\b/g, name: 'h100', replacement: '.hfull or +h(100%)' },
671
- { regex: /\bmin-w-0\b/g, name: 'min-w-0', replacement: '.min0' },
672
- { regex: /\bclass="[^"]*\bstack\b[^"]*"/g, name: 'class="stack"', replacement: '+stack() in SASS (no markup class)' },
673
- { regex: /\bclass="[^"]*\bcluster\b[^"]*"/g, name: 'class="cluster"', replacement: '+cluster() in SASS (no markup class)' },
674
- { regex: /\bclass="[^"]*\bappshell\b[^"]*"/g, name: 'class="appshell"', replacement: '.app-shell' }
675
- ];
676
- for (const p of legacyClassPatterns) {
677
- if (p.regex.test(code)) {
678
- diagnostics.push({
679
- severity: 'error',
680
- message: `Found legacy v1 pattern "${p.name}". In fractalstyler2 use: ${p.replacement}.`
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 detected in CSS. Prefer token resolvers like +gap(s) or +pad(m).'
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 tier = args.tier || 'all';
708
- if (tier === 'all') {
709
- return { content: [{ type: 'text', text: JSON.stringify(FRACTAL_CATALOG, null, 2) }] };
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
- return {
712
- content: [{ type: 'text', text: JSON.stringify({ [tier]: FRACTAL_CATALOG[tier] || [] }, null, 2) }]
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: 'Fractal Mixin Catalog',
734
- description: 'Catalog of atom and molecule mixins, signatures, and descriptions.',
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 follow fractalstyler2 SASS mixin rules.'
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: `Please review the following code and refactor it into idiomatic fractalstyler2 Svelte 5 + indented SASS (.sass):\n\n${GUIDELINES}`
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 scoped indented SASS with fractal mixins (+surface, +stack, +cluster, +cols). Adhere to the golden rules:\n\n${GUIDELINES}`
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
  ]