meno-core 1.1.3 → 1.1.5

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 (57) hide show
  1. package/dist/chunks/{chunk-3JXK2QFU.js → chunk-H6EOTPJA.js} +2 -2
  2. package/dist/chunks/{chunk-LOZL5HOF.js → chunk-RLBV2R6J.js} +603 -17
  3. package/dist/chunks/chunk-RLBV2R6J.js.map +7 -0
  4. package/dist/chunks/{chunk-FQBIC2OB.js → chunk-YMBUSHII.js} +1 -1
  5. package/dist/chunks/{chunk-FQBIC2OB.js.map → chunk-YMBUSHII.js.map} +1 -1
  6. package/dist/lib/client/index.js +46 -28
  7. package/dist/lib/client/index.js.map +2 -2
  8. package/dist/lib/server/index.js +1304 -241
  9. package/dist/lib/server/index.js.map +4 -4
  10. package/dist/lib/shared/index.js +4 -2
  11. package/dist/lib/shared/index.js.map +1 -1
  12. package/lib/client/core/ComponentBuilder.test.ts +32 -0
  13. package/lib/client/core/ComponentBuilder.ts +30 -0
  14. package/lib/client/theme.test.ts +2 -2
  15. package/lib/client/theme.ts +28 -26
  16. package/lib/server/createServer.ts +5 -1
  17. package/lib/server/cssAudit.test.ts +270 -0
  18. package/lib/server/cssAudit.ts +815 -0
  19. package/lib/server/deMirror.test.ts +61 -0
  20. package/lib/server/deMirror.ts +317 -0
  21. package/lib/server/fileWatcher.test.ts +113 -0
  22. package/lib/server/fileWatcher.ts +42 -1
  23. package/lib/server/index.ts +19 -0
  24. package/lib/server/routes/api/core-routes.ts +18 -0
  25. package/lib/server/routes/api/cssAudit.test.ts +101 -0
  26. package/lib/server/routes/api/cssAudit.ts +138 -0
  27. package/lib/server/routes/api/deMirror.test.ts +105 -0
  28. package/lib/server/routes/api/deMirror.ts +127 -0
  29. package/lib/server/routes/api/pages.ts +2 -2
  30. package/lib/server/services/componentService.test.ts +43 -0
  31. package/lib/server/services/componentService.ts +49 -12
  32. package/lib/server/services/configService.ts +0 -5
  33. package/lib/server/services/pageService.test.ts +15 -0
  34. package/lib/server/services/pageService.ts +44 -22
  35. package/lib/server/ssr/htmlGenerator.test.ts +29 -1
  36. package/lib/server/ssr/htmlGenerator.ts +86 -14
  37. package/lib/server/themeCssCodec.test.ts +67 -0
  38. package/lib/server/themeCssCodec.ts +67 -5
  39. package/lib/server/webflow/templateWrapper.ts +54 -0
  40. package/lib/shared/cssGeneration.test.ts +64 -0
  41. package/lib/shared/cssGeneration.ts +6 -1
  42. package/lib/shared/interfaces/contentProvider.ts +9 -0
  43. package/lib/shared/tailwindThemeScale.ts +1 -0
  44. package/lib/shared/types/cms.ts +1 -0
  45. package/lib/shared/types/comment.ts +9 -3
  46. package/lib/shared/utilityClassConfig.ts +471 -10
  47. package/lib/shared/utilityClassMapper.test.ts +173 -2
  48. package/lib/shared/utilityClassMapper.ts +37 -0
  49. package/lib/shared/utilityClassNames.ts +164 -0
  50. package/lib/shared/utils/fileUtils.ts +11 -0
  51. package/lib/shared/validation/schemas.ts +1 -0
  52. package/lib/shared/viewportUnits.test.ts +34 -1
  53. package/lib/shared/viewportUnits.ts +43 -0
  54. package/package.json +3 -1
  55. package/vite.config.ts +4 -1
  56. package/dist/chunks/chunk-LOZL5HOF.js.map +0 -7
  57. /package/dist/chunks/{chunk-3JXK2QFU.js.map → chunk-H6EOTPJA.js.map} +0 -0
@@ -4,7 +4,7 @@
4
4
  * Used by both client and server to ensure consistent CSS output
5
5
  */
6
6
 
