@rebasepro/ui 0.12.1-canary.gf5f1d39 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/components/Button.d.ts +2 -1
  2. package/dist/components/Menubar.d.ts +9 -2
  3. package/dist/components/SearchBar.d.ts +1 -1
  4. package/dist/components/VirtualTable/VirtualTable.d.ts +12 -6
  5. package/dist/components/VirtualTable/fields/VirtualTableDateField.d.ts +1 -0
  6. package/dist/components/index.d.ts +0 -1
  7. package/dist/icons/GitHubIcon.d.ts +1 -1
  8. package/dist/icons/HandleIcon.d.ts +3 -1
  9. package/dist/index.css +29 -61
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.es.js +484 -298
  12. package/dist/index.es.js.map +1 -1
  13. package/dist/src/index.css +29 -61
  14. package/dist/src/theme.css +109 -0
  15. package/dist/styles.d.ts +28 -0
  16. package/dist/theme.css +109 -0
  17. package/dist/util/chip_colors.d.ts +29 -3
  18. package/dist/views/CollectionView/index.d.ts +0 -6
  19. package/dist/views/Kanban/BoardColumn.d.ts +1 -0
  20. package/dist/views/Kanban/BoardSortableList.d.ts +2 -1
  21. package/dist/views/Kanban/board_types.d.ts +10 -0
  22. package/dist/views/Kanban/placement.d.ts +26 -0
  23. package/dist/views/index.d.ts +0 -1
  24. package/package.json +31 -29
  25. package/src/components/BooleanSwitch.tsx +44 -17
  26. package/src/components/BooleanSwitchWithLabel.tsx +6 -6
  27. package/src/components/Button.tsx +9 -13
  28. package/src/components/Checkbox.tsx +16 -6
  29. package/src/components/Chip.tsx +5 -2
  30. package/src/components/ColorPicker.tsx +56 -43
  31. package/src/components/DateTimeField.tsx +18 -6
  32. package/src/components/DialogActions.tsx +3 -1
  33. package/src/components/IconButton.tsx +35 -6
  34. package/src/components/Menubar.tsx +19 -3
  35. package/src/components/MultiSelect.tsx +9 -5
  36. package/src/components/SearchBar.tsx +6 -4
  37. package/src/components/Select.tsx +6 -6
  38. package/src/components/TextField.tsx +17 -7
  39. package/src/components/VirtualTable/VirtualTable.performance.test.tsx +35 -341
  40. package/src/components/VirtualTable/VirtualTable.tsx +19 -1
  41. package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +6 -1
  42. package/src/components/index.tsx +2 -1
  43. package/src/icons/GitHubIcon.tsx +11 -5
  44. package/src/icons/HandleIcon.tsx +15 -2
  45. package/src/index.css +29 -61
  46. package/src/index.ts +2 -1
  47. package/src/styles.ts +47 -0
  48. package/src/theme.css +109 -0
  49. package/src/util/chip_colors.ts +109 -67
  50. package/src/views/CollectionView/index.ts +6 -11
  51. package/src/views/Kanban/Board.tsx +80 -117
  52. package/src/views/Kanban/BoardColumn.tsx +3 -0
  53. package/src/views/Kanban/BoardSortableList.tsx +16 -4
  54. package/src/views/Kanban/board_types.ts +10 -0
  55. package/src/views/Kanban/placement.ts +62 -0
  56. package/src/views/index.ts +1 -1
  57. package/dist/views/TableView.d.ts +0 -6
  58. package/src/views/TableView.tsx +0 -7
