@rebasepro/ui 0.17.3 → 0.18.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 (127) hide show
  1. package/README.md +4 -0
  2. package/dist/components/MultiSelect.d.ts +10 -0
  3. package/dist/index.es.js +2 -1
  4. package/dist/index.es.js.map +1 -1
  5. package/package.json +32 -23
  6. package/src/components/Alert.tsx +0 -118
  7. package/src/components/Autocomplete.tsx +0 -93
  8. package/src/components/Avatar.tsx +0 -73
  9. package/src/components/Badge.tsx +0 -44
  10. package/src/components/BooleanSwitch.tsx +0 -116
  11. package/src/components/BooleanSwitchWithLabel.tsx +0 -120
  12. package/src/components/Button.tsx +0 -110
  13. package/src/components/Card.tsx +0 -43
  14. package/src/components/CenteredView.tsx +0 -35
  15. package/src/components/Checkbox.tsx +0 -129
  16. package/src/components/Chip.tsx +0 -131
  17. package/src/components/CircularProgress.tsx +0 -53
  18. package/src/components/CircularProgressCenter.tsx +0 -27
  19. package/src/components/Collapse.tsx +0 -66
  20. package/src/components/ColorPicker.tsx +0 -140
  21. package/src/components/Container.tsx +0 -51
  22. package/src/components/DateTimeField.tsx +0 -363
  23. package/src/components/DebouncedTextField.tsx +0 -76
  24. package/src/components/Dialog.tsx +0 -159
  25. package/src/components/DialogActions.tsx +0 -29
  26. package/src/components/DialogContent.tsx +0 -27
  27. package/src/components/DialogTitle.tsx +0 -41
  28. package/src/components/ErrorBoundary.tsx +0 -236
  29. package/src/components/ExpandablePanel.tsx +0 -130
  30. package/src/components/FileUpload.tsx +0 -103
  31. package/src/components/FilterChip.tsx +0 -89
  32. package/src/components/IconButton.tsx +0 -101
  33. package/src/components/InfoLabel.tsx +0 -23
  34. package/src/components/InputLabel.tsx +0 -40
  35. package/src/components/Label.tsx +0 -32
  36. package/src/components/LoadingButton.tsx +0 -32
  37. package/src/components/Markdown.tsx +0 -55
  38. package/src/components/Menu.tsx +0 -114
  39. package/src/components/Menubar.tsx +0 -355
  40. package/src/components/MultiSelect.tsx +0 -433
  41. package/src/components/Paper.tsx +0 -23
  42. package/src/components/Popover.tsx +0 -162
  43. package/src/components/RadioGroup.tsx +0 -74
  44. package/src/components/ResizablePanels.tsx +0 -189
  45. package/src/components/SearchBar.tsx +0 -136
  46. package/src/components/Select.tsx +0 -326
  47. package/src/components/Separator.tsx +0 -28
  48. package/src/components/Sheet.tsx +0 -135
  49. package/src/components/Skeleton.tsx +0 -54
  50. package/src/components/Slider.tsx +0 -110
  51. package/src/components/Table.tsx +0 -178
  52. package/src/components/Tabs.tsx +0 -185
  53. package/src/components/TextField.tsx +0 -270
  54. package/src/components/TextareaAutosize.tsx +0 -368
  55. package/src/components/ToggleButtonGroup.tsx +0 -69
  56. package/src/components/Tooltip.tsx +0 -153
  57. package/src/components/Typography.tsx +0 -132
  58. package/src/components/VirtualTable/VirtualTable.performance.test.tsx +0 -90
  59. package/src/components/VirtualTable/VirtualTable.tsx +0 -700
  60. package/src/components/VirtualTable/VirtualTableCell.tsx +0 -59
  61. package/src/components/VirtualTable/VirtualTableHeader.tsx +0 -327
  62. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +0 -250
  63. package/src/components/VirtualTable/VirtualTableProps.tsx +0 -339
  64. package/src/components/VirtualTable/VirtualTableRow.tsx +0 -51
  65. package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +0 -44
  66. package/src/components/VirtualTable/fields/VirtualTableInput.tsx +0 -89
  67. package/src/components/VirtualTable/fields/VirtualTableNumberInput.tsx +0 -71
  68. package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +0 -107
  69. package/src/components/VirtualTable/fields/VirtualTableSwitch.tsx +0 -28
  70. package/src/components/VirtualTable/index.tsx +0 -10
  71. package/src/components/VirtualTable/selection/SelectionContext.tsx +0 -36
  72. package/src/components/VirtualTable/selection/SelectionStore.ts +0 -54
  73. package/src/components/VirtualTable/types.tsx +0 -47
  74. package/src/components/common/SelectInputLabel.tsx +0 -23
  75. package/src/components/index.tsx +0 -62
  76. package/src/hooks/PortalContainerContext.tsx +0 -50
  77. package/src/hooks/index.ts +0 -7
  78. package/src/hooks/useDebounceCallback.tsx +0 -21
  79. package/src/hooks/useDebounceValue.tsx +0 -20
  80. package/src/hooks/useDebouncedCallback.ts +0 -39
  81. package/src/hooks/useInjectStyles.tsx +0 -31
  82. package/src/hooks/useOutsideAlerter.tsx +0 -46
  83. package/src/icons/GitHubIcon.tsx +0 -21
  84. package/src/icons/HandleIcon.tsx +0 -23
  85. package/src/icons/Icon.tsx +0 -33
  86. package/src/icons/LucideIconByName.tsx +0 -117
  87. package/src/icons/cool_icon_keys.ts +0 -12
  88. package/src/icons/icon_keys.ts +0 -1936
  89. package/src/icons/index.ts +0 -179
  90. package/src/index.css +0 -169
  91. package/src/index.ts +0 -47
  92. package/src/scripts/fixIconSizes.cjs +0 -92
  93. package/src/scripts/fixIconSizesToConst.cjs +0 -106
  94. package/src/scripts/generateIconKeys.ts +0 -41
  95. package/src/scripts/generateIcons.ts +0 -4
  96. package/src/scripts/saveIconFiles.ts +0 -89
  97. package/src/styles.ts +0 -141
  98. package/src/theme.css +0 -168
  99. package/src/util/chip_colors.ts +0 -242
  100. package/src/util/cls.ts +0 -8
  101. package/src/util/debounce.ts +0 -28
  102. package/src/util/hash.ts +0 -11
  103. package/src/util/index.ts +0 -4
  104. package/src/util/key_to_icon_component.ts +0 -11
  105. package/src/util/lazy_chunk.ts +0 -101
  106. package/src/views/CardView.tsx +0 -281
  107. package/src/views/CollectionView/CollectionCardView.tsx +0 -265
  108. package/src/views/CollectionView/CollectionKanbanView.tsx +0 -246
  109. package/src/views/CollectionView/CollectionListView.tsx +0 -240
  110. package/src/views/CollectionView/CollectionTableView.tsx +0 -229
  111. package/src/views/CollectionView/CollectionView.tsx +0 -450
  112. package/src/views/CollectionView/CollectionViewToolbar.tsx +0 -242
  113. package/src/views/CollectionView/CollectionViewTypes.ts +0 -181
  114. package/src/views/CollectionView/DefaultCellRenderer.tsx +0 -404
  115. package/src/views/CollectionView/index.ts +0 -39
  116. package/src/views/CollectionView/resolveTitleProperty.ts +0 -52
  117. package/src/views/CollectionView/utils.ts +0 -14
  118. package/src/views/Kanban/Board.tsx +0 -384
  119. package/src/views/Kanban/BoardColumn.tsx +0 -138
  120. package/src/views/Kanban/BoardColumnTitle.tsx +0 -47
  121. package/src/views/Kanban/BoardSortableList.tsx +0 -182
  122. package/src/views/Kanban/board_types.ts +0 -80
  123. package/src/views/Kanban/index.tsx +0 -5
  124. package/src/views/Kanban/placement.ts +0 -62
  125. package/src/views/KanbanView.tsx +0 -32
  126. package/src/views/ListView.tsx +0 -296
  127. package/src/views/index.ts +0 -5
