@domandigital/craft 0.1.0 → 0.3.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,66 @@
1
1
  # @domandigital/craft
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2075e76: `checkRestraint` measures the surface, not the framework underneath it.
8
+
9
+ Two defects, both found by running the checker against a real compiled
10
+ stylesheet — `apps/portal` in the Doman Digital monorepo — rather than against
11
+ its own fixtures.
12
+
13
+ **It rejected `EASE.inOut`, one of its own canonical curves.** The ease-in test
14
+ compared only the first control point (`y1 < x1`), which is true of every
15
+ ease-in-_out_ as well. So the standard failed its own checker on any surface
16
+ that adopted the token the standard recommends. Section 7's objection to ease-in
17
+ is about the end of the movement — a curve that accelerates into its final
18
+ position reads as the interface pulling away — so the test is now the end
19
+ tangent: rejected when the curve covers more ground per unit time arriving than
20
+ it averaged. `EASE.out`, `EASE.inOut`, `EASE.drawer`, `EASE.reveal`, Tailwind's
21
+ default and `linear` all pass; the `ease-in` keyword's curve, the portal's old
22
+ `--ease-exit`, and a back-loaded custom curve are all still rejected.
23
+
24
+ **It counted the framework's reset and theme as house vocabulary.** Tailwind v4
25
+ alone contributes three font sizes nobody chose (`small` at 80%, `sub`/`sup` at
26
+ 75%, `code` at 1em) and two timing functions, which was enough to report a
27
+ surface as over its font-size budget for values its author never wrote.
28
+ `@layer theme` and `@layer base` are now excluded by default via a new
29
+ `ignoreAtLayers` option; pass `[]` to count everything, as before.
30
+
31
+ **Its reduced-motion check read the rest of the file, not the block.** It sliced
32
+ from the first `@media (prefers-reduced-motion: reduce)` to the end of the sheet,
33
+ so every rule after that point counted as being inside it. On a real sheet that
34
+ meant `[data-craft-lcp] { animation: none }` — this package's own section 7 rule,
35
+ correctly outside any media query — was reported as a section 8 violation. The
36
+ scan is now brace-matched to each block's own body, and reads every such block
37
+ rather than only the first.
38
+
39
+ **CSS-wide keywords are no longer counted as vocabulary.** `inherit` and
40
+ `initial` were already excluded; `unset`, `revert` and `revert-layer` were not,
41
+ and `revert` appears in compiled output. A declaration that defers to the cascade
42
+ names no value, so it is not a step of anything.
43
+
44
+ ## 0.2.0
45
+
46
+ ### Minor Changes
47
+
48
+ - 6462b3a: Use the `text-wrap-style` longhand, and stop applying orphan control to every
49
+ `li`.
50
+
51
+ `text-wrap` is shorthand for `text-wrap-mode` + `text-wrap-style`, so
52
+ `text-wrap: pretty` silently resets the mode to `wrap`. `white-space: nowrap` is
53
+ itself shorthand setting `text-wrap-mode: nowrap`, so the shorthand defeated it
54
+ along with any `text-overflow: ellipsis` truncation that relied on it.
55
+
56
+ Caught by a visual regression suite: `li { text-wrap: pretty }` expanded a
57
+ breadcrumb that truncates to one line into three lines and pushed the page 40px
58
+ taller. The longhand sets only the style, so an element that asked not to wrap
59
+ still does not.
60
+
61
+ `li` is now scoped to `.craft-prose li`. Breadcrumbs, navs, tab strips and menus
62
+ are all `li` and none of them are body copy.
63
+
3
64
  ## 0.1.0
4
65
 
5
66
  ### Minor Changes
package/STANDARD.md CHANGED
@@ -83,7 +83,10 @@ needs. Set once on a shell via `[data-density]`, inherited below.
83
83
  - **Transform and opacity only.** Everything else cannot be composited.
84
84
  - **Exits run at 0.75× their entrance.** Arriving content is asking to be read;
85
85
  leaving content lingering reads as lag.
86
- - **No ease-in for UI, ever.** It accelerates away from the user.
86
+ - **No ease-in for UI, ever.** It accelerates away from the user. The objection
87
+ is about the *end* of the movement, not the start, which is why ease-in-out is
88
+ fine: a curve is rejected when its end tangent is steeper than the diagonal,
89
+ meaning it covers more ground per unit time arriving than it averaged.
87
90
  - **UI durations ≤ 300ms.** Drawers and one gated reveal may exceed it.