package/src/index.css CHANGED
@@ -1,57 +1,15 @@
1
- @theme {
2
- /* Font Families */
3
- --font-sans: 'Rubik', 'Roboto', 'Helvetica', 'Arial', sans-serif;
4
- --font-headers: 'Rubik', 'Roboto', 'Helvetica', 'Arial', sans-serif;
5
- --font-mono: 'JetBrains Mono', 'Space Mono', 'Lucida Console', monospace;
6
-
7
- /* Colors */
8
- --color-primary: #0070F4;
9
- --color-primary-light: oklch(from var(--color-primary) calc(l + 0.15) c h);
10
- --color-primary-dark: oklch(from var(--color-primary) calc(l - 0.15) c h);
11
- --color-secondary: #FF5B79;
12
- --color-secondary-light: oklch(from var(--color-secondary) calc(l + 0.15) c h);
13
- --color-secondary-dark: oklch(from var(--color-secondary) calc(l - 0.15) c h);
14
-
15
- --color-primary-bg: oklch(from var(--color-primary) l c h / 0.1);
16
- --color-secondary-bg: oklch(from var(--color-secondary) l c h / 0.1);
17
-
18
- /* Field Colors */
19
- --color-field-disabled: rgb(224 224 226);
20
- --color-field-disabled-dark: rgb(35 35 37);
21
-
22
- /* Text Colors — using opaque values so icon strokes render solidly */
23
- --color-text-primary: #212121;
24
- --color-text-secondary: #757575;
25
- --color-text-disabled: #9e9e9e;
26
- --color-text-primary-dark: #ffffff;
27
- --color-text-secondary-dark: #a0a0a9;
28
- --color-text-disabled-dark: #757580;
29
-
30
- /* Surface Colors */
31
- --color-surface-50: #fafafa;
32
- --color-surface-100: #f5f5f5;
33
- --color-surface-200: #e5e5e5;
34
- --color-surface-300: #d4d4d4;
35
- --color-surface-400: #a3a3a3;
36
- --color-surface-500: #737373;
37
- --color-surface-600: #404040;
38
- --color-surface-700: #262626;
39
- --color-surface-800: #111111;
40
- --color-surface-900: #0a0a0a;
41
- --color-surface-950: #000000;
42
-
43
- /* Surface Accent Colors */
44
- --color-surface-accent-50: #f8fafc;
45
- --color-surface-accent-100: #f1f5f9;
46
- --color-surface-accent-200: #e2e8f0;
47
- --color-surface-accent-300: #cbd5e1;
48
- --color-surface-accent-400: #94a3b8;
49
- --color-surface-accent-500: #64748b;
50
- --color-surface-accent-600: #475569;
51
- --color-surface-accent-700: #334155;
52
- --color-surface-accent-800: #1e293b;
53
- --color-surface-accent-900: #172033;
54
- --color-surface-accent-950: #0f172a;
1
+ @import "./theme.css";
2
+
3
+ /* Grayscale antialiasing, not macOS's subpixel default.
4
+ This is the single largest reason the same typeface can look like a
5
+ different one between two pages: subpixel-antialiased text renders visibly
6
+ heavier and slightly fuzzier, and the effect is worst for light text on dark
7
+ backgrounds — which is most of this product and all of the marketing site.
8
+ Design comps that set this and shipped pages that do not will never match,
9
+ no matter how carefully the font, size, weight and tracking are aligned. */
10
+ html {
11
+ -webkit-font-smoothing: antialiased;
12
+ -moz-osx-font-smoothing: grayscale;
55
13
  }
56
14
 
57
15
  /* Native UI (scrollbars, form controls) must follow the active theme,
@@ -74,32 +32,42 @@ html.dark {
74
32
  scrollbar-width: none; /* Firefox */
75
33
  }
76
34
 
35
+ /* Optical ladder: weight comes down as size comes down, tracking tightens as
36
+ size goes up. Seven consecutive levels of semibold was a Rubik-era setting,
37
+ and holding 600 all the way down to text-sm made every heading shout equally
38
+ — a heading stops being a headline somewhere, and the scale should say where.
39
+ 600 is display-scale only; the small end carries 500.
40
+
41
+ With a single face carrying both headings and body, weight and tracking are
42
+ the only things separating a heading from the copy around it — so the tiers
43
+ have to actually differ, rather than all sitting at 600. */
44
+
77
45
  .typography-h1 {
78
- @apply text-4xl font-headers font-semibold tracking-tight;
46
+ @apply text-4xl font-headers font-semibold tracking-display;
79
47
  }
