cursedbelt 5.46.2 → 5.48.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/dist/react/charts/IntervalTimeline.d.ts.map +1 -1
- package/dist/react/charts/IntervalTimeline.js +2 -2
- package/dist/react/charts/IntervalTimeline.js.map +1 -1
- package/dist/react/chat/DateMarker.d.ts +4 -0
- package/dist/react/chat/DateMarker.d.ts.map +1 -1
- package/dist/react/chat/DateMarker.js +15 -2
- package/dist/react/chat/DateMarker.js.map +1 -1
- package/dist/react/components/Button.d.ts.map +1 -1
- package/dist/react/components/Button.js +5 -0
- package/dist/react/components/Button.js.map +1 -1
- package/dist/react/components/DataTable/DataTable.d.ts.map +1 -1
- package/dist/react/components/DataTable/DataTable.js +1 -1
- package/dist/react/components/DataTable/DataTable.js.map +1 -1
- package/dist/react/components/DataTable/SortHeader.d.ts +4 -1
- package/dist/react/components/DataTable/SortHeader.d.ts.map +1 -1
- package/dist/react/components/DataTable/SortHeader.js +22 -5
- package/dist/react/components/DataTable/SortHeader.js.map +1 -1
- package/dist/react/components/DataTable/types.d.ts +10 -0
- package/dist/react/components/DataTable/types.d.ts.map +1 -1
- package/dist/react/components/DataTable/types.js.map +1 -1
- package/dist/react/components/DropdownMenu.js +10 -4
- package/dist/react/components/DropdownMenu.js.map +1 -1
- package/dist/react/components/Field.d.ts +9 -1
- package/dist/react/components/Field.d.ts.map +1 -1
- package/dist/react/components/Field.js +3 -3
- package/dist/react/components/Field.js.map +1 -1
- package/dist/react/components/SecretInput.d.ts +4 -2
- package/dist/react/components/SecretInput.d.ts.map +1 -1
- package/dist/react/components/SecretInput.js +2 -2
- package/dist/react/components/SecretInput.js.map +1 -1
- package/dist/react/components/auth/AuthCard.d.ts.map +1 -1
- package/dist/react/components/auth/AuthCard.js +5 -1
- package/dist/react/components/auth/AuthCard.js.map +1 -1
- package/dist/react/components/auth/PasswordSetFields.d.ts.map +1 -1
- package/dist/react/components/auth/PasswordSetFields.js +2 -2
- package/dist/react/components/auth/PasswordSetFields.js.map +1 -1
- package/dist/react/components/auth/PasswordStrengthMeter.d.ts.map +1 -1
- package/dist/react/components/auth/PasswordStrengthMeter.js +3 -1
- package/dist/react/components/auth/PasswordStrengthMeter.js.map +1 -1
- package/dist/react/components/auth/RegisterForm.js +1 -1
- package/dist/react/components/auth/RegisterForm.js.map +1 -1
- package/dist/react/components/auth/SignupForm.d.ts.map +1 -1
- package/dist/react/components/auth/SignupForm.js +4 -2
- package/dist/react/components/auth/SignupForm.js.map +1 -1
- package/dist/react/components/auth/TotpEntry.d.ts.map +1 -1
- package/dist/react/components/auth/TotpEntry.js +18 -1
- package/dist/react/components/auth/TotpEntry.js.map +1 -1
- package/dist/react/components/auth/fields.d.ts +3 -1
- package/dist/react/components/auth/fields.d.ts.map +1 -1
- package/dist/react/components/auth/fields.js +6 -2
- package/dist/react/components/auth/fields.js.map +1 -1
- package/dist/react/components/calendar/DayAgenda.d.ts.map +1 -1
- package/dist/react/components/calendar/DayAgenda.js +4 -2
- package/dist/react/components/calendar/DayAgenda.js.map +1 -1
- package/dist/react/components/calendar/EventChip.d.ts +5 -1
- package/dist/react/components/calendar/EventChip.d.ts.map +1 -1
- package/dist/react/components/calendar/EventChip.js +10 -3
- package/dist/react/components/calendar/EventChip.js.map +1 -1
- package/dist/react/components/calendar/MonthGrid.js +1 -1
- package/dist/react/components/calendar/MonthGrid.js.map +1 -1
- package/dist/react/components/calendar/WeekView.d.ts.map +1 -1
- package/dist/react/components/calendar/WeekView.js +2 -2
- package/dist/react/components/calendar/WeekView.js.map +1 -1
- package/dist/react/components/calendar/colorUtils.d.ts.map +1 -1
- package/dist/react/components/calendar/colorUtils.js +13 -11
- package/dist/react/components/calendar/colorUtils.js.map +1 -1
- package/dist/react/components/fieldErrorText.d.ts +12 -0
- package/dist/react/components/fieldErrorText.d.ts.map +1 -0
- package/dist/react/components/fieldErrorText.js +12 -0
- package/dist/react/components/fieldErrorText.js.map +1 -0
- package/dist/react/deep-link/resume.d.ts.map +1 -1
- package/dist/react/deep-link/resume.js +9 -2
- package/dist/react/deep-link/resume.js.map +1 -1
- package/dist/react/file-tree/FileTree.d.ts +3 -0
- package/dist/react/file-tree/FileTree.d.ts.map +1 -1
- package/dist/react/file-tree/FileTree.js +12 -1
- package/dist/react/file-tree/FileTree.js.map +1 -1
- package/dist/react/file-tree/FolderViewMenus.d.ts +3 -1
- package/dist/react/file-tree/FolderViewMenus.d.ts.map +1 -1
- package/dist/react/file-tree/FolderViewMenus.js +51 -15
- package/dist/react/file-tree/FolderViewMenus.js.map +1 -1
- package/dist/react/file-tree/folderView.d.ts +25 -4
- package/dist/react/file-tree/folderView.d.ts.map +1 -1
- package/dist/react/file-tree/folderView.js +49 -9
- package/dist/react/file-tree/folderView.js.map +1 -1
- package/dist/react/file-tree/index.d.ts +2 -1
- package/dist/react/file-tree/index.d.ts.map +1 -1
- package/dist/react/file-tree/index.js +2 -1
- package/dist/react/file-tree/index.js.map +1 -1
- package/dist/react/file-tree/useFolderView.d.ts +34 -4
- package/dist/react/file-tree/useFolderView.d.ts.map +1 -1
- package/dist/react/file-tree/useFolderView.js +43 -11
- package/dist/react/file-tree/useFolderView.js.map +1 -1
- package/dist/react/media/DenseGalleryShell.d.ts +8 -1
- package/dist/react/media/DenseGalleryShell.d.ts.map +1 -1
- package/dist/react/media/DenseGalleryShell.js +33 -8
- package/dist/react/media/DenseGalleryShell.js.map +1 -1
- package/dist/react/media/VideoPlayer.d.ts +12 -1
- package/dist/react/media/VideoPlayer.d.ts.map +1 -1
- package/dist/react/media/VideoPlayer.js +30 -2
- package/dist/react/media/VideoPlayer.js.map +1 -1
- package/dist/react/media/controlBar.d.ts +3 -2
- package/dist/react/media/controlBar.d.ts.map +1 -1
- package/dist/react/media/controlBar.js +16 -10
- package/dist/react/media/controlBar.js.map +1 -1
- package/dist/react/media/denseGallery.css +56 -18
- package/dist/react/media/videoPlayer.css +2 -1
- package/dist/react/media-gallery/MediaGallery.d.ts.map +1 -1
- package/dist/react/media-gallery/MediaGallery.js +8 -4
- package/dist/react/media-gallery/MediaGallery.js.map +1 -1
- package/dist/styles-areas/analytics.css +26 -25
- package/dist/styles-areas/calendar.css +4 -4
- package/dist/styles-areas/data-table.css +26 -25
- package/dist/styles-utilities.css +24 -9
- package/package.json +5 -1
- package/scripts/demoCoverage.spec.ts +33 -0
- package/scripts/demoCoverage.ts +124 -0
- package/src/react/charts/IntervalTimeline.tsx +6 -4
- package/src/react/chat/DateMarker.tsx +22 -6
- package/src/react/chat/MessageThread.spec.tsx +25 -1
- package/src/react/components/Button.spec.tsx +11 -0
- package/src/react/components/Button.tsx +5 -0
- package/src/react/components/DataTable/DataTable.spec.tsx +43 -0
- package/src/react/components/DataTable/DataTable.tsx +1 -0
- package/src/react/components/DataTable/SortHeader.tsx +45 -5
- package/src/react/components/DataTable/types.ts +10 -0
- package/src/react/components/DropdownMenu.spec.tsx +26 -0
- package/src/react/components/DropdownMenu.tsx +10 -3
- package/src/react/components/Field.spec.tsx +24 -0
- package/src/react/components/Field.tsx +15 -5
- package/src/react/components/SecretInput.tsx +5 -1
- package/src/react/components/auth/AuthCard.spec.tsx +8 -0
- package/src/react/components/auth/AuthCard.tsx +4 -1
- package/src/react/components/auth/PasswordSetFields.tsx +7 -11
- package/src/react/components/auth/PasswordStrengthMeter.tsx +5 -3
- package/src/react/components/auth/RegisterForm.tsx +6 -6
- package/src/react/components/auth/SignupForm.spec.tsx +17 -0
- package/src/react/components/auth/SignupForm.tsx +11 -10
- package/src/react/components/auth/TotpEntry.spec.tsx +27 -0
- package/src/react/components/auth/TotpEntry.tsx +18 -1
- package/src/react/components/auth/fields.tsx +8 -1
- package/src/react/components/calendar/DayAgenda.spec.tsx +9 -0
- package/src/react/components/calendar/DayAgenda.tsx +5 -2
- package/src/react/components/calendar/EventChip.spec.tsx +12 -8
- package/src/react/components/calendar/EventChip.tsx +11 -6
- package/src/react/components/calendar/MonthGrid.tsx +1 -1
- package/src/react/components/calendar/WeekView.tsx +5 -3
- package/src/react/components/calendar/colorUtils.ts +13 -11
- package/src/react/components/fieldErrorText.tsx +17 -0
- package/src/react/deep-link/resume.spec.tsx +16 -0
- package/src/react/deep-link/resume.ts +9 -2
- package/src/react/file-tree/FileTree.tsx +14 -1
- package/src/react/file-tree/FolderViewMenus.tsx +57 -17
- package/src/react/file-tree/folderView.spec.ts +63 -1
- package/src/react/file-tree/folderView.ts +63 -12
- package/src/react/file-tree/folderViewTree.spec.tsx +111 -16
- package/src/react/file-tree/index.ts +15 -1
- package/src/react/file-tree/useFolderView.ts +73 -12
- package/src/react/media/DenseGalleryShell.spec.tsx +9 -7
- package/src/react/media/DenseGalleryShell.tsx +53 -11
- package/src/react/media/VideoPlayer.tsx +78 -2
- package/src/react/media/controlBar.tsx +17 -10
- package/src/react/media/denseGallery.css +56 -18
- package/src/react/media/videoPlayer.css +2 -1
- package/src/react/media-gallery/MediaGallery.spec.tsx +4 -4
- package/src/react/media-gallery/MediaGallery.tsx +12 -0
- package/src/styles-areas/analytics.css +26 -25
- package/src/styles-areas/calendar.css +4 -4
- package/src/styles-areas/data-table.css +26 -25
- package/src/styles-utilities.css +24 -9
|
@@ -18,35 +18,36 @@
|
|
|
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
|
+
* 154 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("-m-1 -mt-2 -mt-6 -mx-1 -my-2.5 -my-3 @3xl:block @3xl:hidden @max-2xl:[&>*]:hidden");
|
|
28
|
-
@source inline("@max-2xl:border-0 @max-2xl:
|
|
27
|
+
@source inline("-m-1 -mt-2 -mt-6 -mx-1 -my-2.5 -my-3 @3xl:block @3xl:hidden @max-2xl @max-2xl:[&>*]:hidden");
|
|
28
|
+
@source inline("@max-2xl:border-0 @max-2xl:inline-flex @max-2xl:justify-center @max-2xl:min-w-0!");
|
|
29
|
+
@source inline("@max-2xl:min-w-11 @max-2xl:p-0! @max-2xl:sr-only @max-2xl:w-0! @max-4xl:[&>*]:hidden");
|
|
29
30
|
@source inline("@max-4xl:border-0 @max-4xl:min-w-0! @max-4xl:p-0! @max-4xl:w-0! @max-6xl:[&>*]:hidden");
|
|
30
31
|
@source inline("@max-6xl:border-0 @max-6xl:min-w-0! @max-6xl:p-0! @max-6xl:w-0! @xl:block @xl:hidden");
|
|
31
|
-
@source inline("[&>*]:hidden [&>*]:max-w-none [&_svg]:size-3 [overflow-wrap:anywhere]
|
|
32
|
-
@source inline("appearance appearance-none bg-[var(--cb-frozen-bg)] bg-card/95
|
|
33
|
-
@source inline("bg-muted/40 bg-muted/50 border-accent-solid border-accent/60
|
|
34
|
-
@source inline("border-r col collapse cursor-grab data-[ending-style]:opacity-0
|
|
35
|
-
@source inline("data-[
|
|
36
|
-
@source inline("data-[
|
|
37
|
-
@source inline("data-[
|
|
38
|
-
@source inline("data-[state=closed]:slide-out-to-
|
|
39
|
-
@source inline("data-[state=open]:slide-in-from-
|
|
40
|
-
@source inline("
|
|
41
|
-
@source inline("focus-
|
|
42
|
-
@source inline("has-[[aria-invalid=true]]:border-field-error
|
|
43
|
-
@source inline("hover:bg-muted/40 hover:border-field-ring hover:text-warning
|
|
44
|
-
@source inline("last:border-0 left-0 left-11 left-2.5 list-none max-h-64 max-h-88
|
|
45
|
-
@source inline("max-h-[var(--sheet-h-
|
|
46
|
-
@source inline("max-w-sm min-h-20 min-h-dvh min-w-0! min-w-20 min-w-24 min-w-52
|
|
47
|
-
@source inline("min-w-max ml-1.5 mr-2 mt-1.5 mt-3 mt-4 not-italic ordinal p-0!
|
|
48
|
-
@source inline("pr-10 pr-16 pr-3 pr-9 pt-1 resize-none resize-y right-3
|
|
49
|
-
@source inline("rounded-r-xl rounded-t-xl self self-center self-stretch
|
|
50
|
-
@source inline("shrink-[2] size-2 size-7 slide-in-from-top-1 table-fixed
|
|
51
|
-
@source inline("text-field-placeholder text-muted-foreground/60 text-success
|
|
52
|
-
@source inline("transition-[transform,opacity] w-0! w-11 w-20 w-32 w-36 w-40 w-88");
|
|
32
|
+
@source inline("[&>*]:hidden [&>*]:max-w-none [&>svg]:size-3.5 [&_svg]:size-3 [overflow-wrap:anywhere]");
|
|
33
|
+
@source inline("active:cursor-grabbing appearance appearance-none bg-[var(--cb-frozen-bg)] bg-card/95");
|
|
34
|
+
@source inline("bg-gradient-to-t bg-muted/30 bg-muted/40 bg-muted/50 border-accent-solid border-accent/60");
|
|
35
|
+
@source inline("border-field-error border-l border-r col collapse cursor-grab data-[ending-style]:opacity-0");
|
|
36
|
+
@source inline("data-[ending-style]:scale-95 data-[highlighted]:bg-accent/10");
|
|
37
|
+
@source inline("data-[highlighted]:text-accent-strong data-[starting-style]:opacity-0");
|
|
38
|
+
@source inline("data-[starting-style]:scale-95 data-[state=closed]:slide-out-to-bottom");
|
|
39
|
+
@source inline("data-[state=closed]:slide-out-to-left data-[state=closed]:slide-out-to-right");
|
|
40
|
+
@source inline("data-[state=open]:slide-in-from-bottom data-[state=open]:slide-in-from-left");
|
|
41
|
+
@source inline("data-[state=open]:slide-in-from-right disabled:opacity-40 duration-100 fade-in");
|
|
42
|
+
@source inline("focus-visible:ring-field-ring/40 focus-within:border-field-ring focus-within:ring-2");
|
|
43
|
+
@source inline("focus-within:ring-field-ring/30 from-card h-105 has-[[aria-invalid=true]]:border-field-error");
|
|
44
|
+
@source inline("hover:bg-background hover:bg-border hover:bg-muted/40 hover:border-field-ring hover:text-warning");
|
|
45
|
+
@source inline("italic justify-self-end last:border-0 left-0 left-11 left-2.5 list-none max-h-64 max-h-88");
|
|
46
|
+
@source inline("max-h-[var(--sheet-h-lg)] max-h-[var(--sheet-h-md)] max-h-[var(--sheet-h-sm)] max-w-32 max-w-36");
|
|
47
|
+
@source inline("max-w-45 max-w-48 max-w-60 max-w-sm min-h-20 min-h-dvh min-w-0! min-w-20 min-w-24 min-w-52");
|
|
48
|
+
@source inline("min-w-[var(--anchor-width)] min-w-max ml-1.5 mr-2 mt-1.5 mt-3 mt-4 not-italic ordinal p-0!");
|
|
49
|
+
@source inline("pb-1.5 pl-1.5 pl-2 pr-0.5 pr-1 pr-10 pr-16 pr-3 pr-9 pt-1 resize-none resize-y right-3");
|
|
50
|
+
@source inline("ring-field-error/30 rounded-l-xl rounded-r-xl rounded-t-xl self self-center self-stretch");
|
|
51
|
+
@source inline("shadow-[1px_0_0_0_var(--color-border)] shrink-[2] size-2 size-7 slide-in-from-top-1 table-fixed");
|
|
52
|
+
@source inline("text-accent-solid-foreground text-field-placeholder text-muted-foreground/60 text-success");
|
|
53
|
+
@source inline("text-xl to-transparent top-3 transition-[transform,opacity] w-0! w-11 w-20 w-32 w-36 w-40 w-88");
|
|
@@ -18,7 +18,7 @@
|
|
|
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
|
+
* 78 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
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
@source inline("dark:bg-sky-900/40 dark:bg-slate-800 dark:bg-teal-900/40 dark:bg-violet-900/40");
|
|
33
33
|
@source inline("dark:text-amber-200 dark:text-blue-200 dark:text-emerald-200 dark:text-green-200");
|
|
34
34
|
@source inline("dark:text-purple-200 dark:text-rose-200 dark:text-sky-200 dark:text-slate-200 dark:text-teal-200");
|
|
35
|
-
@source inline("dark:text-violet-200 focus:opacity-100
|
|
35
|
+
@source inline("dark:text-violet-200 focus:opacity-100 grid-cols-[repeat(7,minmax(0,1fr))]");
|
|
36
36
|
@source inline("group-focus-within:opacity-100 h-3.5 hover:bg-muted/30 hover:bg-muted/40 hover:border-border/80");
|
|
37
37
|
@source inline("last:border-r-0 line-through min-h-28 min-h-40 outline-1 outline-dashed outline-tone-amber-ink");
|
|
38
|
-
@source inline("p-1.5 sm:
|
|
38
|
+
@source inline("p-1.5 sm:inline sm:min-h-0 sm:min-h-24 sm:px-1.5 sm:text-xs text-accent-solid-foreground");
|
|
39
39
|
@source inline("text-amber-900 text-blue-800 text-emerald-800 text-green-800 text-purple-800 text-rose-800");
|
|
40
|
-
@source inline("text-sky-800 text-slate-800 text-teal-800 text-tone-amber-ink text-violet-800 w-3.5");
|
|
40
|
+
@source inline("text-sky-800 text-slate-800 text-teal-800 text-tone-amber-ink text-violet-800 w-20 w-3.5");
|
|
@@ -18,35 +18,36 @@
|
|
|
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
|
+
* 150 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("-m-1 -mt-2 -mt-6 -mx-1 -my-2.5 -my-3 @3xl:block @3xl:hidden @max-2xl:[&>*]:hidden");
|
|
28
|
-
@source inline("@max-2xl:border-0 @max-2xl:
|
|
27
|
+
@source inline("-m-1 -mt-2 -mt-6 -mx-1 -my-2.5 -my-3 @3xl:block @3xl:hidden @max-2xl @max-2xl:[&>*]:hidden");
|
|
28
|
+
@source inline("@max-2xl:border-0 @max-2xl:inline-flex @max-2xl:justify-center @max-2xl:min-w-0!");
|
|
29
|
+
@source inline("@max-2xl:min-w-11 @max-2xl:p-0! @max-2xl:sr-only @max-2xl:w-0! @max-4xl:[&>*]:hidden");
|
|
29
30
|
@source inline("@max-4xl:border-0 @max-4xl:min-w-0! @max-4xl:p-0! @max-4xl:w-0! @max-6xl:[&>*]:hidden");
|
|
30
31
|
@source inline("@max-6xl:border-0 @max-6xl:min-w-0! @max-6xl:p-0! @max-6xl:w-0! @xl:block @xl:hidden");
|
|
31
|
-
@source inline("[&>*]:hidden [&>*]:max-w-none [&_svg]:size-3 [overflow-wrap:anywhere]
|
|
32
|
-
@source inline("appearance appearance-none bg-[var(--cb-frozen-bg)] bg-card/95
|
|
33
|
-
@source inline("bg-muted/50 border-accent-solid border-accent/60 border-field-error
|
|
34
|
-
@source inline("collapse cursor-grab data-[ending-style]:opacity-0
|
|
35
|
-
@source inline("data-[
|
|
36
|
-
@source inline("data-[
|
|
37
|
-
@source inline("data-[
|
|
38
|
-
@source inline("data-[state=closed]:slide-out-to-
|
|
39
|
-
@source inline("data-[state=open]:slide-in-from-
|
|
40
|
-
@source inline("
|
|
41
|
-
@source inline("focus-
|
|
42
|
-
@source inline("has-[[aria-invalid=true]]:border-field-error
|
|
43
|
-
@source inline("hover:bg-muted/40 hover:border-field-ring hover:text-warning
|
|
44
|
-
@source inline("left-0 left-11 left-2.5 list-none max-h-64 max-h-88
|
|
45
|
-
@source inline("max-h-[var(--sheet-h-
|
|
46
|
-
@source inline("max-w-sm min-h-20 min-h-dvh min-w-0! min-w-24 min-w-52
|
|
47
|
-
@source inline("ml-1.5 mr-2 mt-1.5 mt-3 mt-4 not-italic ordinal p-0!
|
|
48
|
-
@source inline("pr-3 pr-9 pt-1 resize-none resize-y right-3
|
|
49
|
-
@source inline("rounded-t-xl self self-center self-stretch
|
|
50
|
-
@source inline("slide-in-from-top-1 table-fixed
|
|
51
|
-
@source inline("text-
|
|
52
|
-
@source inline("transition-[transform,opacity] w-0! w-11 w-20 w-32 w-36 w-40 w-88");
|
|
32
|
+
@source inline("[&>*]:hidden [&>*]:max-w-none [&>svg]:size-3.5 [&_svg]:size-3 [overflow-wrap:anywhere]");
|
|
33
|
+
@source inline("active:cursor-grabbing appearance appearance-none bg-[var(--cb-frozen-bg)] bg-card/95");
|
|
34
|
+
@source inline("bg-gradient-to-t bg-muted/30 bg-muted/50 border-accent-solid border-accent/60 border-field-error");
|
|
35
|
+
@source inline("border-l border-r col collapse cursor-grab data-[ending-style]:opacity-0");
|
|
36
|
+
@source inline("data-[ending-style]:scale-95 data-[highlighted]:bg-accent/10");
|
|
37
|
+
@source inline("data-[highlighted]:text-accent-strong data-[starting-style]:opacity-0");
|
|
38
|
+
@source inline("data-[starting-style]:scale-95 data-[state=closed]:slide-out-to-bottom");
|
|
39
|
+
@source inline("data-[state=closed]:slide-out-to-left data-[state=closed]:slide-out-to-right");
|
|
40
|
+
@source inline("data-[state=open]:slide-in-from-bottom data-[state=open]:slide-in-from-left");
|
|
41
|
+
@source inline("data-[state=open]:slide-in-from-right disabled:opacity-40 duration-100 fade-in");
|
|
42
|
+
@source inline("focus-visible:ring-field-ring/40 focus-within:border-field-ring focus-within:ring-2");
|
|
43
|
+
@source inline("focus-within:ring-field-ring/30 from-card h-105 has-[[aria-invalid=true]]:border-field-error");
|
|
44
|
+
@source inline("hover:bg-background hover:bg-border hover:bg-muted/40 hover:border-field-ring hover:text-warning");
|
|
45
|
+
@source inline("justify-self-end last:border-0 left-0 left-11 left-2.5 list-none max-h-64 max-h-88");
|
|
46
|
+
@source inline("max-h-[var(--sheet-h-lg)] max-h-[var(--sheet-h-md)] max-h-[var(--sheet-h-sm)] max-w-32 max-w-36");
|
|
47
|
+
@source inline("max-w-45 max-w-48 max-w-60 max-w-sm min-h-20 min-h-dvh min-w-0! min-w-24 min-w-52");
|
|
48
|
+
@source inline("min-w-[var(--anchor-width)] min-w-max ml-1.5 mr-2 mt-1.5 mt-3 mt-4 not-italic ordinal p-0!");
|
|
49
|
+
@source inline("pb-1.5 pl-1.5 pl-2 pr-0.5 pr-1 pr-10 pr-16 pr-3 pr-9 pt-1 resize-none resize-y right-3");
|
|
50
|
+
@source inline("ring-field-error/30 rounded-l-xl rounded-r-xl rounded-t-xl self self-center self-stretch");
|
|
51
|
+
@source inline("shadow-[1px_0_0_0_var(--color-border)] size-2 size-7 slide-in-from-top-1 table-fixed");
|
|
52
|
+
@source inline("text-accent-solid-foreground text-field-placeholder text-muted-foreground/60 text-success");
|
|
53
|
+
@source inline("text-xl to-transparent top-3 transition-[transform,opacity] w-0! w-11 w-20 w-32 w-36 w-40 w-88");
|
|
@@ -4337,18 +4337,9 @@
|
|
|
4337
4337
|
.sm\:grid-cols-\[minmax\(7rem\,34rem\)_minmax\(4rem\,1fr\)_auto\] {
|
|
4338
4338
|
grid-template-columns: minmax(7rem,34rem) minmax(4rem,1fr) auto;
|
|
4339
4339
|
}
|
|
4340
|
-
.sm\:flex-row {
|
|
4341
|
-
flex-direction: row;
|
|
4342
|
-
}
|
|
4343
4340
|
.sm\:flex-nowrap {
|
|
4344
4341
|
flex-wrap: nowrap;
|
|
4345
4342
|
}
|
|
4346
|
-
.sm\:items-center {
|
|
4347
|
-
align-items: center;
|
|
4348
|
-
}
|
|
4349
|
-
.sm\:gap-1 {
|
|
4350
|
-
gap: var(--spacing);
|
|
4351
|
-
}
|
|
4352
4343
|
.sm\:gap-3 {
|
|
4353
4344
|
gap: calc(var(--spacing) * 3);
|
|
4354
4345
|
}
|
|
@@ -4504,12 +4495,32 @@
|
|
|
4504
4495
|
}
|
|
4505
4496
|
}
|
|
4506
4497
|
@container (width < 42rem) {
|
|
4498
|
+
.\@max-2xl\:sr-only {
|
|
4499
|
+
position: absolute;
|
|
4500
|
+
width: 1px;
|
|
4501
|
+
height: 1px;
|
|
4502
|
+
padding: 0;
|
|
4503
|
+
margin: -1px;
|
|
4504
|
+
overflow: hidden;
|
|
4505
|
+
clip-path: inset(50%);
|
|
4506
|
+
white-space: nowrap;
|
|
4507
|
+
border-width: 0;
|
|
4508
|
+
}
|
|
4509
|
+
.\@max-2xl\:inline-flex {
|
|
4510
|
+
display: inline-flex;
|
|
4511
|
+
}
|
|
4507
4512
|
.\@max-2xl\:w-0\! {
|
|
4508
4513
|
width: 0px !important;
|
|
4509
4514
|
}
|
|
4510
4515
|
.\@max-2xl\:min-w-0\! {
|
|
4511
4516
|
min-width: 0px !important;
|
|
4512
4517
|
}
|
|
4518
|
+
.\@max-2xl\:min-w-11 {
|
|
4519
|
+
min-width: calc(var(--spacing) * 11);
|
|
4520
|
+
}
|
|
4521
|
+
.\@max-2xl\:justify-center {
|
|
4522
|
+
justify-content: center;
|
|
4523
|
+
}
|
|
4513
4524
|
.\@max-2xl\:border-0 {
|
|
4514
4525
|
border-style: var(--tw-border-style);
|
|
4515
4526
|
border-width: 0px;
|
|
@@ -4957,6 +4968,10 @@
|
|
|
4957
4968
|
.\[\&\>span\>button\]\:rounded-none > span > button {
|
|
4958
4969
|
border-radius: 0;
|
|
4959
4970
|
}
|
|
4971
|
+
.\[\&\>svg\]\:size-3\.5 > svg {
|
|
4972
|
+
width: calc(var(--spacing) * 3.5);
|
|
4973
|
+
height: calc(var(--spacing) * 3.5);
|
|
4974
|
+
}
|
|
4960
4975
|
}
|
|
4961
4976
|
@property --tw-translate-x {
|
|
4962
4977
|
syntax: "*";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.48.0",
|
|
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.",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"typecheck:scripts": "tsc --noEmit --project tsconfig.scripts.json",
|
|
70
70
|
"demo:check": "tsc --noEmit --project demo/tsconfig.json",
|
|
71
71
|
"demo:paths": "bun run demo/generatePaths.ts --write",
|
|
72
|
+
"demo:coverage": "bun run scripts/demoCoverage.ts",
|
|
72
73
|
"guardrails:report": "bun run scripts/guardrailsEnforce.ts no-raw-action-button,no-raw-overflow,no-raw-inputs,no-hardcoded-colors,no-raw-modal-footer,no-unguarded-editing-modal,control-height-tokens,no-raw-zindex,no-raw-data-table,no-tailwind-palette,no-raw-title-attr,micro-typography-floor,choice-needs-empty-state,no-select-as-action-menu,no-native-dialog,no-fast-tooltip,no-raw-password,no-raw-lazy,no-raw-popover,no-hand-rolled-scroll-memory,no-hand-rolled-surface,no-inline-style,no-arbitrary-length,opt-out-count,opt-out-needs-cause --scope cursedbelt src/react src/styles.css",
|
|
73
74
|
"lint": "biome lint --error-on-warnings src e2e demo scripts",
|
|
74
75
|
"styles": "bun run styles:static && bun run styles:utilities && bun run styles:areas",
|
|
@@ -902,6 +903,9 @@
|
|
|
902
903
|
{
|
|
903
904
|
"script": "demo:check"
|
|
904
905
|
},
|
|
906
|
+
{
|
|
907
|
+
"script": "demo:coverage"
|
|
908
|
+
},
|
|
905
909
|
{
|
|
906
910
|
"script": "lint"
|
|
907
911
|
},
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { BASELINE, compare, demoText, exportedComponents, readBaseline, undemoed } from "./demoCoverage";
|
|
3
|
+
|
|
4
|
+
describe("demo coverage — a component with no demo does not ship (forge task 2466)", () => {
|
|
5
|
+
test("a component named only in an import or a comment is NOT demoed", () => {
|
|
6
|
+
const components = new Map([
|
|
7
|
+
["Shown", ["./react"]],
|
|
8
|
+
["Imported", ["./react"]],
|
|
9
|
+
["Mentioned", ["./react"]],
|
|
10
|
+
]);
|
|
11
|
+
// demoText strips these the same way before matching; this is what survives of a section.
|
|
12
|
+
const text = "export function Section() { return <Shown label='x' />; }";
|
|
13
|
+
expect(undemoed(components, text)).toEqual(["Imported", "Mentioned"]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("🔴 a new component with no demo is a fault, and so is a baseline entry that has outlived its gap", () => {
|
|
17
|
+
expect(compare(["Old", "New"], ["Old", "Fixed"])).toEqual({ added: ["New"], stale: ["Fixed"] });
|
|
18
|
+
expect(compare(["Old"], ["Old"])).toEqual({ added: [], stale: [] });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("the export map is read through the type checker — components in, hooks and types out", () => {
|
|
22
|
+
const components = exportedComponents();
|
|
23
|
+
expect(components.get("Button")).toContain("./react/button");
|
|
24
|
+
expect(components.has("DataTable")).toBe(true);
|
|
25
|
+
expect(components.has("useToast")).toBe(false);
|
|
26
|
+
expect(components.has("ButtonProps")).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("this repo matches its baseline exactly", () => {
|
|
30
|
+
const missing = undemoed(exportedComponents(), demoText());
|
|
31
|
+
expect(compare(missing, readBaseline(BASELINE))).toEqual({ added: [], stale: [] });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bun run demo:coverage [--prune]` — every exported component has a demo, or it does not ship.
|
|
3
|
+
*
|
|
4
|
+
* ## The trap this exists for
|
|
5
|
+
*
|
|
6
|
+
* Forge task 2466 (2026-10-02) set out to review every exported component against the demo, "the
|
|
7
|
+
* visual contract", and its first step was the list. Generated from the export map rather than
|
|
8
|
+
* remembered, it said **242 of 383** exported components appeared in no demo page at all —
|
|
9
|
+
* `Tabs`, `TopBar`, `Menu`, the whole auth kit, chat, calendar — so no review, matrix or design
|
|
10
|
+
* audit had ever drawn them, and nothing stopped the 384th arriving the same way.
|
|
11
|
+
*
|
|
12
|
+
* ## What counts
|
|
13
|
+
*
|
|
14
|
+
* A COMPONENT is a capitalised value export of a `./react*` subpath (`package.json` `exports`,
|
|
15
|
+
* through its `source` file) whose type is a React component: an exotic component (`memo`,
|
|
16
|
+
* `forwardRef`, `lazy`) or a function returning an element. A component is DEMOED when a file under
|
|
17
|
+
* `demo/` (not `demo/dist/`) names it outside its imports and comments — a demo that imports a
|
|
18
|
+
* component and never renders it has not shown it.
|
|
19
|
+
*
|
|
20
|
+
* `demoCoverage.baseline` lists the ones that had no demo when this was written. It is a CEILING:
|
|
21
|
+
* a component missing from the demo and from the baseline fails; a baseline entry that now HAS a
|
|
22
|
+
* demo (or is no longer exported) fails too, so the list only shrinks — `--prune` removes those,
|
|
23
|
+
* and refuses to add anything.
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import * as ts from "typescript";
|
|
28
|
+
|
|
29
|
+
const ROOT = join(import.meta.dir, "..");
|
|
30
|
+
export const BASELINE = join(ROOT, "demoCoverage.baseline");
|
|
31
|
+
|
|
32
|
+
/** Every component name → the subpaths that export it. */
|
|
33
|
+
export function exportedComponents(root: string = ROOT): Map<string, string[]> {
|
|
34
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { exports: Record<string, unknown> };
|
|
35
|
+
const entries: [string, string][] = [];
|
|
36
|
+
for (const [subpath, target] of Object.entries(pkg.exports)) {
|
|
37
|
+
const source = typeof target === "object" && target !== null ? (target as { source?: unknown }).source : undefined;
|
|
38
|
+
if (subpath.startsWith("./react") && typeof source === "string") entries.push([subpath, join(root, source)]);
|
|
39
|
+
}
|
|
40
|
+
const program = ts.createProgram(
|
|
41
|
+
entries.map(([, file]) => file),
|
|
42
|
+
{
|
|
43
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
44
|
+
module: ts.ModuleKind.ESNext,
|
|
45
|
+
target: ts.ScriptTarget.ES2022,
|
|
46
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
47
|
+
allowImportingTsExtensions: true,
|
|
48
|
+
noEmit: true,
|
|
49
|
+
skipLibCheck: true,
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
const checker = program.getTypeChecker();
|
|
53
|
+
const found = new Map<string, string[]>();
|
|
54
|
+
for (const [subpath, file] of entries) {
|
|
55
|
+
const source = program.getSourceFile(file);
|
|
56
|
+
const module = source && checker.getSymbolAtLocation(source);
|
|
57
|
+
if (!module) continue;
|
|
58
|
+
for (const exported of checker.getExportsOfModule(module)) {
|
|
59
|
+
const name = exported.getName();
|
|
60
|
+
if (!/^[A-Z][a-z0-9]/.test(name)) continue;
|
|
61
|
+
const symbol = exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported;
|
|
62
|
+
const declaration = symbol.valueDeclaration;
|
|
63
|
+
if (!(symbol.flags & ts.SymbolFlags.Value) || !declaration) continue;
|
|
64
|
+
const type = checker.getTypeOfSymbolAtLocation(symbol, declaration);
|
|
65
|
+
const component =
|
|
66
|
+
/ExoticComponent|FC<|FunctionComponent/.test(checker.typeToString(type)) ||
|
|
67
|
+
type.getCallSignatures().some((s) => /Element|ReactNode|ReactPortal/.test(checker.typeToString(s.getReturnType())));
|
|
68
|
+
if (!component) continue;
|
|
69
|
+
found.set(name, [...(found.get(name) ?? []), subpath]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return found;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Every demo source with its imports and comments taken out, as one text. */
|
|
76
|
+
export function demoText(root: string = ROOT): string {
|
|
77
|
+
const files = [...new Bun.Glob("demo/**/*.{ts,tsx}").scanSync(root)].filter((f) => !f.startsWith("demo/dist/"));
|
|
78
|
+
return files
|
|
79
|
+
.map((f) =>
|
|
80
|
+
readFileSync(join(root, f), "utf8")
|
|
81
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
82
|
+
.replace(/(^|[^:])\/\/.*$/gm, "$1")
|
|
83
|
+
.replace(/^\s*import[\s\S]*?from\s+["'][^"']+["'];?/gm, ""),
|
|
84
|
+
)
|
|
85
|
+
.join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The components no demo names, sorted. */
|
|
89
|
+
export function undemoed(components: ReadonlyMap<string, string[]>, text: string): string[] {
|
|
90
|
+
return [...components.keys()].filter((name) => !new RegExp(`\\b${name}\\b`).test(text)).sort();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function readBaseline(file: string = BASELINE): string[] {
|
|
94
|
+
return readFileSync(file, "utf8")
|
|
95
|
+
.split("\n")
|
|
96
|
+
.map((l) => l.trim())
|
|
97
|
+
.filter((l) => l && !l.startsWith("#"));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** What the check says: the new gaps (a fault), and the entries that have outlived their gap (also a fault). */
|
|
101
|
+
export function compare(missing: readonly string[], baseline: readonly string[]): { added: string[]; stale: string[] } {
|
|
102
|
+
const now = new Set(missing);
|
|
103
|
+
const allowed = new Set(baseline);
|
|
104
|
+
return { added: missing.filter((n) => !allowed.has(n)), stale: baseline.filter((n) => !now.has(n)) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (import.meta.main) {
|
|
108
|
+
const components = exportedComponents();
|
|
109
|
+
const missing = undemoed(components, demoText());
|
|
110
|
+
const baseline = readBaseline();
|
|
111
|
+
const { added, stale } = compare(missing, baseline);
|
|
112
|
+
const prune = process.argv.includes("--prune");
|
|
113
|
+
if (prune && stale.length > 0) {
|
|
114
|
+
const text = readFileSync(BASELINE, "utf8");
|
|
115
|
+
const gone = new Set(stale);
|
|
116
|
+
writeFileSync(BASELINE, text.split("\n").filter((l) => !gone.has(l.trim())).join("\n"));
|
|
117
|
+
console.log(`pruned ${stale.length}: ${stale.join(", ")}`);
|
|
118
|
+
}
|
|
119
|
+
for (const name of added) console.error(`✗ ${name} (${components.get(name)?.join(", ")}) has no demo — add it to a demo/sections page before it ships.`);
|
|
120
|
+
if (!prune) for (const name of stale) console.error(`✗ ${name} is in demoCoverage.baseline but no longer needs to be — \`bun run demo:coverage --prune\`.`);
|
|
121
|
+
const faults = added.length + (prune ? 0 : stale.length);
|
|
122
|
+
console.log(`${components.size} exported components · ${components.size - missing.length} demoed · ${missing.length} without a demo (baseline ${baseline.length - (prune ? stale.length : 0)})`);
|
|
123
|
+
process.exit(faults > 0 ? 1 : 0);
|
|
124
|
+
}
|
|
@@ -243,22 +243,24 @@ export function IntervalTimeline({
|
|
|
243
243
|
</div>
|
|
244
244
|
)}
|
|
245
245
|
|
|
246
|
-
|
|
246
|
+
{/* The two ends of the axis never wrap ("02:30 / PM" at 390px), and the overflow note takes
|
|
247
|
+
its own full-width row instead of squeezing between them (forge task 2591). */}
|
|
248
|
+
<div className='flex flex-wrap justify-between gap-x-3 text-caption text-muted-foreground tabular-nums'>
|
|
247
249
|
{/* Bands and markers are drawn `aria-hidden` and carry no `title` — a native
|
|
248
250
|
tooltip does not exist on touch and cannot be themed (DESIGN.md §4). They
|
|
249
251
|
are NAMED here instead, in text every reader gets. */}
|
|
250
|
-
<span>
|
|
252
|
+
<span className='whitespace-nowrap'>
|
|
251
253
|
{formatTime(view.from)}
|
|
252
254
|
{bands.length > 0 && ' · shaded = paused'}
|
|
253
255
|
{markers.filter((m) => m.label).length > 0 &&
|
|
254
256
|
` · ${markers.filter((m) => m.label).map((m) => m.label).join(', ')} marked`}
|
|
255
257
|
</span>
|
|
258
|
+
<span className='whitespace-nowrap'>{formatTime(view.to)}</span>
|
|
256
259
|
{lanes.overflow > 0 && (
|
|
257
|
-
<span className='text-warning'>
|
|
260
|
+
<span className='w-full text-warning'>
|
|
258
261
|
+{lanes.overflow} more not drawn — the chart stacks {maxLanes} lanes
|
|
259
262
|
</span>
|
|
260
263
|
)}
|
|
261
|
-
<span>{formatTime(view.to)}</span>
|
|
262
264
|
</div>
|
|
263
265
|
</div>
|
|
264
266
|
);
|
|
@@ -17,6 +17,16 @@ function formatMarkerDate(date: Date, now = new Date()): string {
|
|
|
17
17
|
return date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The LOCAL calendar day as `YYYY-MM-DD` — the same day the label names. `toISOString()` is the
|
|
22
|
+
* UTC day, so a message at 19:00 in Chicago was labelled "Wednesday, July 1" and stamped
|
|
23
|
+
* `2026-07-02` (forge task 2591).
|
|
24
|
+
*/
|
|
25
|
+
function localDayKey(date: Date): string {
|
|
26
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
27
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
// ── Component ─────────────────────────────────────────────────────────────────
|
|
21
31
|
|
|
22
32
|
export interface DateMarkerProps {
|
|
@@ -27,23 +37,29 @@ export interface DateMarkerProps {
|
|
|
27
37
|
/**
|
|
28
38
|
* Centered date-separator row inserted between messages from different calendar
|
|
29
39
|
* days. Renders "Today", "Yesterday", or a full date ("Monday, July 1").
|
|
40
|
+
*
|
|
41
|
+
* A `separator` with the date as its name, not `aria-hidden`: in a transcript the day change is
|
|
42
|
+
* the only place a screen reader learns WHEN the messages below were sent (Slack and iMessage
|
|
43
|
+
* both announce it).
|
|
30
44
|
*/
|
|
31
45
|
export function DateMarker({ date, className }: DateMarkerProps) {
|
|
32
46
|
const d = date instanceof Date ? date : new Date(date);
|
|
33
47
|
const label = formatMarkerDate(d);
|
|
34
|
-
const
|
|
48
|
+
const day = localDayKey(d);
|
|
35
49
|
|
|
36
50
|
return (
|
|
51
|
+
// biome-ignore lint/a11y/useSemanticElements: an <hr> cannot hold the date it announces; a named separator role on the row is the labelled-divider pattern
|
|
37
52
|
<div
|
|
38
|
-
data-chat-date={
|
|
53
|
+
data-chat-date={day}
|
|
54
|
+
role='separator'
|
|
55
|
+
aria-label={label}
|
|
39
56
|
className={cn('flex items-center gap-3 px-4 py-2', className)}
|
|
40
|
-
aria-hidden='true'
|
|
41
57
|
>
|
|
42
|
-
<span className='h-px flex-1 bg-border' />
|
|
43
|
-
<time dateTime={
|
|
58
|
+
<span aria-hidden className='h-px flex-1 bg-border' />
|
|
59
|
+
<time dateTime={day} className='text-xs font-medium text-muted-foreground'>
|
|
44
60
|
{label}
|
|
45
61
|
</time>
|
|
46
|
-
<span className='h-px flex-1 bg-border' />
|
|
62
|
+
<span aria-hidden className='h-px flex-1 bg-border' />
|
|
47
63
|
</div>
|
|
48
64
|
);
|
|
49
65
|
}
|
|
@@ -122,9 +122,33 @@ describe('DateMarker', () => {
|
|
|
122
122
|
});
|
|
123
123
|
|
|
124
124
|
test('has data-chat-date attribute', () => {
|
|
125
|
-
const { container } = render(<DateMarker date={new Date(
|
|
125
|
+
const { container } = render(<DateMarker date={new Date(2026, 6, 2, 12)} />);
|
|
126
126
|
expect(container.querySelector('[data-chat-date="2026-07-02"]')).toBeTruthy();
|
|
127
127
|
});
|
|
128
|
+
|
|
129
|
+
test('stamps the LOCAL day the label names, at either end of it — not the UTC day', () => {
|
|
130
|
+
// `bun test` runs in UTC, where the local and UTC days agree and the bug cannot show — so the
|
|
131
|
+
// zone is set here: west of UTC the late message crosses into tomorrow's UTC day, east of it
|
|
132
|
+
// the early one falls back into yesterday's.
|
|
133
|
+
const zone = process.env.TZ;
|
|
134
|
+
try {
|
|
135
|
+
for (const [tz, hour] of [['America/Chicago', 23], ['Asia/Tokyo', 0]] as const) {
|
|
136
|
+
process.env.TZ = tz;
|
|
137
|
+
const { container, unmount } = render(<DateMarker date={new Date(2026, 6, 2, hour, 30)} />);
|
|
138
|
+
const marker = container.querySelector('[data-chat-date]');
|
|
139
|
+
expect(marker?.getAttribute('data-chat-date')).toBe('2026-07-02');
|
|
140
|
+
expect(marker?.querySelector('time')?.getAttribute('dateTime')).toBe('2026-07-02');
|
|
141
|
+
unmount();
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
process.env.TZ = zone;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('is a named separator a screen reader announces, not hidden from it', () => {
|
|
149
|
+
render(<DateMarker date={new Date()} />);
|
|
150
|
+
expect(screen.getByRole('separator', { name: 'Today' })).toBeTruthy();
|
|
151
|
+
});
|
|
128
152
|
});
|
|
129
153
|
|
|
130
154
|
// ── StatusMarker ──────────────────────────────────────────────────────────────
|
|
@@ -34,6 +34,17 @@ describe('Button', () => {
|
|
|
34
34
|
expect(cls).toContain('h-[var(--control-h-md)]'); // md, the shared control-height token
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
test('a link is unpadded text at every size, so it lines up with the text around it (task 2466)', () => {
|
|
38
|
+
for (const size of [undefined, 'sm', 'md', 'lg'] as const) {
|
|
39
|
+
const { getByRole, unmount } = render(<Button tooltip='L' variant='link' size={size}>L</Button>);
|
|
40
|
+
const cls = getByRole('button').className.split(/\s+/);
|
|
41
|
+
expect(cls).toContain('px-0');
|
|
42
|
+
expect(cls).toContain('h-auto');
|
|
43
|
+
expect(cls.some((c) => /^px-[1-9]/.test(c) || c.startsWith('h-[var(--control-h'))).toBe(false);
|
|
44
|
+
unmount();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
37
48
|
test('primary uses the saturated solid accent fill; destructive is quiet at rest', () => {
|
|
38
49
|
const { getByRole, rerender } = render(<Button tooltip='P' variant='primary'>P</Button>);
|
|
39
50
|
expect(getByRole('button').className).toContain('bg-accent-solid');
|
|
@@ -119,6 +119,11 @@ export const buttonVariants = cva(
|
|
|
119
119
|
'icon-lg': 'h-[var(--control-h-lg)] w-[var(--control-h-lg)] p-0 pointer-coarse:min-w-11',
|
|
120
120
|
},
|
|
121
121
|
},
|
|
122
|
+
// 🔴 A link is a TEXT link at every size (forge task 2466). `link`'s own `px-0 h-auto` came
|
|
123
|
+
// before the size's `px-3`/`px-4` and fixed height, so the size won the merge and every link
|
|
124
|
+
// button — sized or on the `md` default — drew a padded box: TotpEntry's "Use a recovery code
|
|
125
|
+
// instead" sat 12px in from the column every other row starts on.
|
|
126
|
+
compoundVariants: [{ variant: 'link', size: ['sm', 'md', 'lg'], class: 'h-auto px-0 py-0' }],
|
|
122
127
|
defaultVariants: { variant: 'secondary', size: 'md' },
|
|
123
128
|
},
|
|
124
129
|
);
|
|
@@ -568,3 +568,46 @@ describe('hidePagination shows every row', () => {
|
|
|
568
568
|
expect(bodyRows(container).length).toBe(50);
|
|
569
569
|
});
|
|
570
570
|
});
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* 🔴 Forge task 2577: apps/music's Length header read "LE…" at 390px. `headerIcon` paints a glyph
|
|
574
|
+
* instead of the label below a TABLE width, and the label stays the header's accessible name.
|
|
575
|
+
*/
|
|
576
|
+
describe('ColDef.headerIcon', () => {
|
|
577
|
+
const CLOCK = <svg data-testid='clock' />;
|
|
578
|
+
const WITH_ICON: ColDef<Person>[] = [
|
|
579
|
+
{ key: 'name', label: 'Name', type: 'text', sortable: true },
|
|
580
|
+
{ key: 'age', label: 'Length', type: 'number', sortable: true, headerIcon: CLOCK },
|
|
581
|
+
{ key: 'nick', label: 'Plain', type: 'text', headerIcon: CLOCK },
|
|
582
|
+
];
|
|
583
|
+
|
|
584
|
+
test('the glyph replaces the label below the table step; the label stays for assistive tech', () => {
|
|
585
|
+
const { container } = render(<DataTable columns={WITH_ICON} data={PEOPLE} />);
|
|
586
|
+
const heads = [...container.querySelectorAll('thead th')];
|
|
587
|
+
const length = heads.find((th) => th.textContent?.includes('Length')) as HTMLElement;
|
|
588
|
+
const label = length.querySelector('[data-cb-header-label]') as HTMLElement;
|
|
589
|
+
const icon = length.querySelector('[data-cb-header-icon]') as HTMLElement;
|
|
590
|
+
expect(label.className).toContain('@max-2xl:sr-only');
|
|
591
|
+
expect(icon.className).toContain('hidden @max-2xl:inline-flex');
|
|
592
|
+
expect(icon.getAttribute('aria-hidden')).not.toBeNull();
|
|
593
|
+
expect(icon.querySelector('[data-testid="clock"]')).not.toBeNull();
|
|
594
|
+
// A sortable header still sorts by its label's name, and stays a 44px target without it.
|
|
595
|
+
expect(length.querySelector('button')?.textContent).toContain('Length');
|
|
596
|
+
expect(length.querySelector('button')?.className).toContain('@max-2xl:min-w-11');
|
|
597
|
+
|
|
598
|
+
const plain = heads.find((th) => th.textContent?.includes('Plain')) as HTMLElement;
|
|
599
|
+
// A plain (unsortable) header swaps the same way.
|
|
600
|
+
expect(plain.querySelector('[data-cb-header-label]')?.className).toContain('@max-2xl:sr-only');
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
test('a column without one renders exactly as before', () => {
|
|
604
|
+
const { container } = render(<DataTable columns={COLUMNS} data={PEOPLE} />);
|
|
605
|
+
expect(container.querySelector('[data-cb-header-icon]')).toBeNull();
|
|
606
|
+
expect(container.querySelector('[data-cb-header-label]')).toBeNull();
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test('the utility build carries every class the swap uses', async () => {
|
|
610
|
+
const css = await Bun.file(new URL('../../../styles-utilities.css', import.meta.url)).text();
|
|
611
|
+
for (const cls of ['sr-only', 'inline-flex', 'min-w-11', 'justify-center']) expect(css).toContain(`.\\@max-2xl\\:${cls}`);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
@@ -1171,6 +1171,7 @@ export function DataTable<TRow extends Record<string, unknown>>({
|
|
|
1171
1171
|
<SortHeader
|
|
1172
1172
|
column={column}
|
|
1173
1173
|
label={col.label}
|
|
1174
|
+
icon={col.headerIcon}
|
|
1174
1175
|
sortable={col.sortable}
|
|
1175
1176
|
sortIndex={stack.length > 1 ? stack.findIndex((s) => s.id === col.key) : -1}
|
|
1176
1177
|
onSort={() => toggleSortKey(col.key)}
|