cursedbelt 5.23.0 → 5.26.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/components/Badge.d.ts +6 -6
- package/dist/react/components/Badge.d.ts.map +1 -1
- package/dist/react/components/Badge.js +13 -5
- package/dist/react/components/Badge.js.map +1 -1
- package/dist/react/components/DataTable/CardView.d.ts +6 -2
- package/dist/react/components/DataTable/CardView.d.ts.map +1 -1
- package/dist/react/components/DataTable/CardView.js +6 -5
- package/dist/react/components/DataTable/CardView.js.map +1 -1
- package/dist/react/components/DataTable/DataTable.d.ts +165 -5
- package/dist/react/components/DataTable/DataTable.d.ts.map +1 -1
- package/dist/react/components/DataTable/DataTable.js +445 -114
- package/dist/react/components/DataTable/DataTable.js.map +1 -1
- package/dist/react/components/DataTable/RowActions.d.ts +3 -1
- package/dist/react/components/DataTable/RowActions.d.ts.map +1 -1
- package/dist/react/components/DataTable/RowActions.js +9 -2
- package/dist/react/components/DataTable/RowActions.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 +7 -1
- package/dist/react/components/DataTable/SortHeader.js.map +1 -1
- package/dist/react/components/DataTable/cells/CellRenderer.d.ts.map +1 -1
- package/dist/react/components/DataTable/cells/CellRenderer.js +4 -1
- package/dist/react/components/DataTable/cells/CellRenderer.js.map +1 -1
- package/dist/react/components/DataTable/index.d.ts +2 -2
- package/dist/react/components/DataTable/index.d.ts.map +1 -1
- package/dist/react/components/DataTable/index.js +1 -1
- package/dist/react/components/DataTable/index.js.map +1 -1
- package/dist/react/components/DataTable/lazyPart.d.ts +27 -0
- package/dist/react/components/DataTable/lazyPart.d.ts.map +1 -0
- package/dist/react/components/DataTable/lazyPart.js +46 -0
- package/dist/react/components/DataTable/lazyPart.js.map +1 -0
- package/dist/react/components/DataTable/types.d.ts +106 -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/auth/SignInCard.d.ts +49 -0
- package/dist/react/components/auth/SignInCard.d.ts.map +1 -0
- package/dist/react/components/auth/SignInCard.js +21 -0
- package/dist/react/components/auth/SignInCard.js.map +1 -0
- package/dist/react/components/auth/index.d.ts +1 -0
- package/dist/react/components/auth/index.d.ts.map +1 -1
- package/dist/react/components/auth/index.js +1 -0
- package/dist/react/components/auth/index.js.map +1 -1
- package/dist/react/deep-link/index.d.ts +1 -1
- package/dist/react/deep-link/index.d.ts.map +1 -1
- package/dist/react/deep-link/index.js.map +1 -1
- package/dist/react/deep-link/useDeepLink.d.ts +21 -15
- package/dist/react/deep-link/useDeepLink.d.ts.map +1 -1
- package/dist/react/deep-link/useDeepLink.js +8 -5
- package/dist/react/deep-link/useDeepLink.js.map +1 -1
- package/dist/react/file-tree/FileTree.d.ts +14 -1
- package/dist/react/file-tree/FileTree.d.ts.map +1 -1
- package/dist/react/file-tree/FileTree.js +2 -2
- package/dist/react/file-tree/FileTree.js.map +1 -1
- package/dist/react/file-tree/fileTree.css +8 -0
- package/dist/react/filter-rail/FilterRail.d.ts +31 -1
- package/dist/react/filter-rail/FilterRail.d.ts.map +1 -1
- package/dist/react/filter-rail/FilterRail.js +20 -5
- package/dist/react/filter-rail/FilterRail.js.map +1 -1
- package/dist/react/filter-rail/RailSection.d.ts +56 -0
- package/dist/react/filter-rail/RailSection.d.ts.map +1 -0
- package/dist/react/filter-rail/RailSection.js +108 -0
- package/dist/react/filter-rail/RailSection.js.map +1 -0
- package/dist/react/filter-rail/index.d.ts +2 -1
- package/dist/react/filter-rail/index.d.ts.map +1 -1
- package/dist/react/filter-rail/index.js +2 -1
- package/dist/react/filter-rail/index.js.map +1 -1
- package/dist/react/hooks/useScrollMemory.d.ts +6 -0
- package/dist/react/hooks/useScrollMemory.d.ts.map +1 -1
- package/dist/react/hooks/useScrollMemory.js +11 -0
- package/dist/react/hooks/useScrollMemory.js.map +1 -1
- package/dist/react/index.d.ts +3 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +6 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/media-gallery/preferenceBus.d.ts +10 -0
- package/dist/react/media-gallery/preferenceBus.d.ts.map +1 -1
- package/dist/react/media-gallery/preferenceBus.js +24 -0
- package/dist/react/media-gallery/preferenceBus.js.map +1 -1
- package/dist/react/media-gallery/preferenceSync.d.ts.map +1 -1
- package/dist/react/media-gallery/preferenceSync.js +3 -0
- package/dist/react/media-gallery/preferenceSync.js.map +1 -1
- package/dist/react/nav/TopBar.d.ts +8 -1
- package/dist/react/nav/TopBar.d.ts.map +1 -1
- package/dist/react/nav/TopBar.js +2 -2
- package/dist/react/nav/TopBar.js.map +1 -1
- package/dist/styles-areas/analytics.css +18 -15
- package/dist/styles-areas/core.css +8 -7
- package/dist/styles-areas/data-table.css +13 -10
- package/dist/styles-areas/filter-rail.css +6 -5
- package/dist/styles-utilities.css +90 -0
- package/package.json +3 -3
- package/src/react/components/Badge.capsule.spec.ts +178 -0
- package/src/react/components/Badge.tsx +13 -5
- package/src/react/components/DataTable/CardView.tsx +8 -1
- package/src/react/components/DataTable/DataTable.spec.tsx +6 -2
- package/src/react/components/DataTable/DataTable.tsx +770 -86
- package/src/react/components/DataTable/RowActions.tsx +18 -10
- package/src/react/components/DataTable/SortHeader.tsx +19 -0
- package/src/react/components/DataTable/cells/CellRenderer.tsx +2 -0
- package/src/react/components/DataTable/index.ts +10 -1
- package/src/react/components/DataTable/lazyPart.spec.tsx +99 -0
- package/src/react/components/DataTable/lazyPart.tsx +58 -0
- package/src/react/components/DataTable/rowHooks.spec.tsx +289 -0
- package/src/react/components/DataTable/selection.spec.tsx +193 -0
- package/src/react/components/DataTable/tableSystem.spec.tsx +6 -2
- package/src/react/components/DataTable/types.ts +110 -0
- package/src/react/components/DataTable/virtualize.spec.tsx +38 -2
- package/src/react/components/auth/SignInCard.spec.tsx +48 -0
- package/src/react/components/auth/SignInCard.tsx +90 -0
- package/src/react/components/auth/index.ts +1 -0
- package/src/react/deep-link/index.ts +1 -0
- package/src/react/deep-link/useDeepLink.spec.tsx +31 -0
- package/src/react/deep-link/useDeepLink.tsx +31 -11
- package/src/react/file-tree/FileTree.tsx +15 -1
- package/src/react/file-tree/fileTree.css +8 -0
- package/src/react/filter-rail/FilterRail.tsx +93 -26
- package/src/react/filter-rail/RailSection.spec.tsx +156 -0
- package/src/react/filter-rail/RailSection.tsx +216 -0
- package/src/react/filter-rail/index.ts +2 -0
- package/src/react/hooks/useScrollMemory.ts +11 -0
- package/src/react/index.ts +10 -0
- package/src/react/media-gallery/MediaGallery.spec.tsx +9 -3
- package/src/react/media-gallery/preferenceBus.ts +28 -0
- package/src/react/media-gallery/preferenceSync.ts +3 -0
- package/src/react/nav/TopBar.spec.tsx +11 -0
- package/src/react/nav/TopBar.tsx +9 -1
- package/src/styles-areas/analytics.css +18 -15
- package/src/styles-areas/core.css +8 -7
- package/src/styles-areas/data-table.css +13 -10
- package/src/styles-areas/filter-rail.css +6 -5
- package/src/styles-utilities.css +90 -0
|
@@ -315,6 +315,9 @@
|
|
|
315
315
|
.left-3 {
|
|
316
316
|
left: calc(var(--spacing) * 3);
|
|
317
317
|
}
|
|
318
|
+
.left-11 {
|
|
319
|
+
left: calc(var(--spacing) * 11);
|
|
320
|
+
}
|
|
318
321
|
.left-\[11px\] {
|
|
319
322
|
left: 11px;
|
|
320
323
|
}
|
|
@@ -408,9 +411,15 @@
|
|
|
408
411
|
.-my-2\.5 {
|
|
409
412
|
margin-block: calc(var(--spacing) * -2.5);
|
|
410
413
|
}
|
|
414
|
+
.-my-3 {
|
|
415
|
+
margin-block: calc(var(--spacing) * -3);
|
|
416
|
+
}
|
|
411
417
|
.my-1 {
|
|
412
418
|
margin-block: var(--spacing);
|
|
413
419
|
}
|
|
420
|
+
.-mt-2 {
|
|
421
|
+
margin-top: calc(var(--spacing) * -2);
|
|
422
|
+
}
|
|
414
423
|
.-mt-5 {
|
|
415
424
|
margin-top: calc(var(--spacing) * -5);
|
|
416
425
|
}
|
|
@@ -841,6 +850,9 @@
|
|
|
841
850
|
.w-10 {
|
|
842
851
|
width: calc(var(--spacing) * 10);
|
|
843
852
|
}
|
|
853
|
+
.w-11 {
|
|
854
|
+
width: calc(var(--spacing) * 11);
|
|
855
|
+
}
|
|
844
856
|
.w-12 {
|
|
845
857
|
width: calc(var(--spacing) * 12);
|
|
846
858
|
}
|
|
@@ -1696,6 +1708,18 @@
|
|
|
1696
1708
|
border-color: color-mix(in oklab, var(--success) 50%, transparent);
|
|
1697
1709
|
}
|
|
1698
1710
|
}
|
|
1711
|
+
.border-tone-sky-fg\/30 {
|
|
1712
|
+
border-color: var(--tone-sky-fg);
|
|
1713
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1714
|
+
border-color: color-mix(in oklab, var(--tone-sky-fg) 30%, transparent);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
.border-tone-slate-fg\/30 {
|
|
1718
|
+
border-color: var(--tone-slate-fg);
|
|
1719
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1720
|
+
border-color: color-mix(in oklab, var(--tone-slate-fg) 30%, transparent);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1699
1723
|
.border-transparent {
|
|
1700
1724
|
border-color: transparent;
|
|
1701
1725
|
}
|
|
@@ -3228,6 +3252,9 @@
|
|
|
3228
3252
|
.group-data-\[state\=open\]\:rotate-180:is(:where(.group)[data-state="open"] *) {
|
|
3229
3253
|
rotate: 180deg;
|
|
3230
3254
|
}
|
|
3255
|
+
.group-data-\[state\=open\]\/rail-section\:rotate-90:is(:where(.group\/rail-section)[data-state="open"] *) {
|
|
3256
|
+
rotate: 90deg;
|
|
3257
|
+
}
|
|
3231
3258
|
.peer-focus\:rounded-lg:is(:where(.peer):focus ~ *) {
|
|
3232
3259
|
border-radius: 1rem;
|
|
3233
3260
|
}
|
|
@@ -4143,6 +4170,51 @@
|
|
|
4143
4170
|
padding-inline: calc(var(--spacing) * 8);
|
|
4144
4171
|
}
|
|
4145
4172
|
}
|
|
4173
|
+
@container (width < 72rem) {
|
|
4174
|
+
.\@max-6xl\:w-0\! {
|
|
4175
|
+
width: 0px !important;
|
|
4176
|
+
}
|
|
4177
|
+
.\@max-6xl\:min-w-0\! {
|
|
4178
|
+
min-width: 0px !important;
|
|
4179
|
+
}
|
|
4180
|
+
.\@max-6xl\:border-0 {
|
|
4181
|
+
border-style: var(--tw-border-style);
|
|
4182
|
+
border-width: 0px;
|
|
4183
|
+
}
|
|
4184
|
+
.\@max-6xl\:p-0\! {
|
|
4185
|
+
padding: 0px !important;
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
@container (width < 56rem) {
|
|
4189
|
+
.\@max-4xl\:w-0\! {
|
|
4190
|
+
width: 0px !important;
|
|
4191
|
+
}
|
|
4192
|
+
.\@max-4xl\:min-w-0\! {
|
|
4193
|
+
min-width: 0px !important;
|
|
4194
|
+
}
|
|
4195
|
+
.\@max-4xl\:border-0 {
|
|
4196
|
+
border-style: var(--tw-border-style);
|
|
4197
|
+
border-width: 0px;
|
|
4198
|
+
}
|
|
4199
|
+
.\@max-4xl\:p-0\! {
|
|
4200
|
+
padding: 0px !important;
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
@container (width < 42rem) {
|
|
4204
|
+
.\@max-2xl\:w-0\! {
|
|
4205
|
+
width: 0px !important;
|
|
4206
|
+
}
|
|
4207
|
+
.\@max-2xl\:min-w-0\! {
|
|
4208
|
+
min-width: 0px !important;
|
|
4209
|
+
}
|
|
4210
|
+
.\@max-2xl\:border-0 {
|
|
4211
|
+
border-style: var(--tw-border-style);
|
|
4212
|
+
border-width: 0px;
|
|
4213
|
+
}
|
|
4214
|
+
.\@max-2xl\:p-0\! {
|
|
4215
|
+
padding: 0px !important;
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4146
4218
|
@container (width >= 32rem) {
|
|
4147
4219
|
.\@lg\:col-span-6 {
|
|
4148
4220
|
grid-column: span 6 / span 6;
|
|
@@ -4471,9 +4543,27 @@
|
|
|
4471
4543
|
.\[\&\:\:-webkit-scrollbar\]\:hidden::-webkit-scrollbar {
|
|
4472
4544
|
display: none;
|
|
4473
4545
|
}
|
|
4546
|
+
.\[\&\>\*\]\:hidden > * {
|
|
4547
|
+
display: none;
|
|
4548
|
+
}
|
|
4474
4549
|
.\[\&\>\*\]\:min-w-0 > * {
|
|
4475
4550
|
min-width: 0px;
|
|
4476
4551
|
}
|
|
4552
|
+
@container (width < 72rem) {
|
|
4553
|
+
.\@max-6xl\:\[\&\>\*\]\:hidden > * {
|
|
4554
|
+
display: none;
|
|
4555
|
+
}
|
|
4556
|
+
}
|
|
4557
|
+
@container (width < 56rem) {
|
|
4558
|
+
.\@max-4xl\:\[\&\>\*\]\:hidden > * {
|
|
4559
|
+
display: none;
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
@container (width < 42rem) {
|
|
4563
|
+
.\@max-2xl\:\[\&\>\*\]\:hidden > * {
|
|
4564
|
+
display: none;
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4477
4567
|
.\[\&\>\:first-child\]\:w-\[var\(--cb-sidebar-side\)\] > :first-child {
|
|
4478
4568
|
width: var(--cb-sidebar-side);
|
|
4479
4569
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.26.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.",
|
|
@@ -706,8 +706,8 @@
|
|
|
706
706
|
"@types/react-dom": "^19.2.3",
|
|
707
707
|
"@vidstack/react": "1.15.6",
|
|
708
708
|
"@vitejs/plugin-react": "^5.2.0",
|
|
709
|
-
"cursedbelt-server": "^4.
|
|
710
|
-
"cursedops": "^0.
|
|
709
|
+
"cursedbelt-server": "^4.40.1",
|
|
710
|
+
"cursedops": "^0.11.0",
|
|
711
711
|
"diff": "^9.0.0",
|
|
712
712
|
"emoji-mart": "^5.6.0",
|
|
713
713
|
"hls.js": "^1.5.0",
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔴 Every Badge tone draws a VISIBLE capsule on every surface a card puts it on (task 2286).
|
|
3
|
+
*
|
|
4
|
+
* DESIGN.md §3: a status tag is a "filled capsule pill shape … never thin low-visibility unstyled
|
|
5
|
+
* outlines". The judge that scores a badge's TEXT (the `-strong` tokens) never scored the capsule
|
|
6
|
+
* against the surface UNDER it, and on 2026-09-28 station's Connections page drew its `neutral`
|
|
7
|
+
* pills as bare text beside a dot on a `Surface level="raised"`: `--muted` #1f2937 on
|
|
8
|
+
* `--card-raised` #0f1929 is 1.2:1. station patched it app-locally with `tone="outline"`.
|
|
9
|
+
*
|
|
10
|
+
* The measure is CIEDE2000 — perceived colour difference — not the WCAG luminance ratio: a pale
|
|
11
|
+
* emerald tint on white is a plainly visible capsule at 1.13:1 luminance, while a grey one at
|
|
12
|
+
* 1.10:1 is not, and only hue tells them apart. Measured over the four scopes theme.css ships
|
|
13
|
+
* (default and aurora, light and dark), the fill-to-surface ΔE00 of the neutral grey is 2.3–5.3
|
|
14
|
+
* (5.3 being the raised dark card the owner saw as bare text) and of the categorical hues 5.5–20.
|
|
15
|
+
* So the floor is {@link CAPSULE_FLOOR}.
|
|
16
|
+
*
|
|
17
|
+
* A tone passes on its EDGE: its border colour when it draws one, else its fill — each composited
|
|
18
|
+
* over the surface first, because `bg-success/15` and the dark tones' `color-mix(… transparent)`
|
|
19
|
+
* are translucent.
|
|
20
|
+
*/
|
|
21
|
+
import { describe, expect, test } from 'bun:test';
|
|
22
|
+
import { readFileSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { BADGE_TONE } from './Badge.js';
|
|
25
|
+
|
|
26
|
+
/** ΔE00 an edge must clear against the surface under it. Above the 5.3 that read as no capsule. */
|
|
27
|
+
const CAPSULE_FLOOR = 6;
|
|
28
|
+
const SURFACES = ['card', 'card-raised', 'background'] as const;
|
|
29
|
+
|
|
30
|
+
const css = readFileSync(join(import.meta.dir, '..', '..', 'theme.css'), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
31
|
+
|
|
32
|
+
/** The custom properties declared in EVERY block whose selector is exactly `selector`. */
|
|
33
|
+
function declared(selector: string): Record<string, string> {
|
|
34
|
+
const out: Record<string, string> = {};
|
|
35
|
+
for (const block of css.matchAll(/(^|\n)([^{}\n][^{}]*?)\s*\{([^{}]*)\}/g)) {
|
|
36
|
+
// What precedes a block's `{` back to the last `;` or `}` is its selector list.
|
|
37
|
+
const selectors = ((block[2] ?? '').split(/[;}]/).pop() ?? '').split(',').map((s) => s.trim());
|
|
38
|
+
if (!selectors.includes(selector)) continue;
|
|
39
|
+
for (const m of (block[3] ?? '').matchAll(/--([a-z0-9-]+):\s*([^;]+);/g)) out[m[1] as string] = (m[2] as string).trim();
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const root = declared(':root');
|
|
45
|
+
const dark = { ...root, ...declared('.dark') };
|
|
46
|
+
const SCOPES: Record<string, Record<string, string>> = {
|
|
47
|
+
light: root,
|
|
48
|
+
dark,
|
|
49
|
+
'aurora light': { ...root, ...declared('.cb-aurora') },
|
|
50
|
+
'aurora dark': { ...dark, ...declared('.cb-aurora'), ...declared('.dark .cb-aurora') },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type Rgba = [number, number, number, number];
|
|
54
|
+
|
|
55
|
+
function hex(text: string): Rgba {
|
|
56
|
+
let h = text.slice(1);
|
|
57
|
+
if (h.length === 3) h = [...h].map((c) => c + c).join('');
|
|
58
|
+
return [Number.parseInt(h.slice(0, 2), 16), Number.parseInt(h.slice(2, 4), 16), Number.parseInt(h.slice(4, 6), 16), 1];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function color(scope: Record<string, string>, token: string): Rgba {
|
|
62
|
+
const value = scope[token];
|
|
63
|
+
if (value === undefined) throw new Error(`theme.css declares no --${token} in this scope`);
|
|
64
|
+
const alias = /^var\(--([a-z0-9-]+)\)$/.exec(value);
|
|
65
|
+
if (alias) return color(scope, alias[1] as string);
|
|
66
|
+
const mix = /^color-mix\(in srgb, (#[0-9a-f]{3,6}) (\d+)%, transparent\)$/i.exec(value);
|
|
67
|
+
if (mix) return [...hex(mix[1] as string).slice(0, 3), Number(mix[2]) / 100] as Rgba;
|
|
68
|
+
if (/^#[0-9a-f]{3,6}$/i.test(value)) return hex(value);
|
|
69
|
+
throw new Error(`--${token}: "${value}" is a colour this spec cannot read — teach it, never skip the tone`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const over = (fg: Rgba, bg: Rgba): Rgba => [0, 1, 2].map((i) => (fg[i] as number) * fg[3] + (bg[i] as number) * (1 - fg[3])).concat(1) as Rgba;
|
|
73
|
+
|
|
74
|
+
function lab(c: Rgba): [number, number, number] {
|
|
75
|
+
const lin = (channel: number) => {
|
|
76
|
+
const v = channel / 255;
|
|
77
|
+
return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
78
|
+
};
|
|
79
|
+
const [r, g, b] = [lin(c[0]), lin(c[1]), lin(c[2])];
|
|
80
|
+
const f = (t: number) => (t > 216 / 24389 ? Math.cbrt(t) : ((24389 / 27) * t + 16) / 116);
|
|
81
|
+
const x = f((r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047);
|
|
82
|
+
const y = f(r * 0.2126 + g * 0.7152 + b * 0.0722);
|
|
83
|
+
const z = f((r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883);
|
|
84
|
+
return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** CIEDE2000 (Sharma, Wu & Dalal 2005). */
|
|
88
|
+
export function deltaE2000(c1: Rgba, c2: Rgba): number {
|
|
89
|
+
const [L1, a1, b1] = lab(c1);
|
|
90
|
+
const [L2, a2, b2] = lab(c2);
|
|
91
|
+
const rad = Math.PI / 180;
|
|
92
|
+
const Cbar = (Math.hypot(a1, b1) + Math.hypot(a2, b2)) / 2;
|
|
93
|
+
const G = 0.5 * (1 - Math.sqrt(Cbar ** 7 / (Cbar ** 7 + 25 ** 7)));
|
|
94
|
+
const ap1 = a1 * (1 + G);
|
|
95
|
+
const ap2 = a2 * (1 + G);
|
|
96
|
+
const Cp1 = Math.hypot(ap1, b1);
|
|
97
|
+
const Cp2 = Math.hypot(ap2, b2);
|
|
98
|
+
const hue = (b: number, a: number) => (Math.atan2(b, a) / rad + 360) % 360;
|
|
99
|
+
const hp1 = hue(b1, ap1);
|
|
100
|
+
const hp2 = hue(b2, ap2);
|
|
101
|
+
let dh = hp2 - hp1;
|
|
102
|
+
if (Cp1 * Cp2 === 0) dh = 0;
|
|
103
|
+
else if (dh > 180) dh -= 360;
|
|
104
|
+
else if (dh < -180) dh += 360;
|
|
105
|
+
const dH = 2 * Math.sqrt(Cp1 * Cp2) * Math.sin((dh / 2) * rad);
|
|
106
|
+
const Lbar = (L1 + L2) / 2;
|
|
107
|
+
const Cpbar = (Cp1 + Cp2) / 2;
|
|
108
|
+
let hbar = hp1 + hp2;
|
|
109
|
+
if (Cp1 * Cp2 !== 0) hbar = Math.abs(hp1 - hp2) > 180 ? (hp1 + hp2 + 360) / 2 : (hp1 + hp2) / 2;
|
|
110
|
+
const T =
|
|
111
|
+
1 - 0.17 * Math.cos((hbar - 30) * rad) + 0.24 * Math.cos(2 * hbar * rad) + 0.32 * Math.cos((3 * hbar + 6) * rad) - 0.2 * Math.cos((4 * hbar - 63) * rad);
|
|
112
|
+
const SL = 1 + (0.015 * (Lbar - 50) ** 2) / Math.sqrt(20 + (Lbar - 50) ** 2);
|
|
113
|
+
const SC = 1 + 0.045 * Cpbar;
|
|
114
|
+
const SH = 1 + 0.015 * Cpbar * T;
|
|
115
|
+
const RT = -2 * Math.sqrt(Cpbar ** 7 / (Cpbar ** 7 + 25 ** 7)) * Math.sin(60 * Math.exp(-(((hbar - 275) / 25) ** 2)) * rad);
|
|
116
|
+
const dL = (L2 - L1) / SL;
|
|
117
|
+
const dC = (Cp2 - Cp1) / SC;
|
|
118
|
+
const dHs = dH / SH;
|
|
119
|
+
return Math.sqrt(dL ** 2 + dC ** 2 + dHs ** 2 + RT * dC * dHs);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A tone's classes → the colour its capsule's EDGE is drawn in: its `border-*`, else its `bg-*`. */
|
|
123
|
+
function edgeOf(classes: string): { token: string; alpha: number; kind: 'border' | 'fill' } {
|
|
124
|
+
const words = classes.split(/\s+/);
|
|
125
|
+
const paint = (prefix: string) => {
|
|
126
|
+
const word = words.find((w) => w.startsWith(prefix) && w !== 'border');
|
|
127
|
+
if (!word) return null;
|
|
128
|
+
const [token, alpha] = word.slice(prefix.length).split('/');
|
|
129
|
+
return { token: token as string, alpha: alpha === undefined ? 1 : Number(alpha) / 100 };
|
|
130
|
+
};
|
|
131
|
+
const border = words.includes('border') ? paint('border-') : null;
|
|
132
|
+
if (border) return { ...border, kind: 'border' };
|
|
133
|
+
const fill = paint('bg-');
|
|
134
|
+
if (!fill) throw new Error(`"${classes}" draws neither a fill nor a border — no capsule at all`);
|
|
135
|
+
return { ...fill, kind: 'fill' };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Every tone × scope × surface whose capsule edge is under the floor. */
|
|
139
|
+
export function invisibleCapsules(tones: Readonly<Record<string, string>>): string[] {
|
|
140
|
+
const out: string[] = [];
|
|
141
|
+
for (const [tone, classes] of Object.entries(tones)) {
|
|
142
|
+
const edge = edgeOf(classes);
|
|
143
|
+
for (const [name, scope] of Object.entries(SCOPES)) {
|
|
144
|
+
for (const surface of SURFACES) {
|
|
145
|
+
const under = color(scope, surface);
|
|
146
|
+
const paint = color(scope, edge.token);
|
|
147
|
+
const drawn = over([paint[0], paint[1], paint[2], paint[3] * edge.alpha], under);
|
|
148
|
+
const dE = deltaE2000(drawn, under);
|
|
149
|
+
if (dE < CAPSULE_FLOOR) out.push(`${tone} (${edge.kind} --${edge.token}) on ${name} --${surface}: ΔE00 ${dE.toFixed(1)}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
describe('Badge — every tone is a visible capsule (DESIGN.md §3, task 2286)', () => {
|
|
157
|
+
test(`every BADGE_TONE's edge clears ΔE00 ${CAPSULE_FLOOR} on card, card-raised and background in all four scopes`, () => {
|
|
158
|
+
expect(invisibleCapsules(BADGE_TONE)).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('🔴 the check can go red: the pre-fix neutral (a bare --muted fill) is invisible on the dark raised card', () => {
|
|
162
|
+
const found = invisibleCapsules({ neutral: 'bg-muted text-muted-foreground' });
|
|
163
|
+
expect(found).toContain('neutral (fill --muted) on dark --card-raised: ΔE00 5.3');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('a border it draws is scored instead of its fill — and a border that is itself invisible fails', () => {
|
|
167
|
+
expect(invisibleCapsules({ edged: 'bg-muted text-muted-foreground border border-border-strong' })).toEqual([]);
|
|
168
|
+
// Assembled: Tailwind's scanner reads specs too, and a literal here would ship that utility to every app.
|
|
169
|
+
const ghost = ['bg-muted', 'border', ['border', 'muted'].join('-')].join(' ');
|
|
170
|
+
expect(invisibleCapsules({ ghost }).length).toBeGreaterThan(0);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('ΔE00 matches the published reference pair (Sharma et al., pair 1 ≈ 2.04)', () => {
|
|
174
|
+
// Via sRGB the Lab pair is not exact, so the anchor is a sanity bound, not the table value.
|
|
175
|
+
expect(deltaE2000([255, 255, 255, 1], [255, 255, 255, 1])).toBe(0);
|
|
176
|
+
expect(deltaE2000([0, 0, 0, 1], [255, 255, 255, 1])).toBeCloseTo(100, 0);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -17,10 +17,15 @@ import { Tooltip } from './Tooltip.js';
|
|
|
17
17
|
* One map → no second copy to drift. Apps map their own category→tone (ca's
|
|
18
18
|
* `toneForCategory`) onto these names instead of redefining the classes.
|
|
19
19
|
*/
|
|
20
|
-
|
|
20
|
+
/** Every tone's classes — exported for `Badge.capsule.spec.ts`, which scores each capsule against the surfaces under it. */
|
|
21
|
+
export const BADGE_TONE = {
|
|
21
22
|
// ---- Semantic (token-driven) -------------------------------------------
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
// 🔴 The quiet tones draw an EDGE (task 2286): a bare `--muted` fill is ΔE00 2.3–5.3 from the
|
|
24
|
+
// card under it — on station's raised dark card it read as bare text beside a dot, not a
|
|
25
|
+
// capsule. `Badge.capsule.spec.ts` scores every tone's edge against every surface. `outline`
|
|
26
|
+
// is the same shape with full-strength text; these stay the quieter of the two.
|
|
27
|
+
default: 'bg-muted text-muted-foreground border border-border-strong',
|
|
28
|
+
neutral: 'bg-muted text-muted-foreground border border-border-strong', // alias of default
|
|
24
29
|
primary: 'bg-accent-solid text-white',
|
|
25
30
|
accent: 'bg-accent/10 text-accent-strong', // soft accent tint (vs. primary's solid fill); -strong per cbux-101
|
|
26
31
|
// Text rides the `-strong` step of each status hue: the base hue as text on
|
|
@@ -44,13 +49,16 @@ const BADGE_TONE = {
|
|
|
44
49
|
// The hues now live in theme.css as `--tone-<hue>-bg/-fg`; the paint is
|
|
45
50
|
// identical, the ownership is not. See DESIGN.md §3.
|
|
46
51
|
emerald: 'bg-tone-emerald-bg text-tone-emerald-fg',
|
|
47
|
-
sky:
|
|
52
|
+
// sky and slate carry a hairline in their own hue, like the status tones: their fills sit
|
|
53
|
+
// under the capsule floor on the aurora canvas (sky ΔE00 5.5) and the raised dark card
|
|
54
|
+
// (slate 5.0) — see `Badge.capsule.spec.ts`. The other hues clear it on fill alone.
|
|
55
|
+
sky: 'bg-tone-sky-bg text-tone-sky-fg border border-tone-sky-fg/30',
|
|
48
56
|
cyan: 'bg-tone-cyan-bg text-tone-cyan-fg',
|
|
49
57
|
amber: 'bg-tone-amber-bg text-tone-amber-fg',
|
|
50
58
|
purple: 'bg-tone-purple-bg text-tone-purple-fg',
|
|
51
59
|
rose: 'bg-tone-rose-bg text-tone-rose-fg',
|
|
52
60
|
orange: 'bg-tone-orange-bg text-tone-orange-fg',
|
|
53
|
-
slate: 'bg-tone-slate-bg text-tone-slate-fg',
|
|
61
|
+
slate: 'bg-tone-slate-bg text-tone-slate-fg border border-tone-slate-fg/30',
|
|
54
62
|
// Four more, 2026-08-20 (unit 15) — see theme.css. Eight hues is enough while a tone
|
|
55
63
|
// means a badge and not enough when the hue is an identity CHANNEL (arcade's Crowns
|
|
56
64
|
// tells ten regions apart by colour alone).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DragEvent } from 'react';
|
|
1
|
+
import type { DragEvent, ReactNode } from 'react';
|
|
2
2
|
import { cn } from '../../lib/cn.js';
|
|
3
3
|
import { Grid } from '../Grid.js';
|
|
4
4
|
import { Stack } from '../layout.js';
|
|
@@ -17,6 +17,10 @@ export interface CardViewProps<TRow extends Record<string, unknown>> {
|
|
|
17
17
|
dragPropsFor?: (
|
|
18
18
|
row: TRow,
|
|
19
19
|
) => { draggable?: boolean; onDragStart?: (event: DragEvent<HTMLElement>) => void };
|
|
20
|
+
/** The row's selection checkbox (`DataTable`'s `selectedIds`), rendered at the
|
|
21
|
+
* card's top-left, outside the open-the-row button. `index` is the row's
|
|
22
|
+
* position in `data`. */
|
|
23
|
+
selectFor?: (row: TRow, index: number) => ReactNode;
|
|
20
24
|
/** Minimum card width before the grid reflows (container-query based). */
|
|
21
25
|
cardMinWidth?: string;
|
|
22
26
|
className?: string;
|
|
@@ -34,6 +38,7 @@ export function CardView<TRow extends Record<string, unknown>>({
|
|
|
34
38
|
getRowId,
|
|
35
39
|
onRowClick,
|
|
36
40
|
dragPropsFor,
|
|
41
|
+
selectFor,
|
|
37
42
|
cardMinWidth = '16rem',
|
|
38
43
|
className,
|
|
39
44
|
}: CardViewProps<TRow>) {
|
|
@@ -44,6 +49,7 @@ export function CardView<TRow extends Record<string, unknown>>({
|
|
|
44
49
|
const key = getRowId ? getRowId(row) : `card-${i}`;
|
|
45
50
|
const body = <CardRowContent columns={visible} row={row} />;
|
|
46
51
|
const dragProps = dragPropsFor?.(row);
|
|
52
|
+
const select = selectFor?.(row, i);
|
|
47
53
|
return (
|
|
48
54
|
<Surface
|
|
49
55
|
key={key}
|
|
@@ -54,6 +60,7 @@ export function CardView<TRow extends Record<string, unknown>>({
|
|
|
54
60
|
{...dragProps}
|
|
55
61
|
className={cn(dragProps?.draggable && 'cursor-grab active:cursor-grabbing')}
|
|
56
62
|
>
|
|
63
|
+
{select != null ? <div className='-mt-2 -ml-2 mb-1'>{select}</div> : null}
|
|
57
64
|
{onRowClick ? (
|
|
58
65
|
<button
|
|
59
66
|
// guardrails-ignore no-raw-action-button: opens the row's detail view —
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from 'bun:test';
|
|
1
|
+
import { afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
|
2
2
|
import type { Table } from '@tanstack/react-table';
|
|
3
3
|
import { act, cleanup, fireEvent, render, waitFor } from '@testing-library/react';
|
|
4
4
|
import { CardView } from './CardView.js';
|
|
5
5
|
import { setAssetUrlResolver } from './cells/CellRenderer.js';
|
|
6
6
|
import { resolveColumnAlign } from './types.js';
|
|
7
|
-
import { AUTO_TRACK_FLOOR_PX, DataTable, declaredTrackFloor } from './DataTable.js';
|
|
7
|
+
import { AUTO_TRACK_FLOOR_PX, DataTable, declaredTrackFloor, preloadDataTableParts } from './DataTable.js';
|
|
8
8
|
import type { ColDef } from './types.js';
|
|
9
9
|
import { TableViewToggle } from './TableViewToggle.js';
|
|
10
10
|
import { ViewOptionsMenu } from './ViewOptionsMenu.js';
|
|
11
11
|
import { hoverHint } from '../../../testing/tooltip.js';
|
|
12
12
|
|
|
13
|
+
// The toolbar, pager and row menu are lazy (forge task 2309); loaded here, they draw on the
|
|
14
|
+
// first render, as these specs query them. `lazyPart.spec.tsx` covers the lazy path itself.
|
|
15
|
+
beforeAll(preloadDataTableParts);
|
|
16
|
+
|
|
13
17
|
afterEach(cleanup);
|
|
14
18
|
|
|
15
19
|
interface Person extends Record<string, unknown> {
|