fractalstyler2 0.4.0 → 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 +334 -237
  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 +57 -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 -373
  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 -136
  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 -373
  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.3.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,29 +51,28 @@ 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: 27,
62
- l: 36,
63
- xl: 54,
64
- '2xl': 72,
65
- '3xl': 108
70
+ sm: 18,
71
+ md: 28,
72
+ lg: 38,
73
+ xl: 56,
74
+ '2xl': 76,
75
+ '3xl': 114
66
76
  }
67
77
  },
68
78
  typography: {
@@ -132,9 +142,20 @@ const DESIGN_TOKENS = {
132
142
  themeColor: 'var(--theme-color)',
133
143
  themeColorAlt: 'var(--theme-color-alt)'
134
144
  },
145
+ status: {
146
+ success: 'var(--success)',
147
+ successHover: 'var(--success-hover)',
148
+ warning: 'var(--warning)',
149
+ warningHover: 'var(--warning-hover)',
150
+ danger: 'var(--danger)',
151
+ dangerHover: 'var(--danger-hover)',
152
+ info: 'var(--info)',
153
+ infoHover: 'var(--info-hover)',
154
+ feedbackError: 'var(--feedback-error)'
155
+ },
135
156
  brand: {
136
- theme: 'var(--theme)',
137
- themeHover: 'var(--theme-hover)',
157
+ theme: 'var(--theme-color)',
158
+ themeHover: 'var(--theme-color-alt)',
138
159
  themeActive: 'var(--theme-active)',
139
160
  ring: 'var(--ring)'
140
161
  },
@@ -147,76 +168,57 @@ const DESIGN_TOKENS = {
147
168
  }
148
169
  };
149
170
  // Catalog of Fractals (Mixins)
