@rebasepro/ui 0.14.2-canary.gf913b76 → 0.15.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.
@@ -36,26 +36,40 @@ html.dark {
36
36
  size goes up. Seven consecutive levels of semibold was a Rubik-era setting,
37
37
  and holding 600 all the way down to text-sm made every heading shout equally
38
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.
39
+
40
+ The ladder now spans three weights rather than two. 600 everywhere at the top
41
+ meant a page title and the section heading inside it were the same voice at
42
+ two sizes, so a screen had no clear first thing to read. 700 is the *display*
43
+ tier only — h1 and h2, page titles and stat numbers — and nothing in the UI
44
+ chrome (nav, labels, buttons, table headers) goes above 600.
45
+
46
+ This costs nothing on the wire: both faces are loaded as VARIABLE fonts
47
+ (@fontsource-variable/inter, @fontsource-variable/instrument-sans), so the
48
+ whole weight axis is already in the file that ships. The earlier "never above
49
+ 600" rule was written when static weights meant every step was another
50
+ download; that constraint no longer exists.
40
51
 
41
52
  With a single face carrying both headings and body, weight and tracking are
42
53
  the only things separating a heading from the copy around it — so the tiers
43
54
  have to actually differ, rather than all sitting at 600. */
44
55
 
45
56
  .typography-h1 {
46
- @apply text-4xl font-headers font-semibold tracking-display;
57
+ @apply text-4xl font-headers font-bold tracking-display;
47
58
  }
48
59
 
49
60
  .typography-h2 {
50
- @apply text-3xl font-headers font-semibold tracking-display;
61
+ @apply text-3xl font-headers font-bold tracking-display;
51
62
  }
52
63
 
53
64
  .typography-h3 {
54
65
  @apply text-2xl font-headers font-semibold tracking-title;
55
66
  }
56
67
 
68
+ /* Semibold, not medium: h4 is the section heading inside a page, and at 20px
69
+ medium sits close enough to body copy that a long page reads as one
70
+ undifferentiated column. */
57
71
  .typography-h4 {
58
- @apply text-xl font-headers font-medium tracking-title;
72
+ @apply text-xl font-headers font-semibold tracking-title;
59
73
  }
60
74
 
61
75
  .typography-h5 {
@@ -98,6 +112,44 @@ html.dark {
98
112
  @apply text-sm font-semibold tracking-wide;
99
113
  }
100
114
 
115
+ /* ---------------------------------------------------------------------------
116
+ * Three tiers the product kept improvising because the system had no name for
117
+ * them. Each existed as hand-written utilities in several places before it
118
+ * existed here, which is the definition of a missing token.
119
+ * ------------------------------------------------------------------------- */
120
+
121
+ /* The sentence under a page title. `body2` (12px) was standing in for it, which
122
+ made the one line explaining what a page is FOR smaller than the page's own
123
+ table rows. A lead is read once, deliberately, and it gets room to be read. */
124
+ .typography-lead {
125
+ @apply text-base leading-relaxed;
126
+ }
127
+
128
+ /* The micro-label above a value: SCANNED, KEPT, TOOK, WHEN. Uppercase and
129
+ tracked so it reads as a field name rather than as copy, and small enough
130
+ that it never competes with the value it names.
131
+
132
+ This is the one product tier below `text-xs`. It earns the exception by never
133
+ carrying a sentence — it is always one or two words naming the thing directly
134
+ underneath it, and at 11px uppercase with this tracking it stays legible
135
+ where 11px prose would not. */
136
+ .typography-micro {
137
+ @apply text-2xs font-medium uppercase tracking-[0.08em];
138
+ }
139
+
140
+ /* A measured value: a count, a duration, a timestamp, an id, a size.
141
+ `tabular-nums` is the point — proportional digits make a column of numbers
142
+ ragged and a live-updating counter jitter as its glyphs change width. */
143
+ .typography-mono {
144
+ @apply font-mono tabular-nums;
145
+ }
146
+
147
+ /* The headline number on a stat tile. Display weight, display tracking, and
148
+ tabular figures so a row of tiles shares a baseline grid. */
149
+ .typography-stat {
150
+ @apply text-3xl font-headers font-bold tracking-display tabular-nums;
151
+ }
152
+
101
153
  :focus-visible {
102
154
  @apply outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-0 focus-visible:ring-offset-transparent
103
155
  }
@@ -30,11 +30,14 @@
30
30
  --tracking-title: -0.018em; /* 20-24px — section titles */
31
31
  --tracking-heading: -0.01em; /* <= 18px — labels; still a heading, not a headline */
32
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. */
33
+ /* Sub-`xs` type. These exist because the marketing site had ~450
34
+ hand-written `text-[10px]` / `text-[11px]` classes: the tier was real, it
35
+ just wasn't in the system, so every page improvised it.
36
+
37
+ In product UI there is exactly ONE sanctioned use — `typography-micro`,
38
+ the uppercase field label above a value — and it earns the exception by
39
+ never carrying a sentence. Everything a person actually *reads* stays at
40
+ `text-xs` (12px) or above. `--text-3xs` remains marketing-only. */
38
41
  --text-2xs: 11px;
39
42
  --text-2xs--line-height: 1.45;
40
43
  --text-3xs: 10px;
package/dist/styles.d.ts CHANGED
@@ -35,6 +35,34 @@ export declare const fieldBackgroundDisabledMixin = "bg-surface-accent-200/50 da
35
35
  export declare const fieldBackgroundHoverMixin = "hover:bg-surface-accent-200/70 hover:dark:bg-white/[0.09]";
36
36
  export declare const defaultBorderMixin = "border-surface-200 dark:border-surface-700/60 ";
37
37
  export declare const paperMixin = "bg-white rounded-lg dark:bg-surface-900 border border-surface-200 dark:border-surface-700";
38
- export declare const cardMixin = "bg-white dark:bg-surface-900 rounded-lg border border-surface-200 dark:border-surface-700";
38
+ export declare const cardMixin = "bg-white dark:bg-surface-900 rounded-xl border border-surface-200 dark:border-surface-700/60";
39
+ /**
40
+ * An inset well: code, a query, a log tail, a connection string.
41
+ *
42
+ * It must read as *recessed into* the surface holding it, which means darker
43
+ * than that surface in dark mode — `surface-950` under a `surface-900` card.
44
+ * The obvious-looking `surface-800` is a trap: `#111111` is LIGHTER than the
45
+ * `#0a0a0a` card around it, so the well appears to float above the thing it is
46
+ * set into. Pair with `font-mono`; this mixin carries the surface only.
47
+ */
48
+ export declare const codeSurfaceMixin = "bg-surface-100 dark:bg-surface-950 rounded-md";
49
+ /**
50
+ * The accent, used as TEXT.
51
+ *
52
+ * `--color-primary` (#0070F4) is tuned to be a fill — white text on it, it on
53
+ * white. Read as text on our dark surfaces it lands at **4.36:1 on a
54
+ * `surface-900` card** (measured), which is below AA for body-sized text, and
55
+ * every accent link in the product sits on exactly that surface. On the page's
56
+ * `surface-950` it only reaches 4.62:1, so the margin was never real.
57
+ *
58
+ * `--color-primary-light` is the same hue lifted 0.15 in OKLCH lightness and
59
+ * measures **7.34:1** on the same card. It is indistinguishable as "the blue"
60
+ * and comfortably legible, so dark mode swaps to it for text.
61
+ *
62
+ * Use this for links, accent labels and any accent-coloured type. It is NOT for
63
+ * fills — a filled button keeps `bg-primary`, where the contrast question runs
64
+ * the other way and #0070F4 is already correct.
65
+ */
66
+ export declare const accentTextMixin = "text-primary dark:text-primary-light";
39
67
  export declare const cardClickableMixin = "hover:bg-primary/5 dark:hover:bg-primary/5 cursor-pointer transition-colors duration-150";
40
68
  export declare const cardSelectedMixin = "bg-primary-bg/30 dark:bg-primary-bg/10 ring-1 ring-primary/75";
package/dist/theme.css CHANGED
@@ -30,11 +30,14 @@
30
30
  --tracking-title: -0.018em; /* 20-24px — section titles */
31
31
  --tracking-heading: -0.01em; /* <= 18px — labels; still a heading, not a headline */
32
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. */
33
+ /* Sub-`xs` type. These exist because the marketing site had ~450
34
+ hand-written `text-[10px]` / `text-[11px]` classes: the tier was real, it
35
+ just wasn't in the system, so every page improvised it.
36
+
37
+ In product UI there is exactly ONE sanctioned use — `typography-micro`,
38
+ the uppercase field label above a value — and it earns the exception by
39
+ never carrying a sentence. Everything a person actually *reads* stays at
40
+ `text-xs` (12px) or above. `--text-3xs` remains marketing-only. */
38
41
  --text-2xs: 11px;
39
42
  --text-2xs--line-height: 1.45;
40
43
  --text-3xs: 10px;
@@ -5,7 +5,21 @@ export type ChipColorScheme = {
5
5
  darkColor?: string;
6
6
  /** Text color override for dark mode */
7
7
  darkText?: string;
8
+ /**
9
+ * Ink for the `outlined` variant, which has no fill of its own.
10
+ *
11
+ * `text`/`darkText` are the ink ON the chip's own background. An outlined
12
+ * chip drops that background and sits on the PAGE, so the same value is
13
+ * being asked to be legible against two different surfaces at once — and
14
+ * for most hues it cannot be. Splitting the roles is what lets the filled
15
+ * ink follow its fill (often dark ink on a bright chip) without dragging
16
+ * the outlined variant down with it.
17
+ */
18
+ outlineText?: string;
19
+ /** Outlined-variant ink in dark mode. */
20
+ darkOutlineText?: string;
8
21
  };
22
+ export declare function contrastRatio(a: string, b: string): number;
9
23
  export declare const CHIP_HUES: readonly ["blue", "cyan", "teal", "green", "yellow", "orange", "red", "pink", "purple", "gray", "indigo", "violet", "fuchsia", "rose", "emerald"];
10
24
  export type ChipHue = typeof CHIP_HUES[number];
11
25
  /**
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import type { CollectionViewMode, CollectionViewSize, CollectionPropertyConfig, CollectionDataController, CellRendererOverride, CollectionSelectionController, KanbanPropertyOption } from "./CollectionViewTypes";
2
+ import type { CollectionViewMode, CollectionViewSize, CollectionPropertyConfig, CollectionDataController, CellRendererOverride, CollectionCustomViewEntry, CollectionSelectionController, KanbanPropertyOption } from "./CollectionViewTypes";
3
3
  /**
4
4
  * Props for the headless {@link CollectionView} component.
5
5
  *
@@ -76,6 +76,26 @@ export interface CollectionViewProps<T = Record<string, unknown>> {
76
76
  cellRenderer?: CellRendererOverride<T>;
77
77
  /** Custom empty state component */
78
78
  emptyComponent?: React.ReactNode;
79
+ /**
80
+ * Additional ways to render these rows, keyed by view mode. A key here can
81
+ * be named by `viewMode`, `defaultViewMode` and `enabledViews`, and gets
82
+ * its own entry in the view switcher.
83
+ *
84
+ * A custom view is another rendering of the *same* `dataController`, so it
85
+ * inherits the toolbar's search, the loading state and selection. A
86
+ * component that fetches its own data does not want to be a view mode —
87
+ * the toolbar above it would be describing a query it does not render.
88
+ *
89
+ * @example
90
+ * ```tsx
91
+ * <CollectionView
92
+ * customViews={{ map: { name: "Map", Component: MapView } }}
93
+ * enabledViews={["table", "map"]}
94
+ * defaultViewMode="map"
95
+ * />
96
+ * ```
97
+ */
98
+ customViews?: Record<string, CollectionCustomViewEntry<T>>;
79
99
  /** Actions rendered at the start (left) of the toolbar */
80
100
  toolbarActionsStart?: React.ReactNode;
81
101
  /** Actions rendered at the end (right) of the toolbar */
@@ -118,4 +138,4 @@ export interface CollectionViewProps<T = Record<string, unknown>> {
118
138
  *
119
139
  * @group Collection View
120
140
  */
121
- export declare function CollectionView<T extends Record<string, unknown> = Record<string, unknown>>({ dataController, properties, propertiesOrder, displayedColumnIds, idProperty, titleProperty, title, viewMode: viewModeProp, defaultViewMode, enabledViews, onViewModeChange: onViewModeChangeProp, size: sizeProp, defaultSize, onSizeChange: onSizeChangeProp, kanbanProperty: kanbanPropertyProp, kanbanPropertyOptions, onKanbanPropertyChange: onKanbanPropertyChangeProp, onRowClick, onRowCreate, canCreate, selectionEnabled, selectionController, highlightedItems, onMultipleDelete, cellRenderer, emptyComponent, toolbarActionsStart, toolbarActionsEnd, hideToolbar, hoverRow, onColumnResize, onColumnsOrderChange, className }: CollectionViewProps<T>): React.JSX.Element;
141
+ export declare function CollectionView<T extends Record<string, unknown> = Record<string, unknown>>({ dataController, properties, propertiesOrder, displayedColumnIds, idProperty, titleProperty, title, viewMode: viewModeProp, defaultViewMode, enabledViews, onViewModeChange: onViewModeChangeProp, size: sizeProp, defaultSize, onSizeChange: onSizeChangeProp, kanbanProperty: kanbanPropertyProp, kanbanPropertyOptions, onKanbanPropertyChange: onKanbanPropertyChangeProp, onRowClick, onRowCreate, canCreate, selectionEnabled, selectionController, highlightedItems, onMultipleDelete, cellRenderer, emptyComponent, customViews, toolbarActionsStart, toolbarActionsEnd, hideToolbar, hoverRow, onColumnResize, onColumnsOrderChange, className }: CollectionViewProps<T>): React.JSX.Element;
@@ -8,6 +8,16 @@ export type CollectionViewToolbarProps = {
8
8
  viewMode: CollectionViewMode;
9
9
  onViewModeChange?: (mode: CollectionViewMode) => void;
10
10
  enabledViews?: CollectionViewMode[];
11
+ /**
12
+ * Custom view modes, supplying the label and icon for their switcher
13
+ * entries. Without these an `enabledViews` key outside the four built-ins
14
+ * has no label and no icon to draw.
15
+ */
16
+ customViews?: {
17
+ key: string;
18
+ name: string;
19
+ icon?: React.ReactNode;
20
+ }[];
11
21
  size?: CollectionViewSize;
12
22
  onSizeChange?: (size: CollectionViewSize) => void;
13
23
  searchString?: string;
@@ -28,4 +38,4 @@ export type CollectionViewToolbarProps = {
28
38
  *
29
39
  * @group Collection View
30
40
  */
31
- export declare function CollectionViewToolbar({ viewMode, onViewModeChange, enabledViews, size, onSizeChange, searchString, onSearchChange, loading, actionsStart, actionsEnd, compact, kanbanPropertyOptions, selectedKanbanProperty, onKanbanPropertyChange, }: CollectionViewToolbarProps): React.JSX.Element;
41
+ export declare function CollectionViewToolbar({ viewMode, onViewModeChange, enabledViews, customViews, size, onSizeChange, searchString, onSearchChange, loading, actionsStart, actionsEnd, compact, kanbanPropertyOptions, selectedKanbanProperty, onKanbanPropertyChange, }: CollectionViewToolbarProps): React.JSX.Element;
@@ -1,7 +1,12 @@
1
1
  import React from "react";
2
2
  import type { VirtualTableSortKey } from "../../components/VirtualTable/VirtualTableProps";
3
- /** Supported view modes */
4
- export type CollectionViewMode = "table" | "cards" | "kanban" | "list";
3
+ /**
4
+ * Supported view modes.
5
+ *
6
+ * Any other string is a key into the `customViews` prop. The `(string & {})`
7
+ * arm keeps the four built-ins in autocomplete while admitting those keys.
8
+ */
9
+ export type CollectionViewMode = "table" | "cards" | "kanban" | "list" | (string & {});
5
10
  /** Supported collection sizes */
6
11
  export type CollectionViewSize = "xs" | "s" | "m" | "l" | "xl";
7
12
  /**
@@ -106,3 +111,36 @@ export interface KanbanPropertyOption {
106
111
  key: string;
107
112
  label: string;
108
113
  }
114
+ /**
115
+ * What a custom view's component receives — the same set the built-in table,
116
+ * card, list and kanban views are given, so a custom view starts at parity
117
+ * with them and inherits the toolbar's search, the empty state and selection.
118
+ */
119
+ export interface CollectionCustomViewProps<T = Record<string, unknown>> {
120
+ dataController: CollectionDataController<T>;
121
+ properties: Record<string, CollectionPropertyConfig>;
122
+ propertiesOrder?: string[];
123
+ idProperty?: string;
124
+ titleProperty?: string;
125
+ onRowClick?: (row: T) => void;
126
+ onRowCreate?: (defaults?: Record<string, unknown>) => void;
127
+ canCreate?: boolean;
128
+ cellRenderer?: CellRendererOverride<T>;
129
+ selectionEnabled?: boolean;
130
+ selectionController?: CollectionSelectionController<T>;
131
+ highlightedItems?: T[];
132
+ emptyComponent?: React.ReactNode;
133
+ size?: CollectionViewSize;
134
+ }
135
+ /**
136
+ * A custom view registration. Either the component on its own, or the
137
+ * component with the label and icon its switcher entry should carry.
138
+ *
139
+ * Without a `name` the switcher falls back to the key, which is legible but
140
+ * rarely what you want on screen.
141
+ */
142
+ export type CollectionCustomViewEntry<T = Record<string, unknown>> = React.ComponentType<CollectionCustomViewProps<T>> | {
143
+ name?: string;
144
+ icon?: React.ReactNode;
145
+ Component: React.ComponentType<CollectionCustomViewProps<T>>;
146
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/ui",
3
3
  "type": "module",
4
- "version": "0.14.2-canary.gf913b76",
4
+ "version": "0.15.0",
5
5
  "description": "Rebase design system — React components and design tokens built on Radix UI, Tailwind CSS and lucide-react",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -82,6 +82,16 @@ export function Chip({
82
82
 
83
83
  if (hasScheme) {
84
84
  if (outlined) {
85
+ // An outlined chip has no fill, so it needs the ink meant for the
86
+ // PAGE, not the ink meant for the fill it just dropped. Those are
87
+ // different colours for most hues — a bright `solid` chip carries
88
+ // dark ink, which on a near-black page would be invisible.
89
+ if (!error && usedColorScheme) {
90
+ const outlineInk = dark
91
+ ? usedColorScheme.darkOutlineText ?? usedColorScheme.darkText
92
+ : usedColorScheme.outlineText;
93
+ if (outlineInk) textColor = outlineInk;
94
+ }
85
95
  bgColor = getRgba(textColor, dark ? 0.1 : 0.06);
86
96
  border = `1px solid ${getRgba(textColor, dark ? 0.2 : 0.14)}`;
87
97
  } else if (error) {
@@ -31,7 +31,11 @@ const typographyVariants = {
31
31
  body2: "p",
32
32
  inherit: "p",
33
33
  caption: "p",
34
- button: "span"
34
+ button: "span",
35
+ lead: "p",
36
+ micro: "span",
37
+ mono: "span",
38
+ stat: "span"
35
39
  };
36
40
 
37
41
  const colorToClasses = {
@@ -57,7 +61,11 @@ const gutterBottomClasses = {
57
61
  inherit: "mb-3",
58
62
  caption: "mb-2",
59
63
  button: "mb-2",
60
- label: "mb-2"
64
+ label: "mb-2",
65
+ lead: "mb-4",
66
+ micro: "mb-1",
67
+ mono: "mb-2",
68
+ stat: "mb-1"
61
69
  };
62
70
 
63
71
  const variantToClasses = {
@@ -74,7 +82,11 @@ const variantToClasses = {
74
82
  label: "typography-label",
75
83
  inherit: "typography-inherit",
76
84
  caption: "typography-caption",
77
- button: "typography-button"
85
+ button: "typography-button",
86
+ lead: "typography-lead",
87
+ micro: "typography-micro",
88
+ mono: "typography-mono",
89
+ stat: "typography-stat"
78
90
  };
79
91
 
80
92
  export function Typography<C extends React.ElementType = "span">(
package/src/index.css CHANGED
@@ -36,26 +36,40 @@ html.dark {
36
36
  size goes up. Seven consecutive levels of semibold was a Rubik-era setting,
37
37
  and holding 600 all the way down to text-sm made every heading shout equally
38
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.
39
+
40
+ The ladder now spans three weights rather than two. 600 everywhere at the top
41
+ meant a page title and the section heading inside it were the same voice at
42
+ two sizes, so a screen had no clear first thing to read. 700 is the *display*
43
+ tier only — h1 and h2, page titles and stat numbers — and nothing in the UI
44
+ chrome (nav, labels, buttons, table headers) goes above 600.
45
+
46
+ This costs nothing on the wire: both faces are loaded as VARIABLE fonts
47
+ (@fontsource-variable/inter, @fontsource-variable/instrument-sans), so the
48
+ whole weight axis is already in the file that ships. The earlier "never above
49
+ 600" rule was written when static weights meant every step was another
50
+ download; that constraint no longer exists.
40
51
 
41
52
  With a single face carrying both headings and body, weight and tracking are
42
53
  the only things separating a heading from the copy around it — so the tiers
43
54
  have to actually differ, rather than all sitting at 600. */
44
55
 
45
56
  .typography-h1 {
46
- @apply text-4xl font-headers font-semibold tracking-display;
57
+ @apply text-4xl font-headers font-bold tracking-display;
47
58
  }
48
59
 
49
60
  .typography-h2 {
50
- @apply text-3xl font-headers font-semibold tracking-display;
61
+ @apply text-3xl font-headers font-bold tracking-display;
51
62
  }
52
63
 
53
64
  .typography-h3 {
54
65
  @apply text-2xl font-headers font-semibold tracking-title;
55
66
  }
56
67
 
68
+ /* Semibold, not medium: h4 is the section heading inside a page, and at 20px
69
+ medium sits close enough to body copy that a long page reads as one
70
+ undifferentiated column. */
57
71
  .typography-h4 {
58
- @apply text-xl font-headers font-medium tracking-title;
72
+ @apply text-xl font-headers font-semibold tracking-title;
59
73
  }
60
74
 
61
75
  .typography-h5 {
@@ -98,6 +112,44 @@ html.dark {
98
112
  @apply text-sm font-semibold tracking-wide;
99
113
  }
100
114
 
115
+ /* ---------------------------------------------------------------------------
116
+ * Three tiers the product kept improvising because the system had no name for
117
+ * them. Each existed as hand-written utilities in several places before it
118
+ * existed here, which is the definition of a missing token.
119
+ * ------------------------------------------------------------------------- */
120
+
121
+ /* The sentence under a page title. `body2` (12px) was standing in for it, which
122
+ made the one line explaining what a page is FOR smaller than the page's own
123
+ table rows. A lead is read once, deliberately, and it gets room to be read. */
124
+ .typography-lead {
125
+ @apply text-base leading-relaxed;
126
+ }
127
+
128
+ /* The micro-label above a value: SCANNED, KEPT, TOOK, WHEN. Uppercase and
129
+ tracked so it reads as a field name rather than as copy, and small enough
130
+ that it never competes with the value it names.
131
+
132
+ This is the one product tier below `text-xs`. It earns the exception by never
133
+ carrying a sentence — it is always one or two words naming the thing directly
134
+ underneath it, and at 11px uppercase with this tracking it stays legible
135
+ where 11px prose would not. */
136
+ .typography-micro {
137
+ @apply text-2xs font-medium uppercase tracking-[0.08em];
138
+ }
139
+
140
+ /* A measured value: a count, a duration, a timestamp, an id, a size.
141
+ `tabular-nums` is the point — proportional digits make a column of numbers
142
+ ragged and a live-updating counter jitter as its glyphs change width. */
143
+ .typography-mono {
144
+ @apply font-mono tabular-nums;
145
+ }
146
+
147
+ /* The headline number on a stat tile. Display weight, display tracking, and
148
+ tabular figures so a row of tiles shares a baseline grid. */
149
+ .typography-stat {
150
+ @apply text-3xl font-headers font-bold tracking-display tabular-nums;
151
+ }
152
+
101
153
  :focus-visible {
102
154
  @apply outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-0 focus-visible:ring-offset-transparent
103
155
  }
package/src/styles.ts CHANGED
@@ -53,7 +53,53 @@ export const fieldBackgroundInvisibleMixin = "bg-surface-accent-200/0 dark:bg-wh
53
53
  export const fieldBackgroundDisabledMixin = "bg-surface-accent-200/50 dark:bg-white/[0.03]";
54
54
  export const fieldBackgroundHoverMixin = "hover:bg-surface-accent-200/70 hover:dark:bg-white/[0.09]";
55
55
  export const defaultBorderMixin = "border-surface-200 dark:border-surface-700/60 ";
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Surfaces: two kinds, and the difference is what the border is for.
59
+ //
60
+ // A **floating** surface — a menu, a dialog, a popover — sits OVER the page. It
61
+ // has to be legible against whatever happens to be underneath it, so its edge
62
+ // is definite: solid `surface-700`. That is `paperMixin`.
63
+ //
64
+ // A **page** surface — a card in the document flow — sits ON the page, and the
65
+ // page is already `surface-950`/`surface-900`. A solid edge there reads as a
66
+ // box drawn around content rather than as the content having a surface, which
67
+ // is why every serious caller was overriding it: 53 `<Card>` sites in the SaaS
68
+ // console alone re-declared this border at `/60`, the same value
69
+ // `defaultBorderMixin` above has always used. The component was wrong and the
70
+ // callers were right, so the component now says what they meant.
71
+ // ---------------------------------------------------------------------------
56
72
  export const paperMixin = "bg-white rounded-lg dark:bg-surface-900 border border-surface-200 dark:border-surface-700";
57
- export const cardMixin = "bg-white dark:bg-surface-900 rounded-lg border border-surface-200 dark:border-surface-700";
73
+ export const cardMixin = "bg-white dark:bg-surface-900 rounded-xl border border-surface-200 dark:border-surface-700/60";
74
+
75
+ /**
76
+ * An inset well: code, a query, a log tail, a connection string.
77
+ *
78
+ * It must read as *recessed into* the surface holding it, which means darker
79
+ * than that surface in dark mode — `surface-950` under a `surface-900` card.
80
+ * The obvious-looking `surface-800` is a trap: `#111111` is LIGHTER than the
81
+ * `#0a0a0a` card around it, so the well appears to float above the thing it is
82
+ * set into. Pair with `font-mono`; this mixin carries the surface only.
83
+ */
84
+ export const codeSurfaceMixin = "bg-surface-100 dark:bg-surface-950 rounded-md";
85
+
86
+ /**
87
+ * The accent, used as TEXT.
88
+ *
89
+ * `--color-primary` (#0070F4) is tuned to be a fill — white text on it, it on
90
+ * white. Read as text on our dark surfaces it lands at **4.36:1 on a
91
+ * `surface-900` card** (measured), which is below AA for body-sized text, and
92
+ * every accent link in the product sits on exactly that surface. On the page's
93
+ * `surface-950` it only reaches 4.62:1, so the margin was never real.
94
+ *
95
+ * `--color-primary-light` is the same hue lifted 0.15 in OKLCH lightness and
96
+ * measures **7.34:1** on the same card. It is indistinguishable as "the blue"
97
+ * and comfortably legible, so dark mode swaps to it for text.
98
+ *
99
+ * Use this for links, accent labels and any accent-coloured type. It is NOT for
100
+ * fills — a filled button keeps `bg-primary`, where the contrast question runs
101
+ * the other way and #0070F4 is already correct.
102
+ */
103
+ export const accentTextMixin = "text-primary dark:text-primary-light";
58
104
  export const cardClickableMixin = "hover:bg-primary/5 dark:hover:bg-primary/5 cursor-pointer transition-colors duration-150";
59
105
  export const cardSelectedMixin = "bg-primary-bg/30 dark:bg-primary-bg/10 ring-1 ring-primary/75";
package/src/theme.css CHANGED
@@ -30,11 +30,14 @@
30
30
  --tracking-title: -0.018em; /* 20-24px — section titles */
31
31
  --tracking-heading: -0.01em; /* <= 18px — labels; still a heading, not a headline */
32
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. */
33
+ /* Sub-`xs` type. These exist because the marketing site had ~450
34
+ hand-written `text-[10px]` / `text-[11px]` classes: the tier was real, it
35
+ just wasn't in the system, so every page improvised it.
36
+
37
+ In product UI there is exactly ONE sanctioned use — `typography-micro`,
38
+ the uppercase field label above a value — and it earns the exception by
39
+ never carrying a sentence. Everything a person actually *reads* stays at
40
+ `text-xs` (12px) or above. `--text-3xs` remains marketing-only. */
38
41
  --text-2xs: 11px;
39
42
  --text-2xs--line-height: 1.45;
40
43
  --text-3xs: 10px;