88
91
  - **Don't animate what fires 100×/day**, and never animate a keyboard-triggered
89
92
  interaction — the user is moving faster than the animation.
@@ -96,7 +99,7 @@ needs. Set once on a shell via `[data-density]`, inherited below.
96
99
  `@media (prefers-reduced-motion: reduce)` sets `animation-duration` and
97
100
  `transition-duration` to **0.01ms** and `animation-iteration-count` to 1.
98
101
 
99
- **Never `animation: none`.** That drops the `forwards` fill along with the
102
+ **Never `animation: none` inside the block.** That drops the `forwards` fill along with the
100
103
  animation, so a forwards-filled entrance never applies its end state and the
101
104
  element keeps `opacity: 0` from its base rule — permanently invisible, for
102
105
  exactly the users who asked for less motion. This shipped.
@@ -114,7 +117,13 @@ Differentiation guards pass anything that differs from its siblings, however
114
117
  ugly. What they cannot see is accumulation — eighteen font sizes, six shadows,
115
118
  four competing accents, each added reasonably, together reading as noise.
116
119
 
117
- *Source: measured against the estate; the budget is the observed ceiling of the surfaces that read as designed.*
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.*
118
127
 
119
128
  ## 10. What is deliberately not a rule
120
129
 
package/css/craft.css CHANGED
@@ -21,6 +21,25 @@ html {
21
21
  text-rendering: optimizeLegibility;
22
22
  }
23
23
 
24
+ /*
25
+ * THE LONGHAND, NOT `text-wrap`. This matters.
26
+ *
27
+ * `text-wrap` is a shorthand for `text-wrap-mode` + `text-wrap-style`, so
28
+ * `text-wrap: pretty` silently resets the MODE to `wrap`. `white-space: nowrap`
29
+ * is itself shorthand for `white-space-collapse: collapse` + `text-wrap-mode:
30
+ * nowrap`, so any element the shorthand touches loses its nowrap — and with it
31
+ * any `text-overflow: ellipsis` truncation that depended on it.
32
+ *
33
+ * That is not hypothetical: `li { text-wrap: pretty }` broke a breadcrumb that
34
+ * truncates to one line, expanding it to three and pushing the page 40px taller.
35
+ * `text-wrap-style` sets only the style and leaves the mode alone, so an element
36
+ * that asked not to wrap still does not.
37
+ *
38
+ * Browsers without `text-wrap-style` simply get no orphan control, which is the
39
+ * correct degradation. A `text-wrap` shorthand fallback would reintroduce the
40
+ * bug, so there deliberately isn't one.
41
+ */
42
+
24
43
  /* A heading that wraps one word onto its own line looks like a mistake.
25
44
  `balance` evens the lines; it is capped at ~6 lines by the spec, which is why
26
45
  it is scoped to headings and not applied to body copy. */
@@ -31,16 +50,22 @@ h4,
31
50
  h5,
32
51
  h6,
33
52
  .craft-balance {
34
- text-wrap: balance;
53
+ text-wrap-style: balance;
35
54
  }
36
55
 
37
- /* `pretty` is the body-copy counterpart: it only prevents orphans, so it stays
38
- cheap on long text where `balance` would not. */
56
+ /*
57
+ * `pretty` is the body-copy counterpart: it only prevents orphans, so it stays
58
+ * cheap on long text where `balance` would not.
59
+ *
60
+ * `li` is deliberately scoped to prose. Breadcrumbs, navs, tab strips and menus
61
+ * are all `li` and none of them are body copy; orphan control there is at best
62
+ * meaningless and at worst fights a deliberate truncation.
63
+ */
39
64
  p,
40
- li,
41
65
  figcaption,