150
- const FRACTAL_CATALOG = {
151
- atoms: [
152
- { name: 'box', signature: '+box($x: null, $y: null)', description: 'Flex column layout with optional cross/main axis alignment.' },
153
- { name: 'row', signature: '+row($x: null, $y: null)', description: 'Flex row layout with optional main/cross axis alignment.' },
154
- { name: 'wrap', signature: '+wrap', description: 'Enables flex-wrap: wrap on container.' },
155
- { name: 'grid', signature: '+grid($cols: 1)', description: 'CSS grid with fixed column count repeat($cols, minmax(0, 1fr)).' },
156
- { name: 'auto-grid', signature: '+auto-grid($min: 15rem, $gap: s)', description: 'Intrinsic auto-fit CSS grid without breakpoints.' },
157
- { name: 'center', signature: '+center', description: 'Dead-center anything using display: grid; place-items: center.' },
158
- { name: 'gap', signature: '+gap($v: s)', description: 'Applies gap from space token or raw px value.' },
159
- { name: 'pad', signature: '+pad($v: s)', description: 'Applies padding on all sides from space token or raw px.' },
160
- { name: 'px', signature: '+px($v: s)', description: 'Applies inline padding (left/right) from space token or raw px.' },
161
- { name: 'py', signature: '+py($v: s)', description: 'Applies block padding (top/bottom) from space token or raw px.' },
162
- { name: 'mx-auto', signature: '+mx-auto', description: 'Sets margin-inline: auto for horizontal centering.' },
163
- { name: 'my-auto', signature: '+my-auto', description: 'Sets margin-block: auto for vertical centering.' },
164
- { name: 'w', signature: '+w($v: 100%)', description: 'Sets width.' },
165
- { name: 'h', signature: '+h($v: 100%)', description: 'Sets height.' },
166
- { name: 'full', signature: '+full', description: 'Sets width: 100% and height: 100%.' },
167
- { name: 'square', signature: '+square($v)', description: 'Sets equal width and height.' },
168
- { name: 'grow', signature: '+grow($n: 1)', description: 'Sets flex-grow.' },
169
- { name: 'shrink', signature: '+shrink($n: 0)', description: 'Sets flex-shrink.' },
170
- { name: 'min0', signature: '+min0', description: 'Sets min-width: 0 and min-height: 0 to prevent overflow.' },
171
- { name: 'bg', signature: '+bg($role: surface)', description: 'Sets background-color to any of the 21 surface tokens.' },
172
- { name: 'ink', signature: '+ink($role: primary)', description: 'Sets text color to primary, secondary, muted, inverse, theme-color, theme-color-alt.' },
173
- { name: 'border', signature: '+border($side: all, $color: var(--border))', description: 'Applies 1px solid border on all sides or a specific side.' },
174
- { name: 'radius', signature: '+radius($v: 6)', description: 'Applies border-radius from radius token or raw px.' },
175
- { name: 'shadow', signature: '+shadow($v: md)', description: 'Applies box-shadow from shadow scale (sm, md, lg).' },
176
- { name: 'type', signature: '+type($v)', description: 'Applies font-size from fluid type scale (xs..4xl).' },
177
- { name: 'weight', signature: '+weight($w: 500)', description: 'Sets font-weight.' },
178
- { name: 'leading', signature: '+leading($lh: 1.5)', description: 'Sets line-height.' },
179
- { name: 'truncate', signature: '+truncate', description: 'Single-line text truncation with ellipsis.' },
180
- { name: 'clamp-lines', signature: '+clamp-lines($n: 2)', description: 'Multi-line clamp using -webkit-line-clamp.' },
181
- { name: 'transition', signature: '+transition($props: all, $dur: 150ms, $ease: ease)', description: 'Smooth CSS transition.' },
182
- { name: 'ring', signature: '+ring($color: var(--ring))', description: 'Focus outline ring with 1px offset.' }
183
- ],
184
- molecules: [
185
- { name: 'stack', signature: '+stack($gap: xs, $x: null)', description: 'Vertical rhythm: flex column + gap.' },
186
- { name: 'cluster', signature: '+cluster($gap: xs, $x: start, $y: center)', description: 'Wrapping row for tags/buttons/chips.' },
187
- { name: 'center-column', signature: '+center-column($max: var(--measure, 60ch), $pad: s)', description: 'Bounded reading column with max-width measure.' },
188
- { name: 'cover', signature: '+cover($min: 100vh, $pad: s)', description: 'Full-height container with vertically centered focal child.' },
189
- { name: 'frame', signature: '+frame($ratio: 16 / 9)', description: 'Aspect-ratio container for media (images, video, iframe).' },
190
- { name: 'reel', signature: '+reel($gap: xs)', description: 'Horizontal scroll-snap rail.' },
191
- { name: 'with-sidebar', signature: '+with-sidebar($rail: 240px, $gap: s, $min: 60%)', description: 'Intrinsic sidebar and fluid main content.' },
192
- { name: 'surface', signature: '+surface($bg: surface, $pad: null, $radius: 6, $elevation: none)', description: 'All-in-one material fractal: skin, radius, pad, and elevation.' },
193
- { name: 'cols', signature: '+cols($map, $gap: s)', description: 'Responsive column grid mapped across breakpoints, e.g. (base: 1, sm: 2, lg: 3).' }
194
- ],
195
- recipes: [
196
- { name: 'card', signature: '+card($bg: surface, $pad: null, $radius: 6, $elevation: none)', description: 'Vertical card container recipe with optional pad and elevation.' },
197
- { name: 'control', signature: '+control($size: md, $radius: 4)', description: 'Universal interactive control recipe (buttons, triggers, inputs).' },
198
- { name: 'select', signature: '+select($size: md, $radius: 4)', description: 'Select input recipe with embedded SVG chevron.' },
199
- { name: 'badge', signature: '+badge($radius: 4)', description: 'Compact status badge recipe.' }
200
- ],
201
- layouts: [
202
- { name: 'grid-3', class: '.grid-3', description: 'Responsive 1 → 2 → 3 column reflow.' },
203
- { name: 'card-grid', class: '.card-grid', description: 'Intrinsic auto-fit grid for cards.' },
204
- { name: 'hero', class: '.hero', description: 'Full viewport cover with centered hero message.' },
205
- { name: 'holy-grail', class: '.holy-grail', description: 'Responsive header / (nav · main · aside) / footer.' },
206
- { name: 'docs', class: '.docs', description: 'Docs template with sidebar nav, center reading column, and right TOC.' },
207
- { name: 'app-shell', class: '.app-shell', description: 'Sticky header, fluid body, and footer application frame.' }
208
- ]
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'
209
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
+ }, {});
210
212
  // Guidelines & Golden Rules
211
213
  const GUIDELINES = `
212
214
  # fractalstyler2 Design System Rules
213
215
 
214
- 1. Never hardcode a value that a token covers (+gap(m), +radius(6), +bg(surface)).
215
- 2. Compose fractals (+surface, +stack, +cluster, +card); write raw CSS only for genuinely unique lines.
216
- 3. Express component state on data-* / aria-* attributes, never modifier classes (e.g. &[data-elevated], &[data-variant='ghost']).
217
- 4. Markup stays thin and semantic: prefer clean tags (<article class="card">) over utility class soup.
218
- 5. Mobile-first: define base styles first, then grow with +at(md/lg/xl) or +cols().
219
- 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).
220
222
  `.trim();
