@loworbitstudio/visor-theme-engine 0.1.0 → 0.4.1

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.
@@ -1,14 +1,17 @@
1
1
  import {
2
2
  FULL_SHADE_STEPS,
3
+ MATERIAL_TEXT_SLOTS,
3
4
  SELECTIVE_SHADE_STEPS,
4
5
  buildVisorFontUrl,
5
6
  generateDarkCss,
6
7
  generateLightCss,
7
8
  generatePrimitivesCss,
9
+ generateShadeScale,
8
10
  header,
11
+ parseColor,
9
12
  resolveThemeFonts,
10
13
  sectionComment
11
- } from "../chunk-ZLXFCNYF.js";
14
+ } from "../chunk-G4B57FB3.js";
12
15
 
13
16
  // src/adapters/layers.ts
14
17
  var LAYER_ORDER = "@layer visor-primitives, visor-semantic, visor-adaptive, visor-bridge;";
@@ -127,7 +130,7 @@ var FUMADOCS_BRIDGE_MAP = {
127
130
  },
128
131
  "fd-secondary": { visorToken: "muted", category: "surface" },
129
132
  "fd-secondary-foreground": { visorToken: "primary", category: "text" },
130
- "fd-popover": { visorToken: "card", category: "surface" },
133
+ "fd-popover": { visorToken: "popover", category: "surface" },
131
134
  "fd-popover-foreground": { visorToken: "primary", category: "text" },
132
135
  "fd-ring": { visorToken: "focus", category: "border" }
133
136
  };
@@ -427,6 +430,7 @@ function docsAdapter(input, options) {
427
430
  lines.push("");
428
431
  }
429
432
  }
433
+ const scale = input.config.typography?.scale ?? 1;
430
434
  const seenFamilies = /* @__PURE__ */ new Set();
