gbs-add-block 2.1.0 → 2.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.
Files changed (129) hide show
  1. package/.gbs/skills/gbs-components/SKILL.md +190 -134
  2. package/.gbs/skills/gbs-components/references/install.md +25 -3
  3. package/.gbs/skills/gbs-components/references/styling.md +246 -207
  4. package/CHANGELOG.md +74 -0
  5. package/README.md +154 -23
  6. package/index.cjs +212 -3
  7. package/package.json +41 -10
  8. package/schema/passport-v1.schema.json +204 -0
  9. package/source/beta-components/accordion/passport.json +259 -0
  10. package/source/beta-components/accordion/styles.css +207 -208
  11. package/source/beta-components/alert/passport.json +250 -0
  12. package/source/beta-components/alert/styles.css +154 -155
  13. package/source/beta-components/avatar/passport.json +294 -0
  14. package/source/beta-components/avatar/styles.css +225 -226
  15. package/source/beta-components/badge/passport.json +332 -0
  16. package/source/beta-components/badge/styles.css +203 -204
  17. package/source/beta-components/breadcrumb/passport.json +243 -0
  18. package/source/beta-components/breadcrumb/styles.css +138 -140
  19. package/source/beta-components/button/passport.json +402 -0
  20. package/source/beta-components/button/passport.manual.json +31 -0
  21. package/source/beta-components/button/styles.css +232 -234
  22. package/source/beta-components/card/passport.json +337 -0
  23. package/source/beta-components/card/styles.css +230 -232
  24. package/source/beta-components/checkbox/passport.json +456 -0
  25. package/source/beta-components/checkbox/styles.css +211 -213
  26. package/source/beta-components/combobox/passport.json +456 -0
  27. package/source/beta-components/combobox/styles.css +419 -417
  28. package/source/beta-components/data-grid/agent/coerce.ts +368 -0
  29. package/source/beta-components/data-grid/agent/contract.ts +410 -0
  30. package/source/beta-components/data-grid/agent/dataset.ts +92 -0
  31. package/source/beta-components/data-grid/agent/engine.ts +470 -0
  32. package/source/beta-components/data-grid/agent/executors.ts +155 -0
  33. package/source/beta-components/data-grid/agent/index.ts +79 -0
  34. package/source/beta-components/data-grid/agent/intent.ts +324 -0
  35. package/source/beta-components/data-grid/agent/operations.ts +335 -0
  36. package/source/beta-components/data-grid/agent/validate.ts +630 -0
  37. package/source/beta-components/data-grid/agent/webmcp.ts +107 -0
  38. package/source/beta-components/data-grid/index.ts +14 -7
  39. package/source/beta-components/data-grid/passport.json +1051 -0
  40. package/source/beta-components/data-grid/passport.manual.json +255 -0
  41. package/source/beta-components/data-grid/react/AskGrid.tsx +164 -0
  42. package/source/beta-components/data-grid/react/DataGrid.tsx +39 -0
  43. package/source/beta-components/data-grid/styles.css +874 -716
  44. package/source/beta-components/date-picker/passport.json +407 -0
  45. package/source/beta-components/date-picker/styles.css +445 -446
  46. package/source/beta-components/dialog/passport.json +344 -0
  47. package/source/beta-components/dialog/styles.css +280 -279
  48. package/source/beta-components/file-uploader/passport.json +518 -0
  49. package/source/beta-components/file-uploader/styles.css +394 -396
  50. package/source/beta-components/input/passport.json +536 -0
  51. package/source/beta-components/input/styles.css +295 -297
  52. package/source/beta-components/menu/passport.json +322 -0
  53. package/source/beta-components/menu/styles.css +224 -223
  54. package/source/beta-components/modal/passport.json +289 -0
  55. package/source/beta-components/modal/styles.css +241 -240
  56. package/source/beta-components/number-input/passport.json +541 -0
  57. package/source/beta-components/number-input/styles.css +230 -231
  58. package/source/beta-components/popover/passport.json +238 -0
  59. package/source/beta-components/popover/styles.css +148 -147
  60. package/source/beta-components/progress/passport.json +270 -0
  61. package/source/beta-components/progress/styles.css +200 -201
  62. package/source/beta-components/radio-group/passport.json +477 -0
  63. package/source/beta-components/radio-group/styles.css +269 -270
  64. package/source/beta-components/shared/core/agent/adapter.ts +65 -0
  65. package/source/beta-components/shared/core/agent/history.ts +120 -0
  66. package/source/beta-components/shared/core/agent/index.ts +46 -0
  67. package/source/beta-components/shared/core/agent/numbers.ts +217 -0
  68. package/source/beta-components/shared/core/agent/schema.ts +180 -0
  69. package/source/beta-components/shared/core/agent/types.ts +169 -0
  70. package/source/beta-components/shared/core/agent/webmcp.ts +328 -0
  71. package/source/beta-components/shared/index.ts +9 -0
  72. package/source/beta-components/shared/react/GramproAIProvider.tsx +50 -0
  73. package/source/beta-components/shared/react/useAskAgent.ts +217 -0
  74. package/source/beta-components/shared/styles.css +79 -0
  75. package/source/beta-components/shared/version.json +4 -4
  76. package/source/beta-components/shared/version.ts +6 -6
  77. package/source/beta-components/skeleton/passport.json +251 -0
  78. package/source/beta-components/skeleton/styles.css +185 -187
  79. package/source/beta-components/spinner/passport.json +245 -0
  80. package/source/beta-components/spinner/styles.css +173 -174
  81. package/source/beta-components/switch/passport.json +421 -0
  82. package/source/beta-components/switch/styles.css +227 -229
  83. package/source/beta-components/tabs/passport.json +315 -0
  84. package/source/beta-components/tabs/styles.css +263 -264
  85. package/source/beta-components/textarea/passport.json +382 -0
  86. package/source/beta-components/textarea/styles.css +158 -160
  87. package/source/beta-components/toaster/passport.json +221 -0
  88. package/source/beta-components/toaster/styles.css +282 -282
  89. package/source/beta-components/tooltip/passport.json +170 -0
  90. package/source/beta-components/tooltip/styles.css +71 -73
  91. package/tools/env.cjs +61 -0
  92. package/tools/passport/cli.cjs +79 -0
  93. package/tools/passport/extract.cjs +493 -0
  94. package/tools/passport/index.cjs +185 -0
  95. package/tools/passport/merge.cjs +131 -0
  96. package/tools/passport/policy.cjs +65 -0
  97. package/tools/passport/validate.cjs +277 -0
  98. package/tools/ts-require.cjs +79 -0
  99. package/source/beta-components/accordion/__tests__/core.test.ts +0 -58
  100. package/source/beta-components/alert/__tests__/core.test.ts +0 -17
  101. package/source/beta-components/avatar/__tests__/core.test.ts +0 -88
  102. package/source/beta-components/badge/__tests__/core.test.ts +0 -46
  103. package/source/beta-components/breadcrumb/__tests__/core.test.ts +0 -58
  104. package/source/beta-components/button/__tests__/core.test.ts +0 -31
  105. package/source/beta-components/card/__tests__/core.test.ts +0 -57
  106. package/source/beta-components/checkbox/__tests__/core.test.ts +0 -40
  107. package/source/beta-components/combobox/__tests__/core.test.ts +0 -134
  108. package/source/beta-components/data-grid/__tests__/core.test.ts +0 -356
  109. package/source/beta-components/data-grid/__tests__/export.test.ts +0 -70
  110. package/source/beta-components/data-grid/__tests__/pdf.test.ts +0 -209
  111. package/source/beta-components/date-picker/__tests__/core.test.ts +0 -273
  112. package/source/beta-components/dialog/__tests__/core.test.ts +0 -86
  113. package/source/beta-components/file-uploader/__tests__/core.test.ts +0 -395
  114. package/source/beta-components/input/__tests__/core.test.ts +0 -75
  115. package/source/beta-components/menu/__tests__/core.test.ts +0 -120
  116. package/source/beta-components/modal/__tests__/core.test.ts +0 -55
  117. package/source/beta-components/number-input/__tests__/core.test.ts +0 -151
  118. package/source/beta-components/progress/__tests__/core.test.ts +0 -56
  119. package/source/beta-components/radio-group/__tests__/core.test.ts +0 -64
  120. package/source/beta-components/shared/__tests__/boundaries.test.ts +0 -95
  121. package/source/beta-components/shared/__tests__/core.test.ts +0 -55
  122. package/source/beta-components/shared/__tests__/position.test.ts +0 -143
  123. package/source/beta-components/skeleton/__tests__/core.test.ts +0 -41
  124. package/source/beta-components/spinner/__tests__/core.test.ts +0 -48
  125. package/source/beta-components/switch/__tests__/core.test.ts +0 -64
  126. package/source/beta-components/tabs/__tests__/core.test.ts +0 -51
  127. package/source/beta-components/textarea/__tests__/core.test.ts +0 -38
  128. package/source/beta-components/toaster/__tests__/core.test.ts +0 -256
  129. package/source/beta-components/tooltip/__tests__/core.test.ts +0 -42