221
223
  // Snapping helper: find closest token
222
224
  function findNearestToken(val, scaleMap) {
@@ -250,13 +252,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
250
252
  tools: [
251
253
  {
252
254
  name: 'compile_fractals',
253
- 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.',
254
256
  inputSchema: {
255
257
  type: 'object',
256
258
  properties: {
257
259
  sassCode: {
258
260
  type: 'string',
259
- 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).'
260
262
  },
261
263
  className: {
262
264
  type: 'string',
@@ -295,7 +297,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
295
297
  },
296
298
  {
297
299
  name: 'css_to_fractals',
298
- 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.',
299
301
  inputSchema: {
300
302
  type: 'object',
301
303
  properties: {
@@ -309,7 +311,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
309
311
  },
310
312
  {
311
313
  name: 'generate_component',
312
- 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.',
313
315
  inputSchema: {
314
316
  type: 'object',
315
317
  properties: {
@@ -331,25 +333,25 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
331
333
  },
332
334
  {
333
335
  name: 'validate_recipe',
334
- 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.',
335
337
  inputSchema: {
336
338
  type: 'object',
337
339
  properties: {
338
- code: { type: 'string', description: 'The SASS or Svelte code to validate.' }
340
+ code: { type: 'string', description: 'The Svelte or HTML markup to validate.' }
339
341
  },
340
342
  required: ['code']
341
343
  }
342
344
  },
343
345
  {
344
346
  name: 'list_fractals',
345
- 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.',
346
348
  inputSchema: {
347
349
  type: 'object',
348
350
  properties: {
349
- tier: {
351
+ layer: {
350
352
  type: 'string',
351
- enum: ['all', 'atoms', 'molecules', 'recipes', 'layouts'],
352
- 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.'
353
355
  }
354
356
  }
355
357
  }
@@ -372,12 +374,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
372
374
  .split('\n')
373
375
  .map((line) => (line.trim() ? `\t${line}` : ''))
374
376
  .join('\n');
375
- const fullSass = `
376
- @use 'tokens'
377
- @use 'base'
378
- @use 'fractals' as *
379
-
380
- .${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}
381
381
  ${indented}
382
382
  `;
383
383
  try {
@@ -426,7 +426,6 @@ ${indented}
426
426
  inputPx: args.gap,
427
427
  nearestToken: match.token,
428
428
  cssVar: `var(--space-${match.token})`,
429
- suggestedMixin: `+gap(${match.token})`,
430
429
  utilityClass: `.gap-${match.token}`
431
430
  };
432
431
  }
@@ -436,7 +435,6 @@ ${indented}
436
435
  inputPx: args.padding,
437
436
  nearestToken: match.token,
438
437
  cssVar: `var(--space-${match.token})`,
439
- suggestedMixin: `+pad(${match.token})`,
440
438
  utilityClass: `.pad-${match.token}`
441
439
  };
442
440
  }
@@ -447,7 +445,6 @@ ${indented}
447
445
  inputPx: args.radius,
448
446
  nearestToken: match.token,
449
447
  cssVar: `var(--radius-${match.token})`,
450
- suggestedMixin: `+radius(${match.token})`,
451
448
  utilityClass: `.radius-${match.token}`
452
449
  };
453
450
  }
@@ -457,7 +454,6 @@ ${indented}
457
454
  inputPx: args.fontSize,
458
455
  nearestToken: match.token,
459
456
  cssVar: `var(--text-${match.token})`,
460
- suggestedMixin: `+type(${match.token})`,
461
457
  utilityClass: `.text-${match.token}`
462
458
  };
463
459
  }
@@ -466,81 +462,197 @@ ${indented}
466
462
  };
467
463
  }
468
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.
469
468
  const rawCss = args.css || '';
470
- const lines = rawCss.split(/[;\n]/).map((l) => l.trim()).filter(Boolean);
471
- const mixins = [];
472
- let hasFlexCol = false;
473
- let hasFlexRow = false;
474
- let gapVal = null;
475
- let padVal = null;
476
- let radiusVal = null;
477
- let bgVal = null;
478
- let elevationVal = null;
479
- for (const line of lines) {
480
- const [prop, val] = line.split(':').map((s) => s?.trim());
481
- 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)
482
485
  continue;
483
- if (prop === 'display' && val === 'flex') {
484
- // wait for flex-direction
485
- }
486
- else if (prop === 'flex-direction' && val === 'column') {
487
- hasFlexCol = true;
488
- }
489
- else if (prop === 'flex-direction' && val === 'row') {
490
- hasFlexRow = true;
491
- }
492
- else if (prop === 'gap') {
493
- const num = parseInt(val, 10);
494
- if (!isNaN(num)) {
495
- gapVal = findNearestToken(num, DESIGN_TOKENS.space.approxPx).token;
496
- }
497
- }
498
- else if (prop === 'padding') {
499
- const num = parseInt(val, 10);
500
- if (!isNaN(num)) {
501
- padVal = 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;
502
575
  }
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}`);
503
614
  }
504
- else if (prop === 'border-radius') {
505
- const num = parseInt(val, 10);
506
- if (!isNaN(num)) {
507
- radiusVal = String(num);
508
- }
509
- }
510
- else if (prop === 'background-color' || prop === 'background') {
511
- if (val.includes('raised'))
512
- bgVal = 'raised';
513
- else if (val.includes('surface') || val === '#ffffff' || val === '#fff')
514
- bgVal = 'surface';
515
- else
516
- bgVal = 'bg';
517
- }
518
- else if (prop === 'box-shadow') {
519
- elevationVal = 'md';
520
- }
521
- }
522
- // If surface combination
523
- if (bgVal || padVal || radiusVal) {
524
- mixins.push(`+surface(${bgVal || 'surface'}, ${padVal || 's'}, ${radiusVal || '12'}${elevationVal ? `, ${elevationVal}` : ''})`);
525
- }
526
- if (hasFlexCol) {
527
- mixins.push(`+stack(${gapVal || 's'})`);
528
615
  }
529
- else if (hasFlexRow) {
530
- 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);
531
642
  }
532
- else if (gapVal && !hasFlexCol && !hasFlexRow) {
533
- 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.`;
534
654
  }