42
- blockquote {
43
- text-wrap: pretty;
66
+ blockquote,
67
+ .craft-prose li {
68
+ text-wrap-style: pretty;
44
69
  }
45
70
 
46
71
  .craft-prose {
package/dist/index.cjs CHANGED
@@ -819,15 +819,64 @@ var HOUSE_BUDGET = {
819
819
  accentHueClusterDeg: 15,
820
820
  maxUiDurationMs: 300
821
821
  };
822
+ var DEFAULT_IGNORED_LAYERS = ["theme", "base"];
822
823
  function stripComments(css) {
823
824
  return css.replace(/\/\*[\s\S]*?\*\//g, "");
824
825
  }
826
+ function stripLayers(css, ignored) {
827
+ if (ignored.length === 0) return css;
828
+ const names = new Set(ignored.map((n) => n.trim().toLowerCase()));
829
+ let out = "";
830
+ let i = 0;
831
+ const pattern = /@layer\s+([a-zA-Z0-9_-]+)\s*\{/gi;
832
+ while (i < css.length) {
833
+ pattern.lastIndex = i;
834
+ const match = pattern.exec(css);
835
+ if (match === null) {
836
+ out += css.slice(i);
837
+ break;
838
+ }
839
+ out += css.slice(i, match.index);
840
+ const bodyStart = match.index + match[0].length;
841
+ let depth = 1;
842
+ let j = bodyStart;
843
+ while (j < css.length && depth > 0) {
844
+ if (css[j] === "{") depth += 1;
845
+ else if (css[j] === "}") depth -= 1;
846
+ j += 1;
847
+ }
848
+ if (!names.has(match[1].toLowerCase())) {
849
+ out += stripLayers(css.slice(bodyStart, j - 1), ignored);
850
+ }
851
+ i = j;
852
+ }
853
+ return out;
854
+ }
855
+ var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set(["inherit", "initial", "unset", "revert", "revert-layer"]);
856
+ function atRuleBodies(css, prelude) {
857
+ const out = [];
858
+ const pattern = new RegExp(prelude.source, prelude.flags.includes("g") ? prelude.flags : `${prelude.flags}g`);
859
+ let match;
860
+ while ((match = pattern.exec(css)) !== null) {
861
+ const start = match.index + match[0].length;
862
+ let depth = 1;
863
+ let i = start;
864
+ while (i < css.length && depth > 0) {
865
+ if (css[i] === "{") depth += 1;
866
+ else if (css[i] === "}") depth -= 1;
867
+ i += 1;
868
+ }
869
+ out.push(css.slice(start, i - 1));
870
+ pattern.lastIndex = i;
871
+ }
872
+ return out;
873
+ }
825
874
  function distinctValues(css, property) {
826
875
  const pattern = new RegExp(`(?:^|[;{\\s])${property}\\s*:\\s*([^;}]+)`, "gi");
827
876
  const seen = /* @__PURE__ */ new Set();
828
877
  for (const match of css.matchAll(pattern)) {
829
878
  const value = match[1].trim().replace(/\s+/g, " ").toLowerCase();
830
- if (value.startsWith("var(") || value === "inherit" || value === "initial") continue;
879
+ if (value.startsWith("var(") || CSS_WIDE_KEYWORDS.has(value)) continue;
831
880
  seen.add(value);
832
881
  }
833
882
  return [...seen];
@@ -843,7 +892,10 @@ function durationsMs(css) {
843
892
  }
844
893
  function checkRestraint(input) {
845
894
  const budget = { ...HOUSE_BUDGET, ...input.budget };
846
- const css = stripComments(input.css);
895
+ const css = stripLayers(
896
+ stripComments(input.css),
897
+ input.ignoreAtLayers ?? DEFAULT_IGNORED_LAYERS
898
+ );
847
899
  const violations = [];
848
900
  const push = (rule, severity, message, found) => {
849
901
  violations.push({ rule, severity, message, found });
@@ -928,12 +980,14 @@ function checkRestraint(input) {
928
980
  }
929
981
  const easeIn = [
930
982
  ...css.matchAll(/(?:transition|animation)[^;}]*\b(ease-in)\b(?!-out)/gi),
931
- ...css.matchAll(/cubic-bezier\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,/gi)
983
+ ...css.matchAll(
984
+ /cubic-bezier\(\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*\)/gi
985
+ )
932
986
  ].map((m) => {
933
987
  if (m[1] === "ease-in") return "ease-in";
934
- const x1 = Number(m[1]);
935
- const y1 = Number(m[2]);
936
- return Number.isFinite(x1) && Number.isFinite(y1) && y1 < x1 ? m[0] : null;
988
+ const p = [m[1], m[2], m[3], m[4]].map(Number);
989
+ if (!p.every((n) => Number.isFinite(n))) return null;
990
+ return acceleratesIntoTheEnd(p[0], p[1], p[2], p[3]) ? m[0] : null;
937
991
  }).filter((v) => v !== null);
938
992
  if (easeIn.length > 0) {
939
993
  push(
@@ -960,8 +1014,8 @@ function checkRestraint(input) {
960
1014
  ["scale(0)"]
961
1015
  );
962
1016
  }
963
- const reducedMotionAt = css.search(/@media[^{]*prefers-reduced-motion:\s*reduce/i);
964
- if (reducedMotionAt === -1) {
1017
+ const reducedMotionBlocks = atRuleBodies(css, /@media[^{]*prefers-reduced-motion:\s*reduce[^{]*\{/gi);
1018
+ if (reducedMotionBlocks.length === 0) {
965
1019
  if (/@keyframes|animation\s*:|transition\s*:/.test(css)) {
966
1020
  push(
967
1021
  "reduced-motion-missing",
@@ -971,7 +1025,7 @@ function checkRestraint(input) {
971
1025
  );
972
1026
  }
973
1027
  } else {
974
- const block = css.slice(reducedMotionAt);
1028
+ const block = reducedMotionBlocks.join("\n");
975
1029
  if (/animation\s*:\s*none/i.test(block)) {
976
1030
  push(
977
1031
  "reduced-motion-animation-none",
@@ -1007,6 +1061,17 @@ function checkRestraint(input) {
1007
1061
  }
1008
1062
  };
1009
1063
  }
1064
+ function acceleratesIntoTheEnd(x1, y1, x2, y2) {
1065
+ const EPSILON = 1e-6;
1066
+ let dx = 1 - x2;
1067
+ let dy = 1 - y2;
1068
+ if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
1069
+ dx = 1 - x1;
1070
+ dy = 1 - y1;
1071
+ }
1072
+ if (Math.abs(dx) < EPSILON) return Math.abs(dy) > EPSILON;
1073
+ return dy / dx > 1 + EPSILON;
1074
+ }
1010
1075
  function hueSpread(hues) {
1011
1076
  const sorted = [...hues].sort((a, b) => a - b);
1012
1077
  let widestGap = 0;
package/dist/index.d.cts CHANGED
@@ -631,6 +631,24 @@ interface CheckRestraintInput {
631
631
  /** Concatenated markup or component source, for class-level rules. */
632
632
  markup?: string;
633
633
  budget?: Partial<RestraintBudget>;
634
+ /**
635
+ * Cascade layers to exclude before counting, by name.
636
+ *
637
+ * A restraint budget measures the vocabulary a surface *chose*. A compiled
638
+ * stylesheet also carries the framework's reset and theme, and counting those
639
+ * makes the report wrong in the one direction that matters: it reports a
640
+ * surface as over budget for values its author never wrote, so the real
641
+ * findings get triaged away with the noise. Measured on this estate, Tailwind
642
+ * v4 contributes three font sizes nobody chose (`small` at 80%, `sub`/`sup` at
643
+ * 75%) and two timing functions (`--default-transition-timing-function`,
644
+ * `--animate-pulse`) from `@layer theme` and `@layer base` alone.
645
+ *
646
+ * The default is those two names, which is the convention every layered
647
+ * framework follows and the one Tailwind enforces. A rule placed in the reset
648
+ * layer is a reset by the author's own declaration; put it outside the layer
649
+ * to have it counted as vocabulary. Pass `[]` to count everything.
650
+ */
651
+ ignoreAtLayers?: string[];
634
652
  }
635
653
  /**
636
654
  * Check a stylesheet against the restraint budget.
package/dist/index.d.ts CHANGED
@@ -631,6 +631,24 @@ interface CheckRestraintInput {
631
631
  /** Concatenated markup or component source, for class-level rules. */
632
632
  markup?: string;
633
633
  budget?: Partial<RestraintBudget>;
634
+ /**
635
+ * Cascade layers to exclude before counting, by name.
636
+ *
637
+ * A restraint budget measures the vocabulary a surface *chose*. A compiled
638
+ * stylesheet also carries the framework's reset and theme, and counting those
639
+ * makes the report wrong in the one direction that matters: it reports a
640
+ * surface as over budget for values its author never wrote, so the real
641
+ * findings get triaged away with the noise. Measured on this estate, Tailwind
642
+ * v4 contributes three font sizes nobody chose (`small` at 80%, `sub`/`sup` at
643
+ * 75%) and two timing functions (`--default-transition-timing-function`,
644
+ * `--animate-pulse`) from `@layer theme` and `@layer base` alone.
645
+ *
646
+ * The default is those two names, which is the convention every layered
647
+ * framework follows and the one Tailwind enforces. A rule placed in the reset
648
+ * layer is a reset by the author's own declaration; put it outside the layer
649
+ * to have it counted as vocabulary. Pass `[]` to count everything.
650
+ */
651
+ ignoreAtLayers?: string[];
634
652
  }
635
653
  /**
636
654
  * Check a stylesheet against the restraint budget.
package/dist/index.js CHANGED
@@ -747,15 +747,64 @@ var HOUSE_BUDGET = {
747
747
  accentHueClusterDeg: 15,
748
748
  maxUiDurationMs: 300
749
749
  };
750
+ var DEFAULT_IGNORED_LAYERS = ["theme", "base"];
750
751
  function stripComments(css) {
751
752
  return css.replace(/\/\*[\s\S]*?\*\//g, "");
752
753
  }
754
+ function stripLayers(css, ignored) {
755
+ if (ignored.length === 0) return css;
756
+ const names = new Set(ignored.map((n) => n.trim().toLowerCase()));
757
+ let out = "";
758
+ let i = 0;
759
+ const pattern = /@layer\s+([a-zA-Z0-9_-]+)\s*\{/gi;
760
+ while (i < css.length) {
761
+ pattern.lastIndex = i;
762
+ const match = pattern.exec(css);
763
+ if (match === null) {
764
+ out += css.slice(i);
765
+ break;
766
+ }
767
+ out += css.slice(i, match.index);
768
+ const bodyStart = match.index + match[0].length;
769
+ let depth = 1;
770
+ let j = bodyStart;
771
+ while (j < css.length && depth > 0) {
772
+ if (css[j] === "{") depth += 1;
773
+ else if (css[j] === "}") depth -= 1;
774
+ j += 1;
775
+ }
776
+ if (!names.has(match[1].toLowerCase())) {
777
+ out += stripLayers(css.slice(bodyStart, j - 1), ignored);
778
+ }
779
+ i = j;
780
+ }
781
+ return out;
782
+ }
783
+ var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set(["inherit", "initial", "unset", "revert", "revert-layer"]);
784
+ function atRuleBodies(css, prelude) {
785
+ const out = [];
786
+ const pattern = new RegExp(prelude.source, prelude.flags.includes("g") ? prelude.flags : `${prelude.flags}g`);
787
+ let match;
788
+ while ((match = pattern.exec(css)) !== null) {
789
+ const start = match.index + match[0].length;
790
+ let depth = 1;
791
+ let i = start;
792
+ while (i < css.length && depth > 0) {
793
+ if (css[i] === "{") depth += 1;
794
+ else if (css[i] === "}") depth -= 1;
795
+ i += 1;
796
+ }
797
+ out.push(css.slice(start, i - 1));
798
+ pattern.lastIndex = i;
799
+ }
800
+ return out;
801
+ }
753
802
  function distinctValues(css, property) {
754
803
  const pattern = new RegExp(`(?:^|[;{\\s])${property}\\s*:\\s*([^;}]+)`, "gi");
755
804
  const seen = /* @__PURE__ */ new Set();
756
805
  for (const match of css.matchAll(pattern)) {
757
806
  const value = match[1].trim().replace(/\s+/g, " ").toLowerCase();
758
- if (value.startsWith("var(") || value === "inherit" || value === "initial") continue;
807
+ if (value.startsWith("var(") || CSS_WIDE_KEYWORDS.has(value)) continue;
759
808
  seen.add(value);
760
809
  }
761
810
  return [...seen];
@@ -771,7 +820,10 @@ function durationsMs(css) {
771
820
  }
772
821
  function checkRestraint(input) {
773
822
  const budget = { ...HOUSE_BUDGET, ...input.budget };
774
- const css = stripComments(input.css);
823
+ const css = stripLayers(
824
+ stripComments(input.css),
825
+ input.ignoreAtLayers ?? DEFAULT_IGNORED_LAYERS
826
+ );
775
827
  const violations = [];
776
828
  const push = (rule, severity, message, found) => {
777
829
  violations.push({ rule, severity, message, found });
@@ -856,12 +908,14 @@ function checkRestraint(input) {
856
908
  }
857
909
  const easeIn = [
858
910
  ...css.matchAll(/(?:transition|animation)[^;}]*\b(ease-in)\b(?!-out)/gi),
859
- ...css.matchAll(/cubic-bezier\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,/gi)
911
+ ...css.matchAll(
912
+ /cubic-bezier\(\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*\)/gi
913
+ )
860
914
  ].map((m) => {
861
915
  if (m[1] === "ease-in") return "ease-in";
862
- const x1 = Number(m[1]);
863
- const y1 = Number(m[2]);
864
- return Number.isFinite(x1) && Number.isFinite(y1) && y1 < x1 ? m[0] : null;
916
+ const p = [m[1], m[2], m[3], m[4]].map(Number);
917
+ if (!p.every((n) => Number.isFinite(n))) return null;
918
+ return acceleratesIntoTheEnd(p[0], p[1], p[2], p[3]) ? m[0] : null;
865
919
  }).filter((v) => v !== null);
866
920
  if (easeIn.length > 0) {
867
921
  push(
@@ -888,8 +942,8 @@ function checkRestraint(input) {
888
942
  ["scale(0)"]
889
943
  );
890
944
  }
891
- const reducedMotionAt = css.search(/@media[^{]*prefers-reduced-motion:\s*reduce/i);
892
- if (reducedMotionAt === -1) {
945
+ const reducedMotionBlocks = atRuleBodies(css, /@media[^{]*prefers-reduced-motion:\s*reduce[^{]*\{/gi);
946
+ if (reducedMotionBlocks.length === 0) {
893
947
  if (/@keyframes|animation\s*:|transition\s*:/.test(css)) {
894
948
  push(
895
949
  "reduced-motion-missing",
@@ -899,7 +953,7 @@ function checkRestraint(input) {
899
953
  );
900
954
  }
901
955
  } else {
902
- const block = css.slice(reducedMotionAt);
956
+ const block = reducedMotionBlocks.join("\n");
903
957
  if (/animation\s*:\s*none/i.test(block)) {
904
958
  push(
905
959
  "reduced-motion-animation-none",
@@ -935,6 +989,17 @@ function checkRestraint(input) {
935
989
  }
936
990
  };
937
991
  }
992
+ function acceleratesIntoTheEnd(x1, y1, x2, y2) {
993
+ const EPSILON = 1e-6;
994
+ let dx = 1 - x2;
995
+ let dy = 1 - y2;
996
+ if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
997
+ dx = 1 - x1;
998
+ dy = 1 - y1;
999
+ }
1000
+ if (Math.abs(dx) < EPSILON) return Math.abs(dy) > EPSILON;
1001
+ return dy / dx > 1 + EPSILON;
1002
+ }
938
1003
  function hueSpread(hues) {
939
1004
  const sorted = [...hues].sort((a, b) => a - b);
940
1005
  let widestGap = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@domandigital/craft",
3
- "version": "0.1.0",
3
+ "version": "0.3.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",
@@ -30,12 +30,6 @@
30
30
  "css",
31
31
  "dist"
32
32
  ],
33
- "scripts": {
34
- "build": "tsup src/index.ts --format cjs,esm --dts --clean",
35
- "typecheck": "tsc --noEmit",
36
- "test": "vitest run",
37
- "prepublishOnly": "pnpm run build"
38
- },
39
33
  "devDependencies": {
40
34
  "tsup": "^8.3.5",
41
35
  "typescript": "^5.6.3",
@@ -44,7 +38,6 @@
44
38
  "engines": {
45
39
  "node": ">=20"
46
40
  },
47
- "packageManager": "pnpm@9.15.9",
48
41
  "repository": {
49
42
  "type": "git",
50
43
  "url": "git+https://github.com/Doman-Digital/dd-packages.git",
@@ -57,5 +50,10 @@
57
50
  "publishConfig": {
58
51
  "access": "public",
59
52
  "provenance": true
53
+ },
54
+ "scripts": {
55
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
56
+ "typecheck": "tsc --noEmit",
57
+ "test": "vitest run"
60
58
  }
61
- }
59
+ }