80
48
 
81
49
  .typography-h2 {
82
- @apply text-3xl font-headers font-semibold tracking-tight;
50
+ @apply text-3xl font-headers font-semibold tracking-display;
83
51
  }
84
52
 
85
53
  .typography-h3 {
86
- @apply text-2xl font-headers font-semibold tracking-tight;
54
+ @apply text-2xl font-headers font-semibold tracking-title;
87
55
  }
88
56
 
89
57
  .typography-h4 {
90
- @apply text-xl font-headers font-semibold tracking-[-0.01em];
58
+ @apply text-xl font-headers font-medium tracking-title;
91
59
  }
92
60
 
93
61
  .typography-h5 {
94
- @apply text-lg font-headers font-semibold tracking-[-0.01em];
62
+ @apply text-lg font-headers font-medium tracking-heading;
95
63
  }
96
64
 
97
65
  .typography-h6 {
98
- @apply text-base font-headers font-semibold tracking-[-0.01em];
66
+ @apply text-base font-headers font-medium tracking-heading;
99
67
  }
100
68
 
101
69
  .typography-subtitle1 {
102
- @apply text-sm font-headers font-semibold tracking-[-0.01em];
70
+ @apply text-sm font-headers font-medium tracking-heading;
103
71
  }
104
72
 
