@domandigital/craft 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # @domandigital/craft
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1b2e8bf: `checkRestraint` resolves token references instead of skipping them.
8
+
9
+ The old rule skipped any value starting with `var(`, on the reasoning that a
10
+ token reference is the token layer doing its job rather than a new value. True
11
+ of a hand-written sheet. On a Tailwind v4 sheet it is close to measuring
12
+ nothing: `text-sm` compiles to `font-size: var(--text-sm)` and `rounded-lg` to
13
+ `border-radius: var(--radius-lg)`, and `shadow-[…]` emits no `box-shadow` at
14
+ all — it assigns `--tw-shadow` and lets one shared composite read it.
15
+
16
+ Measured on the estate this was hiding, per surface: 19 font sizes reported
17
+ against 28 shipped, 5 shadows against 18, 7 radii against 13. A second surface
18
+ reported `ok: true` on every axis and is over budget on four.
19
+
20
+ What changes:
21
+
22
+ - A value that is exactly one `var()` is resolved through the token chain to
23
+ the value that renders, and counted.
24
+ - `--tw-shadow` assignments count as shadows; Tailwind's composite plumbing
25
+ value does not, and neither does its `0 0 #0000` initial.
26
+ - `var(--tw-shadow-color, X)` is unwrapped to `X`, so the report names what a
27
+ reader would see.
28
+ - Definitions are read from the whole sheet, including ignored layers, while
29
+ usage is still read only from kept ones. The layer exclusion is unchanged in
30
+ intent and now works: a framework default nothing uses still counts for
31
+ nothing. Used versus unused was always the real distinction; which layer the
32
+ definition sat in never was.
33
+ - New `unresolved-token` warning for a declaration whose token is defined
34
+ nowhere and which carries no fallback. It applies nothing, so it is not
35
+ vocabulary — it is a bug, and it now says so. It found two on first contact.
36
+
37
+ **Every consumer's baseline will move**, upward, on surfaces built with a
38
+ utility framework. That is the correction, not a regression.
39
+
3
40
  ## 0.3.0
4
41
 
5
42
  ### Minor Changes
package/STANDARD.md CHANGED
@@ -117,13 +117,34 @@ Differentiation guards pass anything that differs from its siblings, however
117
117
  ugly. What they cannot see is accumulation — eighteen font sizes, six shadows,
118
118
  four competing accents, each added reasonably, together reading as noise.
119
119
 
120
- The budget counts the vocabulary a surface **chose**. A compiled stylesheet also
121
- carries the framework's reset and theme, and `@layer theme` / `@layer base` are
122
- excluded by default for that reason — counting them reports a surface as over
123
- budget for values its author never wrote, and real findings then get triaged
124
- away with the noise.
125
-
126
- *Source: measured against the estate; the budget is the observed ceiling of the surfaces that read as designed. The layer exclusion is measured too — Tailwind v4's reset and theme contribute three font sizes and two timing functions to every sheet built with it.*
120
+ The budget counts the vocabulary a surface **chose**, and the whole difficulty
121
+ is telling that apart from what the framework brought.
122
+
123
+ The first rule drew the line at the layer: `@layer theme` and `@layer base` were
124
+ excluded, because counting them reported a surface as over budget for values its
125
+ author never wrote. Right about the noise, wrong about the boundary. Tailwind v4
126
+ compiles `text-sm` to `font-size: var(--text-sm)` and `rounded-lg` to
127
+ `border-radius: var(--radius-lg)`, so with the definitions excluded *and* token
128
+ references skipped, a surface using `text-sm` seven hundred times reported it
129
+ zero times. One estate measured 19 font sizes and shipped 28; 5 shadows and
130
+ shipped 18. A count wrong in the under-reporting direction is worse than the
131
+ over-reporting kind it replaced: the first buries real findings in noise, the
132
+ second reports a pass.
133
+
134
+ The line is **used versus unused**, which is not a property of the layer:
135
+
136
+ - References are **resolved**, not skipped — through the token chain, to the
137
+ value that renders.
138
+ - A definition is only ever reached from a declaration in a kept layer, so a
139
+ framework default nothing uses still counts for nothing.
140
+ - `--tw-shadow` assignments count as shadows. `shadow-[…]` emits no literal
141
+ `box-shadow` at all; it assigns that property and lets one shared composite
142
+ read it.
143
+ - A reference that resolves to nothing is **not** vocabulary. It is a
144
+ declaration that silently applies nothing, and it is reported as its own
145
+ warning.
146
+
147
+ *Source: measured against the estate. The budget is the observed ceiling of the surfaces that read as designed; the resolution rule is what it took to measure them at all.*
127
148
 