@@ -1,89 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { fileURLToPath } from "url";
4
-
5
- export function saveIconFiles(iconKeys: string[]) {
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- const componentsDir = path.join(__dirname, "../icons/components");
8
-
9
- // Clean out old generated components to remove stale files (e.g. Lucide-prefixed duplicates)
10
- if (fs.existsSync(componentsDir)) {
11
- fs.rmSync(componentsDir, { recursive: true });
12
- }
13
- fs.mkdirSync(componentsDir, { recursive: true });
14
-
15
- // Initialise index with manual exports
16
- fs.writeFileSync(path.join(__dirname, "../icons/index.ts"), 'export * from "./icon_keys";\nexport * from "./cool_icon_keys";\nexport * from "./Icon";\nexport * from "./GitHubIcon";\nexport * from "./HandleIcon";\n');
17
-
18
- const generatedComponents = new Set<string>();
19
-
20
- iconKeys.forEach((key: string) => {
21
- if (!key) return;
22
-
23
- // Ensure key works as component name
24
- let componentName = `${key}Icon`;
25
-
26
- // Handle names that might start with numbers like "10k" -> "_10k"
27
- if (/^[0-9]/.test(componentName)) {
28
- componentName = `_${componentName}`;
29
- }
30
-
31
- if (componentName === "Icon") {
32
- return;
33
- }
34
-
35
- if (generatedComponents.has(componentName.toLowerCase())) {
36
- return;
37
- }
38
- generatedComponents.add(componentName.toLowerCase());
39
-
40
- const iconComponent = `import React from "react";
41
- import { IconProps } from "../Icon";
42
- import { ${key} as LucideIcon } from "lucide-react";
43
- import { cls } from "../../util";
44
-
45
- /**
46
- * @group Icons
47
- */
48
- export const ${componentName} = React.forwardRef<SVGSVGElement, IconProps>((props, ref) => {
49
- let sizeInPx: number;
50
- switch (props.size) {
51
- case "smallest": sizeInPx = 16; break;
52
- case "small": sizeInPx = 20; break;
53
- case "medium": sizeInPx = 24; break;
54
- case "large": sizeInPx = 28; break;
55
- default: sizeInPx = typeof props.size === "number" ? props.size : 24;
56
- }
57
-
58
- const colorClassesMapping: Record<string, string> = {
59
- inherit: "",
60
- primary: "text-primary",
61
- success: "text-green-500",
62
- warning: "text-yellow-500",
63
- secondary: "text-secondary",
64
- disabled: "text-text-disabled dark:text-text-disabled-dark",
65
- error: "text-red-500"
66
- };
67
-
68
- return <LucideIcon
69
- ref={ref}
70
- size={sizeInPx}
71
- className={cls(
72
- props.color ? colorClassesMapping[props.color] : "",
73
- "select-none shrink-0",
74
- props.className
75
- )}
76
- style={props.style}
77
- onClick={props.onClick}
78
- />;
79
- });
80
-
81
- ${componentName}.displayName = "${componentName}";
82
- `;
83
-
84
- const filePath = path.join(componentsDir, `${componentName}.tsx`);
85
- fs.writeFileSync(filePath, iconComponent);
86
-
87
- fs.appendFileSync(path.join(__dirname, "../icons/index.ts"), `export * from "./components/${componentName}";\n`, { flag: "a" });
88
- });
89
- }
package/src/styles.ts DELETED
@@ -1,141 +0,0 @@
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
-
48
- export const focusedDisabled = "focus-visible:ring-0 focus-visible:ring-offset-0";
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";
50
-
51
- /**
52
- * The focus ring — the single most-seen interaction state in the product, and
53
- * until now the least tended.
54
- *
55
- * Three dead declarations were removed rather than reshuffled:
56
- * `outline-hidden` + `outline-none` — the same instruction twice.
57
- * `ring-primary` + `ring-primary/50` — the first could never win.
58
- * `ring-opacity-50` — Tailwind v3 syntax. v4 expresses
59
- * opacity with the `/50` slash form, so
60
- * this generated nothing at all.
61
- *
62
- * What is left is the ring that was actually rendering, at 60% rather than 50%:
63
- * on `surface-900` the old value sat close enough to the field's own border to
64
- * be missed at a glance, which is the one thing a focus ring may not be.
65
- * `ring-offset-0` + a transparent offset stay — without them the ring draws a
66
- * white halo on dark surfaces.
67
- */
68
- export const focusedClasses = "z-30 outline-none ring-2 ring-primary/60 ring-offset-0 ring-offset-transparent";
69
-
70
- /**
71
- * Field surfaces.
72
- *
73
- * The inset top hairline is the whole difference between a field that reads as
74
- * a filled rectangle and one that reads as recessed into the surface. It is a
75
- * 1px inset highlight, not a gradient — the same device `.frame` already uses
76
- * on the marketing site, and the flat-surface rule stands.
77
- *
78
- * Light mode gets none: on a white page the highlight has nothing to catch.
79
- */
80
- export const fieldBackgroundMixin = "bg-surface-accent-200/50 dark:bg-white/[0.055] dark:shadow-[inset_0_1px_0_rgb(255_255_255/0.045)]";
81
- export const fieldBackgroundInvisibleMixin = "bg-surface-accent-200/0 dark:bg-white/0";
82
- export const fieldBackgroundDisabledMixin = "bg-surface-accent-200/50 dark:bg-white/[0.03]";
83
-
84
- /**
85
- * `transition-colors` here, because every other interactive surface in the kit
86
- * has it and fields did not — `cardClickableMixin` eases, a TextField snapped.
87
- * On a form of eight fields that difference is the whole feel of the screen.
88
- */
89
- export const fieldBackgroundHoverMixin = "transition-colors duration-150 hover:bg-surface-accent-200/70 hover:dark:bg-white/[0.09]";
90
-
91
- export const defaultBorderMixin = "border-surface-200 dark:border-surface-700/60";
92
-
93
- // ---------------------------------------------------------------------------
94
- // Surfaces: two kinds, and the difference is what the border is for.
95
- //
96
- // A **floating** surface — a menu, a dialog, a popover — sits OVER the page. It
97
- // has to be legible against whatever happens to be underneath it, so its edge
98
- // is definite: solid `surface-700`. That is `paperMixin`.
99
- //
100
- // A **page** surface — a card in the document flow — sits ON the page, and the
101
- // page is already `surface-950`/`surface-900`. A solid edge there reads as a
102
- // box drawn around content rather than as the content having a surface, which
103
- // is why every serious caller was overriding it: 53 `<Card>` sites in the SaaS
104
- // console alone re-declared this border at `/60`, the same value
105
- // `defaultBorderMixin` above has always used. The component was wrong and the
106
- // callers were right, so the component now says what they meant.
107
- // ---------------------------------------------------------------------------
108
- export const paperMixin = "bg-white rounded-lg dark:bg-surface-900 border border-surface-200 dark:border-surface-700";
109
- export const cardMixin = "bg-white dark:bg-surface-900 rounded-xl border border-surface-200 dark:border-surface-700/60";
110
-
111
- /**
112
- * An inset well: code, a query, a log tail, a connection string.
113
- *
114
- * It must read as *recessed into* the surface holding it, which means darker
115
- * than that surface in dark mode — `surface-950` under a `surface-900` card.
116
- * The obvious-looking `surface-800` is a trap: `#111111` is LIGHTER than the
117
- * `#0a0a0a` card around it, so the well appears to float above the thing it is
118
- * set into. Pair with `font-mono`; this mixin carries the surface only.
119
- */
120
- export const codeSurfaceMixin = "bg-surface-100 dark:bg-surface-950 rounded-md";
121
-
122
- /**
123
- * The accent, used as TEXT.
124
- *
125
- * `--color-primary` (#0070F4) is tuned to be a fill — white text on it, it on
126
- * white. Read as text on our dark surfaces it lands at **4.36:1 on a
127
- * `surface-900` card** (measured), which is below AA for body-sized text, and
128
- * every accent link in the product sits on exactly that surface. On the page's
129
- * `surface-950` it only reaches 4.62:1, so the margin was never real.
130
- *
131
- * `--color-primary-light` is the same hue lifted 0.15 in OKLCH lightness and
132
- * measures **7.34:1** on the same card. It is indistinguishable as "the blue"
133
- * and comfortably legible, so dark mode swaps to it for text.
134
- *
135
- * Use this for links, accent labels and any accent-coloured type. It is NOT for
136
- * fills — a filled button keeps `bg-primary`, where the contrast question runs
137
- * the other way and #0070F4 is already correct.
138
- */
139
- export const accentTextMixin = "text-primary dark:text-primary-light";
140
- export const cardClickableMixin = "hover:bg-primary/5 dark:hover:bg-primary/5 cursor-pointer transition-colors duration-150";
141
- export const cardSelectedMixin = "bg-primary-bg/30 dark:bg-primary-bg/10 ring-1 ring-primary/75";
package/src/theme.css DELETED
@@ -1,168 +0,0 @@
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
- /* -0.025em, revised 2026-08-24 from -0.022em. The website's dev-only
30
- TypeDevPanel has shipped `td: -25` as its "SHIPPED" default all along, so
31
- every local preview and every design decision taken in front of one was
32
- made at -0.025em while the built site served -0.022em. Of the panel's
33
- eleven defaults these two — tracking and display weight — were the only
34
- ones that did not match the tokens. Both are now true. */
35
- --tracking-display: -0.025em; /* >= 30px — the headline tier; matches the marketing site */
36
- --tracking-title: -0.018em; /* 20-24px — section titles */
37
- --tracking-heading: -0.01em; /* <= 18px — labels; still a heading, not a headline */
38
-
39
- /* Sub-`xs` type. These exist because the marketing site had ~450
40
- hand-written `text-[10px]` / `text-[11px]` classes: the tier was real, it
41
- just wasn't in the system, so every page improvised it.
42
-
43
- In product UI there is exactly ONE sanctioned use — `typography-micro`,
44
- the uppercase field label above a value — and it earns the exception by
45
- never carrying a sentence. Everything a person actually *reads* stays at
46
- `text-xs` (12px) or above. `--text-3xs` remains marketing-only. */
47
- --text-2xs: 11px;
48
- --text-2xs--line-height: 1.45;
49
- --text-3xs: 10px;
50
- --text-3xs--line-height: 1.4;
51
-
52
- /* Display tier — above `text-4xl`, where the scale stops being a heading
53
- and starts being a statement. Added 2026-08-24.
54
-
55
- Fluid rather than fixed because the same token has to survive four
56
- locales: German and French run 15-25% longer than English, and a fixed
57
- 112px headline that fits "The Postgres you already have" does not fit
58
- "Das Postgres, das Sie bereits haben". The `vw` middle term shrinks
59
- before it can overflow; the `rem` floor protects small screens, where
60
- the type is already proportionally large.
61
-
62
- Both carry their own line-height and tracking: at this size 1.1 reads as
63
- a gap and -0.025em reads as loose. Weight stays on the 600 ceiling —
64
- this tier separates itself by SIZE, which is the whole point of adding
65
- it rather than reaching for 700. */
66
- --text-display-2: clamp(2.25rem, 4.4vw, 4rem); /* 36 -> 64px */
67
- --text-display-2--line-height: 1.0;
68
- --text-display-2--letter-spacing: -0.028em;
69
- /* 7.2vw capped at 7rem was too much: on a wide window the headline filled
70
- the viewport on its own and pushed the install terminal below the fold,
71
- which breaks the rule the hero exists to satisfy (SITE-STORY §5 — the
72
- hero carries a product artifact). Still ~1.7x the 48px it replaced. */
73
- --text-display-1: clamp(2.5rem, 5.2vw, 4.75rem); /* 40 -> 76px */
74
- --text-display-1--line-height: 0.94;
75
- --text-display-1--letter-spacing: -0.032em;
76
-
77
- /* Control heights — the one scale every inline control resolves to, so a
78
- Button, TextField and Select at the same size share a baseline. Mirrors
79
- CONTROL_HEIGHT in styles.ts; that is the source for React components,
80
- these are for everyone else.
81
-
82
- Marketing pages and docs are plain markup and cannot import a JS mixin,
83
- so without these they hand-roll their own paddings and drift: the site's
84
- two hero CTAs sat at 52px and 54px, on no scale and not even matching
85
- each other. Reach for `min-h-(--control-xl)` instead of a magic number. */
86
- --control-smallest: 28px;
87
- --control-small: 32px;
88
- --control-medium: 40px;
89
- --control-large: 48px;
90
- --control-xl: 56px; /* buttons only */
91
- --control-2xl: 64px; /* buttons only */
92
-
93
- /* Colors */
94
- --color-primary: #0070F4;
95
- --color-primary-light: oklch(from var(--color-primary) calc(l + 0.15) c h);
96
- --color-primary-dark: oklch(from var(--color-primary) calc(l - 0.15) c h);
97
- /* rose-600. #FF5B79 was only 2.99:1 against white, so white text on it
98
- and it as text both failed WCAG AA. Same hue family, 4.70:1. */
99
- --color-secondary: #E11D48;
100
- --color-secondary-light: oklch(from var(--color-secondary) calc(l + 0.15) c h);
101
- --color-secondary-dark: oklch(from var(--color-secondary) calc(l - 0.15) c h);
102
-
103
- --color-primary-bg: oklch(from var(--color-primary) l c h / 0.1);
104
- --color-secondary-bg: oklch(from var(--color-secondary) l c h / 0.1);
105
-
106
- /* Field Colors */
107
- --color-field-disabled: rgb(224 224 226);
108
- --color-field-disabled-dark: rgb(35 35 37);
109
-
110
- /* Text Colors — using opaque values so icon strokes render solidly */
111
- --color-text-primary: #212121;
112
- /* #737373, not #757575. Two units, and they are the two that matter.
113
- The app's light page ground is #fafafa, not #ffffff, and #757575 lands at
114
- 4.41:1 there — under AA for the 14px type this token carries. It clears
115
- 4.5 only against pure white, which is not a surface the product actually
116
- uses. Every field label in light mode was failing by a whisker.
117
- #737373 measures 4.54:1 on #fafafa and 4.74:1 on #ffffff, so it passes on
118
- both grounds instead of the theoretical one. Same fix, and the same
119
- reasoning, as the `--color-surface-500` move recorded below. */
120
- --color-text-secondary: #737373;
121
- /* #8b8b8b, not #9e9e9e. Measured against the field fill a disabled label
122
- actually sits on (`surface-accent-200/50` over #fafafa ≈ #eef1f5),
123
- #9e9e9e reached 2.36:1 — present, but not readable.
124
- Full AA is not achievable here and should not be chased: 4.5:1 on that
125
- fill needs #6e6e6e, which is DARKER than the #737373 an ENABLED label
126
- uses, so the disabled state would read as the emphasised one. WCAG 1.4.3
127
- exempts inactive controls for exactly this reason.
128
- #8b8b8b lands at ~3:1 — legible at a glance, still unmistakably muted
129
- against its enabled neighbour. */
130
- --color-text-disabled: #8b8b8b;
131
- --color-text-primary-dark: #ffffff;
132
- --color-text-secondary-dark: #a0a0a9;
133
- --color-text-disabled-dark: #757580;
134
-
135
- /* Surface Colors */
136
- --color-surface-50: #fafafa;
137
- --color-surface-100: #f5f5f5;
138
- --color-surface-200: #e5e5e5;
139
- --color-surface-300: #d4d4d4;
140
- --color-surface-400: #a3a3a3;
141
- /* Muted text: captions, nav descriptions, table headers, eyebrows.
142
- #737373 was tuned for light and measured 4.74:1 on white but only
143
- 4.43:1 on #000 and 4.18:1 on #0a0a0a, so every dark surface — the
144
- marketing site, the console, the admin panel in dark mode — carried a
145
- muted tier below AA. #797979 is the balance point: it is the single
146
- value where the white and #111111 curves cross, at 4.35:1 on each,
147
- and it clears 4.5:1 outright on the two backgrounds that carry most of
148
- this token (4.82:1 on #000, 4.55:1 on #0a0a0a). */
149
- --color-surface-500: #797979;
150
- --color-surface-600: #404040;
151
- --color-surface-700: #262626;
152
- --color-surface-800: #111111;
153
- --color-surface-900: #0a0a0a;
154
- --color-surface-950: #000000;
155
-
156
- /* Surface Accent Colors */
157
- --color-surface-accent-50: #f8fafc;
158
- --color-surface-accent-100: #f1f5f9;
159
- --color-surface-accent-200: #e2e8f0;
160
- --color-surface-accent-300: #cbd5e1;
161
- --color-surface-accent-400: #94a3b8;
162
- --color-surface-accent-500: #64748b;
163
- --color-surface-accent-600: #475569;
164
- --color-surface-accent-700: #334155;
165
- --color-surface-accent-800: #1e293b;
166
- --color-surface-accent-900: #172033;
167
- --color-surface-accent-950: #0f172a;
168
- }
@@ -1,242 +0,0 @@
1
- import { hashString } from "./hash";
2
-
3
- export type ChipColorScheme = {
4
- color: string;
5
- text: string;
6
- /** Background color override for dark mode */
7
- darkColor?: string;
8
- /** Text color override for dark mode */
9
- darkText?: string;
10
- /**
11
- * Ink for the `outlined` variant, which has no fill of its own.
12
- *
13
- * `text`/`darkText` are the ink ON the chip's own background. An outlined
14
- * chip drops that background and sits on the PAGE, so the same value is
15
- * being asked to be legible against two different surfaces at once — and
16
- * for most hues it cannot be. Splitting the roles is what lets the filled
17
- * ink follow its fill (often dark ink on a bright chip) without dragging
18
- * the outlined variant down with it.
19
- */
20
- outlineText?: string;
21
- /** Outlined-variant ink in dark mode. */
22
- darkOutlineText?: string;
23
- }
24
-
25
- /* ────────────────────────────────────────────────────────────────────────────
26
- Contrast
27
- ────────────────────────────────────────────────────────────────────────────
28
-
29
- Chip ink is DERIVED, not hand-picked. It used to be written down per tone —
30
- `"#fff"` on every `solid` background, the hue's `pale` on every `deep` one —
31
- and an audit of all 120 hue/tone/mode pairs found **63 of them below WCAG AA**.
32
- The worst was white on `teal.solid` at **1.76:1**, which is not a near miss;
33
- it is unreadable. The palette is an Airtable-style one whose mid stops are
34
- bright enough to need dark ink, and hardcoding light ink ignored that.
35
-
36
- So the ink is measured against the background it will actually sit on, and
37
- pushed toward black or white only as far as it must go to clear the floor.
38
- Starting from the hue's own dark/light tints rather than from flat `#000`
39
- and `#fff` keeps the family looking like a family: `blue.solid` gets a very
40
- dark navy, not black.
41
-
42
- Deriving it also means a new hue cannot be added below AA — there is no
43
- per-tone ink to forget to check. */
44
-
45
- /** WCAG floor, plus a little margin so rounding cannot drop a pair below 4.5. */
46
- const CONTRAST_TARGET = 4.6;
47
-
48
- /** Page backgrounds the outlined variant sits on: `bg-white` and `surface-950`. */
49
- const PAGE_LIGHT = "#ffffff";
50
- const PAGE_DARK = "#0a0a0a";
51
-
52
- function toRgb(hex: string): [number, number, number] {
53
- let h = hex.replace("#", "");
54
- if (h.length === 3) h = h.split("").map(c => c + c).join("");
55
- return [0, 2, 4].map(i => parseInt(h.slice(i, i + 2), 16)) as [number, number, number];
56
- }
57
-
58
- function toHex(rgb: number[]): string {
59
- return "#" + rgb.map(v => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0")).join("");
60
- }
61
-
62
- function relativeLuminance(hex: string): number {
63
- const [r, g, b] = toRgb(hex).map(v => {
64
- const c = v / 255;
65
- return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
66
- });
67
- return 0.2126 * r + 0.7152 * g + 0.0722 * b;
68
- }
69
-
70
- export function contrastRatio(a: string, b: string): number {
71
- const x = relativeLuminance(a);
72
- const y = relativeLuminance(b);
73
- const [hi, lo] = x > y ? [x, y] : [y, x];
74
- return (hi + 0.05) / (lo + 0.05);
75
- }
76
-
77
- function mixHex(from: string, to: string, t: number): string {
78
- const A = toRgb(from);
79
- const B = toRgb(to);
80
- return toHex([0, 1, 2].map(i => A[i] + (B[i] - A[i]) * t));
81
- }
82
-
83
- /**
84
- * Walk a tinted ink toward black or white until it clears the floor on `bg`.
85
- *
86
- * Stops at the first passing step rather than going all the way, so the ink
87
- * keeps as much of its hue as legibility allows.
88
- */
89
- function push(base: string, toward: string, bg: string): string {
90
- for (let t = 0; t <= 1.0001; t += 0.02) {
91
- const candidate = mixHex(base, toward, t);
92
- if (contrastRatio(candidate, bg) >= CONTRAST_TARGET) return candidate;
93
- }
94
- return toward;
95
- }
96
-
97
- /** The ink for a chip filled with `bg`: whichever direction has more headroom. */
98
- function inkOn(stops: HueStops, bg: string): string {
99
- const dark = push(stops.text, "#000000", bg);
100
- const light = push(stops.onDeep ?? stops.pale, "#ffffff", bg);
101
- return contrastRatio(dark, bg) >= contrastRatio(light, bg) ? dark : light;
102
- }
103
-
104
- /**
105
- * The hues a chip can take, and the four stops each one is built from.
106
- *
107
- * `pale` and `mid` are backgrounds for dark text; `solid` and `deep` are
108
- * backgrounds for light text. `text` is the dark ink used on the two pale
109
- * stops, and `onDeep` the light ink on `deep` — it defaults to `pale`, and is
110
- * only set where a hue needs a tint of its own to stay legible.
111
- */
112
- type HueStops = {
113
- pale: string;
114
- mid: string;
115
- solid: string;
116
- deep: string;
117
- text: string;
118
- onDeep?: string;
119
- }
120
-
121
- export const CHIP_HUES = [
122
- "blue", "cyan", "teal", "green", "yellow", "orange", "red", "pink", "purple", "gray",
123
- "indigo", "violet", "fuchsia", "rose", "emerald"
124
- ] as const;
125
-
126
- export type ChipHue = typeof CHIP_HUES[number];
127
-
128
- /**
129
- * How light or saturated a chip of a given hue is.
130
- *
131
- * A hue on its own (`"blue"`) means `Lighter` — the tone the picker offers and
132
- * the one seeded chips used to be limited to.
133
- */
134
- export type ChipTone = "Lighter" | "Light" | "Dark" | "Darker";
135
-
136
- export type ChipColorKey = ChipHue | `${ChipHue}${ChipTone}`;
137
-
138
- const HUE_STOPS: Record<ChipHue, HueStops> = {
139
- blue: { pale: "#cfdfff", mid: "#9cc7ff", solid: "#2d7ff9", deep: "#2750ae", text: "#102046" },
140
- cyan: { pale: "#d0f0fd", mid: "#77d1f3", solid: "#18bfff", deep: "#0b76b7", text: "#04283f" },
141
- teal: { pale: "#c2f5e9", mid: "#72ddc3", solid: "#20d9d2", deep: "#06a09b", text: "#012524", onDeep: "#daf3e9" },
142
- green: { pale: "#d1f7c4", mid: "#93e088", solid: "#20c933", deep: "#338a17", text: "#0b1d05" },
143
- yellow: { pale: "#ffeab6", mid: "#ffd66e", solid: "#fcb400", deep: "#b87503", text: "#3b2501" },
144
- orange: { pale: "#fee2d5", mid: "#ffa981", solid: "#ff6f2c", deep: "#d74d26", text: "#6b2613" },
145
- red: { pale: "#ffdce5", mid: "#ff9eb7", solid: "#f82b60", deep: "#ba1e45", text: "#4c0c1c" },
146
- pink: { pale: "#ffdaf6", mid: "#f99de2", solid: "#ff08c2", deep: "#b2158b", text: "#400832" },
147
- purple: { pale: "#ede2fe", mid: "#cdb0ff", solid: "#8b46ff", deep: "#6b1cb0", text: "#280b42" },
148
- gray: { pale: "#eeeeee", mid: "#cccccc", solid: "#666666", deep: "#444444", text: "#040404" },
149
- indigo: { pale: "#e0e7ff", mid: "#a5b4fc", solid: "#6366f1", deep: "#4f46e5", text: "#312e81" },
150
- violet: { pale: "#ede9fe", mid: "#c4b5fd", solid: "#8b5cf6", deep: "#7c3aed", text: "#4c1d95" },
151
- fuchsia: { pale: "#fae8ff", mid: "#f0abfc", solid: "#d946ef", deep: "#c026d3", text: "#701a75" },
152
- rose: { pale: "#ffe4e6", mid: "#fda4af", solid: "#f43f5e", deep: "#e11d48", text: "#881337" },
153
- emerald: { pale: "#d1fae5", mid: "#6ee7b7", solid: "#10b981", deep: "#059669", text: "#064e3b" }
154
- };
155
-
156
- /**
157
- * One tone of one hue.
158
- *
159
- * Backgrounds are the palette's, unchanged — the stops were not the problem and
160
- * moving them would have restyled every chip in the product. Only the ink moved,
161
- * and it moved to wherever the measurement says it has to be.
162
- */
163
- function tone(stops: HueStops, chipTone: ChipTone): ChipColorScheme {
164
- // The outlined variant is a property of the HUE, not of the tone: it has no
165
- // fill, so every tone of a hue sits on the same page background and wants
166
- // the same ink.
167
- const outline = {
168
- outlineText: push(stops.text, "#000000", PAGE_LIGHT),
169
- darkOutlineText: push(stops.onDeep ?? stops.pale, "#ffffff", PAGE_DARK)
170
- };
171
-
172
- const filled = (light: string, dark: string): ChipColorScheme => ({
173
- color: light,
174
- text: inkOn(stops, light),
175
- darkColor: dark,
176
- darkText: inkOn(stops, dark),
177
- ...outline
178
- });
179
-
180
- switch (chipTone) {
181
- case "Light":
182
- return filled(stops.mid, stops.solid);
183
- case "Dark":
184
- return filled(stops.solid, stops.solid);
185
- case "Darker":
186
- return filled(stops.deep, stops.deep);
187
- case "Lighter":
188
- default:
189
- return filled(stops.pale, stops.deep);
190
- }
191
- }
192
-
193
- const CHIP_TONES: ChipTone[] = ["Lighter", "Light", "Dark", "Darker"];
194
-
195
- function buildChipColors(): Record<ChipColorKey, ChipColorScheme> {
196
- const colors = {} as Record<ChipColorKey, ChipColorScheme>;
197
- for (const hue of CHIP_HUES) {
198
- const stops = HUE_STOPS[hue];
199
- for (const chipTone of CHIP_TONES) {
200
- colors[`${hue}${chipTone}` as ChipColorKey] = tone(stops, chipTone);
201
- }
202
- // The bare hue is the lightest tone, which is what every existing
203
- // `color: "blue"` in a collection config already means.
204
- colors[hue] = tone(stops, "Lighter");
205
- }
206
- return colors;
207
- }
208
-
209
- /**
210
- * Every chip scheme, keyed by hue and tone.
211
- *
212
- * This table used to hold four tones per hue and was flattened to one, which
213
- * left `blueDark`, `redDarker` and friends resolving to `undefined` — a chip
214
- * with a colour in its config rendering with no colour at all — and left seeded
215
- * chips picking from ten schemes, so a five-value enum routinely drew the same
216
- * background three times. The tones are generated from {@link HUE_STOPS} now,
217
- * so a new hue brings its whole family with it.
218
- */
219
- export const CHIP_COLORS: Record<ChipColorKey, ChipColorScheme> = buildChipColors();
220
-
221
- /**
222
- * The keys a seeded chip may be assigned, in a stable order.
223
- *
224
- * The bare-hue aliases are excluded: they are the same scheme as `<hue>Lighter`
225
- * and would make the palest tone twice as likely as any other.
226
- */
227
- export const CHIP_SEED_KEYS: ChipColorKey[] = CHIP_HUES.flatMap(
228
- hue => CHIP_TONES.map(chipTone => `${hue}${chipTone}` as ChipColorKey));
229
-
230
- // `string & {}` keeps the known keys in autocomplete while still accepting the
231
- // arbitrary column colours a board hands over.
232
- export function getColorSchemeForKey(key: ChipColorKey | (string & {})): ChipColorScheme {
233
- // An unknown key is a config that names a colour this build does not have —
234
- // a renamed hue, or a value typed by hand. Seeding from the key keeps it
235
- // coloured and keeps it stable, rather than dropping it to no colour.
236
- return CHIP_COLORS[key as ChipColorKey] ?? getColorSchemeForSeed(String(key));
237
- }
238
-
239
- export function getColorSchemeForSeed(seed: string): ChipColorScheme {
240
- const hash: number = hashString(seed);
241
- return CHIP_COLORS[CHIP_SEED_KEYS[hash % CHIP_SEED_KEYS.length]];
242
- }
package/src/util/cls.ts DELETED
@@ -1,8 +0,0 @@
1
- import { type ClassValue, clsx } from "clsx";
2
- import { twMerge } from "tailwind-merge";
3
-
4
- export function cls(...classes: ClassValue[]) {
5
- return twMerge(clsx(classes));
6
- }
7
-
8
-
@@ -1,28 +0,0 @@
1
- /**
2
- * @ignore
3
- */
4
-
5
- export function debounce<T extends (...args: any[]) => unknown>(func: T, wait = 166) {
6
- let timeout: ReturnType<typeof setTimeout>;
7
-
8
- function debounced(this: unknown, ...args: Parameters<T>) {
9
- const later = () => {
10
- func.apply(this, args);
11
- };
12
- clearTimeout(timeout);
13
- timeout = setTimeout(later, wait);
14
- }
15
-
16
- debounced.clear = () => {
17
- clearTimeout(timeout);
18
- };
19
-
20
- return debounced as T & Cancelable;
21
- }
22
-
23
- /**
24
- * @ignore
25
- */
26
- export interface Cancelable {
27
- clear(): void;
28
- }