cursedbelt 5.91.0 → 5.92.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.
- package/dist/react/activity-bar/activityBar.css +5 -0
- package/dist/react/code-editor/markdownRules.d.ts +8 -2
- package/dist/react/code-editor/markdownRules.d.ts.map +1 -1
- package/dist/react/code-editor/markdownRules.js +7 -3
- package/dist/react/code-editor/markdownRules.js.map +1 -1
- package/dist/react/components/Autocomplete.d.ts +11 -2
- package/dist/react/components/Autocomplete.d.ts.map +1 -1
- package/dist/react/components/Autocomplete.js +7 -6
- package/dist/react/components/Autocomplete.js.map +1 -1
- package/dist/react/components/Button.d.ts.map +1 -1
- package/dist/react/components/Button.js +7 -1
- package/dist/react/components/Button.js.map +1 -1
- package/dist/react/components/FileDropZone.d.ts.map +1 -1
- package/dist/react/components/FileDropZone.js +12 -76
- package/dist/react/components/FileDropZone.js.map +1 -1
- package/dist/react/components/TagsInput.d.ts +46 -4
- package/dist/react/components/TagsInput.d.ts.map +1 -1
- package/dist/react/components/TagsInput.js +92 -39
- package/dist/react/components/TagsInput.js.map +1 -1
- package/dist/react/components/TagsInputSuggesting.d.ts +8 -0
- package/dist/react/components/TagsInputSuggesting.d.ts.map +1 -0
- package/dist/react/components/TagsInputSuggesting.js +48 -0
- package/dist/react/components/TagsInputSuggesting.js.map +1 -0
- package/dist/react/components/dropTransfer.d.ts +53 -0
- package/dist/react/components/dropTransfer.d.ts.map +1 -0
- package/dist/react/components/dropTransfer.js +116 -0
- package/dist/react/components/dropTransfer.js.map +1 -0
- package/dist/react/components/suggestionList.d.ts +13 -0
- package/dist/react/components/suggestionList.d.ts.map +1 -0
- package/dist/react/components/suggestionList.js +19 -0
- package/dist/react/components/suggestionList.js.map +1 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/media-gallery/MediaMetaEditor.d.ts +4 -4
- package/dist/react/optimistic/optimistic.d.ts +11 -2
- package/dist/react/optimistic/optimistic.d.ts.map +1 -1
- package/dist/react/optimistic/optimistic.js +4 -2
- package/dist/react/optimistic/optimistic.js.map +1 -1
- package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
- package/dist/styles-areas/analytics.css +14 -19
- package/dist/styles-areas/auth.css +3 -4
- package/dist/styles-areas/canvas-hud.css +3 -3
- package/dist/styles-areas/chat.css +10 -11
- package/dist/styles-areas/code-editor.css +2 -2
- package/dist/styles-areas/core.css +60 -54
- package/dist/styles-areas/data-table.css +13 -17
- package/dist/styles-areas/disk-usage.css +6 -7
- package/dist/styles-areas/emoji.css +2 -3
- package/dist/styles-areas/fields.css +15 -20
- package/dist/styles-areas/master-detail.css +3 -4
- package/dist/styles-areas/master-lock.css +1 -2
- package/dist/styles-areas/media-gallery.css +8 -10
- package/dist/styles-areas/rich-text.css +3 -3
- package/dist/styles-areas/stats.css +13 -17
- package/dist/styles-areas/wizard.css +6 -11
- package/package.json +7 -1
- package/scripts/guardrails-baseline.json +1 -1
- package/scripts/guardrailsEnforce.spec.ts +13 -0
- package/scripts/guardrailsEnforce.ts +5 -3
- package/scripts/styleAreas.ts +3 -0
- package/src/react/activity-bar/activityBar.css +5 -0
- package/src/react/code-editor/markdownRules.spec.ts +10 -2
- package/src/react/code-editor/markdownRules.ts +11 -3
- package/src/react/components/Autocomplete.spec.tsx +55 -1
- package/src/react/components/Autocomplete.tsx +18 -43
- package/src/react/components/Button.spec.tsx +17 -0
- package/src/react/components/Button.tsx +7 -1
- package/src/react/components/FileDropZone.tsx +12 -83
- package/src/react/components/TagsInput.spec.tsx +129 -14
- package/src/react/components/TagsInput.tsx +153 -85
- package/src/react/components/TagsInputSuggesting.tsx +94 -0
- package/src/react/components/dropTransfer.spec.ts +76 -0
- package/src/react/components/dropTransfer.ts +133 -0
- package/src/react/components/suggestionList.tsx +66 -0
- package/src/react/disk-usage/DiskUsage.spec.tsx +3 -0
- package/src/react/index.ts +1 -0
- package/src/react/lib/floatingStyles.spec.ts +2 -1
- package/src/react/media-gallery/MediaMetaEditor.tsx +4 -4
- package/src/react/optimistic/optimistic.spec.tsx +41 -0
- package/src/react/optimistic/optimistic.ts +13 -4
- package/src/styles-areas/analytics.css +14 -19
- package/src/styles-areas/auth.css +3 -4
- package/src/styles-areas/canvas-hud.css +3 -3
- package/src/styles-areas/chat.css +10 -11
- package/src/styles-areas/code-editor.css +2 -2
- package/src/styles-areas/core.css +60 -54
- package/src/styles-areas/data-table.css +13 -17
- package/src/styles-areas/disk-usage.css +6 -7
- package/src/styles-areas/emoji.css +2 -3
- package/src/styles-areas/fields.css +15 -20
- package/src/styles-areas/master-detail.css +3 -4
- package/src/styles-areas/master-lock.css +1 -2
- package/src/styles-areas/media-gallery.css +8 -10
- package/src/styles-areas/rich-text.css +3 -3
- package/src/styles-areas/stats.css +13 -17
- package/src/styles-areas/wizard.css +6 -11
|
@@ -18,15 +18,14 @@
|
|
|
18
18
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
19
19
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
20
20
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
21
|
-
*
|
|
21
|
+
* 26 candidate(s).
|
|
22
22
|
*
|
|
23
23
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
24
24
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
25
25
|
* import closures rather than a hand-written list, is scripts/styleAreas.ts.
|
|
26
26
|
*/
|
|
27
|
-
@source inline("
|
|
28
|
-
@source inline("
|
|
29
|
-
@source inline("
|
|
30
|
-
@source inline("
|
|
31
|
-
@source inline("
|
|
32
|
-
@source inline("text-[length:var(--text-glyph)] text-muted-foreground/60 text-success");
|
|
27
|
+
@source inline("bg-accent/60 collapse cursor-help duration-150 first:pt-1 group-focus-visible/tile:opacity-100");
|
|
28
|
+
@source inline("group-hover/du-row:opacity-100 group-hover/tile:opacity-100 group/du-row group/tile");
|
|
29
|
+
@source inline("hover:bg-muted-foreground/10 italic max-w-56 max-w-96 mb-2 min-h-8 min-w-56 mr-0.5 opacity-100");
|
|
30
|
+
@source inline("ring-inset ring-ring self-stretch text-3xl text-[length:var(--text-glyph)]");
|
|
31
|
+
@source inline("text-muted-foreground/60 text-success");
|
|
@@ -18,11 +18,10 @@
|
|
|
18
18
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
19
19
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
20
20
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
21
|
-
*
|
|
21
|
+
* 8 candidate(s).
|
|
22
22
|
*
|
|
23
23
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
24
24
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
25
25
|
* import closures rather than a hand-written list, is scripts/styleAreas.ts.
|
|
26
26
|
*/
|
|
27
|
-
@source inline("h-105 hover:border-field-ring
|
|
28
|
-
@source inline("text-xl w-88");
|
|
27
|
+
@source inline("h-105 hover:border-field-ring min-w-48 shadow-popover size-7 text-field-placeholder text-xl w-88");
|
|
@@ -18,32 +18,27 @@
|
|
|
18
18
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
19
19
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
20
20
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
21
|
-
*
|
|
21
|
+
* 87 candidate(s).
|
|
22
22
|
*
|
|
23
23
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
24
24
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
25
25
|
* import closures rather than a hand-written list, is scripts/styleAreas.ts.
|
|
26
26
|
*/
|
|
27
|
-
@source inline("-mx-1 [&::-webkit-scrollbar]:hidden [
|
|
28
|
-
@source inline("[
|
|
29
|
-
@source inline("aria-checked:ring-
|
|
30
|
-
@source inline("
|
|
31
|
-
@source inline("
|
|
32
|
-
@source inline("
|
|
33
|
-
@source inline("
|
|
34
|
-
@source inline("
|
|
35
|
-
@source inline("
|
|
36
|
-
@source inline("
|
|
37
|
-
@source inline("
|
|
38
|
-
@source inline("group-focus-within:opacity-100 h-105 h-12 h-3.5 has-[[aria-invalid=true]]:border-field-error");
|
|
39
|
-
@source inline("hover:bg-muted/50 hover:border-field-ring hover:scale-110 hover:text-warning italic left-2.5 li");
|
|
40
|
-
@source inline("list-none max-h-64 md:flex-col md:gap-0.5 md:justify-start md:overflow-visible md:text-left");
|
|
41
|
-
@source inline("md:w-full min-h-20 min-w-24 min-w-64 min-w-[var(--anchor-width)] min-w-full min-w-max");
|
|
42
|
-
@source inline("mix-blend-difference mr-2 pl-1.5 pl-2 pl-2.5 pointer-coarse:before:-left-1");
|
|
27
|
+
@source inline("-mx-1 [&::-webkit-scrollbar]:hidden [background:var(--cb-swatch-fill,var(--muted))]");
|
|
28
|
+
@source inline("[scrollbar-width:none] appearance-none aria-checked:ring-2 aria-checked:ring-foreground");
|
|
29
|
+
@source inline("aria-checked:ring-offset-1 bg-accent/15 bg-surface-sunken/80 bg-warning/10 border-border/40");
|
|
30
|
+
@source inline("border-field-error caret-transparent cursor-text data-[state=checked]:ring-foreground");
|
|
31
|
+
@source inline("data-[state=checked]:ring-offset-1 disabled:hover:scale-100 disabled:opacity-40");
|
|
32
|
+
@source inline("focus-visible:border-border-strong focus-visible:ring-0 focus-visible:ring-field-ring/40");
|
|
33
|
+
@source inline("focus-within:ring-offset-1 focus-within:ring-ring group-focus-within:opacity-100 h-105 h-12");
|
|
34
|
+
@source inline("h-3.5 has-[[aria-invalid=true]]:border-field-error hover:bg-muted/50 hover:border-field-ring");
|
|
35
|
+
@source inline("hover:scale-110 hover:text-warning italic left-2.5 li list-none md:flex-col md:gap-0.5");
|
|
36
|
+
@source inline("md:justify-start md:overflow-visible md:text-left md:w-full min-h-20 min-w-64 min-w-full");
|
|
37
|
+
@source inline("min-w-max mix-blend-difference mr-2 pl-1.5 pl-2.5 pointer-coarse:before:-left-1");
|
|
43
38
|
@source inline("pointer-coarse:before:-right-1 pointer-coarse:before:absolute");
|
|
44
39
|
@source inline("pointer-coarse:before:bottom-[calc(var(--hit-slop-y)*-1)]");
|
|
45
|
-
@source inline("pointer-coarse:before:top-[calc(var(--hit-slop-y)*-1)] pointer-coarse:items-center pr-
|
|
46
|
-
@source inline("pr-
|
|
40
|
+
@source inline("pointer-coarse:before:top-[calc(var(--hit-slop-y)*-1)] pointer-coarse:items-center pr-16 pr-2");
|
|
41
|
+
@source inline("pr-3 pr-9 pt-1 resize-none resize-y right-1.5 right-3 ring-accent/30 ring-field-error/30");
|
|
47
42
|
@source inline("self-stretch size-7 size-9 size-full sm:w-11 sm:w-56 text-field-placeholder text-success text-xl");
|
|
48
|
-
@source inline("top-1.5
|
|
43
|
+
@source inline("top-1.5 w-20 w-32 w-36 w-40 w-88 w-fit w-max whitespace-pre");
|
|
49
44
|
@source inline('pointer-coarse:before:content-[""]');
|
|
@@ -18,14 +18,13 @@
|
|
|
18
18
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
19
19
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
20
20
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
21
|
-
*
|
|
21
|
+
* 21 candidate(s).
|
|
22
22
|
*
|
|
23
23
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
24
24
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
25
25
|
* import closures rather than a hand-written list, is scripts/styleAreas.ts.
|
|
26
26
|
*/
|
|
27
27
|
@source inline("[&::-webkit-scrollbar]:hidden [scrollbar-width:none] bg-[var(--accent)]/12 bg-[var(--card)]");
|
|
28
|
-
@source inline("bg-accent/15 border-[var(--border)] flex-row
|
|
29
|
-
@source inline("
|
|
30
|
-
@source inline("hover:bg-[var(--muted)] hover:bg-muted/80 mb-1.5 mb-4 min-h min-h-full pb-0.5 pr-1 ring-1");
|
|
28
|
+
@source inline("bg-accent/15 border-[var(--border)] flex-row has-[[aria-invalid=true]]:border-field-error");
|
|
29
|
+
@source inline("hover:bg-[var(--muted)] hover:bg-muted/80 mb-1.5 mb-4 min-h min-h-full pb-0.5 ring-1");
|
|
31
30
|
@source inline("ring-[var(--accent)]/40 ring-accent/30 ring-inset self-stretch shrink-[1000]");
|
|
@@ -18,11 +18,10 @@
|
|
|
18
18
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
19
19
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
20
20
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
21
|
-
*
|
|
21
|
+
* 2 candidate(s).
|
|
22
22
|
*
|
|
23
23
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
24
24
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
25
25
|
* import closures rather than a hand-written list, is scripts/styleAreas.ts.
|
|
26
26
|
*/
|
|
27
|
-
@source inline("focus-within:border-field-ring focus-within:ring-2 focus-within:ring-field-ring/30");
|
|
28
27
|
@source inline("has-[[aria-invalid=true]]:border-field-error self-stretch");
|
|
@@ -18,18 +18,16 @@
|
|
|
18
18
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
19
19
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
20
20
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
21
|
-
*
|
|
21
|
+
* 49 candidate(s).
|
|
22
22
|
*
|
|
23
23
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
24
24
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
25
25
|
* import closures rather than a hand-written list, is scripts/styleAreas.ts.
|
|
26
26
|
*/
|
|
27
|
-
@source inline("
|
|
28
|
-
@source inline("bg-
|
|
29
|
-
@source inline("
|
|
30
|
-
@source inline("
|
|
31
|
-
@source inline("
|
|
32
|
-
@source inline("
|
|
33
|
-
@source inline("
|
|
34
|
-
@source inline("text-tone-rose-ink text-tone-sky-ink text-tone-slate-ink text-white/70 top-1 top-4 translate");
|
|
35
|
-
@source inline("translate-x-px w-40 w-48 zoom zoom-in");
|
|
27
|
+
@source inline("appearance-none aspect-video backdrop-blur-xs bg-black/55 bg-black/80 bg-black/85 bg-black/95");
|
|
28
|
+
@source inline("bg-white/10 border-none bottom-1 col collapse ease-in-out f focus-visible:ring-offset-black");
|
|
29
|
+
@source inline("hover:bg-white/20 hover:border-accent-solid hover:text-white inset-4 left-1 max-h-[40vh]");
|
|
30
|
+
@source inline("min-h-20 min-h-dvh min-w-12 min-w-16 min-w-20 mx-1 place-items-center resize-none right-1");
|
|
31
|
+
@source inline("right-4 size-9 text-tone-amber-ink text-tone-cyan-ink text-tone-emerald-ink text-tone-orange-ink");
|
|
32
|
+
@source inline("text-tone-purple-ink text-tone-rose-ink text-tone-sky-ink text-tone-slate-ink text-white/70");
|
|
33
|
+
@source inline("top-1 top-4 translate translate-x-px w-40 w-48 zoom zoom-in");
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
19
19
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
20
20
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
21
|
-
*
|
|
21
|
+
* 7 candidate(s).
|
|
22
22
|
*
|
|
23
23
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
24
24
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
25
25
|
* import closures rather than a hand-written list, is scripts/styleAreas.ts.
|
|
26
26
|
*/
|
|
27
|
-
@source inline("flex-nowrap focus-within:ring-
|
|
28
|
-
@source inline("
|
|
27
|
+
@source inline("flex-nowrap focus-within:ring-offset-0 focus-within:ring-ring italic min-h-32 sm:flex-wrap");
|
|
28
|
+
@source inline("top-full");
|
|
@@ -18,29 +18,25 @@
|
|
|
18
18
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
19
19
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
20
20
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
21
|
-
*
|
|
21
|
+
* 86 candidate(s).
|
|
22
22
|
*
|
|
23
23
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
24
24
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
25
25
|
* import closures rather than a hand-written list, is scripts/styleAreas.ts.
|
|
26
26
|
*/
|
|
27
|
-
@source inline("[&>div]:sm:[grid-template-columns:…] [&_svg]:size-3
|
|
28
|
-
@source inline("bg-
|
|
29
|
-
@source inline("border-l-
|
|
30
|
-
@source inline("
|
|
31
|
-
@source inline("
|
|
32
|
-
@source inline("
|
|
33
|
-
@source inline("
|
|
34
|
-
@source inline("
|
|
35
|
-
@source inline("
|
|
36
|
-
@source inline("
|
|
37
|
-
@source inline("
|
|
38
|
-
@source inline("has-[[aria-invalid=true]]:border-field-error hover:bg-muted/50 hover:border-field-ring");
|
|
39
|
-
@source inline("hover:text-warning last:pr-1 left-3 left-[var(--timeline-rail-x)] list-none max-h-64 min-h-20");
|
|
40
|
-
@source inline("min-h-9 min-w-24 min-w-34 min-w-[var(--anchor-width)] min-w-full mr-0.5 mr-1.5 mr-2 normal-case");
|
|
41
|
-
@source inline("pl-1.5 pl-2 pr-1 pr-16 pr-3 pt-1 resize-none resize-y ring-field-error/30 rounded-b-md");
|
|
27
|
+
@source inline("[&>div]:sm:[grid-template-columns:…] [&_svg]:size-3.5 basis-40 bg-accent/15 bg-accent/40");
|
|
28
|
+
@source inline("bg-background/70 bg-success/10 bg-warning/10 bg-warning/20 border-field-error border-l-2");
|
|
29
|
+
@source inline("border-l-border-strong border-l-destructive border-l-warning bottom-3 col col-span-2 col-start-2");
|
|
30
|
+
@source inline("collapse cursor-help divide-x first:pl-1 focus-visible:ring-field-ring/40 gap-x-1.5 gap-x-2");
|
|
31
|
+
@source inline("gap-y-0.5 grid-cols-[minmax(0,1fr)_auto] group-focus-visible/meter:opacity-100");
|
|
32
|
+
@source inline("group-focus-visible/stat:opacity-100 group-focus-visible/tile:opacity-100");
|
|
33
|
+
@source inline("group-hover/meter:opacity-100 group-hover/stat:opacity-100 group-hover/tile:opacity-100");
|
|
34
|
+
@source inline("group/meter group/stat group/tile h-105 has-[[aria-invalid=true]]:border-field-error");
|
|
35
|
+
@source inline("hover:bg-muted/50 hover:border-field-ring hover:text-warning last:pr-1 left-3");
|
|
36
|
+
@source inline("left-[var(--timeline-rail-x)] list-none min-h-20 min-h-9 min-w-34 min-w-full mr-0.5 mr-1.5 mr-2");
|
|
37
|
+
@source inline("normal-case pl-1.5 pr-16 pr-3 pt-1 resize-none resize-y ring-field-error/30 rounded-b-md");
|
|
42
38
|
@source inline("rounded-t-md row-start-1 self-stretch shadow-md size-7 sm:col-auto sm:col-span-1 sm:flex-nowrap");
|
|
43
39
|
@source inline("sm:grid-cols-[minmax(7rem,18rem)_minmax(4rem,1fr)_auto]");
|
|
44
40
|
@source inline("sm:grid-cols-[minmax(7rem,34rem)_minmax(4rem,1fr)_auto] sm:row-auto sm:truncate text-3xl");
|
|
45
41
|
@source inline("text-[length:var(--text-glyph)] text-field-placeholder text-foreground/80 text-success text-xl");
|
|
46
|
-
@source inline("top-7 tracking-normal
|
|
42
|
+
@source inline("top-7 tracking-normal w-36 w-48 w-88 whitespace-pre z-[1]");
|
|
@@ -18,19 +18,14 @@
|
|
|
18
18
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
19
19
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
20
20
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
21
|
-
*
|
|
21
|
+
* 25 candidate(s).
|
|
22
22
|
*
|
|
23
23
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
24
24
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
25
25
|
* import closures rather than a hand-written list, is scripts/styleAreas.ts.
|
|
26
26
|
*/
|
|
27
|
-
@source inline("
|
|
28
|
-
@source inline("
|
|
29
|
-
@source inline("
|
|
30
|
-
@source inline("
|
|
31
|
-
@source inline("
|
|
32
|
-
@source inline("focus-within:ring-field-ring/30 h-105 has-[[aria-invalid=true]]:border-field-error");
|
|
33
|
-
@source inline("hover:border-field-ring hover:text-warning max-h-64 min-h-20 min-w-24");
|
|
34
|
-
@source inline("min-w-[var(--anchor-width)] mr-1.5 mr-2 pl-1.5 pl-2 pr-1 pr-16 pr-3 resize-none resize-y");
|
|
35
|
-
@source inline("ring-field-error/30 self-stretch size-7 sm:inline text-field-placeholder text-xl");
|
|
36
|
-
@source inline("transition-[transform,opacity] w-36 w-88");
|
|
27
|
+
@source inline("border-field-error collapse disabled:bg-transparent disabled:opacity-100");
|
|
28
|
+
@source inline("focus-visible:ring-field-ring/40 h-105 has-[[aria-invalid=true]]:border-field-error");
|
|
29
|
+
@source inline("hover:border-field-ring hover:text-warning min-h-20 mr-1.5 mr-2 pl-1.5 pr-16 pr-3 resize-none");
|
|
30
|
+
@source inline("resize-y ring-field-error/30 self-stretch size-7 sm:inline text-field-placeholder text-xl w-36");
|
|
31
|
+
@source inline("w-88");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.92.1",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The React design system of the cursedbelt split — components, styles, theme. cursedbelt-core below it; server tier in cursedbelt-server.",
|
|
@@ -528,6 +528,12 @@
|
|
|
528
528
|
"source": "./src/react/components/Modal.tsx",
|
|
529
529
|
"import": "./dist/react/components/Modal.js"
|
|
530
530
|
},
|
|
531
|
+
"./react/autocomplete": {
|
|
532
|
+
"types": "./dist/react/components/Autocomplete.d.ts",
|
|
533
|
+
"bun": "./src/react/components/Autocomplete.tsx",
|
|
534
|
+
"source": "./src/react/components/Autocomplete.tsx",
|
|
535
|
+
"import": "./dist/react/components/Autocomplete.js"
|
|
536
|
+
},
|
|
531
537
|
"./react/multi-select-field": {
|
|
532
538
|
"types": "./dist/react/components/MultiSelectField.d.ts",
|
|
533
539
|
"bun": "./src/react/components/MultiSelectField.tsx",
|
|
@@ -1063,6 +1063,19 @@ describe('no-native-select — the OS draws its list (owner, 2026-10-10)', () =>
|
|
|
1063
1063
|
expect(check('no-native-select', `<selection-bar />`)).toHaveLength(0);
|
|
1064
1064
|
});
|
|
1065
1065
|
|
|
1066
|
+
// forge task 3602, the owner 2026-10-23: *"When I click the add a tag dropdown, the dropdown does
|
|
1067
|
+
// not look styled … I was in dark mode and it popped up light."* That was an `<input list>`.
|
|
1068
|
+
test('a <datalist> is the same OS list, and counts too (forge task 3602)', () => {
|
|
1069
|
+
const source = `<input list="known-tags" value={tag} onChange={(e) => setTag(e.target.value)} />
|
|
1070
|
+
<datalist id="known-tags">
|
|
1071
|
+
{tags.map((t) => <option key={t} value={t} />)}
|
|
1072
|
+
</datalist>`;
|
|
1073
|
+
expect(check('no-native-select', source)).toHaveLength(1);
|
|
1074
|
+
expect(check('no-native-select', `<datalist id={\`ft-record-\${category}\`}>`)).toHaveLength(1);
|
|
1075
|
+
expect(check('no-native-select', `// was a <datalist> until 3602\n<AutocompleteField suggestions={tags} />`)).toHaveLength(0);
|
|
1076
|
+
expect(check('no-native-select', `<datalistish />`)).toHaveLength(0);
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1066
1079
|
test('the styled controls, and a comment that names it, are not one', () => {
|
|
1067
1080
|
expect(check('no-native-select', `<SelectField value={sort} options={SORTS} />`)).toHaveLength(0);
|
|
1068
1081
|
expect(check('no-native-select', `// was a <NativeSelect> until 2026-10-10\n<PropertyPicker name="Type" />`)).toHaveLength(0);
|
|
@@ -2092,7 +2092,9 @@ const noSelectAsActionMenu: Rule = {
|
|
|
2092
2092
|
// even the kit's frame (forge task 3097: desk had eight under `no-raw-inputs`, which no count of
|
|
2093
2093
|
// NativeSelect could see) — in its two-sided ratchet, so the number only falls:
|
|
2094
2094
|
// `SelectField` / `ComboboxField` for a field, `PropertyPicker` for a record's property pill,
|
|
2095
|
-
// `ChoiceMenu` to choose-and-act. A
|
|
2095
|
+
// `ChoiceMenu` to choose-and-act. A `<datalist>` counts too (forge task 3602, the owner 2026-10-23:
|
|
2096
|
+
// a tag field's suggestions *"popped up light"* in dark mode) — `<input list>`'s dropdown is the
|
|
2097
|
+
// same OS list; `AutocompleteField` or `TagsInput` suggest in the page. A use that is genuinely right — a form on a phone where the OS
|
|
2096
2098
|
// wheel IS the better control — says so on the line above:
|
|
2097
2099
|
// `// guardrails-ignore no-native-select: <why>`.
|
|
2098
2100
|
const NATIVE_SELECT_IGNORE_RE = /guardrails-ignore\s+no-native-select\s*:/;
|
|
@@ -2100,7 +2102,7 @@ const NATIVE_SELECT_IGNORE_RE = /guardrails-ignore\s+no-native-select\s*:/;
|
|
|
2100
2102
|
const noNativeSelect: Rule = {
|
|
2101
2103
|
name: "no-native-select",
|
|
2102
2104
|
description:
|
|
2103
|
-
"Owner, 2026-10-10: unstyled dropdowns break the app's cohesiveness. A `NativeSelect`'s (or a raw `<select>`'s) list is drawn by the OS, outside the page, where no stylesheet reaches it. Use `SelectField` / `ComboboxField` for a form field, `PropertyPicker` for a record's property (Type, Folder, Tags), or `ChoiceMenu` to choose-and-act. Where the OS picker is the better control, say why on the line above: `// guardrails-ignore no-native-select: <why>`.",
|
|
2105
|
+
"Owner, 2026-10-10: unstyled dropdowns break the app's cohesiveness. A `NativeSelect`'s (or a raw `<select>`'s, or a `<datalist>`'s) list is drawn by the OS, outside the page, where no stylesheet reaches it. Use `SelectField` / `ComboboxField` for a form field, `AutocompleteField` / `TagsInput` for suggestions as you type, `PropertyPicker` for a record's property (Type, Folder, Tags), or `ChoiceMenu` to choose-and-act. Where the OS picker is the better control, say why on the line above: `// guardrails-ignore no-native-select: <why>`.",
|
|
2104
2106
|
check(filePath, source) {
|
|
2105
2107
|
const unix = filePath.replace(/\\/g, "/");
|
|
2106
2108
|
// The kit is held to its own law (forge task 3063: its DataTable filter builder and
|
|
@@ -2112,7 +2114,7 @@ const noNativeSelect: Rule = {
|
|
|
2112
2114
|
const scannable = blankComments(source);
|
|
2113
2115
|
const lines = source.split("\n");
|
|
2114
2116
|
const out: Violation[] = [];
|
|
2115
|
-
for (const match of scannable.matchAll(/<(?:NativeSelect|select)\b/g)) {
|
|
2117
|
+
for (const match of scannable.matchAll(/<(?:NativeSelect|select|datalist)\b/g)) {
|
|
2116
2118
|
const line = lineOf(source, match.index ?? 0);
|
|
2117
2119
|
if (NATIVE_SELECT_IGNORE_RE.test(lines[line - 1] ?? "") || NATIVE_SELECT_IGNORE_RE.test(blockAbove(lines, line))) continue;
|
|
2118
2120
|
out.push({ file: filePath, line, snippet: lines[line - 1]?.trim() ?? "" });
|
package/scripts/styleAreas.ts
CHANGED
|
@@ -181,6 +181,9 @@ export const AREAS: readonly Area[] = [
|
|
|
181
181
|
'components/IconButton.tsx',
|
|
182
182
|
'components/InfoHint.tsx',
|
|
183
183
|
'components/InfoHintImpl.tsx',
|
|
184
|
+
// TagsInput `import()`s it (forge task 3614) from seven areas' closures (lazyImportFault) — named
|
|
185
|
+
// here once, so whichever area reaches TagsInput, its suggestion list is styled.
|
|
186
|
+
'components/TagsInputSuggesting.tsx',
|
|
184
187
|
'components/Input.tsx',
|
|
185
188
|
'components/ListPanel.tsx',
|
|
186
189
|
'components/Menu.tsx',
|
|
@@ -264,6 +264,11 @@
|
|
|
264
264
|
.cbab-slot {
|
|
265
265
|
inline-size: 44px;
|
|
266
266
|
block-size: 44px;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* 12px centers the rail's OWN 20px glyph. A slot holds the consumer's control, which
|
|
270
|
+
`max-inline-size: 100%` would squeeze to that 20px — it gets the whole 44 instead. */
|
|
271
|
+
.cbab-btn {
|
|
267
272
|
padding-inline: 12px;
|
|
268
273
|
}
|
|
269
274
|
}
|
|
@@ -6,8 +6,11 @@ import { ruleLines } from './markdownRules.js';
|
|
|
6
6
|
|
|
7
7
|
function rules(doc: string, caret: number): number[] {
|
|
8
8
|
const state = EditorState.create({ doc, selection: EditorSelection.cursor(caret), extensions: markdown() });
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// The whole parse, handed over: the state's own tree is only what its 20 ms first pass reached —
|
|
10
|
+
// short of the rule under a loaded gate, and short of anything past the first 3000 characters.
|
|
11
|
+
const tree = ensureSyntaxTree(state, state.doc.length, 5000);
|
|
12
|
+
if (!tree) throw new Error('the markdown parse did not finish');
|
|
13
|
+
return ruleLines(state, [{ from: 0, to: state.doc.length }], tree);
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
describe('markdownRules — `---` drawn as a rule while writing (forge task 3184)', () => {
|
|
@@ -16,6 +19,11 @@ describe('markdownRules — `---` drawn as a rule while writing (forge task 3184
|
|
|
16
19
|
expect(rules('***\n\n___', 0)).toEqual([5]);
|
|
17
20
|
});
|
|
18
21
|
|
|
22
|
+
test('a rule past the first parse pass is found — the whole document is read, not its first 3000 characters', () => {
|
|
23
|
+
const filler = 'a line of prose\n\n'.repeat(250);
|
|
24
|
+
expect(rules(`${filler}---\n\ntwo`, 0)).toEqual([filler.length]);
|
|
25
|
+
});
|
|
26
|
+
|
|
19
27
|
test('with the caret on it, the dashes show', () => {
|
|
20
28
|
expect(rules('one\n\n---\n\ntwo', 7)).toEqual([]);
|
|
21
29
|
});
|
|
@@ -11,10 +11,18 @@ import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate
|
|
|
11
11
|
*/
|
|
12
12
|
const rule = Decoration.line({ class: 'cb-md-rule' });
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The line starts of every rule in the visible ranges, less any line a selection touches. `tree`
|
|
16
|
+
* is the state's own unless given: a caller that parsed further (`ensureSyntaxTree`) passes what
|
|
17
|
+
* it got, because that parse never writes back into the state — its tree stays the one the
|
|
18
|
+
* state's 20 ms first pass reached.
|
|
19
|
+
*/
|
|
20
|
+
export function ruleLines(
|
|
21
|
+
state: EditorState,
|
|
22
|
+
ranges: readonly { from: number; to: number }[],
|
|
23
|
+
tree: ReturnType<typeof syntaxTree> = syntaxTree(state),
|
|
24
|
+
): number[] {
|
|
16
25
|
const starts: number[] = [];
|
|
17
|
-
const tree = syntaxTree(state);
|
|
18
26
|
for (const { from, to } of ranges) {
|
|
19
27
|
tree.iterate({
|
|
20
28
|
from,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, mock, test } from 'bun:test';
|
|
2
2
|
import { act, cleanup, fireEvent, render, waitFor } from '@testing-library/react';
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import { createRef, useState } from 'react';
|
|
4
4
|
import { AutocompleteField } from './Autocomplete.js';
|
|
5
5
|
|
|
6
6
|
afterEach(cleanup);
|
|
@@ -79,4 +79,58 @@ describe('AutocompleteField', () => {
|
|
|
79
79
|
// Suggestions should be visible in the popup (portaled to document.body)
|
|
80
80
|
expect(getByText('Apple')).not.toBeNull();
|
|
81
81
|
});
|
|
82
|
+
|
|
83
|
+
// 🔴 Seen on music-stage, 2026-10-23: "zz" still offered every folder — the list drew the whole
|
|
84
|
+
// static array, never Base UI's filtered one (forge task 3602).
|
|
85
|
+
test('static suggestions narrow to what contains the typed text, in any case', () => {
|
|
86
|
+
const { container, queryByText } = render(<Controlled initial='AP' />);
|
|
87
|
+
const input = container.querySelector('input') as HTMLInputElement;
|
|
88
|
+
act(() => {
|
|
89
|
+
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
|
90
|
+
});
|
|
91
|
+
expect(queryByText('Apple')).not.toBeNull();
|
|
92
|
+
expect(queryByText('Apricot')).not.toBeNull();
|
|
93
|
+
expect(queryByText('Banana')).toBeNull();
|
|
94
|
+
expect(queryByText('Cherry')).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('inputRef is the input, so a form can focus the field it names as missing', () => {
|
|
98
|
+
const ref = createRef<HTMLInputElement>();
|
|
99
|
+
const { container } = render(<AutocompleteField inputRef={ref} suggestions={SUGGESTIONS} />);
|
|
100
|
+
expect(ref.current).toBe(container.querySelector('input'));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('a field that is more than its query brings its own match', () => {
|
|
104
|
+
// A comma list: each suggestion is the whole next value, and only the LAST entry is the query.
|
|
105
|
+
const last = (text: string) => text.slice(text.lastIndexOf(',') + 1).trim().toLowerCase();
|
|
106
|
+
const { container, queryByText } = render(
|
|
107
|
+
<AutocompleteField
|
|
108
|
+
value='rock, an'
|
|
109
|
+
suggestions={['rock, trance', 'rock, ambient', 'rock, jazz']}
|
|
110
|
+
filter={(item, query) => last(item).includes(last(query))}
|
|
111
|
+
/>,
|
|
112
|
+
);
|
|
113
|
+
act(() => {
|
|
114
|
+
fireEvent.keyDown(container.querySelector('input') as HTMLInputElement, { key: 'ArrowDown' });
|
|
115
|
+
});
|
|
116
|
+
expect(queryByText('rock, trance')).not.toBeNull();
|
|
117
|
+
expect(queryByText('rock, ambient')).toBeNull();
|
|
118
|
+
expect(queryByText('rock, jazz')).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('loadSuggestions answers are drawn as given — the loader already searched', async () => {
|
|
122
|
+
const loadSuggestions = mock(async () => ['Zebra', 'Yak']);
|
|
123
|
+
function Async() {
|
|
124
|
+
const [value, setValue] = useState('qq');
|
|
125
|
+
return <AutocompleteField value={value} onChange={setValue} loadSuggestions={loadSuggestions} />;
|
|
126
|
+
}
|
|
127
|
+
const { container, queryByText } = render(<Async />);
|
|
128
|
+
await waitFor(() => expect(loadSuggestions).toHaveBeenCalled(), { timeout: 1000 });
|
|
129
|
+
const input = container.querySelector('input') as HTMLInputElement;
|
|
130
|
+
await act(async () => {
|
|
131
|
+
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
|
132
|
+
});
|
|
133
|
+
await waitFor(() => expect(queryByText('Zebra')).not.toBeNull(), { timeout: 1000 });
|
|
134
|
+
expect(queryByText('Yak')).not.toBeNull();
|
|
135
|
+
});
|
|
82
136
|
});
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { Autocomplete } from '@base-ui/react/autocomplete';
|
|
4
4
|
import { ChevronDown, Loader2 } from 'lucide-react';
|
|
5
|
-
import { type InputHTMLAttributes, type ReactNode, useCallback, useEffect, useState } from 'react';
|
|
5
|
+
import { type InputHTMLAttributes, type ReactNode, type Ref, useCallback, useEffect, useState } from 'react';
|
|
6
6
|
import { useDebouncedCallback } from '../hooks/useDebouncedCallback.js';
|
|
7
7
|
import { cn } from '../lib/cn.js';
|
|
8
8
|
import { type ControlSize, fieldControlClass } from '../lib/field.js';
|
|
9
|
-
import {
|
|
10
|
-
FLOATING_CONTENT_BASE, FLOATING_ITEM_BASE } from '../lib/floatingStyles.js';
|
|
9
|
+
import { SuggestionList } from './suggestionList.js';
|
|
11
10
|
|
|
12
11
|
export interface AutocompleteProps<S = string>
|
|
13
12
|
extends Omit<
|
|
@@ -30,6 +29,13 @@ export interface AutocompleteProps<S = string>
|
|
|
30
29
|
onBlur?: () => void;
|
|
31
30
|
/** Static suggestion items shown in the dropdown. */
|
|
32
31
|
suggestions?: S[];
|
|
32
|
+
/**
|
|
33
|
+
* Whether a static suggestion matches what is typed. Default: its text CONTAINS the typed text,
|
|
34
|
+
* any case (Base UI's own match). A field whose value is more than the query — a comma list
|
|
35
|
+
* where each suggestion completes the LAST entry — passes its own. `null` shows them as given.
|
|
36
|
+
* Never applied to {@link loadSuggestions}: the loader's answer already is the search.
|
|
37
|
+
*/
|
|
38
|
+
filter?: ((item: S, query: string) => boolean) | null;
|
|
33
39
|
/** Async fetch — called on every keystroke (debounced 250 ms).
|
|
34
40
|
* When provided, replaces the static `suggestions`. */
|
|
35
41
|
loadSuggestions?: (query: string) => Promise<S[]>;
|
|
@@ -54,6 +60,8 @@ export interface AutocompleteProps<S = string>
|
|
|
54
60
|
/** Height on the shared control-height scale (`--control-h-*`, task cbux-41).
|
|
55
61
|
* Default `md`. */
|
|
56
62
|
size?: ControlSize;
|
|
63
|
+
/** The input element — to focus it from a form that names what is missing. */
|
|
64
|
+
inputRef?: Ref<HTMLInputElement>;
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
const DEBOUNCE_MS = 250;
|
|
@@ -74,6 +82,7 @@ export function AutocompleteField<S = string>({
|
|
|
74
82
|
onChange,
|
|
75
83
|
onBlur,
|
|
76
84
|
suggestions = [],
|
|
85
|
+
filter,
|
|
77
86
|
loadSuggestions,
|
|
78
87
|
itemToString = (item) => String(item),
|
|
79
88
|
renderSuggestion,
|
|
@@ -85,6 +94,7 @@ export function AutocompleteField<S = string>({
|
|
|
85
94
|
'aria-describedby': ariaDescribedBy,
|
|
86
95
|
className,
|
|
87
96
|
size = 'md',
|
|
97
|
+
inputRef,
|
|
88
98
|
...rest
|
|
89
99
|
}: AutocompleteProps<S>) {
|
|
90
100
|
const isAsync = Boolean(loadSuggestions);
|
|
@@ -127,11 +137,15 @@ export function AutocompleteField<S = string>({
|
|
|
127
137
|
itemToStringValue={itemToString}
|
|
128
138
|
disabled={disabled}
|
|
129
139
|
mode='list'
|
|
140
|
+
// Static suggestions narrow to what is typed (by default Base UI's own match: contains, any
|
|
141
|
+
// case, accents folded). A loader's answer is already the search — drawn as it came.
|
|
142
|
+
filter={isAsync ? null : filter}
|
|
130
143
|
autoHighlight={false}
|
|
131
144
|
>
|
|
132
145
|
<Autocomplete.InputGroup className='relative flex items-center'>
|
|
133
146
|
<Autocomplete.Input
|
|
134
147
|
{...rest}
|
|
148
|
+
ref={inputRef}
|
|
135
149
|
id={id}
|
|
136
150
|
name={name}
|
|
137
151
|
aria-invalid={ariaInvalid}
|
|
@@ -153,46 +167,7 @@ export function AutocompleteField<S = string>({
|
|
|
153
167
|
</span>
|
|
154
168
|
</Autocomplete.InputGroup>
|
|
155
169
|
|
|
156
|
-
<
|
|
157
|
-
{/* 🔴 The z-index goes on the POSITIONER, not only the popup. Base UI's positioner is
|
|
158
|
-
the element that is `position: absolute` (with a transform); the popup inside it is
|
|
159
|
-
`position: static`, so the popup's own `z-[var(--z-popover)]` does nothing and the
|
|
160
|
-
list painted UNDER any sibling with a z-index — measured 2026-09-22 in the
|
|
161
|
-
binary-server inspector, a sticky table header (z-index 1) covering the second option
|
|
162
|
-
of an open MultiSelectField. */}
|
|
163
|
-
<Autocomplete.Positioner
|
|
164
|
-
sideOffset={4}
|
|
165
|
-
collisionPadding={FLOATING_COLLISION_PADDING}
|
|
166
|
-
className='z-[var(--z-popover)]'
|
|
167
|
-
>
|
|
168
|
-
<Autocomplete.Popup
|
|
169
|
-
className={cn(
|
|
170
|
-
FLOATING_CONTENT_BASE,
|
|
171
|
-
// guardrails-ignore no-raw-overflow: floating popover panel outside the page's ScrollArea, height-capped by max-h-64 — no keyboard-inset concern.
|
|
172
|
-
'max-h-64 min-w-[var(--anchor-width)] overflow-y-auto p-0 py-1',
|
|
173
|
-
'transition-[transform,opacity] duration-100',
|
|
174
|
-
'data-[starting-style]:scale-95 data-[starting-style]:opacity-0',
|
|
175
|
-
'data-[ending-style]:scale-95 data-[ending-style]:opacity-0',
|
|
176
|
-
)}
|
|
177
|
-
>
|
|
178
|
-
<Autocomplete.List>
|
|
179
|
-
{activeSuggestions.map((s, index) => (
|
|
180
|
-
<Autocomplete.Item
|
|
181
|
-
key={itemToString(s)}
|
|
182
|
-
value={s}
|
|
183
|
-
index={index}
|
|
184
|
-
className={cn(
|
|
185
|
-
FLOATING_ITEM_BASE,
|
|
186
|
-
'data-[highlighted]:bg-accent/10 data-[highlighted]:text-accent-strong',
|
|
187
|
-
)}
|
|
188
|
-
>
|
|
189
|
-
{renderSuggestion ? renderSuggestion(s) : itemToString(s)}
|
|
190
|
-
</Autocomplete.Item>
|
|
191
|
-
))}
|
|
192
|
-
</Autocomplete.List>
|
|
193
|
-
</Autocomplete.Popup>
|
|
194
|
-
</Autocomplete.Positioner>
|
|
195
|
-
</Autocomplete.Portal>
|
|
170
|
+
<SuggestionList itemToString={itemToString} renderSuggestion={renderSuggestion} />
|
|
196
171
|
</Autocomplete.Root>
|
|
197
172
|
);
|
|
198
173
|
}
|
|
@@ -35,6 +35,23 @@ describe('Button', () => {
|
|
|
35
35
|
expect(cls).toContain('h-[var(--control-h-md)]'); // md, the shared control-height token
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
test('🔴 a label never wraps inside the fixed control height — at every variant and size', () => {
|
|
39
|
+
for (const variant of ['primary', 'secondary', 'tertiary', 'destructive'] as const) {
|
|
40
|
+
for (const size of ['sm', 'md', 'lg'] as const) {
|
|
41
|
+
const { container, unmount } = render(<Button tooltip='Deploy now' variant={variant} size={size}>Deploy now</Button>);
|
|
42
|
+
expect(container.querySelector('button')?.className.split(/\s+/)).toContain('whitespace-nowrap');
|
|
43
|
+
unmount();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// A text link sits in a sentence and wraps with it — it has no fixed height to overflow.
|
|
47
|
+
const link = render(<Button tooltip='L' variant='link'>Use a recovery code instead</Button>);
|
|
48
|
+
expect(link.container.querySelector('button')?.className.split(/\s+/)).toContain('whitespace-normal');
|
|
49
|
+
expect(link.container.querySelector('button')?.className.split(/\s+/)).not.toContain('whitespace-nowrap');
|
|
50
|
+
link.unmount();
|
|
51
|
+
const { container } = render(<ButtonLink to='/x' tooltip='Open'>Open the deploy</ButtonLink>);
|
|
52
|
+
expect(container.querySelector('a')?.className.split(/\s+/)).toContain('whitespace-nowrap');
|
|
53
|
+
});
|
|
54
|
+
|
|
38
55
|
test('a link is unpadded text at every size, so it lines up with the text around it (task 2466)', () => {
|
|
39
56
|
for (const size of [undefined, 'sm', 'md', 'lg'] as const) {
|
|
40
57
|
const { getByRole, unmount } = render(<Button tooltip='L' variant='link' size={size}>L</Button>);
|