128
149
  ## 10. What is deliberately not a rule
129
150
 
package/dist/index.cjs CHANGED
@@ -852,10 +852,19 @@ function stripLayers(css, ignored) {
852
852
  }
853
853
  return out;
854
854
  }
855
- var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set(["inherit", "initial", "unset", "revert", "revert-layer"]);
855
+ var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set([
856
+ "inherit",
857
+ "initial",
858
+ "unset",
859
+ "revert",
860
+ "revert-layer"
861
+ ]);
856
862
  function atRuleBodies(css, prelude) {
857
863
  const out = [];
858
- const pattern = new RegExp(prelude.source, prelude.flags.includes("g") ? prelude.flags : `${prelude.flags}g`);
864
+ const pattern = new RegExp(
865
+ prelude.source,
866
+ prelude.flags.includes("g") ? prelude.flags : `${prelude.flags}g`
867
+ );
859
868
  let match;
860
869
  while ((match = pattern.exec(css)) !== null) {
861
870
  const start = match.index + match[0].length;
@@ -871,13 +880,64 @@ function atRuleBodies(css, prelude) {
871
880
  }
872
881
  return out;
873
882
  }
874
- function distinctValues(css, property) {
883
+ function customProperties(css) {
884
+ const out = /* @__PURE__ */ new Map();
885
+ for (const match of css.matchAll(/(--[\w-]+)\s*:\s*([^;}]+)/g)) {
886
+ out.set(match[1].toLowerCase(), match[2].trim().replace(/\s+/g, " "));
887
+ }
888
+ return out;
889
+ }
890
+ function unwrapShadowColor(value) {
891
+ return value.replace(
892
+ /var\(\s*--tw-shadow-color\s*,\s*([^)]*(?:\([^)]*\))?[^)]*)\)/g,
893
+ "$1"
894
+ );
895
+ }
896
+ var SINGLE_VAR = /^var\(\s*(--[\w-]+)\s*(?:,([\s\S]*))?\)$/;
897
+ function resolveValue(value, props, depth = 0) {
898
+ if (depth > 8) return null;
899
+ const match = SINGLE_VAR.exec(value.trim());
900
+ if (match === null) return value.trim();
901
+ const target = props.get(match[1].toLowerCase());
902
+ if (target !== void 0 && target !== "initial")
903
+ return resolveValue(target, props, depth + 1);
904
+ const fallback = match[2]?.trim();
905
+ return fallback === void 0 || fallback === "" ? null : resolveValue(fallback, props, depth + 1);
906
+ }
907
+ function isVarOnlyList(value) {
908
+ const parts = value.split(",");
909
+ return parts.length > 1 && parts.every((part) => /^\s*var\(\s*--[\w-]+\s*\)\s*$/.test(part));
910
+ }
911
+ function distinctValues(css, property, props, unresolved) {
875
912
  const pattern = new RegExp(`(?:^|[;{\\s])${property}\\s*:\\s*([^;}]+)`, "gi");
876
913
  const seen = /* @__PURE__ */ new Set();
877
914
  for (const match of css.matchAll(pattern)) {
878
- const value = match[1].trim().replace(/\s+/g, " ").toLowerCase();
879
- if (value.startsWith("var(") || CSS_WIDE_KEYWORDS.has(value)) continue;
880
- seen.add(value);
915
+ const raw = match[1].trim().replace(/\s+/g, " ").toLowerCase();
916
+ if (CSS_WIDE_KEYWORDS.has(raw)) continue;
917
+ const value = unwrapShadowColor(raw);
918
+ if (isVarOnlyList(value)) continue;
919
+ if (!SINGLE_VAR.test(value)) {
920
+ seen.add(value);
921
+ continue;
922
+ }
923
+ if (props === void 0) continue;
924
+ const resolved = resolveValue(value, props);
925
+ if (resolved === null) {
926
+ unresolved?.add(`${property}: ${value}`);
927
+ continue;
928
+ }
929
+ seen.add(unwrapShadowColor(resolved).toLowerCase());
930
+ }
931
+ return [...seen];
932
+ }
933
+ function twShadowValues(css, props) {
934
+ const seen = /* @__PURE__ */ new Set();
935
+ for (const match of css.matchAll(/--tw-shadow\s*:\s*([^;}]+)/g)) {
936
+ const raw = match[1].trim().replace(/\s+/g, " ").toLowerCase();
937
+ if (raw === "0 0 #0000" || raw === "none") continue;
938
+ const value = SINGLE_VAR.test(raw) ? resolveValue(raw, props) : raw;
939
+ if (value === null) continue;
940
+ seen.add(unwrapShadowColor(value).toLowerCase());
881
941
  }