7
- import { propertyOrder, staticUtilityReverse, presetClassReverse } from './utilityClassConfig';
7
+ import { propertyOrder, staticUtilityReverse, presetClassReverse, ROUNDED_SIDE_CORNERS } from './utilityClassConfig';
8
8
  import {
9
9
  parseUtilityClass,
10
10
  splitVariantPrefix,
@@ -217,6 +217,11 @@ export function generateRuleForClass(className: string, knownTokens?: ReadonlySe
217
217
  if (root === 'size') {
218
218
  return `width: ${value}; height: ${value};`;
219
219
  }
220
+ // Side border-radius: no CSS per-side shorthand — emit both corner longhands.
221
+ const sideCorners = root ? ROUNDED_SIDE_CORNERS[root] : undefined;
222
+ if (sideCorners) {
223
+ return `${sideCorners[0]}: ${value}; ${sideCorners[1]}: ${value};`;
224
+ }
220
225
  if (root === 'px') {
221
226
  return `padding-left: ${value}; padding-right: ${value};`;
222
227
  }
@@ -56,6 +56,15 @@ export interface PageProvider {
56
56
  * Optional — callers fall back to ".json" when not provided.
57
57
  */
58
58
  extension?(): string;
59
+
60
+ /**
61
+ * Absolute path to the source file backing a page path, or null if it doesn't
62
+ * exist. Unlike the naive `<pagesDir>/<path>.<ext>` mapping, this resolves cases
63
+ * where the logical editor path diverges from the on-disk file — notably CMS
64
+ * template pages (`/templates/<col>` → `src/pages/<routeDir>/[slug].astro`). Used
65
+ * to point external tools ("Open source file") at the real file. Optional.
66
+ */
67
+ resolveSourcePath?(path: string): Promise<string | null>;
59
68
  }
60
69
 
61
70
  /**
@@ -137,6 +137,7 @@ const SPECS: ScaleSpec[] = [
137
137
  ['lg', '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)'],
138
138
  ['xl', '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)'],
139
139
  ['2xl', '0 25px 50px -12px rgb(0 0 0 / 0.25)'],
140
+ ['inner', 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)'],
140
141
  ],
141
142
  },
142
143
  {
@@ -27,6 +27,7 @@ export interface CMSFieldDefinition {
27
27
  accept?: string; // For 'file' type - MIME filter (e.g. "application/pdf", "*/*")
28
28
  collection?: string; // For 'reference' type - target collection ID
29
29
  multiple?: boolean; // For 'reference' type - allows array of IDs
30
+ editor?: 'basic' | 'extended'; // For 'rich-text' type: which editor + render helper (Basic → richText(); Extended → richTextWithComponents())
30
31
  }
31
32
 
32
33
  /** Data delivery strategy for client-side CMS data */
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Comments are Figma-style pins placed on components in the studio canvas.
5
5
  * One file per pin (thread embedded). Storage layout (status as top folder):
6
- * {projectRoot}/comments/<status>/<pageSlug>--<seq>--<titleSlug>.json
7
- * e.g. comments/open/blog__post--3--hero-is-too-dark.json
6
+ * {projectRoot}/comments/<status>/<pageSlug>--<seq>--<titleSlug>--<idFrag>.json
7
+ * e.g. comments/open/blog__post--3--hero-is-too-dark--550e8400.json
8
8
  *
9
9
  * Pins are an authoring-time concept — they live in studio only.
10
10
  */
@@ -119,7 +119,13 @@ export interface Comment {
119
119
  _filename: string;
120
120
  /** Page path the pin lives on (e.g. `"blog/post"`). */
121
121
  _pagePath: string;
122
- /** Stable per-page badge number. Server assigns at create time. */
122
+ /**
123
+ * Best-effort creation counter (server stamps `max+1` at create time), used
124
+ * as the human-scannable middle segment of the filename. NOT collision-proof
125
+ * under concurrent/offline creation and NOT the badge shown to users — the pin
126
+ * badge is derived client-side from the full set (see `assignDisplaySeq`), so a
127
+ * repeated `_seq` here is harmless.
128
+ */
123
129
  _seq: number;
124
130
  /** ISO timestamps. */
125
131
  _createdAt: string;
@@ -259,6 +259,35 @@ export const propertyMap: Record<string, string> = {
259
259
  textShadow: 'text-shadow',
260
260
  filter: 'filter',
261
261
  backdropFilter: 'backdrop-filter',
262
+ mixBlendMode: 'mix-blend',
263
+ backgroundBlendMode: 'bg-blend',
264
+ willChange: 'will-change',
265
+ columns: 'columns',
266
+ overscrollBehavior: 'overscroll',
267
+ overscrollBehaviorX: 'overscroll-x',
268
+ overscrollBehaviorY: 'overscroll-y',
269
+ touchAction: 'touch',
270
+ breakAfter: 'break-after',
271
+ breakBefore: 'break-before',
272
+ breakInside: 'break-inside',
273
+ textUnderlineOffset: 'underline-offset',
274
+ textDecorationColor: 'decoration',
275
+ textDecorationThickness: 'decoration',
276
+ caretColor: 'caret',
277
+ fill: 'fill',
278
+ stroke: 'stroke',
279
+ strokeWidth: 'stroke',
280
+ borderSpacing: 'border-spacing',
281
+ scrollMargin: 'scroll-m',
282
+ scrollMarginTop: 'scroll-mt',
283
+ scrollMarginRight: 'scroll-mr',
284
+ scrollMarginBottom: 'scroll-mb',
285
+ scrollMarginLeft: 'scroll-ml',
286
+ scrollPadding: 'scroll-p',
287
+ scrollPaddingTop: 'scroll-pt',
288
+ scrollPaddingRight: 'scroll-pr',
289
+ scrollPaddingBottom: 'scroll-pb',
290
+ scrollPaddingLeft: 'scroll-pl',
262
291
 
263
292
  // Positioning
264
293
  top: 'top',
@@ -266,6 +295,8 @@ export const propertyMap: Record<string, string> = {
266
295
  bottom: 'bottom',
267
296
  left: 'left',
268
297
  inset: 'inset',
298
+ insetInlineStart: 'start',
299
+ insetInlineEnd: 'end',
269
300
  zIndex: 'z',
270
301
 
271
302
  // Grid Layout
@@ -339,6 +370,8 @@ export const prefixToCSSProperty: Record<string, string> = {
339
370
  'border-r': 'border-right',
340
371
  'border-b': 'border-bottom',
341
372
  'border-l': 'border-left',
373
+ 'border-x': 'border-inline',
374
+ 'border-y': 'border-block',
342
375
  rounded: 'border-radius',
343
376
  'rounded-tl': 'border-top-left-radius',
344
377
  'rounded-tr': 'border-top-right-radius',
@@ -392,6 +425,71 @@ export const prefixToCSSProperty: Record<string, string> = {
392
425
  'text-shadow': 'text-shadow',
393
426
  filter: 'filter',
394
427
  'backdrop-filter': 'backdrop-filter',
428
+ // Filter-function roots (value is wrapped in the function — see FILTER_FN_ROOTS +
429
+ // parseRootValue's filter branch). Kept here too so hash/dynamic property derivation
430
+ // and SORTED_ROOTS matching see them like any other root.
431
+ blur: 'filter',
432
+ brightness: 'filter',
433
+ contrast: 'filter',
434
+ saturate: 'filter',
435
+ grayscale: 'filter',
436
+ invert: 'filter',
437
+ sepia: 'filter',
438
+ 'hue-rotate': 'filter',
439
+ 'drop-shadow': 'filter',
440
+ 'backdrop-blur': 'backdrop-filter',
441
+ 'backdrop-brightness': 'backdrop-filter',
442
+ 'backdrop-contrast': 'backdrop-filter',
443
+ 'backdrop-saturate': 'backdrop-filter',
444
+ 'backdrop-grayscale': 'backdrop-filter',
445
+ 'backdrop-invert': 'backdrop-filter',
446
+ 'backdrop-sepia': 'backdrop-filter',
447
+ 'backdrop-hue-rotate': 'backdrop-filter',
448
+ 'backdrop-opacity': 'backdrop-filter',
449
+ // Per-axis transforms (value composed into the axis slot — see parseRootValue)
450
+ 'skew-x': 'transform',
451
+ 'skew-y': 'transform',
452
+ 'scale-x': 'scale',
453
+ 'scale-y': 'scale',
454
+ // Side border-radius (two corners — expanded in generateRuleForClass / classesToStyles)
455
+ 'rounded-t': 'border-top-left-radius',
456
+ 'rounded-r': 'border-top-right-radius',
457
+ 'rounded-b': 'border-bottom-left-radius',
458
+ 'rounded-l': 'border-top-left-radius',
459
+ // Blend / misc keyword roots
460
+ 'mix-blend': 'mix-blend-mode',
461
+ 'bg-blend': 'background-blend-mode',
462
+ 'will-change': 'will-change',
463
+ columns: 'columns',
464
+ overscroll: 'overscroll-behavior',
465
+ 'overscroll-x': 'overscroll-behavior-x',
466
+ 'overscroll-y': 'overscroll-behavior-y',
467
+ touch: 'touch-action',
468
+ 'break-after': 'break-after',
469
+ 'break-before': 'break-before',
470
+ 'break-inside': 'break-inside',
471
+
472
+ // Text decoration & SVG (shared roots disambiguated by value type — see resolveRootProperty)
473
+ 'underline-offset': 'text-underline-offset',
474
+ decoration: 'text-decoration-color',
475
+ caret: 'caret-color',
476
+ fill: 'fill',
477
+ stroke: 'stroke',
478
+
479
+ // Tables
480
+ 'border-spacing': 'border-spacing',
481
+
482
+ // Scroll margin/padding (Tailwind spacing scale)
483
+ 'scroll-m': 'scroll-margin',
484
+ 'scroll-mt': 'scroll-margin-top',
485
+ 'scroll-mr': 'scroll-margin-right',
486
+ 'scroll-mb': 'scroll-margin-bottom',
487
+ 'scroll-ml': 'scroll-margin-left',
488
+ 'scroll-p': 'scroll-padding',
489
+ 'scroll-pt': 'scroll-padding-top',
490
+ 'scroll-pr': 'scroll-padding-right',
491
+ 'scroll-pb': 'scroll-padding-bottom',
492
+ 'scroll-pl': 'scroll-padding-left',
395
493
 
396
494
  // Positioning
397
495
  top: 'top',
@@ -401,6 +499,8 @@ export const prefixToCSSProperty: Record<string, string> = {
401
499
  inset: 'inset',
402
500
  'inset-x': 'inset-inline',
403
501
  'inset-y': 'inset-block',
502
+ start: 'inset-inline-start',
503
+ end: 'inset-inline-end',
404
504
  z: 'z-index',
405
505
 
406
506
  // Outline
@@ -416,6 +516,75 @@ export const prefixToCSSProperty: Record<string, string> = {
416
516
  object: 'object-position',
417
517
  };
418
518
 
519
+ /**
520
+ * Tailwind filter-function utilities: the class value is an ARGUMENT to a CSS filter
521
+ * function, not the property value itself (`blur-[70px]` → `filter: blur(70px)`,
522
+ * `brightness-50` → `filter: brightness(0.5)`). `scale` describes how a bare numeric
523
+ * step converts to the argument (Tailwind's scale semantics): `ratio` = n/100
524
+ * (brightness-150 → 1.5), `percent` = n% (invert-25 → 25%), `deg` = ndeg
525
+ * (hue-rotate-90 → 90deg). Roots without `scale` (blur, drop-shadow) take only the
526
+ * arbitrary/var forms — their named steps are length/shadow theme values Meno
527
+ * deliberately doesn't bake in.
528
+ */
529
+ export type FilterFnScale = 'ratio' | 'percent' | 'deg';
530
+ export const FILTER_FN_ROOTS: Record<string, { property: string; fn: string; scale?: FilterFnScale }> = {
531
+ blur: { property: 'filter', fn: 'blur' },
532
+ brightness: { property: 'filter', fn: 'brightness', scale: 'ratio' },
533
+ contrast: { property: 'filter', fn: 'contrast', scale: 'ratio' },
534
+ saturate: { property: 'filter', fn: 'saturate', scale: 'ratio' },
535
+ grayscale: { property: 'filter', fn: 'grayscale', scale: 'percent' },
536
+ invert: { property: 'filter', fn: 'invert', scale: 'percent' },
537
+ sepia: { property: 'filter', fn: 'sepia', scale: 'percent' },
538
+ 'hue-rotate': { property: 'filter', fn: 'hue-rotate', scale: 'deg' },
539
+ 'drop-shadow': { property: 'filter', fn: 'drop-shadow' },
540
+ 'backdrop-blur': { property: 'backdrop-filter', fn: 'blur' },
541
+ 'backdrop-brightness': { property: 'backdrop-filter', fn: 'brightness', scale: 'ratio' },
542
+ 'backdrop-contrast': { property: 'backdrop-filter', fn: 'contrast', scale: 'ratio' },
543
+ 'backdrop-saturate': { property: 'backdrop-filter', fn: 'saturate', scale: 'ratio' },
544
+ 'backdrop-grayscale': { property: 'backdrop-filter', fn: 'grayscale', scale: 'percent' },
545
+ 'backdrop-invert': { property: 'backdrop-filter', fn: 'invert', scale: 'percent' },
546
+ 'backdrop-sepia': { property: 'backdrop-filter', fn: 'sepia', scale: 'percent' },
547
+ 'backdrop-hue-rotate': { property: 'backdrop-filter', fn: 'hue-rotate', scale: 'deg' },
548
+ 'backdrop-opacity': { property: 'backdrop-filter', fn: 'opacity', scale: 'ratio' },
549
+ };
550
+
551
+ /** `<kebab property>|<fn>` → filter root, for the inverse mapper (`filter: brightness(0.5)` → `brightness-50`). */
552
+ export const FILTER_ROOT_BY_PROP_FN: ReadonlyMap<string, string> = new Map(
553
+ Object.entries(FILTER_FN_ROOTS).map(([root, e]) => [`${e.property}|${e.fn}`, root]),
554
+ );
555
+
556
+ /** Convert a bare numeric step to the filter-function argument per the root's scale. */
557
+ export function filterStepToArg(scale: FilterFnScale, step: string): string {
558
+ if (scale === 'ratio') return String(Number(step) / 100);
559
+ if (scale === 'percent') return `${step}%`;
560
+ return `${step}deg`;
561
+ }
562
+
563
+ /** Inverse of {@link filterStepToArg}: recover the numeric step from an argument, or null. */
564
+ export function filterArgToStep(scale: FilterFnScale, arg: string): string | null {
565
+ if (scale === 'ratio') {
566
+ const m = arg.match(/^(\d+(?:\.\d+)?)$/);
567
+ if (!m) return null;
568
+ const step = Number(m[1]) * 100;
569
+ return Number.isInteger(step) ? String(step) : null;
570
+ }
571
+ const m = scale === 'percent' ? arg.match(/^(\d+)%$/) : arg.match(/^(\d+)deg$/);
572
+ return m ? (m[1] ?? null) : null;
573
+ }
574
+
575
+ /**
576
+ * Side border-radius roots (`rounded-t-lg`, `rounded-l-[8px]`) → their two corner
577
+ * longhands. CSS has no per-side radius shorthand, so the CSS generator emits both
578
+ * corners and `classesToStyles` mirrors the parsed first corner onto the second
579
+ * (like `size-*` does for width/height).
580
+ */
581
+ export const ROUNDED_SIDE_CORNERS: Record<string, [string, string]> = {
582
+ 'rounded-t': ['border-top-left-radius', 'border-top-right-radius'],
583
+ 'rounded-r': ['border-top-right-radius', 'border-bottom-right-radius'],
584
+ 'rounded-b': ['border-bottom-left-radius', 'border-bottom-right-radius'],
585
+ 'rounded-l': ['border-top-left-radius', 'border-bottom-left-radius'],
586
+ };
587
+
419
588
  /**
420
589
  * Static utilities: property → value → Tailwind class name.
421
590
  * These are emitted as-is (no brackets) and have fixed reverse mappings.
@@ -436,6 +605,14 @@ export const staticUtilities: Record<string, Record<string, string>> = {
436
605
  contents: 'contents',
437
606
  'flow-root': 'flow-root',
438
607
  'list-item': 'list-item',
608
+ 'table-row': 'table-row',
609
+ 'table-cell': 'table-cell',
610
+ 'table-caption': 'table-caption',
611
+ 'table-row-group': 'table-row-group',
612
+ 'table-header-group': 'table-header-group',
613
+ 'table-footer-group': 'table-footer-group',
614
+ 'table-column': 'table-column',
615
+ 'table-column-group': 'table-column-group',
439
616
  },
440
617
  flexDirection: {
441
618
  column: 'flex-col',
@@ -692,6 +869,7 @@ export const staticUtilities: Record<string, Record<string, string>> = {
692
869
  },
693
870
  transform: {
694
871
  none: 'transform-none',
872
+ 'translateZ(0)': 'transform-gpu',
695
873
  },
696
874
  boxShadow: {
697
875
  none: 'shadow-none',
@@ -699,6 +877,176 @@ export const staticUtilities: Record<string, Record<string, string>> = {
699
877
  outline: {
700
878
  none: 'outline-none',
701
879
  },
880
+ outlineStyle: {
881
+ solid: 'outline-solid',
882
+ dashed: 'outline-dashed',
883
+ dotted: 'outline-dotted',
884
+ double: 'outline-double',
885
+ },
886
+ fontStyle: {
887
+ italic: 'italic',
888
+ normal: 'not-italic',
889
+ },
890
+ isolation: {
891
+ isolate: 'isolate',
892
+ auto: 'isolation-auto',
893
+ },
894
+ borderCollapse: {
895
+ collapse: 'border-collapse',
896
+ separate: 'border-separate',
897
+ },
898
+ tableLayout: {
899
+ auto: 'table-auto',
900
+ fixed: 'table-fixed',
901
+ },
902
+ appearance: {
903
+ none: 'appearance-none',
904
+ auto: 'appearance-auto',
905
+ },
906
+ fontVariantNumeric: {
907
+ 'tabular-nums': 'tabular-nums',
908
+ normal: 'normal-nums',
909
+ ordinal: 'ordinal',
910
+ 'slashed-zero': 'slashed-zero',
911
+ 'lining-nums': 'lining-nums',
912
+ 'oldstyle-nums': 'oldstyle-nums',
913
+ 'proportional-nums': 'proportional-nums',
914
+ 'diagonal-fractions': 'diagonal-fractions',
915
+ 'stacked-fractions': 'stacked-fractions',
916
+ },
917
+ // Tailwind pairs snap-x/y/both with a separate strictness class composed via a CSS
918
+ // var; without var composition the common `snap-x snap-mandatory` intent collapses
919
+ // into the type class itself. `snap-proximity`/`snap-mandatory` alone stay foreign.
920
+ scrollSnapType: {
921
+ 'x mandatory': 'snap-x',
922
+ 'y mandatory': 'snap-y',
923
+ 'both mandatory': 'snap-both',
924
+ none: 'snap-none',
925
+ },
926
+ scrollSnapAlign: {
927
+ start: 'snap-start',
928
+ center: 'snap-center',
929
+ end: 'snap-end',
930
+ none: 'snap-align-none',
931
+ },
932
+ // Bare filter-function switches (the 100% forms); numeric/arbitrary forms flow
933
+ // through FILTER_FN_ROOTS (`grayscale-50`, `invert-[0.3]`).
934
+ filter: {
935
+ 'grayscale(100%)': 'grayscale',
936
+ 'invert(100%)': 'invert',
937
+ 'sepia(100%)': 'sepia',
938
+ none: 'filter-none',
939
+ },
940
+ backdropFilter: {
941
+ 'grayscale(100%)': 'backdrop-grayscale',
942
+ 'invert(100%)': 'backdrop-invert',
943
+ 'sepia(100%)': 'backdrop-sepia',
944
+ none: 'backdrop-filter-none',
945
+ },
946
+ float: {
947
+ left: 'float-left',
948
+ right: 'float-right',
949
+ none: 'float-none',
950
+ 'inline-start': 'float-start',
951
+ 'inline-end': 'float-end',
952
+ },
953
+ clear: {
954
+ both: 'clear-both',
955
+ left: 'clear-left',
956
+ right: 'clear-right',
957
+ none: 'clear-none',
958
+ },
959
+ backgroundSize: {
960
+ cover: 'bg-cover',
961
+ contain: 'bg-contain',
962
+ auto: 'bg-auto',
963
+ },
964
+ backgroundAttachment: {
965
+ fixed: 'bg-fixed',
966
+ local: 'bg-local',
967
+ scroll: 'bg-scroll',
968
+ },
969
+ backgroundRepeat: {
970
+ 'no-repeat': 'bg-no-repeat',
971
+ repeat: 'bg-repeat',
972
+ 'repeat-x': 'bg-repeat-x',
973
+ 'repeat-y': 'bg-repeat-y',
974
+ round: 'bg-repeat-round',
975
+ space: 'bg-repeat-space',
976
+ },
977
+ backgroundPosition: {
978
+ center: 'bg-center',
979
+ top: 'bg-top',
980
+ bottom: 'bg-bottom',
981
+ left: 'bg-left',
982
+ right: 'bg-right',
983
+ 'left top': 'bg-left-top',
984
+ 'left bottom': 'bg-left-bottom',
985
+ 'right top': 'bg-right-top',
986
+ 'right bottom': 'bg-right-bottom',
987
+ },
988
+ backgroundClip: {
989
+ 'border-box': 'bg-clip-border',
990
+ 'padding-box': 'bg-clip-padding',
991
+ 'content-box': 'bg-clip-content',
992
+ text: 'bg-clip-text',
993
+ },
994
+ backgroundOrigin: {
995
+ 'border-box': 'bg-origin-border',
996
+ 'padding-box': 'bg-origin-padding',
997
+ 'content-box': 'bg-origin-content',
998
+ },
999
+ textOverflow: {
1000
+ ellipsis: 'text-ellipsis',
1001
+ clip: 'text-clip',
1002
+ },
1003
+ // `text-nowrap` deliberately stays a whiteSpace alias (same rendering) — see staticClassAliases.
1004
+ textWrap: {
1005
+ wrap: 'text-wrap',
1006
+ balance: 'text-balance',
1007
+ pretty: 'text-pretty',
1008
+ },
1009
+ textDecorationStyle: {
1010
+ solid: 'decoration-solid',
1011
+ dashed: 'decoration-dashed',
1012
+ dotted: 'decoration-dotted',
1013
+ double: 'decoration-double',
1014
+ wavy: 'decoration-wavy',
1015
+ },
1016
+ hyphens: {
1017
+ auto: 'hyphens-auto',
1018
+ none: 'hyphens-none',
1019
+ manual: 'hyphens-manual',
1020
+ },
1021
+ content: {
1022
+ none: 'content-none',
1023
+ },
1024
+ captionSide: {
1025
+ top: 'caption-top',
1026
+ bottom: 'caption-bottom',
1027
+ },
1028
+ animation: {
1029
+ none: 'animate-none',
1030
+ },
1031
+ // SVG paint statics; token/arbitrary colors flow through the fill/stroke roots.
1032
+ fill: {
1033
+ currentColor: 'fill-current',
1034
+ none: 'fill-none',
1035
+ },
1036
+ stroke: {
1037
+ currentColor: 'stroke-current',
1038
+ none: 'stroke-none',
1039
+ },
1040
+ };
1041
+
1042
+ /**
1043
+ * Read-side aliases: Tailwind class names that map to a value whose CANONICAL Meno
1044
+ * class is spelled differently (the forward mapper keeps emitting the canonical
1045
+ * form). Merged into `staticUtilityReverse` below.
1046
+ */
1047
+ const staticClassAliases: Record<string, { prop: string; value: string }> = {
1048
+ 'text-nowrap': { prop: 'whiteSpace', value: 'nowrap' }, // canonical: whitespace-nowrap
1049
+ 'break-words': { prop: 'overflowWrap', value: 'break-word' }, // canonical: wrap-break-word
702
1050
  };
703
1051
 
704
1052
  /**
@@ -789,6 +1137,20 @@ export const SPACING_SCALE_ROOTS: Set<string> = new Set([
789
1137
  'inset',
790
1138
  'inset-x',
791
1139
  'inset-y',
1140
+ 'start',
1141
+ 'end',
1142
+ 'indent',
1143
+ 'border-spacing',
1144
+ 'scroll-m',
1145
+ 'scroll-mt',
1146
+ 'scroll-mr',
1147
+ 'scroll-mb',
1148
+ 'scroll-ml',
1149
+ 'scroll-p',
1150
+ 'scroll-pt',
1151
+ 'scroll-pr',
1152
+ 'scroll-pb',
1153
+ 'scroll-pl',
792
1154
  'w',
793
1155
  'h',
794
1156
  'size',
@@ -805,11 +1167,21 @@ export const SPACING_SCALE_ROOTS: Set<string> = new Set([
805
1167
  * keyed by camelCase CSS property). Used by the inverse parser to recognize
806
1168
  * `bg-primary` / `text-text` / `border-border` etc.
807
1169
  */
808
- export const COLOR_TOKEN_ROOTS: Set<string> = new Set(['text', 'bg', 'border', 'accent', 'outline']);
1170
+ export const COLOR_TOKEN_ROOTS: Set<string> = new Set([
1171
+ 'text',
1172
+ 'bg',
1173
+ 'border',
1174
+ 'accent',
1175
+ 'outline',
1176
+ 'decoration',
1177
+ 'caret',
1178
+ 'fill',
1179
+ 'stroke',
1180
+ ]);
809
1181
 
810
1182
  /** Reverse mapping for static utilities: class → { prop, value } (canonical value). */
811
1183
  export const staticUtilityReverse: Record<string, { prop: string; value: string }> = (() => {
812
- const reverse: Record<string, { prop: string; value: string }> = {};
1184
+ const reverse: Record<string, { prop: string; value: string }> = { ...staticClassAliases };
813
1185
  for (const [prop, values] of Object.entries(staticUtilities)) {
814
1186
  for (const [value, className] of Object.entries(values)) {
815
1187
  if (!reverse[className]) {
@@ -825,13 +1197,72 @@ export const staticUtilityReverse: Record<string, { prop: string; value: string
825
1197
  * Mirrors Tailwind's named sizes (`w-full`, `h-screen`, `m-auto`, `rounded-full`).
826
1198
  */
827
1199
  export const keywordValues: Record<string, Record<string, string>> = {
828
- w: { auto: 'auto', full: '100%', screen: '100vw', fit: 'fit-content', min: 'min-content', max: 'max-content' },
829
- h: { auto: 'auto', full: '100%', screen: '100vh', fit: 'fit-content', min: 'min-content', max: 'max-content' },
830
- 'max-w': { full: '100%', none: 'none', screen: '100vw', fit: 'fit-content', min: 'min-content', max: 'max-content' },
831
- 'max-h': { full: '100%', none: 'none', screen: '100vh', fit: 'fit-content', min: 'min-content', max: 'max-content' },
1200
+ w: {
1201
+ auto: 'auto',
1202
+ full: '100%',
1203
+ screen: '100vw',
1204
+ svw: '100svw',
1205
+ dvw: '100dvw',
1206
+ lvw: '100lvw',
1207
+ fit: 'fit-content',
1208
+ min: 'min-content',
1209
+ max: 'max-content',
1210
+ },
1211
+ h: {
1212
+ auto: 'auto',
1213
+ full: '100%',
1214
+ screen: '100vh',
1215
+ svh: '100svh',
1216
+ dvh: '100dvh',
1217
+ lvh: '100lvh',
1218
+ fit: 'fit-content',
1219
+ min: 'min-content',
1220
+ max: 'max-content',
1221
+ },
1222
+ 'max-w': {
1223
+ full: '100%',
1224
+ none: 'none',
1225
+ screen: '100vw',
1226
+ svw: '100svw',
1227
+ dvw: '100dvw',
1228
+ lvw: '100lvw',
1229
+ fit: 'fit-content',
1230
+ min: 'min-content',
1231
+ max: 'max-content',
1232
+ },
1233
+ 'max-h': {
1234
+ full: '100%',
1235
+ none: 'none',
1236
+ screen: '100vh',
1237
+ svh: '100svh',
1238
+ dvh: '100dvh',
1239
+ lvh: '100lvh',
1240
+ fit: 'fit-content',
1241
+ min: 'min-content',
1242
+ max: 'max-content',
1243
+ },
832
1244
  size: { auto: 'auto', full: '100%', fit: 'fit-content', min: 'min-content', max: 'max-content' },
833
- 'min-w': { auto: 'auto', full: '100%', fit: 'fit-content', min: 'min-content', max: 'max-content' },
834
- 'min-h': { auto: 'auto', full: '100%', screen: '100vh', fit: 'fit-content', min: 'min-content', max: 'max-content' },
1245
+ 'min-w': {
1246
+ auto: 'auto',
1247
+ full: '100%',
1248
+ svw: '100svw',
1249
+ dvw: '100dvw',
1250
+ lvw: '100lvw',
1251
+ fit: 'fit-content',
1252
+ min: 'min-content',
1253
+ max: 'max-content',
1254
+ },
1255
+ 'min-h': {
1256
+ auto: 'auto',
1257
+ full: '100%',
1258
+ screen: '100vh',
1259
+ svh: '100svh',
1260
+ dvh: '100dvh',
1261
+ lvh: '100lvh',
1262
+ fit: 'fit-content',
1263
+ min: 'min-content',
1264
+ max: 'max-content',
1265
+ },
835
1266
  m: { auto: 'auto' },
836
1267
  mt: { auto: 'auto' },
837
1268
  mr: { auto: 'auto' },
@@ -857,19 +1288,45 @@ export const keywordValues: Record<string, Record<string, string>> = {
857
1288
  'inset-y': { auto: 'auto', full: '100%' },
858
1289
  aspect: { auto: 'auto', square: '1 / 1', video: '16 / 9' },
859
1290
  order: { first: '-9999', last: '9999', none: '0' },
1291
+ 'will-change': { scroll: 'scroll-position' }, // other values (transform, contents, auto) pass through generically
1292
+ columns: { auto: 'auto' },
1293
+ start: { auto: 'auto', full: '100%' },
1294
+ end: { auto: 'auto', full: '100%' },
1295
+ 'underline-offset': { auto: 'auto' },
1296
+ 'auto-cols': { auto: 'auto', min: 'min-content', max: 'max-content', fr: 'minmax(0, 1fr)' },
1297
+ 'auto-rows': { auto: 'auto', min: 'min-content', max: 'max-content', fr: 'minmax(0, 1fr)' },
1298
+ col: { auto: 'auto' },
1299
+ row: { auto: 'auto' },
1300
+ decoration: { 'from-font': 'from-font' }, // thickness keyword; style keywords are statics, colors flow via value type
860
1301
  };
861
1302
 
862
1303
  /**
863
1304
  * Roots whose plain lowercase keyword values pass through verbatim:
864
1305
  * `cursor-pointer`, `align-middle`, `origin-center`, `object-center`, …
865
1306
  */
866
- export const genericKeywordRoots: Set<string> = new Set(['cursor', 'align', 'origin', 'object', 'list']);
1307
+ export const genericKeywordRoots: Set<string> = new Set([
1308
+ 'cursor',
1309
+ 'align',
1310
+ 'origin',
1311
+ 'object',
1312
+ 'list',
1313
+ 'mix-blend',
1314
+ 'bg-blend',
1315
+ 'will-change',
1316
+ 'overscroll',
1317
+ 'overscroll-x',
1318
+ 'overscroll-y',
1319
+ 'touch',
1320
+ 'break-after',
1321
+ 'break-before',
1322
+ 'break-inside',
1323
+ ]);
867
1324
 
868
1325
  /**
869
1326
  * Roots that accept bare numeric values Tailwind-style: `z-10`, `order-2`,
870
1327
  * `flex-2`, `grow-2`. (`opacity` is handled separately — Tailwind's 0–100 scale.)
871
1328
  */
872
- export const integerValueRoots: Set<string> = new Set(['z', 'order', 'flex', 'grow', 'shrink']);
1329
+ export const integerValueRoots: Set<string> = new Set(['z', 'order', 'flex', 'grow', 'shrink', 'columns', 'stroke']);
873
1330
 
874
1331
  /**
875
1332
  * Properties whose values are color tokens by Meno convention: a bare token
@@ -882,6 +1339,10 @@ export const colorTokenProps: Set<string> = new Set([
882
1339
  'borderColor',
883
1340
  'accentColor',
884
1341
  'outlineColor',
1342
+ 'textDecorationColor',
1343
+ 'caretColor',
1344
+ 'fill',
1345
+ 'stroke',
885
1346
  ]);
886
1347
 
887
1348
  /**