105
73
  .typography-subtitle2 {
package/src/index.ts CHANGED
@@ -18,7 +18,8 @@ export * from "./icons";
18
18
  // =============================================================================
19
19
  export * from "./styles";
20
20
  export { cls } from "./util/cls";
21
- export { CHIP_COLORS, getColorSchemeForKey, getColorSchemeForSeed } from "./util/chip_colors";
21
+ export { CHIP_COLORS, CHIP_HUES, CHIP_SEED_KEYS, getColorSchemeForKey, getColorSchemeForSeed } from "./util/chip_colors";
22
+ export type { ChipHue, ChipTone } from "./util/chip_colors";
22
23
 
23
24
  // =============================================================================
24
25
  // Public Hooks
package/src/styles.ts CHANGED
@@ -1,3 +1,50 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Control size scale — the single source of truth for how tall an inline
3
+ // control is.
4
+ //
5
+ // Every control that sits on a line with another control (buttons, text
6
+ // fields, selects, search bars, date fields, icon buttons, checkboxes) resolves
7
+ // its height from this map, so a Button and a TextField at the same `size` are
8
+ // pixel-identical and share a baseline. Before this existed each component
9
+ // carried its own map and `size="large"` meant 42px on a Button and 64px on a
10
+ // TextField.
11
+ //
12
+ // The scale is 28/32/40/48 — the rhythm IconButton and Checkbox already used.
13
+ // `xl`/`2xl` continue above it and are button-only.
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export type ControlSize = "smallest" | "small" | "medium" | "large";
17
+ export type ButtonSize = ControlSize | "xl" | "2xl";
18
+
19
+ export const CONTROL_HEIGHT = {
20
+ smallest: 28,
21
+ small: 32,
22
+ medium: 40,
23
+ large: 48,
24
+ xl: 56,
25
+ "2xl": 64
26
+ } as const satisfies Record<ButtonSize, number>;
27
+
28
+ /** Tailwind min-height class per control size. Keep in sync with CONTROL_HEIGHT. */
29
+ export const controlHeightMixin = {
30
+ smallest: "min-h-[28px]",
31
+ small: "min-h-[32px]",
32
+ medium: "min-h-[40px]",
33
+ large: "min-h-[48px]",
34
+ xl: "min-h-[56px]",
35
+ "2xl": "min-h-[64px]"
36
+ } as const satisfies Record<ButtonSize, string>;
37
+
38
+ /** Horizontal padding paired with each size, so gutters scale with height. */
39
+ export const controlPaddingMixin = {
40
+ smallest: "px-2",
41
+ small: "px-2",
42
+ medium: "px-3",
43
+ large: "px-4",
44
+ xl: "px-6",
45
+ "2xl": "px-10"
46
+ } as const satisfies Record<ButtonSize, string>;
47
+
1
48
  export const focusedDisabled = "focus-visible:ring-0 focus-visible:ring-offset-0";
2
49
  export const focusedInvisibleMixin = "focus:bg-opacity-70 focus:bg-surface-accent-100 focus:dark:bg-white/[0.07] focus:bg-surface-accent-100/70";
3
50
  export const focusedClasses = "z-30 outline-hidden outline-none ring-2 ring-primary ring-opacity-50 ring-primary/50 ring-offset-0 ring-offset-transparent ";
package/src/theme.css ADDED
@@ -0,0 +1,109 @@
1
+ /* ---------------------------------------------------------------------------
2
+ * Design tokens — the single source for fonts, type scale and colour.
3
+ *
4
+ * This lives in its own file for one reason: Tailwind only picks up `@theme`
5
+ * from an UNLAYERED import. Consumers that pull the component styles in with
6
+ * `@import "@rebasepro/ui/index.css" layer(base)` — which the marketing site
7
+ * and the docs both do, to keep component CSS below their own utilities — get
8
+ * the layer wrapped around everything transitively, and the theme silently
9
+ * does not register. That is why the font stacks used to be copy-pasted into
10
+ * every consumer's own `@theme`.
11
+ *
12
+ * Import this file unlayered, alongside the layered index.css:
13
+ *
14
+ * @import "@rebasepro/ui/theme.css";
15
+ * @import "@rebasepro/ui/index.css" layer(base);
16
+ * ------------------------------------------------------------------------- */
17
+
18
+ @theme {
19
+ /* Font Families — Instrument Sans for headings, Inter for UI and body copy. */
20
+ --font-sans: 'Inter Variable', 'Inter', ui-sans-serif, system-ui, 'Helvetica', 'Arial', sans-serif;
21
+ --font-headers: 'Instrument Sans Variable', 'Instrument Sans', 'Inter Variable', 'Inter', ui-sans-serif, system-ui, sans-serif;
22
+ --font-mono: 'JetBrains Mono', 'Space Mono', 'Lucida Console', monospace;
23
+
24
+ /* Type scale — three tiers keyed to RENDERED SIZE, not tag depth.
25
+ Tailwind turns each into a `tracking-*` utility, so the admin panel, the
26
+ console and the marketing site all spell the same intent the same way
27
+ instead of each hardcoding its own em value. Pair display/title with
28
+ font-semibold and heading with font-medium. */
29
+ --tracking-display: -0.025em; /* >= 30px — the headline tier */
30
+ --tracking-title: -0.018em; /* 20-24px — section titles */
31
+ --tracking-heading: -0.01em; /* <= 18px — labels; still a heading, not a headline */
32
+
33
+ /* Sub-`xs` type. MARKETING ONLY — product UI must not go below `text-xs`
34
+ (12px). These exist because the marketing site had ~450 hand-written
35
+ `text-[10px]` / `text-[11px]` classes: the tier was real, it just wasn't
36
+ in the system, so every page improvised it. Naming it stops the drift
37
+ without pretending 10px is acceptable for an admin panel. */
38
+ --text-2xs: 11px;
39
+ --text-2xs--line-height: 1.45;
40
+ --text-3xs: 10px;
41
+ --text-3xs--line-height: 1.4;
42
+
43
+ /* Control heights — the one scale every inline control resolves to, so a
44
+ Button, TextField and Select at the same size share a baseline. Mirrors
45
+ CONTROL_HEIGHT in styles.ts; that is the source for React components,
46
+ these are for everyone else.
47
+
48
+ Marketing pages and docs are plain markup and cannot import a JS mixin,
49
+ so without these they hand-roll their own paddings and drift: the site's
50
+ two hero CTAs sat at 52px and 54px, on no scale and not even matching
51
+ each other. Reach for `min-h-(--control-xl)` instead of a magic number. */
52
+ --control-smallest: 28px;
53
+ --control-small: 32px;
54
+ --control-medium: 40px;
55
+ --control-large: 48px;
56
+ --control-xl: 56px; /* buttons only */
57
+ --control-2xl: 64px; /* buttons only */
58
+
59
+ /* Colors */
60
+ --color-primary: #0070F4;
61
+ --color-primary-light: oklch(from var(--color-primary) calc(l + 0.15) c h);
62
+ --color-primary-dark: oklch(from var(--color-primary) calc(l - 0.15) c h);
63
+ /* rose-600. #FF5B79 was only 2.99:1 against white, so white text on it
64
+ and it as text both failed WCAG AA. Same hue family, 4.70:1. */
65
+ --color-secondary: #E11D48;
66
+ --color-secondary-light: oklch(from var(--color-secondary) calc(l + 0.15) c h);
67
+ --color-secondary-dark: oklch(from var(--color-secondary) calc(l - 0.15) c h);
68
+
69
+ --color-primary-bg: oklch(from var(--color-primary) l c h / 0.1);
70
+ --color-secondary-bg: oklch(from var(--color-secondary) l c h / 0.1);
71
+
72
+ /* Field Colors */
73
+ --color-field-disabled: rgb(224 224 226);
74
+ --color-field-disabled-dark: rgb(35 35 37);
75
+
76
+ /* Text Colors — using opaque values so icon strokes render solidly */
77
+ --color-text-primary: #212121;
78
+ --color-text-secondary: #757575;
79
+ --color-text-disabled: #9e9e9e;
80
+ --color-text-primary-dark: #ffffff;
81
+ --color-text-secondary-dark: #a0a0a9;
82
+ --color-text-disabled-dark: #757580;
83
+
84
+ /* Surface Colors */
85
+ --color-surface-50: #fafafa;
86
+ --color-surface-100: #f5f5f5;
87
+ --color-surface-200: #e5e5e5;
88
+ --color-surface-300: #d4d4d4;
89
+ --color-surface-400: #a3a3a3;
90
+ --color-surface-500: #737373;
91
+ --color-surface-600: #404040;
92
+ --color-surface-700: #262626;
93
+ --color-surface-800: #111111;
94
+ --color-surface-900: #0a0a0a;
95
+ --color-surface-950: #000000;
96
+
97
+ /* Surface Accent Colors */
98
+ --color-surface-accent-50: #f8fafc;
99
+ --color-surface-accent-100: #f1f5f9;
100
+ --color-surface-accent-200: #e2e8f0;
101
+ --color-surface-accent-300: #cbd5e1;
102
+ --color-surface-accent-400: #94a3b8;
103
+ --color-surface-accent-500: #64748b;
104
+ --color-surface-accent-600: #475569;
105
+ --color-surface-accent-700: #334155;
106
+ --color-surface-accent-800: #1e293b;
107
+ --color-surface-accent-900: #172033;
108
+ --color-surface-accent-950: #0f172a;
109
+ }
@@ -9,78 +9,120 @@ export type ChipColorScheme = {
9
9
  darkText?: string;
10
10
  }
11
11
 
12
- export const CHIP_COLORS: Record<string, ChipColorScheme> = {
13
- blue: { color: "#cfdfff",
14
- text: "#102046",
15
- darkColor: "#2750ae",
16
- darkText: "#cfdfff" },
17
- teal: { color: "#c2f5e9",
18
- text: "#012524",
19
- darkColor: "#06a09b",
20
- darkText: "#daf3e9" },
21
- yellow: { color: "#ffeab6",
22
- text: "#3b2501",
23
- darkColor: "#b87503",
24
- darkText: "#ffeab6" },
25
- pink: { color: "#ffdaf6",
26
- text: "#400832",
27
- darkColor: "#b2158b",
28
- darkText: "#ffdaf6" },
29
- purple: { color: "#ede2fe",
30
- text: "#280b42",
31
- darkColor: "#6b1cb0",
32
- darkText: "#ede2fe" },
33
- cyan: { color: "#d0f0fd",
34
- text: "#04283f",
35
- darkColor: "#0b76b7",
36
- darkText: "#d0f0fd" },
37
- orange: { color: "#fee2d5",
38
- text: "#6b2613",
39
- darkColor: "#d74d26",
40
- darkText: "#fee2d5" },
41
- green: { color: "#d1f7c4",
42
- text: "#0b1d05",
43
- darkColor: "#338a17",
44
- darkText: "#d1f7c4" },
45
- red: { color: "#ffdce5",
46
- text: "#4c0c1c",
47
- darkColor: "#ba1e45",
48
- darkText: "#ffdce5" },
49
- gray: { color: "#eee",
50
- text: "#040404",
51
- darkColor: "#444",
52
- darkText: "#eee" },
53
- indigo: { color: "#e0e7ff",
54
- text: "#312e81",
55
- darkColor: "#4f46e5",
56
- darkText: "#e0e7ff" },
57
- violet: { color: "#ede9fe",
58
- text: "#4c1d95",
59
- darkColor: "#7c3aed",
60
- darkText: "#ede9fe" },
61
- fuchsia: { color: "#fae8ff",
62
- text: "#701a75",
63
- darkColor: "#c026d3",
64
- darkText: "#fae8ff" },
65
- rose: { color: "#ffe4e6",
66
- text: "#881337",
67
- darkColor: "#e11d48",
68
- darkText: "#ffe4e6" },
69
- emerald: { color: "#d1fae5",
70
- text: "#064e3b",
71
- darkColor: "#059669",
72
- darkText: "#d1fae5" }
12
+ /**
13
+ * The hues a chip can take, and the four stops each one is built from.
14
+ *
15
+ * `pale` and `mid` are backgrounds for dark text; `solid` and `deep` are
16
+ * backgrounds for light text. `text` is the dark ink used on the two pale
17
+ * stops, and `onDeep` the light ink on `deep` — it defaults to `pale`, and is
18
+ * only set where a hue needs a tint of its own to stay legible.
19
+ */
20
+ type HueStops = {
21
+ pale: string;
22
+ mid: string;
23
+ solid: string;
24
+ deep: string;
25
+ text: string;
26
+ onDeep?: string;
27
+ }
28
+
29
+ export const CHIP_HUES = [
30
+ "blue", "cyan", "teal", "green", "yellow", "orange", "red", "pink", "purple", "gray",
31
+ "indigo", "violet", "fuchsia", "rose", "emerald"
32
+ ] as const;
33
+
34
+ export type ChipHue = typeof CHIP_HUES[number];
35
+
36
+ /**
37
+ * How light or saturated a chip of a given hue is.
38
+ *
39
+ * A hue on its own (`"blue"`) means `Lighter` — the tone the picker offers and
40
+ * the one seeded chips used to be limited to.
41
+ */
42
+ export type ChipTone = "Lighter" | "Light" | "Dark" | "Darker";
43
+
44
+ export type ChipColorKey = ChipHue | `${ChipHue}${ChipTone}`;
45
+
46
+ const HUE_STOPS: Record<ChipHue, HueStops> = {
47
+ blue: { pale: "#cfdfff", mid: "#9cc7ff", solid: "#2d7ff9", deep: "#2750ae", text: "#102046" },
48
+ cyan: { pale: "#d0f0fd", mid: "#77d1f3", solid: "#18bfff", deep: "#0b76b7", text: "#04283f" },
49
+ teal: { pale: "#c2f5e9", mid: "#72ddc3", solid: "#20d9d2", deep: "#06a09b", text: "#012524", onDeep: "#daf3e9" },
50
+ green: { pale: "#d1f7c4", mid: "#93e088", solid: "#20c933", deep: "#338a17", text: "#0b1d05" },
51
+ yellow: { pale: "#ffeab6", mid: "#ffd66e", solid: "#fcb400", deep: "#b87503", text: "#3b2501" },
52
+ orange: { pale: "#fee2d5", mid: "#ffa981", solid: "#ff6f2c", deep: "#d74d26", text: "#6b2613" },
53
+ red: { pale: "#ffdce5", mid: "#ff9eb7", solid: "#f82b60", deep: "#ba1e45", text: "#4c0c1c" },
54
+ pink: { pale: "#ffdaf6", mid: "#f99de2", solid: "#ff08c2", deep: "#b2158b", text: "#400832" },
55
+ purple: { pale: "#ede2fe", mid: "#cdb0ff", solid: "#8b46ff", deep: "#6b1cb0", text: "#280b42" },
56
+ gray: { pale: "#eeeeee", mid: "#cccccc", solid: "#666666", deep: "#444444", text: "#040404" },
57
+ indigo: { pale: "#e0e7ff", mid: "#a5b4fc", solid: "#6366f1", deep: "#4f46e5", text: "#312e81" },
58
+ violet: { pale: "#ede9fe", mid: "#c4b5fd", solid: "#8b5cf6", deep: "#7c3aed", text: "#4c1d95" },
59
+ fuchsia: { pale: "#fae8ff", mid: "#f0abfc", solid: "#d946ef", deep: "#c026d3", text: "#701a75" },
60
+ rose: { pale: "#ffe4e6", mid: "#fda4af", solid: "#f43f5e", deep: "#e11d48", text: "#881337" },
61
+ emerald: { pale: "#d1fae5", mid: "#6ee7b7", solid: "#10b981", deep: "#059669", text: "#064e3b" }
73
62
  };
74
63
 
75
- export type ChipColorKey = keyof typeof CHIP_COLORS;
64
+ function tone(stops: HueStops, chipTone: ChipTone): ChipColorScheme {
65
+ const onDeep = stops.onDeep ?? stops.pale;
66
+ switch (chipTone) {
67
+ case "Light":
68
+ return { color: stops.mid, text: stops.text, darkColor: stops.solid, darkText: "#fff" };
69
+ case "Dark":
70
+ return { color: stops.solid, text: "#fff", darkColor: stops.solid, darkText: "#fff" };
71
+ case "Darker":
72
+ return { color: stops.deep, text: onDeep, darkColor: stops.deep, darkText: onDeep };
73
+ case "Lighter":
74
+ default:
75
+ return { color: stops.pale, text: stops.text, darkColor: stops.deep, darkText: onDeep };
76
+ }
77
+ }
78
+
79
+ const CHIP_TONES: ChipTone[] = ["Lighter", "Light", "Dark", "Darker"];
80
+
81
+ function buildChipColors(): Record<ChipColorKey, ChipColorScheme> {
82
+ const colors = {} as Record<ChipColorKey, ChipColorScheme>;
83
+ for (const hue of CHIP_HUES) {
84
+ const stops = HUE_STOPS[hue];
85
+ for (const chipTone of CHIP_TONES) {
86
+ colors[`${hue}${chipTone}` as ChipColorKey] = tone(stops, chipTone);
87
+ }
88
+ // The bare hue is the lightest tone, which is what every existing
89
+ // `color: "blue"` in a collection config already means.
90
+ colors[hue] = tone(stops, "Lighter");
91
+ }
92
+ return colors;
93
+ }
94
+
95
+ /**
96
+ * Every chip scheme, keyed by hue and tone.
97
+ *
98
+ * This table used to hold four tones per hue and was flattened to one, which
99
+ * left `blueDark`, `redDarker` and friends resolving to `undefined` — a chip
100
+ * with a colour in its config rendering with no colour at all — and left seeded
101
+ * chips picking from ten schemes, so a five-value enum routinely drew the same
102
+ * background three times. The tones are generated from {@link HUE_STOPS} now,
103
+ * so a new hue brings its whole family with it.
104
+ */
105
+ export const CHIP_COLORS: Record<ChipColorKey, ChipColorScheme> = buildChipColors();
106
+
107
+ /**
108
+ * The keys a seeded chip may be assigned, in a stable order.
109
+ *
110
+ * The bare-hue aliases are excluded: they are the same scheme as `<hue>Lighter`
111
+ * and would make the palest tone twice as likely as any other.
112
+ */
113
+ export const CHIP_SEED_KEYS: ChipColorKey[] = CHIP_HUES.flatMap(
114
+ hue => CHIP_TONES.map(chipTone => `${hue}${chipTone}` as ChipColorKey));
76
115
 
77
- export function getColorSchemeForKey(key: ChipColorKey): ChipColorScheme {
78
- return CHIP_COLORS[key];
116
+ // `string & {}` keeps the known keys in autocomplete while still accepting the
117
+ // arbitrary column colours a board hands over.
118
+ export function getColorSchemeForKey(key: ChipColorKey | (string & {})): ChipColorScheme {
119
+ // An unknown key is a config that names a colour this build does not have —
120
+ // a renamed hue, or a value typed by hand. Seeding from the key keeps it
121
+ // coloured and keeps it stable, rather than dropping it to no colour.
122
+ return CHIP_COLORS[key as ChipColorKey] ?? getColorSchemeForSeed(String(key));
79
123
  }
80
124
 
81
125
  export function getColorSchemeForSeed(seed: string): ChipColorScheme {
82
126
  const hash: number = hashString(seed);
83
- const colorKeys = Object.keys(CHIP_COLORS);
84
- const index = hash % colorKeys.length;
85
- return CHIP_COLORS[colorKeys[index]];
127
+ return CHIP_COLORS[CHIP_SEED_KEYS[hash % CHIP_SEED_KEYS.length]];
86
128
  }
@@ -23,22 +23,17 @@ export type {
23
23
  } from "./CollectionViewTypes";
24
24
 
25
25
  // Components
26
+ //
27
+ // `CollectionView` is the entry point. The per-mode views
28
+ // (table/card/list/kanban), the toolbar and the default cell renderer are
29
+ // internal: `CollectionView` selects and wires them from `mode`, and nothing
30
+ // outside this package consumed them directly. Their prop types stay exported
31
+ // so callers can still type overrides.
26
32
  export { CollectionView } from "./CollectionView";
27
33
  export type { CollectionViewProps } from "./CollectionView";
28
34
 
29
- export { CollectionViewToolbar } from "./CollectionViewToolbar";
30
35
  export type { CollectionViewToolbarProps } from "./CollectionViewToolbar";
31
-
32
- export { CollectionTableView } from "./CollectionTableView";
33
36
  export type { CollectionTableViewProps } from "./CollectionTableView";
34
-
35
- export { CollectionCardView } from "./CollectionCardView";
36
37
  export type { CollectionCardViewProps } from "./CollectionCardView";
37
-
38
- export { CollectionListView } from "./CollectionListView";
39
38
  export type { CollectionListViewProps } from "./CollectionListView";
40
-
41
- export { CollectionKanbanView } from "./CollectionKanbanView";
42
39
  export type { CollectionKanbanViewProps } from "./CollectionKanbanView";
43
-
44
- export { DefaultCellRenderer } from "./DefaultCellRenderer";