535
- const output = mixins.length > 0 ? mixins.join('\n') : '// No direct fractal match; use atoms:\n+box\n+gap(s)';
536
- return {
537
- content: [
538
- {
539
- type: 'text',
540
- text: `Suggested fractalstyler2 recipe:\n\n${output}`
541
- }
542
- ]
543
- };
655
+ return { content: [{ type: 'text', text }] };
544
656
  }
545
657
  case 'generate_component': {
546
658
  const compName = args.name || 'CustomCard';
@@ -555,89 +667,69 @@ ${indented}
555
667
  let { title = 'Card Title', description = '${desc || 'Card summary text'}', children } = $props();
556
668
  </script>
557
669
 
558
- <article class="card"${elevation !== 'none' ? ' data-elevated' : ''}>
559
- <div class="row ycenter xbetween">
560
- <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>
561
673
  <span class="badge">Active</span>
562
674
  </div>
563
- <p class="body muted">{description}</p>
675
+ <p class="text-sm text-secondary">{description}</p>
564
676
  {#if children}
565
677
  {@render children()}
566
678
  {/if}
567
679
  </article>
568
-
569
- <style lang="sass">
570
- @use '$lib/styles/fractals' as *
571
- </style>`;
680
+ `;
572
681
  break;
573
682
  case 'panel':
574
683
  template = `<script lang="ts">
575
684
  let { heading = 'Panel Heading', children } = $props();
576
685
  </script>
577
686
 
578
- <section class="panel">
579
- <header class="row ycenter xbetween">
580
- <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>
581
690
  </header>
582
- <div class="box gap-s">
691
+ <div class="box gap-sm">
583
692
  {#if children}
584
693
  {@render children()}
585
694
  {/if}
586
695
  </div>
587
696
  </section>
588
-
589
- <style lang="sass">
590
- @use '$lib/styles/fractals' as *
591
- </style>`;
697
+ `;
592
698
  break;
593
699
  case 'button':
594
700
  template = `<script lang="ts">
595
701
  let { variant = 'primary', disabled = false, onclick, children } = $props();
596
702
  </script>
597
703
 
598
- <button class="button" data-variant={variant} {disabled} {onclick}>
704
+ <button class="button {variant}" {disabled} {onclick}>
599
705
  {#if children}
600
706
  {@render children()}
601
707
  {:else}
602
708
  Action
603
709
  {/if}
604
710
  </button>
605
-
606
- <style lang="sass">
607
- @use '$lib/styles/fractals' as *
608
- </style>`;
711
+ `;
609
712
  break;
610
713
  case 'badge':
611
714
  template = `<script lang="ts">
612
715
  let { label = 'Badge', variant = 'default' } = $props();
613
716
  </script>
614
717
 
615
- <span class="badge" data-variant={variant}>
718
+ <span class="badge" data-status={variant}>
616
719
  {label}
617
720
  </span>
618
-
619
- <style lang="sass">
620
- @use '$lib/styles/fractals' as *
621
- </style>`;
721
+ `;
622
722
  break;
623
723
  default:
624
724
  template = `<script lang="ts">
625
725
  let { children } = $props();
626
726
  </script>
627
727
 
628
- <div class="custom-container">
728
+ <div class="box gap-sm pad-md surface border radius-md">
629
729
  {#if children}
630
730
  {@render children()}
631
731
  {/if}
632
- </div>
633
-
634
- <style lang="sass">
635
- @use '$lib/styles/fractals' as *
636
-
637
- .custom-container
638
- +surface(surface, m, 16)
639
- +stack(s)
640
- </style>`;
732
+ </div>`;
641
733
  break;
642
734
  }
643
735
  return {
@@ -652,28 +744,34 @@ ${indented}
652
744
  case 'validate_recipe': {
653
745
  const code = args.code || '';
654
746
  const diagnostics = [];
655
- const legacyClassPatterns = [
656
- { regex: /\bgap\d+\b/g, name: 'gapN (e.g. gap8)', replacement: '.gap-xs / .gap-s or +gap(N)' },
657
- { regex: /\bpad\d+\b/g, name: 'padN (e.g. pad16)', replacement: '.pad-s or +pad(N)' },
658
- { regex: /\bw100\b/g, name: 'w100', replacement: '.wfull or +w(100%)' },
659
- { regex: /\bh100\b/g, name: 'h100', replacement: '.hfull or +h(100%)' },
660
- { regex: /\bmin-w-0\b/g, name: 'min-w-0', replacement: '.min0' },
661
- { regex: /\bclass="[^"]*\bstack\b[^"]*"/g, name: 'class="stack"', replacement: '+stack() in SASS (no markup class)' },
662
- { regex: /\bclass="[^"]*\bcluster\b[^"]*"/g, name: 'class="cluster"', replacement: '+cluster() in SASS (no markup class)' },
663
- { regex: /\bclass="[^"]*\bappshell\b[^"]*"/g, name: 'class="appshell"', replacement: '.app-shell' }
664
- ];
665
- for (const p of legacyClassPatterns) {
666
- if (p.regex.test(code)) {
667
- diagnostics.push({
668
- severity: 'error',
669
- message: `Found legacy v1 pattern "${p.name}". In fractalstyler2 use: ${p.replacement}.`
670
- });
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
+ }
671
763
  }
672
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
+ }
673
771
  if (/padding:\s*\d+px/i.test(code) || /gap:\s*\d+px/i.test(code)) {
674
772
  diagnostics.push({
675
773
  severity: 'warning',
676
- 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.'
677
775
  });
678
776
  }
679
777
  if (code.includes('.is-active') || code.includes('--active')) {
@@ -693,13 +791,12 @@ ${indented}
693
791
  };
694
792
  }
695
793
  case 'list_fractals': {
696
- const tier = args.tier || 'all';
697
- if (tier === 'all') {
698
- 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.');
699
797
  }
700
- return {
701
- content: [{ type: 'text', text: JSON.stringify({ [tier]: FRACTAL_CATALOG[tier] || [] }, null, 2) }]
702
- };
798
+ const payload = layer === 'all' ? FRACTAL_CATALOG : { [layer]: FRACTAL_CATALOG[layer] ?? [] };
799
+ return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
703
800
  }
704
801
  default:
705
802
  throw new Error(`Unknown tool: ${name}`);
@@ -719,8 +816,8 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
719
816
  },
720
817
  {
721
818
  uri: 'fractalstyler2://fractals',
722
- name: 'Fractal Mixin Catalog',
723
- 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.',
724
821
  mimeType: 'application/json'
725
822
  },
726
823
  {
@@ -780,7 +877,7 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
780
877
  prompts: [
781
878
  {
782
879
  name: 'design_system_review',
783
- 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.'
784
881
  },
785
882
  {
786
883
  name: 'generate_ui',
@@ -802,7 +899,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
802
899
  role: 'user',
803
900
  content: {
804
901
  type: 'text',
805
- 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}`
806
903
  }
807
904
  }
808
905
  ]
@@ -816,7 +913,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
816
913
  role: 'user',
817
914
  content: {
818
915
  type: 'text',
819
- 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}`
820
917
  }
821
918
  }
822
919
  ]