882
942
  return [...seen];
883
943
  }
@@ -892,15 +952,18 @@ function durationsMs(css) {
892
952
  }
893
953
  function checkRestraint(input) {
894
954
  const budget = { ...HOUSE_BUDGET, ...input.budget };
955
+ const whole = stripComments(input.css);
956
+ const props = customProperties(whole);
895
957
  const css = stripLayers(
896
- stripComments(input.css),
958
+ whole,
897
959
  input.ignoreAtLayers ?? DEFAULT_IGNORED_LAYERS
898
960
  );
961
+ const unresolved = /* @__PURE__ */ new Set();
899
962
  const violations = [];
900
963
  const push = (rule, severity, message, found) => {
901
964
  violations.push({ rule, severity, message, found });
902
965
  };
903
- const fontSizes = distinctValues(css, "font-size");
966
+ const fontSizes = distinctValues(css, "font-size", props, unresolved);
904
967
  if (fontSizes.length > budget.maxFontSizes) {
905
968
  push(
906
969
  "font-sizes",
@@ -909,10 +972,15 @@ function checkRestraint(input) {
909
972
  fontSizes
910
973
  );
911
974
  }
912
- const weights = distinctValues(css, "font-weight").filter((w) => w !== "normal" && w !== "bold");
913
- const namedWeights = distinctValues(css, "font-weight").filter(
914
- (w) => w === "normal" || w === "bold"
975
+ const weights = distinctValues(css, "font-weight", props, unresolved).filter(
976
+ (w) => w !== "normal" && w !== "bold"
915
977
  );
978
+ const namedWeights = distinctValues(
979
+ css,
980
+ "font-weight",
981
+ props,
982
+ unresolved
983
+ ).filter((w) => w === "normal" || w === "bold");
916
984
  const allWeights = [.../* @__PURE__ */ new Set([...weights, ...namedWeights])];
917
985
  if (allWeights.length > budget.maxFontWeights) {
918
986
  push(
@@ -922,7 +990,7 @@ function checkRestraint(input) {
922
990
  allWeights
923
991
  );
924
992
  }
925
- const families = distinctValues(css, "font-family").filter(
993
+ const families = distinctValues(css, "font-family", props, unresolved).filter(
926
994
  (f) => !/\bmonospace\b|\bmono\b/.test(f)
927
995
  );
928
996
  if (families.length > budget.maxFontFamilies) {
@@ -933,11 +1001,23 @@ function checkRestraint(input) {
933
1001
  families
934
1002
  );
935
1003
  }
936
- const radii = distinctValues(css, "border-radius").filter((r) => r !== "0" && r !== "0px");
1004
+ const radii = distinctValues(css, "border-radius", props, unresolved).filter(
1005
+ (r) => r !== "0" && r !== "0px"
1006
+ );
937
1007
  if (radii.length > budget.maxRadii) {
938
- push("radii", "error", `${radii.length} corner radii, budget ${budget.maxRadii}.`, radii);
1008
+ push(
1009
+ "radii",
1010
+ "error",
1011
+ `${radii.length} corner radii, budget ${budget.maxRadii}.`,
1012
+ radii
1013
+ );
939
1014
  }
940
- const shadows = distinctValues(css, "box-shadow").filter((s) => s !== "none");
1015
+ const shadows = [
1016
+ .../* @__PURE__ */ new Set([
1017
+ ...distinctValues(css, "box-shadow", props, unresolved),
1018
+ ...twShadowValues(css, props)
1019
+ ])
1020
+ ].filter((s) => s !== "none");
941
1021
  if (shadows.length > budget.maxShadows) {
942
1022
  push(
943
1023
  "shadows",
@@ -946,6 +1026,14 @@ function checkRestraint(input) {
946
1026
  shadows
947
1027
  );
948
1028
  }
1029
+ if (unresolved.size > 0) {
1030
+ push(
1031
+ "unresolved-token",
1032
+ "warning",
1033
+ `${unresolved.size} declaration(s) reference a custom property that is never defined and carry no fallback. Each one applies nothing.`,
1034
+ [...unresolved].sort()
1035
+ );
1036
+ }
949
1037
  const accentHexes = [
950
1038
  ...css.matchAll(/--[a-z0-9-]*accent[a-z0-9-]*\s*:\s*(#[0-9a-fA-F]{3,8})/g)
951
1039
  ].map((m) => m[1]);
@@ -997,7 +1085,9 @@ function checkRestraint(input) {
997
1085
  [...new Set(easeIn)]
998
1086
  );
999
1087
  }
1000
- const overBudget = durationsMs(css).filter((d) => d.ms > budget.maxUiDurationMs);
1088
+ const overBudget = durationsMs(css).filter(
1089
+ (d) => d.ms > budget.maxUiDurationMs
1090
+ );
1001
1091
  if (overBudget.length > 0) {
1002
1092
  push(
1003
1093
  "duration",
@@ -1014,7 +1104,10 @@ function checkRestraint(input) {
1014
1104
  ["scale(0)"]
1015
1105
  );
1016
1106
  }
1017
- const reducedMotionBlocks = atRuleBodies(css, /@media[^{]*prefers-reduced-motion:\s*reduce[^{]*\{/gi);
1107
+ const reducedMotionBlocks = atRuleBodies(
1108
+ css,
1109
+ /@media[^{]*prefers-reduced-motion:\s*reduce[^{]*\{/gi
1110
+ );
1018
1111
  if (reducedMotionBlocks.length === 0) {
1019
1112
  if (/@keyframes|animation\s*:|transition\s*:/.test(css)) {
1020
1113
  push(
package/dist/index.js CHANGED
@@ -780,10 +780,19 @@ function stripLayers(css, ignored) {
780
780
  }
781
781
  return out;
782
782
  }
783
- var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set(["inherit", "initial", "unset", "revert", "revert-layer"]);
783
+ var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set([
784
+ "inherit",
785
+ "initial",
786
+ "unset",
787
+ "revert",
788
+ "revert-layer"
789
+ ]);
784
790
  function atRuleBodies(css, prelude) {
785
791
  const out = [];
786
- const pattern = new RegExp(prelude.source, prelude.flags.includes("g") ? prelude.flags : `${prelude.flags}g`);
792
+ const pattern = new RegExp(
793
+ prelude.source,
794
+ prelude.flags.includes("g") ? prelude.flags : `${prelude.flags}g`
795
+ );
787
796
  let match;
788
797
  while ((match = pattern.exec(css)) !== null) {
789
798
  const start = match.index + match[0].length;
@@ -799,13 +808,64 @@ function atRuleBodies(css, prelude) {
799
808
  }
800
809
  return out;
801
810
  }
802
- function distinctValues(css, property) {
811
+ function customProperties(css) {
812
+ const out = /* @__PURE__ */ new Map();
813
+ for (const match of css.matchAll(/(--[\w-]+)\s*:\s*([^;}]+)/g)) {
814
+ out.set(match[1].toLowerCase(), match[2].trim().replace(/\s+/g, " "));
815
+ }
816
+ return out;
817
+ }
818
+ function unwrapShadowColor(value) {
819
+ return value.replace(
820
+ /var\(\s*--tw-shadow-color\s*,\s*([^)]*(?:\([^)]*\))?[^)]*)\)/g,
821
+ "$1"
822
+ );
823
+ }
824
+ var SINGLE_VAR = /^var\(\s*(--[\w-]+)\s*(?:,([\s\S]*))?\)$/;
825
+ function resolveValue(value, props, depth = 0) {
826
+ if (depth > 8) return null;
827
+ const match = SINGLE_VAR.exec(value.trim());
828
+ if (match === null) return value.trim();
829
+ const target = props.get(match[1].toLowerCase());
830
+ if (target !== void 0 && target !== "initial")
831
+ return resolveValue(target, props, depth + 1);
832
+ const fallback = match[2]?.trim();
833
+ return fallback === void 0 || fallback === "" ? null : resolveValue(fallback, props, depth + 1);
834
+ }
835
+ function isVarOnlyList(value) {
836
+ const parts = value.split(",");
837
+ return parts.length > 1 && parts.every((part) => /^\s*var\(\s*--[\w-]+\s*\)\s*$/.test(part));
838
+ }
839
+ function distinctValues(css, property, props, unresolved) {
803
840
  const pattern = new RegExp(`(?:^|[;{\\s])${property}\\s*:\\s*([^;}]+)`, "gi");
804
841
  const seen = /* @__PURE__ */ new Set();
805
842
  for (const match of css.matchAll(pattern)) {
806
- const value = match[1].trim().replace(/\s+/g, " ").toLowerCase();
807
- if (value.startsWith("var(") || CSS_WIDE_KEYWORDS.has(value)) continue;
808
- seen.add(value);
843
+ const raw = match[1].trim().replace(/\s+/g, " ").toLowerCase();
844
+ if (CSS_WIDE_KEYWORDS.has(raw)) continue;
845
+ const value = unwrapShadowColor(raw);
846
+ if (isVarOnlyList(value)) continue;
847
+ if (!SINGLE_VAR.test(value)) {
848
+ seen.add(value);
849
+ continue;
850
+ }
851
+ if (props === void 0) continue;
852
+ const resolved = resolveValue(value, props);
853
+ if (resolved === null) {
854
+ unresolved?.add(`${property}: ${value}`);
855
+ continue;
856
+ }
857
+ seen.add(unwrapShadowColor(resolved).toLowerCase());
858
+ }
859
+ return [...seen];
860
+ }
861
+ function twShadowValues(css, props) {
862
+ const seen = /* @__PURE__ */ new Set();
863
+ for (const match of css.matchAll(/--tw-shadow\s*:\s*([^;}]+)/g)) {
864
+ const raw = match[1].trim().replace(/\s+/g, " ").toLowerCase();
865
+ if (raw === "0 0 #0000" || raw === "none") continue;
866
+ const value = SINGLE_VAR.test(raw) ? resolveValue(raw, props) : raw;
867
+ if (value === null) continue;
868
+ seen.add(unwrapShadowColor(value).toLowerCase());
809
869
  }
810
870
  return [...seen];
811
871
  }
@@ -820,15 +880,18 @@ function durationsMs(css) {
820
880
  }
821
881
  function checkRestraint(input) {
822
882
  const budget = { ...HOUSE_BUDGET, ...input.budget };
883
+ const whole = stripComments(input.css);
884
+ const props = customProperties(whole);
823
885
  const css = stripLayers(
824
- stripComments(input.css),
886
+ whole,
825
887
  input.ignoreAtLayers ?? DEFAULT_IGNORED_LAYERS
826
888
  );
889
+ const unresolved = /* @__PURE__ */ new Set();
827
890
  const violations = [];
828
891
  const push = (rule, severity, message, found) => {
829
892
  violations.push({ rule, severity, message, found });
830
893
  };
831
- const fontSizes = distinctValues(css, "font-size");
894
+ const fontSizes = distinctValues(css, "font-size", props, unresolved);
832
895
  if (fontSizes.length > budget.maxFontSizes) {
833
896
  push(
834
897
  "font-sizes",
@@ -837,10 +900,15 @@ function checkRestraint(input) {
837
900
  fontSizes
838
901
  );
839
902
  }
840
- const weights = distinctValues(css, "font-weight").filter((w) => w !== "normal" && w !== "bold");
841
- const namedWeights = distinctValues(css, "font-weight").filter(
842
- (w) => w === "normal" || w === "bold"
903
+ const weights = distinctValues(css, "font-weight", props, unresolved).filter(
904
+ (w) => w !== "normal" && w !== "bold"
843
905
  );
906
+ const namedWeights = distinctValues(
907
+ css,
908
+ "font-weight",
909
+ props,
910
+ unresolved
911
+ ).filter((w) => w === "normal" || w === "bold");
844
912
  const allWeights = [.../* @__PURE__ */ new Set([...weights, ...namedWeights])];
845
913
  if (allWeights.length > budget.maxFontWeights) {
846
914
  push(
@@ -850,7 +918,7 @@ function checkRestraint(input) {
850
918
  allWeights
851
919
  );
852
920
  }
853
- const families = distinctValues(css, "font-family").filter(
921
+ const families = distinctValues(css, "font-family", props, unresolved).filter(
854
922
  (f) => !/\bmonospace\b|\bmono\b/.test(f)
855
923
  );
856
924
  if (families.length > budget.maxFontFamilies) {
@@ -861,11 +929,23 @@ function checkRestraint(input) {
861
929
  families
862
930
  );
863
931
  }
864
- const radii = distinctValues(css, "border-radius").filter((r) => r !== "0" && r !== "0px");
932
+ const radii = distinctValues(css, "border-radius", props, unresolved).filter(
933
+ (r) => r !== "0" && r !== "0px"
934
+ );
865
935
  if (radii.length > budget.maxRadii) {
866
- push("radii", "error", `${radii.length} corner radii, budget ${budget.maxRadii}.`, radii);
936
+ push(
937
+ "radii",
938
+ "error",
939
+ `${radii.length} corner radii, budget ${budget.maxRadii}.`,
940
+ radii
941
+ );
867
942
  }
868
- const shadows = distinctValues(css, "box-shadow").filter((s) => s !== "none");
943
+ const shadows = [
944
+ .../* @__PURE__ */ new Set([
945
+ ...distinctValues(css, "box-shadow", props, unresolved),
946
+ ...twShadowValues(css, props)
947
+ ])
948
+ ].filter((s) => s !== "none");
869
949
  if (shadows.length > budget.maxShadows) {
870
950
  push(
871
951
  "shadows",
@@ -874,6 +954,14 @@ function checkRestraint(input) {
874
954
  shadows
875
955
  );
876
956
  }
957
+ if (unresolved.size > 0) {
958
+ push(
959
+ "unresolved-token",
960
+ "warning",
961
+ `${unresolved.size} declaration(s) reference a custom property that is never defined and carry no fallback. Each one applies nothing.`,
962
+ [...unresolved].sort()
963
+ );
964
+ }
877
965
  const accentHexes = [
878
966
  ...css.matchAll(/--[a-z0-9-]*accent[a-z0-9-]*\s*:\s*(#[0-9a-fA-F]{3,8})/g)
879
967
  ].map((m) => m[1]);
@@ -925,7 +1013,9 @@ function checkRestraint(input) {
925
1013
  [...new Set(easeIn)]
926
1014
  );
927
1015
  }
928
- const overBudget = durationsMs(css).filter((d) => d.ms > budget.maxUiDurationMs);
1016
+ const overBudget = durationsMs(css).filter(
1017
+ (d) => d.ms > budget.maxUiDurationMs
1018
+ );
929
1019
  if (overBudget.length > 0) {
930
1020
  push(
931
1021
  "duration",
@@ -942,7 +1032,10 @@ function checkRestraint(input) {
942
1032
  ["scale(0)"]
943
1033
  );
944
1034
  }
945
- const reducedMotionBlocks = atRuleBodies(css, /@media[^{]*prefers-reduced-motion:\s*reduce[^{]*\{/gi);
1035
+ const reducedMotionBlocks = atRuleBodies(
1036
+ css,
1037
+ /@media[^{]*prefers-reduced-motion:\s*reduce[^{]*\{/gi
1038
+ );
946
1039
  if (reducedMotionBlocks.length === 0) {
947
1040
  if (/@keyframes|animation\s*:|transition\s*:/.test(css)) {
948
1041
  push(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@domandigital/craft",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "description": "The Doman Digital house craft standard as numbers: OKLCh colour ramps that snap to existing anchors, derived semantic tokens with measured WCAG and APCA contrast, one shared motion vocabulary for CSS and JS, and a base stylesheet. Zero dependencies.",
6
6
  "license": "Apache-2.0",