@@ -1,151 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { formatNumber } from "../core/format";
3
- import { localeParts, parseNumber, toEditText } from "../core/parse";
4
- import { clamp, decimalsOf, round, snapToStep, stepBy } from "../core/step";
5
-
6
- describe("localeParts", () => {
7
- it("reads each locale's notation out of Intl", () => {
8
- expect(localeParts("en-US")).toMatchObject({ group: ",", decimal: "." });
9
- expect(localeParts("de-DE")).toMatchObject({ group: ".", decimal: "," });
10
- expect(localeParts("fr-FR")).toMatchObject({ decimal: "," });
11
- expect(localeParts("ar-EG").digits[0]).toBe("٠");
12
- });
13
- });
14
-
15
- describe("parseNumber", () => {
16
- it("reads the locale's own notation", () => {
17
- expect(parseNumber("1,234.56", "en-US")).toBe(1234.56);
18
- expect(parseNumber("1.234,56", "de-DE")).toBe(1234.56);
19
- // The separator here is a narrow no-break space, not one anybody types.
20
- expect(parseNumber("1 234,56", "fr-FR")).toBe(1234.56);
21
- });
22
-
23
- it("reads the locale's digits", () => {
24
- expect(parseNumber("١٢٣", "ar-EG")).toBe(123);
25
- });
26
-
27
- it("ignores currency symbols, percent signs and spare spaces", () => {
28
- expect(parseNumber("$1,234.56", "en-US")).toBe(1234.56);
29
- expect(parseNumber("45 %", "en-US")).toBe(45);
30
- expect(parseNumber(" 12 ", "en-US")).toBe(12);
31
- });
32
-
33
- it("reads negatives, including the typographic minus", () => {
34
- expect(parseNumber("-42", "en-US")).toBe(-42);
35
- expect(parseNumber("−42", "en-US")).toBe(-42);
36
- expect(parseNumber("-1.234,5", "de-DE")).toBe(-1234.5);
37
- });
38
-
39
- it("refuses what is not a number, and keeps that apart from empty", () => {
40
- expect(parseNumber("", "en-US")).toBeNull();
41
- expect(parseNumber("abc", "en-US")).toBeNull();
42
- expect(parseNumber("1.2.3", "en-US")).toBeNull();
43
- expect(parseNumber(".", "en-US")).toBeNull();
44
- expect(parseNumber("-", "en-US")).toBeNull();
45
- });
46
-
47
- it("does not mistake one locale's group separator for another's decimal", () => {
48
- // The whole point: the same five characters, two different numbers.
49
- expect(parseNumber("1.234", "en-US")).toBe(1.234);
50
- expect(parseNumber("1.234", "de-DE")).toBe(1234);
51
- });
52
- });
53
-
54
- describe("toEditText", () => {
55
- it("gives the plain form, in the locale's decimal separator", () => {
56
- expect(toEditText(1234.5, "en-US")).toBe("1234.5");
57
- expect(toEditText(1234.5, "de-DE")).toBe("1234,5");
58
- expect(toEditText(null, "en-US")).toBe("");
59
- });
60
-
61
- it("round-trips through parseNumber", () => {
62
- for (const locale of ["en-US", "de-DE", "fr-FR"]) {
63
- expect(parseNumber(toEditText(-1234.56, locale), locale)).toBe(-1234.56);
64
- }
65
- });
66
- });
67
-
68
- describe("formatNumber", () => {
69
- it("groups, and respects a fixed number of decimals", () => {
70
- expect(formatNumber(1234.5, { locale: "en-US", decimals: 2 })).toBe("1,234.50");
71
- expect(formatNumber(1234.5, { locale: "en-US", useGrouping: false })).toBe("1234.5");
72
- });
73
-
74
- it("formats money and percentages", () => {
75
- expect(formatNumber(1234.5, { locale: "en-US", style: "currency", currency: "USD" })).toBe("$1,234.50");
76
- expect(formatNumber(0.45, { locale: "en-US", style: "percent" })).toBe("45%");
77
- });
78
-
79
- it("falls back to a plain number when a currency is asked for without a code", () => {
80
- // Intl throws on style:"currency" with no currency; a field mid-configuration
81
- // should not take the page down with it.
82
- expect(formatNumber(12, { locale: "en-US", style: "currency" })).toBe("12");
83
- });
84
-
85
- it("has nothing to show for no value", () => {
86
- expect(formatNumber(null, { locale: "en-US" })).toBe("");
87
- });
88
- });
89
-
90
- describe("decimalsOf and round", () => {
91
- it("counts fraction digits, including exponent notation", () => {
92
- expect(decimalsOf(1)).toBe(0);
93
- expect(decimalsOf(0.25)).toBe(2);
94
- expect(decimalsOf(1e-7)).toBe(7);
95
- });
96
-
97
- it("rounds the way the number reads, not the way the float sits", () => {
98
- expect(round(1.005, 2)).toBe(1.01);
99
- expect(round(2.675, 2)).toBe(2.68);
100
- expect(round(-1.005, 2)).toBe(-1.01);
101
- });
102
- });
103
-
104
- describe("stepBy", () => {
105
- it("adds a step without floating-point drift", () => {
106
- expect(stepBy(0.1, 1, { step: 0.2 })).toBe(0.3);
107
- expect(stepBy(0.3, -1, { step: 0.1 })).toBe(0.2);
108
- expect(stepBy(1.1, 1, { step: 0.1 })).toBe(1.2);
109
- });
110
-
111
- it("starts an empty field at min, or one step from zero", () => {
112
- expect(stepBy(null, 1, { step: 1 })).toBe(1);
113
- expect(stepBy(null, -1, { step: 1 })).toBe(-1);
114
- expect(stepBy(null, 1, { step: 1, min: 5 })).toBe(5);
115
- expect(stepBy(null, -1, { step: 1, min: 5 })).toBe(5);
116
- });
117
-
118
- it("stops at the limits", () => {
119
- expect(stepBy(9, 1, { step: 5, max: 10 })).toBe(10);
120
- expect(stepBy(1, -1, { step: 5, min: 0 })).toBe(0);
121
- });
122
-
123
- it("treats a step of zero as one, rather than freezing", () => {
124
- expect(stepBy(3, 1, { step: 0 })).toBe(4);
125
- });
126
- });
127
-
128
- describe("snapToStep", () => {
129
- it("counts the grid from min", () => {
130
- expect(snapToStep(7, { step: 5, min: 0 })).toBe(5);
131
- expect(snapToStep(8, { step: 5, min: 0 })).toBe(10);
132
- expect(snapToStep(7, { step: 5, min: 1 })).toBe(6);
133
- });
134
-
135
- it("works in fractions", () => {
136
- expect(snapToStep(0.27, { step: 0.05, min: 0 })).toBe(0.25);
137
- });
138
-
139
- it("stays inside the limits", () => {
140
- expect(snapToStep(99, { step: 10, min: 0, max: 95 })).toBe(95);
141
- });
142
- });
143
-
144
- describe("clamp", () => {
145
- it("pulls a value inside whichever limits exist", () => {
146
- expect(clamp(5, 0, 10)).toBe(5);
147
- expect(clamp(-1, 0, 10)).toBe(0);
148
- expect(clamp(11, 0, 10)).toBe(10);
149
- expect(clamp(11, undefined, undefined)).toBe(11);
150
- });
151
- });
@@ -1,56 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { describeProgress, stepFraction } from "../core/progress";
3
-
4
- describe("describeProgress", () => {
5
- it("measures a value against its max", () => {
6
- expect(describeProgress(25)).toMatchObject({ fraction: 0.25, percent: 25, value: 25 });
7
- expect(describeProgress(3, 8)).toMatchObject({ fraction: 0.375, percent: 38 });
8
- });
9
-
10
- it("treats no value as unknown, not as zero", () => {
11
- // The difference decides whether a screen reader says "busy" or "0 percent".
12
- for (const value of [null, undefined, Number.NaN]) {
13
- expect(describeProgress(value)).toEqual({
14
- indeterminate: true,
15
- value: null,
16
- fraction: 0,
17
- percent: 0,
18
- });
19
- }
20
- });
21
-
22
- it("clamps into range rather than drawing past the end", () => {
23
- expect(describeProgress(140).percent).toBe(100);
24
- expect(describeProgress(-20).percent).toBe(0);
25
- });
26
-
27
- it("survives a task with nothing in it", () => {
28
- // 0 of 0 files: without the guard this is NaN% and an empty bar drawn at
29
- // Infinity.
30
- const state = describeProgress(0, 0);
31
- expect(state.fraction).toBe(0);
32
- expect(Number.isNaN(state.percent)).toBe(false);
33
- });
34
-
35
- it("keeps the drawn fraction unrounded", () => {
36
- // 99.6% must not snap the bar to full while bytes are still moving.
37
- const state = describeProgress(99.6);
38
- expect(state.percent).toBe(100);
39
- expect(state.fraction).toBeCloseTo(0.996);
40
- });
41
- });
42
-
43
- describe("stepFraction", () => {
44
- it("counts a step as done, not as started", () => {
45
- // "Step 2 of 5" is two fifths finished.
46
- expect(stepFraction(2, 5)).toBe(0.4);
47
- expect(stepFraction(0, 5)).toBe(0);
48
- expect(stepFraction(5, 5)).toBe(1);
49
- });
50
-
51
- it("stays inside the wizard", () => {
52
- expect(stepFraction(9, 5)).toBe(1);
53
- expect(stepFraction(-1, 5)).toBe(0);
54
- expect(stepFraction(1, 0)).toBe(0);
55
- });
56
- });
@@ -1,64 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { hasEnabledOption, normalizeOptions, resolveValue } from "../core/group";
3
-
4
- describe("normalizeOptions", () => {
5
- it("turns plain strings into options labelled with themselves", () => {
6
- expect(normalizeOptions(["daily", "weekly"])).toEqual([
7
- { value: "daily", label: "daily" },
8
- { value: "weekly", label: "weekly" },
9
- ]);
10
- });
11
-
12
- it("leaves full options as they are, mixed in with strings", () => {
13
- const options = normalizeOptions([
14
- "daily",
15
- { value: "weekly", label: "Every week", description: "On Mondays", disabled: true },
16
- ]);
17
- expect(options[1]).toEqual({
18
- value: "weekly",
19
- label: "Every week",
20
- description: "On Mondays",
21
- disabled: true,
22
- });
23
- });
24
- });
25
-
26
- describe("resolveValue", () => {
27
- const options = normalizeOptions([
28
- "email",
29
- { value: "sms", label: "SMS", disabled: true },
30
- ]);
31
-
32
- it("selects the matching option", () => {
33
- expect(resolveValue(options, "email")).toBe("email");
34
- });
35
-
36
- it("selects a disabled option, because a locked-in choice still has to show", () => {
37
- expect(resolveValue(options, "sms")).toBe("sms");
38
- });
39
-
40
- it("selects nothing for a value the group does not offer", () => {
41
- // A stale value from a record: picking some other option instead would
42
- // claim a choice that was never made.
43
- expect(resolveValue(options, "pigeon")).toBeNull();
44
- });
45
-
46
- it("selects nothing for no value at all", () => {
47
- expect(resolveValue(options, null)).toBeNull();
48
- expect(resolveValue(options, undefined)).toBeNull();
49
- });
50
- });
51
-
52
- describe("hasEnabledOption", () => {
53
- const options = normalizeOptions(["email", { value: "sms", label: "SMS", disabled: true }]);
54
-
55
- it("is true while something can still be chosen", () => {
56
- expect(hasEnabledOption(options, false)).toBe(true);
57
- });
58
-
59
- it("is false when the group is disabled or has nothing to offer", () => {
60
- expect(hasEnabledOption(options, true)).toBe(false);
61
- expect(hasEnabledOption(normalizeOptions([{ value: "sms", label: "SMS", disabled: true }]), false)).toBe(false);
62
- expect(hasEnabledOption([], false)).toBe(false);
63
- });
64
- });
@@ -1,95 +0,0 @@
1
- import { readdirSync, readFileSync, statSync } from "node:fs";
2
- import { join, relative, resolve, sep } from "node:path";
3
- import { describe, expect, it } from "vitest";
4
-
5
- /*
6
- * The rules that keep the library installable.
7
- *
8
- * Components are copied into a project one folder at a time, so a component
9
- * that reaches into a sibling folder breaks as soon as someone installs it on
10
- * its own. `shared/` is the single exception: the installer copies it with
11
- * every install. These tests fail the moment a new import would break that.
12
- */
13
-
14
- const LIB = resolve(import.meta.dirname, "..", "..");
15
- const SHARED = "shared";
16
-
17
- /** Bare specifiers a component may import. The library ships no dependencies. */
18
- const ALLOWED_PACKAGES = new Set(["react", "react-dom", "react-dom/client", "vitest"]);
19
-
20
- function packages(): string[] {
21
- return readdirSync(LIB)
22
- .filter((name) => statSync(join(LIB, name)).isDirectory())
23
- .sort();
24
- }
25
-
26
- function sourceFiles(dir: string): string[] {
27
- return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
28
- const path = join(dir, entry.name);
29
- if (entry.isDirectory()) return sourceFiles(path);
30
- return /\.tsx?$/.test(entry.name) ? [path] : [];
31
- });
32
- }
33
-
34
- /** Every `from "…"` specifier in a file, with `import()` and `export … from` included. */
35
- function specifiers(file: string): string[] {
36
- return [...readFileSync(file, "utf8").matchAll(/from\s+"([^"]+)"|import\("([^"]+)"\)/g)].map(
37
- (match) => match[1] ?? match[2],
38
- );
39
- }
40
-
41
- /** Which package a relative import lands in, or null when it leaves the library. */
42
- function targetPackage(file: string, specifier: string): string | null {
43
- const target = resolve(file, "..", specifier);
44
- const rel = relative(LIB, target);
45
- if (rel.startsWith("..")) return null;
46
- return rel.split(sep)[0];
47
- }
48
-
49
- describe.each(packages().filter((name) => name !== SHARED))("%s", (pkg) => {
50
- const files = sourceFiles(join(LIB, pkg));
51
-
52
- it("imports nothing from another component", () => {
53
- const offenders = files.flatMap((file) =>
54
- specifiers(file)
55
- .filter((specifier) => specifier.startsWith("."))
56
- .map((specifier) => ({ file: relative(LIB, file), specifier, to: targetPackage(file, specifier) }))
57
- .filter((entry) => entry.to !== pkg && entry.to !== SHARED),
58
- );
59
- expect(offenders).toEqual([]);
60
- });
61
-
62
- it("imports no third-party package", () => {
63
- const offenders = files.flatMap((file) =>
64
- specifiers(file)
65
- .filter((specifier) => !specifier.startsWith(".") && !ALLOWED_PACKAGES.has(specifier))
66
- .map((specifier) => `${relative(LIB, file)} -> ${specifier}`),
67
- );
68
- expect(offenders).toEqual([]);
69
- });
70
- });
71
-
72
- describe("shared", () => {
73
- it("never imports a component, so it can be installed on its own", () => {
74
- const offenders = sourceFiles(join(LIB, SHARED)).flatMap((file) =>
75
- specifiers(file)
76
- .filter((specifier) => specifier.startsWith("."))
77
- .map((specifier) => ({ file: relative(LIB, file), to: targetPackage(file, specifier) }))
78
- .filter((entry) => entry.to !== SHARED),
79
- );
80
- expect(offenders).toEqual([]);
81
- });
82
-
83
- it("is reached only as ../../shared, the path the installer guarantees", () => {
84
- const offenders = packages()
85
- .filter((name) => name !== SHARED)
86
- .flatMap((pkg) => sourceFiles(join(LIB, pkg)))
87
- .flatMap((file) =>
88
- specifiers(file)
89
- .filter((specifier) => specifier.includes(SHARED))
90
- .filter((specifier) => !specifier.startsWith("../../shared/"))
91
- .map((specifier) => `${relative(LIB, file)} -> ${specifier}`),
92
- );
93
- expect(offenders).toEqual([]);
94
- });
95
- });
@@ -1,55 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { cx } from "../core/cx";
3
- import { describeField } from "../core/field";
4
- import { countCharacters } from "../core/text";
5
- import { version } from "../version";
6
- import manifest from "../version.json";
7
-
8
- describe("cx", () => {
9
- it("joins the truthy names", () => {
10
- expect(cx("a", "b")).toBe("a b");
11
- });
12
-
13
- it("drops undefined, null and false, so an unset className adds nothing", () => {
14
- expect(cx("a", undefined, null, false, "b")).toBe("a b");
15
- expect(cx(undefined, false)).toBe("");
16
- });
17
- });
18
-
19
- describe("countCharacters", () => {
20
- it("counts what a person would call a character", () => {
21
- expect(countCharacters("abc")).toBe(3);
22
- // Four UTF-16 units, one character.
23
- expect(countCharacters("👍🏽")).toBe(1);
24
- // A base letter plus a combining accent.
25
- expect(countCharacters("e\u0301")).toBe(1);
26
- });
27
-
28
- it("is 0 for an empty string", () => {
29
- expect(countCharacters("")).toBe(0);
30
- });
31
- });
32
-
33
- describe("describeField", () => {
34
- it("is undefined when nothing describes the control", () => {
35
- expect(describeField("f", {})).toBeUndefined();
36
- expect(describeField("f", { description: "", error: null })).toBeUndefined();
37
- });
38
-
39
- it("reads the caller's own ids first and the error last", () => {
40
- expect(
41
- describeField("f", { extra: "outside", hint: "h", description: "d", error: "e" }),
42
- ).toBe("outside f-hint f-description f-error");
43
- });
44
-
45
- it("includes only the parts that are rendered", () => {
46
- expect(describeField("f", { error: "Required" })).toBe("f-error");
47
- expect(describeField("f", { description: "Help" })).toBe("f-description");
48
- });
49
- });
50
-
51
- describe("version", () => {
52
- it("matches the manifest the installer reads", () => {
53
- expect(manifest.version).toBe(version);
54
- });
55
- });
@@ -1,143 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { placePopover, type AnchorRect, type PlacementOptions } from "../core/position";
3
-
4
- const VIEWPORT = { width: 1000, height: 800 };
5
-
6
- const anchorAt = (left: number, top: number, width = 120, height = 32): AnchorRect => ({
7
- left,
8
- top,
9
- width,
10
- height,
11
- right: left + width,
12
- bottom: top + height,
13
- });
14
-
15
- const options = (overrides: Partial<PlacementOptions> = {}): PlacementOptions => ({
16
- side: "bottom",
17
- align: "start",
18
- gap: 4,
19
- edge: 8,
20
- rtl: false,
21
- fitHeight: false,
22
- minHeight: 140,
23
- ...overrides,
24
- });
25
-
26
- describe("below the anchor", () => {
27
- it("sits under the anchor, aligned to its left edge", () => {
28
- const placement = placePopover(anchorAt(100, 100), { width: 200, height: 150 }, VIEWPORT, options());
29
- expect(placement).toMatchObject({ side: "bottom", left: 100, top: 136 });
30
- });
31
-
32
- it("aligns to the right edge when asked", () => {
33
- const placement = placePopover(
34
- anchorAt(100, 100),
35
- { width: 200, height: 150 },
36
- VIEWPORT,
37
- options({ align: "end" }),
38
- );
39
- // The anchor's right edge is 220, so a 200-wide panel starts at 20.
40
- expect(placement.left).toBe(20);
41
- });
42
-
43
- it("moves above when there is no room below and it fits above", () => {
44
- const placement = placePopover(anchorAt(100, 700), { width: 200, height: 150 }, VIEWPORT, options());
45
- expect(placement).toMatchObject({ side: "top", top: 546 });
46
- });
47
-
48
- it("stays below when it fits neither side, rather than running off the top", () => {
49
- const placement = placePopover(anchorAt(100, 300), { width: 200, height: 780 }, VIEWPORT, options());
50
- expect(placement.side).toBe("bottom");
51
- });
52
- });
53
-
54
- describe("fitHeight", () => {
55
- it("takes the roomier side and caps the height to it", () => {
56
- // 700px from the top: 68px below, 688px above.
57
- const placement = placePopover(
58
- anchorAt(100, 700),
59
- { width: 200, height: 600 },
60
- VIEWPORT,
61
- options({ fitHeight: true }),
62
- );
63
- expect(placement.side).toBe("top");
64
- expect(placement.maxHeight).toBe(688);
65
- });
66
-
67
- it("never caps below minHeight, however cramped the viewport", () => {
68
- const placement = placePopover(
69
- anchorAt(100, 380),
70
- { width: 200, height: 300 },
71
- { width: 1000, height: 420 },
72
- options({ fitHeight: true, minHeight: 140 }),
73
- );
74
- expect(placement.maxHeight).toBeGreaterThanOrEqual(140);
75
- });
76
- });
77
-
78
- describe("staying on screen", () => {
79
- it("pulls a panel back from the right edge", () => {
80
- const placement = placePopover(anchorAt(950, 100), { width: 300, height: 100 }, VIEWPORT, options());
81
- expect(placement.left).toBe(1000 - 300 - 8);
82
- });
83
-
84
- it("keeps the left gutter when the panel is wider than the viewport", () => {
85
- const placement = placePopover(anchorAt(10, 100), { width: 1200, height: 100 }, VIEWPORT, options());
86
- expect(placement.left).toBe(8);
87
- });
88
- });
89
-
90
- describe("sideways", () => {
91
- it("opens to the right of the anchor, top edges level", () => {
92
- const placement = placePopover(
93
- anchorAt(100, 100),
94
- { width: 200, height: 150 },
95
- VIEWPORT,
96
- options({ side: "right" }),
97
- );
98
- expect(placement).toMatchObject({ side: "right", left: 224, top: 100 });
99
- });
100
-
101
- it("flips to the left when the right has no room", () => {
102
- const placement = placePopover(
103
- anchorAt(850, 100),
104
- { width: 200, height: 150 },
105
- VIEWPORT,
106
- options({ side: "right" }),
107
- );
108
- expect(placement).toMatchObject({ side: "left", left: 646 });
109
- });
110
-
111
- it("keeps a tall panel inside the viewport", () => {
112
- const placement = placePopover(
113
- anchorAt(100, 760),
114
- { width: 200, height: 400 },
115
- VIEWPORT,
116
- options({ side: "right" }),
117
- );
118
- expect(placement.top).toBe(800 - 400 - 8);
119
- });
120
- });
121
-
122
- describe("right to left", () => {
123
- it("mirrors which edge `start` means", () => {
124
- const placement = placePopover(
125
- anchorAt(100, 100),
126
- { width: 200, height: 150 },
127
- VIEWPORT,
128
- options({ rtl: true }),
129
- );
130
- // Reading starts on the right, so the panel's right edge meets the anchor's.
131
- expect(placement.left).toBe(20);
132
- });
133
-
134
- it("mirrors left and right", () => {
135
- const placement = placePopover(
136
- anchorAt(500, 100),
137
- { width: 200, height: 150 },
138
- VIEWPORT,
139
- options({ side: "right", rtl: true }),
140
- );
141
- expect(placement).toMatchObject({ side: "left", left: 296 });
142
- });
143
- });
@@ -1,41 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { lineWidths, toLength } from "../core/lines";
3
-
4
- describe("lineWidths", () => {
5
- it("draws the last line short, the way a paragraph ends", () => {
6
- expect(lineWidths(3)).toEqual(["100%", "100%", "60%"]);
7
- });
8
-
9
- it("leaves a single line full width, so it does not read as a label", () => {
10
- expect(lineWidths(1)).toEqual(["100%"]);
11
- });
12
-
13
- it("takes the width of the last line", () => {
14
- expect(lineWidths(2, 40)).toEqual(["100%", "40%"]);
15
- });
16
-
17
- it("keeps the last line visible however small it is asked to be", () => {
18
- expect(lineWidths(2, 0)).toEqual(["100%", "10%"]);
19
- expect(lineWidths(2, 400)).toEqual(["100%", "100%"]);
20
- });
21
-
22
- it("is empty for nothing to draw", () => {
23
- expect(lineWidths(0)).toEqual([]);
24
- expect(lineWidths(-3)).toEqual([]);
25
- });
26
- });
27
-
28
- describe("toLength", () => {
29
- it("turns a number into pixels", () => {
30
- expect(toLength(24)).toBe("24px");
31
- });
32
-
33
- it("passes a string through, so any CSS length works", () => {
34
- expect(toLength("50%")).toBe("50%");
35
- expect(toLength("3rem")).toBe("3rem");
36
- });
37
-
38
- it("is undefined when unset, leaving the stylesheet's default", () => {
39
- expect(toLength(undefined)).toBeUndefined();
40
- });
41
- });
@@ -1,48 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { spinnerPixels, stepVisibility } from "../core/visibility";
3
-
4
- const hidden = { visible: false, loadingSince: null, visibleSince: null };
5
-
6
- describe("visibility", () => {
7
- it("shows at once without a delay", () => {
8
- expect(stepVisibility(true, { ...hidden, loadingSince: 1000 }, 1000)).toEqual({ visible: true, recheckIn: null });
9
- });
10
-
11
- it("waits out the delay, then shows", () => {
12
- const marks = { ...hidden, loadingSince: 1000 };
13
- expect(stepVisibility(true, marks, 1100, { delay: 300 })).toEqual({ visible: false, recheckIn: 200 });
14
- expect(stepVisibility(true, marks, 1300, { delay: 300 })).toEqual({ visible: true, recheckIn: null });
15
- });
16
-
17
- it("never shows for work that ends within the delay", () => {
18
- expect(stepVisibility(false, hidden, 1200, { delay: 300 })).toEqual({ visible: false, recheckIn: null });
19
- });
20
-
21
- it("stays for the minimum duration once shown", () => {
22
- const shown = { visible: true, loadingSince: null, visibleSince: 2000 };
23
- expect(stepVisibility(false, shown, 2100, { minDuration: 500 })).toEqual({ visible: true, recheckIn: 400 });
24
- expect(stepVisibility(false, shown, 2500, { minDuration: 500 })).toEqual({ visible: false, recheckIn: null });
25
- });
26
-
27
- it("keeps showing while loading continues", () => {
28
- const shown = { visible: true, loadingSince: 1000, visibleSince: 1300 };
29
- expect(stepVisibility(true, shown, 9000, { delay: 300, minDuration: 500 })).toEqual({
30
- visible: true,
31
- recheckIn: null,
32
- });
33
- });
34
-
35
- it("treats negative options as zero", () => {
36
- expect(stepVisibility(true, { ...hidden, loadingSince: 0 }, 0, { delay: -50 }).visible).toBe(true);
37
- });
38
- });
39
-
40
- describe("size", () => {
41
- it("maps names to pixels and passes numbers through", () => {
42
- expect(spinnerPixels()).toBe(24);
43
- expect(spinnerPixels("xs")).toBe(12);
44
- expect(spinnerPixels("xl")).toBe(48);
45
- expect(spinnerPixels(20)).toBe(20);
46
- expect(spinnerPixels(0)).toBe(1);
47
- });
48
- });