431
435
  for (const font of fontSlots) {
432
436
  if (font && font.source === "visor-fonts" && !seenFamilies.has(font.family)) {
@@ -439,6 +443,9 @@ function docsAdapter(input, options) {
439
443
  lines.push(` font-weight: ${weight};`);
440
444
  lines.push(` font-style: ${font.italic ? "italic" : "normal"};`);
441
445
  lines.push(` font-display: ${font.display};`);
446
+ if (scale !== 1) {
447
+ lines.push(` size-adjust: ${Math.round(scale * 100)}%;`);
448
+ }
442
449
  lines.push("}");
443
450
  lines.push("");
444
451
  }
@@ -480,6 +487,29 @@ function docsAdapter(input, options) {
480
487
  lines.push("");
481
488
  lines.push("\n/* \u2500\u2500 Section 2: Dark mode overrides \u2500\u2500 */");
482
489
  const darkDecls = generateSemanticDecls2(input.tokens, "dark");
490
+ const colorsDark = input.config["colors-dark"];
491
+ const darkPrimitiveOverrides = [];
492
+ for (const role of FULL_SCALE_ROLES2) {
493
+ if (colorsDark?.[role]) {
494
+ const scale = generateShadeScale(colorsDark[role], role);
495
+ for (const step of FULL_SHADE_STEPS) {
496
+ darkPrimitiveOverrides.push(`--color-${role}-${step}: ${scale[step]};`);
497
+ }
498
+ }
499
+ }
500
+ for (const role of SELECTIVE_SCALE_ROLES2) {
501
+ if (colorsDark?.[role]) {
502
+ const scale = generateShadeScale(colorsDark[role], role);
503
+ for (const step of SELECTIVE_SHADE_STEPS) {
504
+ darkPrimitiveOverrides.push(`--color-${role}-${step}: ${scale[step]};`);
505
+ }
506
+ }
507
+ }
508
+ if (darkPrimitiveOverrides.length > 0) {
509
+ lines.push(sectionComment2("Primitive overrides (dark) \u2014 dark brand color anchors at shade 500"));
510
+ lines.push(block(`.dark ${scopeClass}`, darkPrimitiveOverrides));
511
+ lines.push("");
512
+ }
483
513
  const categories = ["Text", "Surface", "Border", "Interactive"];
484
514
  const categoryDecls = [
485
515
  Object.entries(input.tokens.text).map(([n, v]) => `--text-${n}: ${v.dark};`),
@@ -503,6 +533,14 @@ function docsAdapter(input, options) {
503
533
  const inner = block(`${scopeClass}:not(.light)`, cat.entries);
504
534
  lines.push(`@media (prefers-color-scheme: dark) {
505
535
  ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
536
+ }`);
537
+ lines.push("");
538
+ }
539
+ if (darkPrimitiveOverrides.length > 0) {
540
+ lines.push(sectionComment2("Primitive overrides (dark) \u2014 prefers-color-scheme"));
541
+ const inner = block(`${scopeClass}:not(.light)`, darkPrimitiveOverrides);
542
+ lines.push(`@media (prefers-color-scheme: dark) {
543
+ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
506
544
  }`);
507
545
  lines.push("");
508
546
  }
@@ -526,12 +564,701 @@ ${inner.split("\n").map((l) => ` ${l}`).join("\n")}
526
564
  lines.push("");
527
565
  return lines.join("\n") + "\n";
528
566
  }
567
+
568
+ // src/flutter/color-to-dart.ts
569
+ function alphaToByte(alphaPct) {
570
+ const clamped = Math.max(0, Math.min(1, alphaPct));
571
+ return Math.round(clamped * 255);
572
+ }
573
+ function byteHex(n) {
574
+ const clamped = Math.max(0, Math.min(255, Math.round(n)));
575
+ return clamped.toString(16).padStart(2, "0").toUpperCase();
576
+ }
577
+ function cssColorToDart(css, alphaOverride) {
578
+ if (css.trim().toLowerCase() === "transparent") {
579
+ const alpha2 = alphaOverride !== void 0 ? alphaOverride : 0;
580
+ const aByte2 = alphaToByte(alpha2);
581
+ return `Color(0x${byteHex(aByte2)}000000)`;
582
+ }
583
+ const parsed = parseColor(css);
584
+ if (!parsed) {
585
+ throw new Error(`Invalid CSS color: ${css}`);
586
+ }
587
+ const [r, g, b] = parsed.rgb;
588
+ const alpha = alphaOverride !== void 0 ? alphaOverride : parsed.alpha ?? 1;
589
+ const aByte = alphaToByte(alpha);
590
+ return `Color(0x${byteHex(aByte)}${byteHex(r)}${byteHex(g)}${byteHex(b)})`;
591
+ }
592
+ function cssColorToOpaqueDart(css) {
593
+ return cssColorToDart(css, 1);
594
+ }
595
+ function opacityVariants(css, percents = [5, 10, 20, 40, 50, 60, 80]) {
596
+ const out = {};
597
+ for (const p of percents) {
598
+ out[`${p}o`] = cssColorToDart(css, p / 100);
599
+ }
600
+ return out;
601
+ }
602
+
603
+ // src/flutter/emit-colors.ts
604
+ var FULL_SCALE_ROLES3 = ["primary", "accent", "neutral"];
605
+ var SELECTIVE_SCALE_ROLES3 = [
606
+ "success",
607
+ "warning",
608
+ "error",
609
+ "info"
610
+ ];
611
+ var DART_HEADER = `// GENERATED BY visor \u2014 DO NOT EDIT.
612
+ // Regenerate with \`visor theme apply --adapter flutter\`.
613
+ // Source: .visor.yaml \u2192 Visor theme engine \u2192 flutter adapter.
614
+
615
+ import 'dart:ui';
616
+
617
+ import 'package:visor_core/visor_core.dart';
618
+ `;
619
+ function emitColorsDart(input) {
620
+ const { primitives, tokens } = input;
621
+ const lines = [];
622
+ lines.push(DART_HEADER);
623
+ lines.push(`/// Generated color tokens for the ${input.config.name} theme.`);
624
+ lines.push("///");
625
+ lines.push("/// Do not edit this file manually \u2014 regenerate from .visor.yaml.");
626
+ lines.push("sealed class VisorColors {");
627
+ lines.push(" // ========================================================================");
628
+ lines.push(" // Anchors");
629
+ lines.push(" // ========================================================================");
630
+ lines.push(" static const Color white = Color(0xFFFFFFFF);");
631
+ lines.push(" static const Color black = Color(0xFF000000);");
632
+ lines.push("");
633
+ for (const role of [...FULL_SCALE_ROLES3, ...SELECTIVE_SCALE_ROLES3]) {
634
+ lines.push(
635
+ ` // ========================================================================`
636
+ );
637
+ lines.push(` // ${capitalize(role)} scale`);
638
+ lines.push(
639
+ ` // ========================================================================`
640
+ );
641
+ const steps = FULL_SCALE_ROLES3.includes(role) ? FULL_SHADE_STEPS : SELECTIVE_SHADE_STEPS;
642
+ const scale = primitives[role];
643
+ for (const step of steps) {
644
+ const value = scale[step];
645
+ lines.push(
646
+ ` static const Color ${role}${step} = ${cssColorToOpaqueDart(value)};`
647
+ );
648
+ }
649
+ lines.push("");
650
+ }
651
+ lines.push(" // ========================================================================");
652
+ lines.push(" // Opacity variants (pre-computed at codegen)");
653
+ lines.push(" // ========================================================================");
654
+ const opacitySources = [
655
+ { name: "primary500", css: primitives.primary[500] },
656
+ { name: "primary600", css: primitives.primary[600] },
657
+ { name: "accent500", css: primitives.accent[500] },
658
+ { name: "white", css: "#FFFFFF" },
659
+ { name: "black", css: "#000000" }
660
+ ];
661
+ for (const source of opacitySources) {
662
+ const variants = opacityVariants(source.css);
663
+ for (const [suffix, dartColor] of Object.entries(variants)) {
664
+ lines.push(` static const Color ${source.name}_${suffix} = ${dartColor};`);
665
+ }
666
+ }
667
+ lines.push("");
668
+ lines.push(" // ========================================================================");
669
+ lines.push(" // Semantic \u2014 Light");
670
+ lines.push(" // ========================================================================");
671
+ lines.push(` static final VisorColorsData light = ${semanticInstance(tokens, "light")};`);
672
+ lines.push("");
673
+ lines.push(" // ========================================================================");
674
+ lines.push(" // Semantic \u2014 Dark");
675
+ lines.push(" // ========================================================================");
676
+ lines.push(` static final VisorColorsData dark = ${semanticInstance(tokens, "dark")};`);
677
+ lines.push("}");
678
+ return lines.join("\n") + "\n";
679
+ }
680
+ function semanticInstance(tokens, mode) {
681
+ const parts = ["VisorColorsData("];
682
+ const emit = (field, category, tokenName) => {
683
+ const token = tokens[category][tokenName];
684
+ if (!token) {
685
+ throw new Error(
686
+ `Missing semantic token: ${String(category)}.${tokenName}`
687
+ );
688
+ }
689
+ parts.push(` ${field}: ${cssColorToDart(token[mode])},`);
690
+ };
691
+ emit("textPrimary", "text", "primary");
692
+ emit("textSecondary", "text", "secondary");
693
+ emit("textTertiary", "text", "tertiary");
694
+ emit("textDisabled", "text", "disabled");
695
+ emit("textInverse", "text", "inverse");
696
+ emit("textInverseSecondary", "text", "inverse-secondary");
697
+ emit("textLink", "text", "link");
698
+ emit("textLinkHover", "text", "link-hover");
699
+ emit("textSuccess", "text", "success");
700
+ emit("textWarning", "text", "warning");
701
+ emit("textError", "text", "error");
702
+ emit("textInfo", "text", "info");
703
+ emit("surfacePage", "surface", "page");
704
+ emit("surfaceCard", "surface", "card");
705
+ emit("surfaceSubtle", "surface", "subtle");
706
+ emit("surfaceMuted", "surface", "muted");
707
+ emit("surfaceOverlay", "surface", "overlay");
708
+ emit("surfaceInteractiveDefault", "surface", "interactive-default");
709
+ emit("surfaceInteractiveHover", "surface", "interactive-hover");
710
+ emit("surfaceInteractiveActive", "surface", "interactive-active");
711
+ emit("surfaceInteractiveDisabled", "surface", "interactive-disabled");
712
+ emit("surfaceSelected", "surface", "selected");
713
+ emit("surfaceAccentSubtle", "surface", "accent-subtle");
714
+ emit("surfaceAccentDefault", "surface", "accent-default");
715
+ emit("surfaceAccentStrong", "surface", "accent-strong");
716
+ emit("surfaceSuccessSubtle", "surface", "success-subtle");
717
+ emit("surfaceSuccessDefault", "surface", "success-default");
718
+ emit("surfaceWarningSubtle", "surface", "warning-subtle");
719
+ emit("surfaceWarningDefault", "surface", "warning-default");
720
+ emit("surfaceErrorSubtle", "surface", "error-subtle");
721
+ emit("surfaceErrorDefault", "surface", "error-default");
722
+ emit("surfaceInfoSubtle", "surface", "info-subtle");
723
+ emit("surfaceInfoDefault", "surface", "info-default");
724
+ emit("borderDefault", "border", "default");
725
+ emit("borderMuted", "border", "muted");
726
+ emit("borderStrong", "border", "strong");
727
+ emit("borderFocus", "border", "focus");
728
+ emit("borderDisabled", "border", "disabled");
729
+ emit("borderSuccess", "border", "success");
730
+ emit("borderWarning", "border", "warning");
731
+ emit("borderError", "border", "error");
732
+ emit("borderInfo", "border", "info");
733
+ emit("interactivePrimaryBg", "interactive", "primary-bg");
734
+ emit("interactivePrimaryBgHover", "interactive", "primary-bg-hover");
735
+ emit("interactivePrimaryBgActive", "interactive", "primary-bg-active");
736
+ emit("interactivePrimaryText", "interactive", "primary-text");
737
+ emit("interactiveSecondaryBg", "interactive", "secondary-bg");
738
+ emit("interactiveSecondaryBgHover", "interactive", "secondary-bg-hover");
739
+ emit("interactiveSecondaryBgActive", "interactive", "secondary-bg-active");
740
+ emit("interactiveSecondaryText", "interactive", "secondary-text");
741
+ emit("interactiveSecondaryBorder", "interactive", "secondary-border");
742
+ emit("interactiveDestructiveBg", "interactive", "destructive-bg");
743
+ emit("interactiveDestructiveBgHover", "interactive", "destructive-bg-hover");
744
+ emit("interactiveDestructiveText", "interactive", "destructive-text");
745
+ emit("interactiveGhostBg", "interactive", "ghost-bg");
746
+ emit("interactiveGhostBgHover", "interactive", "ghost-bg-hover");
747
+ parts.push(" )");
748
+ void cssColorToDart;
749
+ return parts.join("\n");
750
+ }
751
+ function capitalize(s) {
752
+ return s.charAt(0).toUpperCase() + s.slice(1);
753
+ }
754
+
755
+ // src/flutter/emit-typography.ts
756
+ var HEADER_COMMENT = `// GENERATED BY visor \u2014 DO NOT EDIT.
757
+ // Regenerate with \`visor theme apply --adapter flutter\`.
758
+ `;
759
+ var MATERIAL_3_DEFAULTS = {
760
+ displayLarge: { size: 57, weight: 400, letterSpacing: -0.25 },
761
+ displayMedium: { size: 45, weight: 400 },
762
+ displaySmall: { size: 36, weight: 400 },
763
+ headlineLarge: { size: 32, weight: 400 },
764
+ headlineMedium: { size: 28, weight: 400 },
765
+ headlineSmall: { size: 24, weight: 400 },
766
+ titleLarge: { size: 22, weight: 400 },
767
+ titleMedium: { size: 16, weight: 500, letterSpacing: 0.15 },
768
+ titleSmall: { size: 14, weight: 500, letterSpacing: 0.1 },
769
+ bodyLarge: { size: 16, weight: 400, letterSpacing: 0.5 },
770
+ bodyMedium: { size: 14, weight: 400, letterSpacing: 0.25 },
771
+ bodySmall: { size: 12, weight: 400, letterSpacing: 0.4 },
772
+ labelLarge: { size: 14, weight: 500, letterSpacing: 0.1 },
773
+ labelMedium: { size: 12, weight: 500, letterSpacing: 0.5 },
774
+ labelSmall: { size: 11, weight: 500, letterSpacing: 0.5 },
775
+ labelXSmall: { size: 10, weight: 500, letterSpacing: 0.5 }
776
+ };
777
+ function emitTypographyDart(input) {
778
+ const slots = input.config.typography.slots;
779
+ const overriddenSlots = MATERIAL_TEXT_SLOTS.filter((name) => slots[name]);
780
+ const hasOverrides = overriddenSlots.length > 0;
781
+ const lines = [];
782
+ lines.push(HEADER_COMMENT);
783
+ if (hasOverrides) {
784
+ lines.push("import 'package:flutter/material.dart';");
785
+ }
786
+ lines.push("import 'package:visor_core/visor_core.dart';");
787
+ lines.push("");
788
+ lines.push(
789
+ "/// Generated typography tokens \u2014 Material 3 type-scale + `labelXSmall`."
790
+ );
791
+ lines.push("///");
792
+ lines.push(
793
+ "/// Font family is not baked in here; `VisorTheme.build()` applies it"
794
+ );
795
+ lines.push(
796
+ "/// globally via `TextTheme.apply(fontFamily: ...)`. Override per-slot"
797
+ );
798
+ lines.push("/// values in `.visor.yaml` under `typography.slots`.");
799
+ lines.push("sealed class VisorTextStyles {");
800
+ if (!hasOverrides) {
801
+ lines.push(" static final VisorTextStylesData instance =");
802
+ lines.push(" VisorTextStylesData.defaults;");
803
+ } else {
804
+ lines.push(" static final VisorTextStylesData instance =");
805
+ lines.push(
806
+ " VisorTextStylesData.defaults.copyWith("
807
+ );
808
+ for (const name of overriddenSlots) {
809
+ const override = slots[name];
810
+ lines.push(...emitSlot(name, override).map((l) => ` ${l}`));
811
+ }
812
+ lines.push(" );");
813
+ }
814
+ lines.push("}");
815
+ return lines.join("\n") + "\n";
816
+ }
817
+ function emitSlot(name, override) {
818
+ const d = MATERIAL_3_DEFAULTS[name];
819
+ const size = override.size ?? d.size;
820
+ const weight = override.weight ?? d.weight;
821
+ const letterSpacing = override["letter-spacing"] !== void 0 ? override["letter-spacing"] : d.letterSpacing;
822
+ const styleLines = [
823
+ `${name}: const TextStyle(`,
824
+ ` fontSize: ${size},`,
825
+ ` fontWeight: FontWeight.w${weight},`
826
+ ];
827
+ if (letterSpacing !== void 0) {
828
+ styleLines.push(` letterSpacing: ${letterSpacing},`);
829
+ }
830
+ styleLines.push(`),`);
831
+ return styleLines;
832
+ }
833
+
834
+ // src/flutter/emit-spacing.ts
835
+ var DART_HEADER2 = `// GENERATED BY visor \u2014 DO NOT EDIT.
836
+ // Regenerate with \`visor theme apply --adapter flutter\`.
837
+
838
+ import 'package:visor_core/visor_core.dart';
839
+ `;
840
+ function emitSpacingDart(input) {
841
+ const base = input.config.spacing.base;
842
+ const lines = [];
843
+ lines.push(DART_HEADER2);
844
+ lines.push("/// Generated spacing tokens.");
845
+ lines.push("///");
846
+ lines.push(`/// Base unit: ${base}px. Scale derived by multiplication:`);
847
+ lines.push("/// xs=1\xD7, sm=2\xD7, md=3\xD7, lg=4\xD7, xl=6\xD7, xxl=8\xD7, xxxl=12\xD7.");
848
+ lines.push("sealed class VisorSpacing {");
849
+ lines.push(" static final VisorSpacingData instance = VisorSpacingData(");
850
+ lines.push(` xs: ${fmt(base * 1)},`);
851
+ lines.push(` sm: ${fmt(base * 2)},`);
852
+ lines.push(` md: ${fmt(base * 3)},`);
853
+ lines.push(` lg: ${fmt(base * 4)},`);
854
+ lines.push(` xl: ${fmt(base * 6)},`);
855
+ lines.push(` xxl: ${fmt(base * 8)},`);
856
+ lines.push(` xxxl: ${fmt(base * 12)},`);
857
+ lines.push(" );");
858
+ lines.push("}");
859
+ return lines.join("\n") + "\n";
860
+ }
861
+ function fmt(n) {
862
+ return Number.isInteger(n) ? `${n}` : `${n}`;
863
+ }
864
+
865
+ // src/flutter/emit-radius.ts
866
+ var DART_HEADER3 = `// GENERATED BY visor \u2014 DO NOT EDIT.
867
+ // Regenerate with \`visor theme apply --adapter flutter\`.
868
+
869
+ import 'package:visor_core/visor_core.dart';
870
+ `;
871
+ function emitRadiusDart(input) {
872
+ const r = input.config.radius;
873
+ const lines = [];
874
+ lines.push(DART_HEADER3);
875
+ lines.push("/// Generated border-radius tokens (raw pixel values).");
876
+ lines.push("sealed class VisorRadius {");
877
+ lines.push(" static final VisorRadiusData instance = VisorRadiusData(");
878
+ lines.push(` sm: ${r.sm},`);
879
+ lines.push(` md: ${r.md},`);
880
+ lines.push(` lg: ${r.lg},`);
881
+ lines.push(` xl: ${r.xl},`);
882
+ lines.push(` pill: ${r.pill},`);
883
+ lines.push(" );");
884
+ lines.push("}");
885
+ return lines.join("\n") + "\n";
886
+ }
887
+
888
+ // src/flutter/emit-shadows.ts
889
+ var DART_HEADER4 = `// GENERATED BY visor \u2014 DO NOT EDIT.
890
+ // Regenerate with \`visor theme apply --adapter flutter\`.
891
+
892
+ import 'package:flutter/material.dart';
893
+ import 'package:visor_core/visor_core.dart';
894
+ `;
895
+ var SHADOW_KEYS = ["xs", "sm", "md", "lg", "xl"];
896
+ function emitShadowsDart(input) {
897
+ const s = input.config.shadows;
898
+ const lines = [];
899
+ lines.push(DART_HEADER4);
900
+ lines.push("/// Generated elevation/shadow tokens.");
901
+ lines.push("///");
902
+ lines.push("/// Each value is a `List<BoxShadow>` \u2014 CSS box-shadow strings");
903
+ lines.push("/// supporting multi-layer shadows compile to the same list here.");
904
+ lines.push("sealed class VisorShadows {");
905
+ lines.push(" static final VisorShadowsData instance = VisorShadowsData(");
906
+ for (const key of SHADOW_KEYS) {
907
+ const dart = parseShadowListToDart(s[key]);
908
+ lines.push(` ${key}: ${dart},`);
909
+ }
910
+ lines.push(" );");
911
+ lines.push("}");
912
+ return lines.join("\n") + "\n";
913
+ }
914
+ function parseShadowListToDart(css) {
915
+ if (css.trim().toLowerCase() === "none") return "const <BoxShadow>[]";
916
+ const shadows = splitTopLevelCommas(css);
917
+ if (shadows.length === 0) return "const <BoxShadow>[]";
918
+ const parts = shadows.map(parseSingleShadow);
919
+ return `const <BoxShadow>[
920
+ ${parts.map((p) => " " + p).join(",\n")},
921
+ ]`;
922
+ }
923
+ function parseSingleShadow(raw) {
924
+ const css = raw.trim();
925
+ if (/\binset\b/i.test(css)) {
926
+ throw new Error(
927
+ `box-shadow 'inset' is not representable on Flutter BoxShadow: "${raw}"`
928
+ );
929
+ }
930
+ const colorRe = /(#[0-9a-f]{3,8}|rgba?\([^)]*\)|hsla?\([^)]*\)|oklch\([^)]*\)|oklab\([^)]*\))\s*$/i;
931
+ const colorMatch = css.match(colorRe);
932
+ if (!colorMatch || colorMatch.index === void 0) {
933
+ throw new Error(`Invalid box-shadow: missing trailing color in "${raw}"`);
934
+ }
935
+ const color = colorMatch[1];
936
+ const lengthsPart = css.slice(0, colorMatch.index).trim();
937
+ const lengths = lengthsPart.split(/\s+/).filter(Boolean);
938
+ if (lengths.length < 2) {
939
+ throw new Error(
940
+ `Invalid box-shadow: need at least x and y offsets in "${raw}"`
941
+ );
942
+ }
943
+ if (lengths.length > 4) {
944
+ throw new Error(
945
+ `Invalid box-shadow: too many length values in "${raw}" (max 4: x y blur spread)`
946
+ );
947
+ }
948
+ const [x, y, blur = "0", spread = "0"] = lengths;
949
+ const offsetX = parseLength(x, raw);
950
+ const offsetY = parseLength(y, raw);
951
+ const blurRadius = parseLength(blur, raw);
952
+ const spreadRadius = parseLength(spread, raw);
953
+ const colorDart = cssColorToDart(color);
954
+ const parts = [
955
+ `offset: Offset(${offsetX}, ${offsetY})`,
956
+ `blurRadius: ${blurRadius}`
957
+ ];
958
+ if (spreadRadius !== 0) {
959
+ parts.push(`spreadRadius: ${spreadRadius}`);
960
+ }
961
+ parts.push(`color: ${colorDart}`);
962
+ return `BoxShadow(${parts.join(", ")})`;
963
+ }
964
+ function parseLength(token, raw) {
965
+ const cleaned = token.trim().replace(/px$/i, "");
966
+ const n = Number(cleaned);
967
+ if (!Number.isFinite(n)) {
968
+ throw new Error(`Invalid shadow length "${token}" in "${raw}"`);
969
+ }
970
+ return n;
971
+ }
972
+ function splitTopLevelCommas(input) {
973
+ const parts = [];
974
+ let depth = 0;
975
+ let buf = "";
976
+ for (const c of input) {
977
+ if (c === "(") depth++;
978
+ else if (c === ")") depth--;
979
+ if (c === "," && depth === 0) {
980
+ if (buf.trim()) parts.push(buf.trim());
981
+ buf = "";
982
+ } else {
983
+ buf += c;
984
+ }
985
+ }
986
+ if (buf.trim()) parts.push(buf.trim());
987
+ return parts;
988
+ }
989
+
990
+ // src/flutter/emit-strokes.ts
991
+ var DART_HEADER5 = `// GENERATED BY visor \u2014 DO NOT EDIT.
992
+ // Regenerate with \`visor theme apply --adapter flutter\`.
993
+
994
+ import 'package:visor_core/visor_core.dart';
995
+ `;
996
+ function emitStrokesDart(input) {
997
+ const s = input.config.strokeWidths;
998
+ const lines = [];
999
+ lines.push(DART_HEADER5);
1000
+ lines.push("/// Generated stroke-width tokens (raw pixel values).");
1001
+ lines.push("sealed class VisorStrokeWidths {");
1002
+ lines.push(" static final VisorStrokeWidthsData instance = VisorStrokeWidthsData(");
1003
+ lines.push(` thin: ${s.thin},`);
1004
+ lines.push(` regular: ${s.regular},`);
1005
+ lines.push(` medium: ${s.medium},`);
1006
+ lines.push(` thick: ${s.thick},`);
1007
+ lines.push(" );");
1008
+ lines.push("}");
1009
+ return lines.join("\n") + "\n";
1010
+ }
1011
+
1012
+ // src/flutter/emit-opacity.ts
1013
+ var DART_HEADER6 = `// GENERATED BY visor \u2014 DO NOT EDIT.
1014
+ // Regenerate with \`visor theme apply --adapter flutter\`.
1015
+
1016
+ import 'package:visor_core/visor_core.dart';
1017
+ `;
1018
+ function emitOpacityDart() {
1019
+ const lines = [];
1020
+ lines.push(DART_HEADER6);
1021
+ lines.push("/// Generated opacity tokens (raw alpha values in [0, 1]).");
1022
+ lines.push("///");
1023
+ lines.push("/// Fixed across themes \u2014 opacity is a math primitive, not a brand decision.");
1024
+ lines.push("sealed class VisorOpacity {");
1025
+ lines.push(" static final VisorOpacityData instance = VisorOpacityData(");
1026
+ lines.push(" alpha5: 0.05,");
1027
+ lines.push(" alpha10: 0.1,");
1028
+ lines.push(" alpha12: 0.12,");
1029
+ lines.push(" alpha20: 0.2,");
1030
+ lines.push(" alpha40: 0.4,");
1031
+ lines.push(" alpha50: 0.5,");
1032
+ lines.push(" alpha60: 0.6,");
1033
+ lines.push(" alpha80: 0.8,");
1034
+ lines.push(" );");
1035
+ lines.push("}");
1036
+ return lines.join("\n") + "\n";
1037
+ }
1038
+
1039
+ // src/flutter/emit-motion.ts
1040
+ var DART_HEADER7 = `// GENERATED BY visor \u2014 DO NOT EDIT.
1041
+ // Regenerate with \`visor theme apply --adapter flutter\`.
1042
+
1043
+ import 'package:flutter/material.dart';
1044
+ import 'package:visor_core/visor_core.dart';
1045
+ `;
1046
+ function emitMotionDart(input) {
1047
+ const m = input.config.motion;
1048
+ const fast = parseDurationToDart(m["duration-fast"]);
1049
+ const normal = parseDurationToDart(m["duration-normal"]);
1050
+ const slow = parseDurationToDart(m["duration-slow"]);
1051
+ const easing = parseEasingToDart(m.easing);
1052
+ const lines = [];
1053
+ lines.push(DART_HEADER7);
1054
+ lines.push("/// Generated motion tokens \u2014 durations + default easing curve.");
1055
+ lines.push("sealed class VisorMotion {");
1056
+ lines.push(" static final VisorMotionData instance = VisorMotionData(");
1057
+ lines.push(` durationFast: ${fast},`);
1058
+ lines.push(` durationNormal: ${normal},`);
1059
+ lines.push(` durationSlow: ${slow},`);
1060
+ lines.push(` easing: ${easing},`);
1061
+ lines.push(" );");
1062
+ lines.push("}");
1063
+ return lines.join("\n") + "\n";
1064
+ }
1065
+ function parseDurationToDart(css) {
1066
+ const trimmed = css.trim().toLowerCase();
1067
+ let ms;
1068
+ if (trimmed.endsWith("ms")) {
1069
+ ms = Number(trimmed.slice(0, -2));
1070
+ } else if (trimmed.endsWith("s")) {
1071
+ ms = Number(trimmed.slice(0, -1)) * 1e3;
1072
+ } else {
1073
+ throw new Error(`Invalid duration "${css}": expected "<N>ms" or "<N>s"`);
1074
+ }
1075
+ if (!Number.isFinite(ms) || ms < 0) {
1076
+ throw new Error(`Invalid duration "${css}": must be a non-negative number`);
1077
+ }
1078
+ return `Duration(milliseconds: ${Math.round(ms)})`;
1079
+ }
1080
+ var KEYWORD_TO_CURVE = {
1081
+ linear: "Curves.linear",
1082
+ ease: "Curves.ease",
1083
+ "ease-in": "Curves.easeIn",
1084
+ "ease-out": "Curves.easeOut",
1085
+ "ease-in-out": "Curves.easeInOut"
1086
+ };
1087
+ function parseEasingToDart(css) {
1088
+ const trimmed = css.trim().toLowerCase();
1089
+ if (trimmed in KEYWORD_TO_CURVE) {
1090
+ return KEYWORD_TO_CURVE[trimmed];
1091
+ }
1092
+ const cbMatch = trimmed.match(
1093
+ /^cubic-bezier\(\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*\)$/
1094
+ );
1095
+ if (cbMatch) {
1096
+ const [, a, b, c, d] = cbMatch;
1097
+ return `Cubic(${a}, ${b}, ${c}, ${d})`;
1098
+ }
1099
+ console.warn(
1100
+ `visor: unsupported easing "${css}" \u2014 falling back to Curves.easeInOut.`
1101
+ );
1102
+ return "Curves.easeInOut";
1103
+ }
1104
+
1105
+ // src/flutter/emit-theme.ts
1106
+ function emitThemeDart(options) {
1107
+ const { themeClassName, emitLight, emitDark, tokenImports } = options;
1108
+ const lines = [
1109
+ `// GENERATED BY visor \u2014 DO NOT EDIT.`,
1110
+ `// Regenerate with \`visor theme apply --adapter flutter\`.`,
1111
+ ``,
1112
+ `import 'package:flutter/material.dart';`,
1113
+ `import 'package:visor_core/visor_core.dart';`,
1114
+ ``,
1115
+ ...tokenImports.map((p) => `import '${p}';`),
1116
+ ``,
1117
+ `/// Assembled Material 3 [ThemeData] for this project.`,
1118
+ `///`,
1119
+ `/// Wire into [MaterialApp]:`,
1120
+ `/// \`\`\`dart`,
1121
+ `/// MaterialApp(`,
1122
+ `/// theme: ${themeClassName}.light,`,
1123
+ `/// darkTheme: ${themeClassName}.dark,`,
1124
+ `/// );`,
1125
+ `/// \`\`\``,
1126
+ `sealed class ${themeClassName} {`
1127
+ ];
1128
+ if (emitLight) {
1129
+ lines.push(
1130
+ ` static ThemeData get light => VisorTheme.build(`,
1131
+ ` colors: VisorColors.light,`,
1132
+ ` brightness: Brightness.light,`,
1133
+ ` textStyles: VisorTextStyles.instance,`,
1134
+ ` spacing: VisorSpacing.instance,`,
1135
+ ` radius: VisorRadius.instance,`,
1136
+ ` shadows: VisorShadows.instance,`,
1137
+ ` strokeWidths: VisorStrokeWidths.instance,`,
1138
+ ` opacity: VisorOpacity.instance,`,
1139
+ ` motion: VisorMotion.instance,`,
1140
+ ` );`,
1141
+ ``
1142
+ );
1143
+ }
1144
+ if (emitDark) {
1145
+ lines.push(
1146
+ ` static ThemeData get dark => VisorTheme.build(`,
1147
+ ` colors: VisorColors.dark,`,
1148
+ ` brightness: Brightness.dark,`,
1149
+ ` textStyles: VisorTextStyles.instance,`,
1150
+ ` spacing: VisorSpacing.instance,`,
1151
+ ` radius: VisorRadius.instance,`,
1152
+ ` shadows: VisorShadows.instance,`,
1153
+ ` strokeWidths: VisorStrokeWidths.instance,`,
1154
+ ` opacity: VisorOpacity.instance,`,
1155
+ ` motion: VisorMotion.instance,`,
1156
+ ` );`,
1157
+ ``
1158
+ );
1159
+ }
1160
+ lines.push(`}`);
1161
+ lines.push(``);
1162
+ return lines.join("\n");
1163
+ }
1164
+
1165
+ // src/adapters/flutter.ts
1166
+ function flutterAdapter(input, options) {
1167
+ const packageName = options?.packageName ?? "ui";
1168
+ const themeClassName = options?.themeClassName ?? "VisorAppTheme";
1169
+ const visorCoreVersion = options?.visorCoreVersion ?? "^0.1.0";
1170
+ const tokensOnly = options?.tokensOnly ?? false;
1171
+ const lightOnly = options?.lightOnly ?? false;
1172
+ const darkOnly = options?.darkOnly ?? false;
1173
+ const files = {};
1174
+ files["lib/src/colors/visor_colors.dart"] = emitColorsDart(input);
1175
+ files["lib/src/typography/visor_text_styles.dart"] = emitTypographyDart(input);
1176
+ files["lib/src/spacing/visor_spacing.dart"] = emitSpacingDart(input);
1177
+ files["lib/src/radius/visor_radius.dart"] = emitRadiusDart(input);
1178
+ files["lib/src/shadows/visor_shadows.dart"] = emitShadowsDart(input);
1179
+ files["lib/src/strokes/visor_stroke_widths.dart"] = emitStrokesDart(input);
1180
+ files["lib/src/opacity/visor_opacity.dart"] = emitOpacityDart();
1181
+ files["lib/src/motion/visor_motion.dart"] = emitMotionDart(input);
1182
+ if (!tokensOnly) {
1183
+ files["pubspec.yaml"] = emitPubspec(packageName, visorCoreVersion);
1184
+ files["lib/ui.dart"] = emitBarrel();
1185
+ files["lib/src/theme/visor_theme.dart"] = emitThemeDart({
1186
+ themeClassName,
1187
+ emitLight: !darkOnly,
1188
+ emitDark: !lightOnly,
1189
+ tokenImports: [
1190
+ "../colors/visor_colors.dart",
1191
+ "../typography/visor_text_styles.dart",
1192
+ "../spacing/visor_spacing.dart",
1193
+ "../radius/visor_radius.dart",
1194
+ "../shadows/visor_shadows.dart",
1195
+ "../strokes/visor_stroke_widths.dart",
1196
+ "../opacity/visor_opacity.dart",
1197
+ "../motion/visor_motion.dart"
1198
+ ]
1199
+ });
1200
+ }
1201
+ return { files };
1202
+ }
1203
+ function emitPubspec(packageName, visorCoreVersion) {
1204
+ return [
1205
+ `name: ${packageName}`,
1206
+ `description: Generated Visor tokens for this project. Regenerate with \`visor theme apply\`.`,
1207
+ `version: 0.1.0+1`,
1208
+ `publish_to: none`,
1209
+ ``,
1210
+ `environment:`,
1211
+ ` sdk: ^3.5.0`,
1212
+ ` flutter: ^3.24.0`,
1213
+ ``,
1214
+ `dependencies:`,
1215
+ ` flutter:`,
1216
+ ` sdk: flutter`,
1217
+ ` visor_core: ${visorCoreVersion}`,
1218
+ ``,
1219
+ `dev_dependencies:`,
1220
+ ` flutter_test:`,
1221
+ ` sdk: flutter`,
1222
+ ``,
1223
+ `flutter:`,
1224
+ ` uses-material-design: true`,
1225
+ ``
1226
+ ].join("\n");
1227
+ }
1228
+ function emitBarrel() {
1229
+ return [
1230
+ `/// Generated barrel for the Visor tokens package.`,
1231
+ `library;`,
1232
+ ``,
1233
+ `export 'src/colors/visor_colors.dart';`,
1234
+ `export 'src/typography/visor_text_styles.dart';`,
1235
+ `export 'src/spacing/visor_spacing.dart';`,
1236
+ `export 'src/radius/visor_radius.dart';`,
1237
+ `export 'src/shadows/visor_shadows.dart';`,
1238
+ `export 'src/strokes/visor_stroke_widths.dart';`,
1239
+ `export 'src/opacity/visor_opacity.dart';`,
1240
+ `export 'src/motion/visor_motion.dart';`,
1241
+ `export 'src/theme/visor_theme.dart';`,
1242
+ ``,
1243
+ `// Re-export visor_core so consumers can access ThemeExtensions with a`,
1244
+ `// single import of this package.`,
1245
+ `export 'package:visor_core/visor_core.dart';`,
1246
+ ``
1247
+ ].join("\n");
1248
+ }
1249
+
1250
+ // src/adapters/types.ts
1251
+ function isAdapterFileMap(output) {
1252
+ return typeof output !== "string";
1253
+ }
529
1254
  export {
530
1255
  FUMADOCS_BRIDGE_MAP,
531
1256
  LAYER_ORDER,
532
1257
  deckAdapter,
533
1258
  docsAdapter,
1259
+ flutterAdapter,
534
1260
  fumadocsAdapter,
1261
+ isAdapterFileMap,
535
1262
  nextjsAdapter,
536
1263
  wrapInLayer
537
1264
  };