@lotics/ui 41.4.1 → 42.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +34 -0
- package/docs/catalog.md +21 -0
- package/docs/composition.md +23 -2
- package/examples/tpl_dashboard.tsx +1 -1
- package/package.json +10 -3
- package/src/avatar.tsx +6 -1
- package/src/avatar.web.tsx +6 -1
- package/src/avatar_size.ts +10 -1
- package/src/color_tokens.ts +422 -0
- package/src/colors.ts +14 -383
- package/src/colors.web.ts +43 -0
- package/src/date_filter.tsx +54 -34
- package/src/date_filter_presets.ts +15 -10
- package/src/locale.tsx +2 -2
- package/src/summary.tsx +217 -0
- package/src/summary_line.tsx +23 -0
- package/src/theme.tsx +13 -50
- package/src/theme.web.tsx +64 -0
- package/src/theme_context.ts +45 -0
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,40 @@ Breaking changes, newest first — normally per major, plus the rare minor that
|
|
|
4
4
|
anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
|
|
5
5
|
this file exists only to move an app from one release to the next.
|
|
6
6
|
|
|
7
|
+
## 42.0.0 — `DateFilter`'s presets are a listbox, and "Custom" is not one of them
|
|
8
|
+
|
|
9
|
+
Three defects with one root: the preset row used EMPHASIS to mean "current state", inside a
|
|
10
|
+
panel where emphasis means "action".
|
|
11
|
+
|
|
12
|
+
**The active preset was painted like a primary button.** `zinc.800` with inverted text — the
|
|
13
|
+
loudest treatment the system has — spent on a row that reports state rather than offering one.
|
|
14
|
+
It outshouted the panel's own Done button. It now uses `MenuButton`'s `selected`, the kit's
|
|
15
|
+
listbox row: a `zinc.100` resting highlight. That prop also carries `aria-selected`, which the
|
|
16
|
+
hand-rolled fill never did — until now the active preset announced **nothing**, so which period
|
|
17
|
+
was selected was information available only to someone who could see the fill.
|
|
18
|
+
|
|
19
|
+
**The panel rendered its phone layout on desktop.** It sized itself with `useContainerSize()`,
|
|
20
|
+
which inside a `Popover` measures the popover BODY — and the body's width is decided by the
|
|
21
|
+
branch that reading picks. Two calendars come to ~617px, always under the 768 phone threshold,
|
|
22
|
+
so the panel resolved "small" on a 1440px viewport and its sidebar branch was unreachable: the
|
|
23
|
+
presets collapsed into a horizontal strip of bare text where the only item with any chrome was
|
|
24
|
+
the active one. It now reads `useScreenSize()` — the same instrument, and for the same reason,
|
|
25
|
+
that `Popover` already uses to choose sheet vs anchored.
|
|
26
|
+
|
|
27
|
+
**`"custom"` is gone from `PresetId`, `PRESET_IDS`, and `DateFilterLabels`.** It was the one
|
|
28
|
+
entry that set no range: it CLEARED the value. So it lit up whenever the range matched no
|
|
29
|
+
preset — the normal state for any hand-picked range, and the opening state of any app whose
|
|
30
|
+
default period is not a preset — and clicking the lit row, the obvious move for "let me pick my
|
|
31
|
+
own dates", threw the range away. The calendar is the custom picker and the footer's Clear is
|
|
32
|
+
the clear.
|
|
33
|
+
|
|
34
|
+
`getPresetValue` is now total: it returns `DateFilterValue`, never `| null`.
|
|
35
|
+
|
|
36
|
+
**To migrate:** delete `custom` from any `DateFilterLabels` / `DateRangeFilterFieldLabels`
|
|
37
|
+
object you pass — it is an excess property now and the compiler will point at it. If you called
|
|
38
|
+
`getPresetValue` and branched on `null`, drop the branch. Nothing else changes: a range that
|
|
39
|
+
matches no preset simply leaves every row unselected, which is what it always meant.
|
|
40
|
+
|
|
7
41
|
## 41.4.0 — embedded markdown is demoted in the OUTLINE, not only on the type ladder
|
|
8
42
|
|
|
9
43
|
`<Markdown variant="embedded">` sized its headings down and left them as the tags the author
|
package/docs/catalog.md
CHANGED
|
@@ -857,6 +857,12 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
857
857
|
for it bare only when the calendar lives permanently on the surface, not behind a field.
|
|
858
858
|
- **`date_filter`** — `DateFilter`: the date+time period filter panel — presets
|
|
859
859
|
(`PresetId`), calendar, optional time segments; the body `DateRangeFilterField` opens.
|
|
860
|
+
**Every preset SETS a range** — the list holds no mode and no null case, so a hand-picked
|
|
861
|
+
range selects nothing rather than lighting a "custom" row whose click cleared the value. The
|
|
862
|
+
presets are a `listbox` of `MenuButton` options: selection is the kit's `zinc.100` row
|
|
863
|
+
highlight plus `aria-selected`, never a filled primary treatment, because the row reports
|
|
864
|
+
state rather than offering an action. Sidebar at viewports of 768 and up, strip below —
|
|
865
|
+
measured on the VIEWPORT, since a popover's own width is decided by that very branch.
|
|
860
866
|
**One selection rule, no modes**: a click OPENS a range, the next CLOSES it (in either
|
|
861
867
|
order — bounds come back sorted), and a single day is the same day clicked twice
|
|
862
868
|
(`start`/`end` equal — which is how a consumer detects "one day" and how the trigger
|
|
@@ -1219,6 +1225,21 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1219
1225
|
a record's identity band reporting that record's own state**: derived verdicts on one record
|
|
1220
1226
|
("documents incomplete", "2 fields need checking") are the checklist a record must not carry
|
|
1221
1227
|
(§`checklist`) and restate at a distance what the sections below state in place.
|
|
1228
|
+
- **`summary`** — `Summary`: the register summary band, for a set with a SHAPE worth showing —
|
|
1229
|
+
a headline figure, a distribution bar, and the buckets named. A COMPOUND:
|
|
1230
|
+
`Summary.Header` / `Summary.Total` / `Summary.Metric` / `Summary.Distribution` /
|
|
1231
|
+
`Summary.Facts`, composed as children. **Declare the buckets ONCE on the root and every part
|
|
1232
|
+
derives from them** — the headline is their sum (there is no `total` prop to go stale), the
|
|
1233
|
+
segments and the legend swatches are the buckets' own colours. That correspondence is the
|
|
1234
|
+
reason it exists: a `segments` array passed beside an `items` array beside a `total` drifts
|
|
1235
|
+
three ways, invisibly. A `SummaryBucket` must PARTITION the set; an aggregate over the same
|
|
1236
|
+
rows that does not (a condition like "overdue" beside "draft / sent / paid", counted again
|
|
1237
|
+
from a different question) goes to `Summary.Facts` as `extra`, where it renders with NO swatch
|
|
1238
|
+
— the visual difference says it is not a slice of the bar. Ordering is the caller's and is
|
|
1239
|
+
never re-ranked, because for a pipeline the sequence is the process. Pick between the four
|
|
1240
|
+
summaries by what you have: only aggregates → `summary_line`; aggregates + a shape →
|
|
1241
|
+
`summary`; a boxed dashboard stat band → `kpi_strip`; a drill-down facet browser over a large
|
|
1242
|
+
population → `breakdown`.
|
|
1222
1243
|
- **`use_selection`** — `useSelection`: always-on multi-select state for a register/list —
|
|
1223
1244
|
the `selected` Set + `toggle`/`setAll`/`allSelected`/`indeterminate`/`count`/`clear`;
|
|
1224
1245
|
selectability gating stays with the caller. The checkbox-always-visible counterpart to
|
package/docs/composition.md
CHANGED
|
@@ -264,9 +264,13 @@ objects — hairlines between them blur into one run-on list.
|
|
|
264
264
|
| Altitude | Component | Where | Shape |
|
|
265
265
|
|---|---|---|---|
|
|
266
266
|
| Dashboard | `<KPIStrip items={[{label, value, trend?, caption?, tone?, compact?, info?}…]}>` | TOP of a dashboard | a boxed stat band — a `Card` of `KPICard` columns |
|
|
267
|
-
| Register / list | `<SummaryLine items={[{label, value, tone?, compact?, trend?, info?}…]}>` | BELOW the toolbar | a light inline line — a small `Metric` + muted label per item |
|
|
267
|
+
| Register / list | `<SummaryLine items={[{label, value, tone?, compact?, trend?, info?, color?}…]}>` | BELOW the toolbar | a light inline line — a small `Metric` + muted label per item |
|
|
268
|
+
| Register / list, with a SHAPE | `<Summary buckets={…}><Summary.Header>…</Summary.Header><Summary.Distribution/><Summary.Facts/></Summary>` | BELOW the toolbar | headline figure + distribution bar + the buckets named, all derived from one bucket list |
|
|
268
269
|
|
|
269
|
-
|
|
270
|
+
`SummaryLine` is `Summary`'s `Facts` row on its own — reach for the compound when the set has a
|
|
271
|
+
distribution worth drawing, and for the bare line when it is only a few aggregates. A
|
|
272
|
+
`SummaryLine`'s `color` exists so it can double as the bar's legend without giving up its `info`
|
|
273
|
+
popovers. Both are recomputed from the FILTERED rows so they always describe WHAT'S SHOWN. Both are
|
|
270
274
|
INFORMATIONAL — they never filter or navigate (that's the tabs/chips' job); if there are no
|
|
271
275
|
cross-cutting numbers, drop them. Do NOT put a `KPIStrip` on a register page or a `SummaryLine` on
|
|
272
276
|
a dashboard. Card stat rails use `KPICard`.
|
|
@@ -474,6 +478,23 @@ content per column and set `flex` from that. A column whose cell holds a PROPORT
|
|
|
474
478
|
bar) is the exception in one direction only: cap it, because a gauge stops reading as an
|
|
475
479
|
instrument once its track is long enough to be a rule across the row.
|
|
476
480
|
|
|
481
|
+
**The FIXED columns' widths are the flexible one's budget — so audit them by what they CARRY,
|
|
482
|
+
not by what they are called.** With one `flex` column among N fixed, every pixel a fixed column
|
|
483
|
+
takes comes out of the flexible one, silently and with no measurement pointing at the culprit:
|
|
484
|
+
the symptom appears somewhere else entirely, as truncated content in the column that was
|
|
485
|
+
supposed to have room. The usual offender is a column sized by its HEADER over single-character
|
|
486
|
+
data — a long label above a column of one-digit counts reserves width for a word nobody reads
|
|
487
|
+
twice, and the register's identity column pays for it. Measure the header row's cells and the
|
|
488
|
+
widest CELL content per column; where a column's width is set by its label rather than its
|
|
489
|
+
values, that difference is the identity column's, not its own.
|
|
490
|
+
|
|
491
|
+
**Space by the RATIO of within-group to between-group, never by absolutes.** Collect the gap
|
|
492
|
+
inside a composed band and the gap between that band and its neighbours, and divide. Near 1 is
|
|
493
|
+
the defect however sensible each number is alone — at 14 inside against 16 between, a headline,
|
|
494
|
+
a chart and a register read as one undifferentiated stack, because nothing on the screen is
|
|
495
|
+
grouped. Aim for roughly 2 or more, and change the BETWEEN gap first: tightening the inside
|
|
496
|
+
crowds the group to buy separation the page should have provided.
|
|
497
|
+
|
|
477
498
|
Make a register SELECTABLE with the `Table` `leading` gutter + `selectAll` slot — a
|
|
478
499
|
`CheckboxInput` per `TableRow` (its `leading` slot) + a select-all in the header band, the ticked
|
|
479
500
|
rows `marked`, paired with a `FloatingActionBar` (its "Clear" escape is locale-resolved — pass
|
|
@@ -98,7 +98,7 @@ const KY_CUOI = "2026-06";
|
|
|
98
98
|
const NHAN_BO_LOC: Partial<DateRangeFilterFieldLabels> = {
|
|
99
99
|
year: "Year", month: "Month", day: "Day", hour: "Hour", minute: "Minute", dayPeriod: "AM/PM",
|
|
100
100
|
today: "Today", yesterday: "Yesterday", tomorrow: "Tomorrow", thisWeek: "This week",
|
|
101
|
-
thisMonth: "This month", lastMonth: "Last month",
|
|
101
|
+
thisMonth: "This month", lastMonth: "Last month", from: "From", to: "To",
|
|
102
102
|
selectDateRange: "Select date range", selectDate: "Select date",
|
|
103
103
|
clear: "Clear", done: "Done", placeholder: "All time",
|
|
104
104
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "42.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./vite": {
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
"default": "./src/vite.mjs"
|
|
9
9
|
},
|
|
10
10
|
"./tokens": "./src/tokens.ts",
|
|
11
|
-
"./colors":
|
|
11
|
+
"./colors": {
|
|
12
|
+
"react-native": "./src/colors.ts",
|
|
13
|
+
"default": "./src/colors.web.ts"
|
|
14
|
+
},
|
|
12
15
|
"./deadline": "./src/deadline.ts",
|
|
13
16
|
"./option_badge": "./src/option_badge.tsx",
|
|
14
17
|
"./member_chip": "./src/member_chip.tsx",
|
|
@@ -67,6 +70,7 @@
|
|
|
67
70
|
"./kpi_card": "./src/kpi_card.tsx",
|
|
68
71
|
"./kpi_strip": "./src/kpi_strip.tsx",
|
|
69
72
|
"./summary_line": "./src/summary_line.tsx",
|
|
73
|
+
"./summary": "./src/summary.tsx",
|
|
70
74
|
"./empty_state": "./src/empty_state.tsx",
|
|
71
75
|
"./error_state": "./src/error_state.tsx",
|
|
72
76
|
"./format_date": "./src/format_date.ts",
|
|
@@ -85,7 +89,10 @@
|
|
|
85
89
|
"./trend_footer": "./src/trend_footer.tsx",
|
|
86
90
|
"./locale": "./src/locale.tsx",
|
|
87
91
|
"./spacing": "./src/spacing.ts",
|
|
88
|
-
"./theme":
|
|
92
|
+
"./theme": {
|
|
93
|
+
"react-native": "./src/theme.tsx",
|
|
94
|
+
"default": "./src/theme.web.tsx"
|
|
95
|
+
},
|
|
89
96
|
"./progress_bar": "./src/progress_bar.tsx",
|
|
90
97
|
"./form_date_picker": "./src/form_date_picker.tsx",
|
|
91
98
|
"./form_picker": "./src/form_picker.tsx",
|
package/src/avatar.tsx
CHANGED
|
@@ -39,7 +39,12 @@ export function Avatar(props: AvatarProps) {
|
|
|
39
39
|
aria-hidden={decorative || undefined}
|
|
40
40
|
style={[
|
|
41
41
|
styles.base,
|
|
42
|
-
|
|
42
|
+
// The initials disc is the one place a member LOOKS like the product,
|
|
43
|
+
// so it carries the brand accent rather than a fixed blue. NOT a
|
|
44
|
+
// restyle: `colors.accent` DEFAULTS to this exact blue-600, so an app
|
|
45
|
+
// that themes nothing renders identical pixels, while one that sets an
|
|
46
|
+
// accent gets its own hue without this file knowing.
|
|
47
|
+
{ backgroundColor: colors.accent, width: px, height: px },
|
|
43
48
|
style,
|
|
44
49
|
]}
|
|
45
50
|
>
|
package/src/avatar.web.tsx
CHANGED
|
@@ -46,7 +46,12 @@ export function Avatar(props: AvatarProps) {
|
|
|
46
46
|
aria-hidden={decorative || undefined}
|
|
47
47
|
style={[
|
|
48
48
|
styles.base,
|
|
49
|
-
|
|
49
|
+
// The initials disc is the one place a member LOOKS like the product,
|
|
50
|
+
// so it carries the brand accent rather than a fixed blue. NOT a
|
|
51
|
+
// restyle: `colors.accent` DEFAULTS to this exact blue-600, so an app
|
|
52
|
+
// that themes nothing renders identical pixels, while one that sets an
|
|
53
|
+
// accent gets its own hue without this file knowing.
|
|
54
|
+
{ backgroundColor: colors.accent, width: px, height: px },
|
|
50
55
|
style,
|
|
51
56
|
]}
|
|
52
57
|
>
|
package/src/avatar_size.ts
CHANGED
|
@@ -46,7 +46,16 @@ export const AVATAR_PX: Record<AvatarSize, number> = {
|
|
|
46
46
|
* account-menu trigger in a top bar, a prominent row's leading figure. Equals
|
|
47
47
|
* `CONTROL_HEIGHT` (`control_surface`), so it lines up with the icon buttons
|
|
48
48
|
* beside it. It was 48, which anchored to nothing: the bar's other controls
|
|
49
|
-
* are 40, and a 48px circle among them reads as the one thing that got out.
|
|
49
|
+
* are 40, and a 48px circle among them reads as the one thing that got out.
|
|
50
|
+
*
|
|
51
|
+
* **In a register, the rung follows what the row IS ABOUT, not the density.**
|
|
52
|
+
* Where a person is one FIELD of the row — an owner, an assignee beside a
|
|
53
|
+
* dozen other columns — the figure is a value like any other and takes `md`.
|
|
54
|
+
* Where the person is the row's SUBJECT (a league table of reps, a roster,
|
|
55
|
+
* anything grouped BY member), the avatar is that row's leading figure and
|
|
56
|
+
* takes this rung; at `md` such a register reads as a list of numbers that
|
|
57
|
+
* happens to carry faces. Budget for it: this rung is 12px wider than `md`,
|
|
58
|
+
* and in a table it takes those 12 from the flexible column the name is in. */
|
|
50
59
|
lg: 40,
|
|
51
60
|
/** A profile's own header. */
|
|
52
61
|
xl: 72,
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
const palette = {
|
|
2
|
+
black: "rgba(0,0,0,1)",
|
|
3
|
+
white: "rgba(255,255,255,1)",
|
|
4
|
+
|
|
5
|
+
// We use RGBA so that we can use React Native Animations with colors
|
|
6
|
+
red: {
|
|
7
|
+
50: "rgba(254, 242, 242, 1)",
|
|
8
|
+
100: "rgba(254, 226, 226, 1)",
|
|
9
|
+
200: "rgba(254, 202, 202, 1)",
|
|
10
|
+
300: "rgba(252, 165, 165, 1)",
|
|
11
|
+
400: "rgba(248, 113, 113, 1)",
|
|
12
|
+
500: "rgba(239, 68, 68, 1)",
|
|
13
|
+
600: "rgba(220, 38, 38, 1)",
|
|
14
|
+
700: "rgba(185, 28, 28, 1)",
|
|
15
|
+
800: "rgba(153, 27, 27, 1)",
|
|
16
|
+
900: "rgba(127, 29, 29, 1)",
|
|
17
|
+
950: "rgba(69, 10, 10, 1)",
|
|
18
|
+
},
|
|
19
|
+
orange: {
|
|
20
|
+
50: "rgba(255, 247, 237, 1)",
|
|
21
|
+
100: "rgba(255, 237, 213, 1)",
|
|
22
|
+
200: "rgba(254, 215, 170, 1)",
|
|
23
|
+
300: "rgba(253, 186, 116, 1)",
|
|
24
|
+
400: "rgba(251, 146, 60, 1)",
|
|
25
|
+
500: "rgba(249, 115, 22, 1)",
|
|
26
|
+
600: "rgba(234, 88, 12, 1)",
|
|
27
|
+
700: "rgba(194, 65, 12, 1)",
|
|
28
|
+
800: "rgba(154, 52, 18, 1)",
|
|
29
|
+
900: "rgba(124, 45, 18, 1)",
|
|
30
|
+
950: "rgba(67, 20, 7, 1)",
|
|
31
|
+
},
|
|
32
|
+
amber: {
|
|
33
|
+
50: "rgba(255, 251, 235, 1)",
|
|
34
|
+
100: "rgba(254, 243, 199, 1)",
|
|
35
|
+
200: "rgba(253, 230, 138, 1)",
|
|
36
|
+
300: "rgba(252, 211, 77, 1)",
|
|
37
|
+
400: "rgba(251, 191, 36, 1)",
|
|
38
|
+
500: "rgba(245, 158, 11, 1)",
|
|
39
|
+
600: "rgba(217, 119, 6, 1)",
|
|
40
|
+
700: "rgba(180, 83, 9, 1)",
|
|
41
|
+
800: "rgba(146, 64, 14, 1)",
|
|
42
|
+
900: "rgba(120, 53, 15, 1)",
|
|
43
|
+
950: "rgba(69, 26, 3, 1)",
|
|
44
|
+
},
|
|
45
|
+
yellow: {
|
|
46
|
+
50: "rgba(254, 252, 232, 1)",
|
|
47
|
+
100: "rgba(254, 249, 195, 1)",
|
|
48
|
+
200: "rgba(254, 240, 138, 1)",
|
|
49
|
+
300: "rgba(253, 224, 71, 1)",
|
|
50
|
+
400: "rgba(250, 204, 21, 1)",
|
|
51
|
+
500: "rgba(234, 179, 8, 1)",
|
|
52
|
+
600: "rgba(202, 138, 4, 1)",
|
|
53
|
+
700: "rgba(161, 98, 7, 1)",
|
|
54
|
+
800: "rgba(133, 77, 14, 1)",
|
|
55
|
+
900: "rgba(113, 63, 18, 1)",
|
|
56
|
+
950: "rgba(66, 32, 6, 1)",
|
|
57
|
+
},
|
|
58
|
+
lime: {
|
|
59
|
+
50: "rgba(247, 254, 231, 1)",
|
|
60
|
+
100: "rgba(236, 252, 203, 1)",
|
|
61
|
+
200: "rgba(217, 249, 157, 1)",
|
|
62
|
+
300: "rgba(190, 242, 100, 1)",
|
|
63
|
+
400: "rgba(163, 230, 53, 1)",
|
|
64
|
+
500: "rgba(132, 204, 22, 1)",
|
|
65
|
+
600: "rgba(101, 163, 13, 1)",
|
|
66
|
+
700: "rgba(77, 124, 15, 1)",
|
|
67
|
+
800: "rgba(63, 98, 18, 1)",
|
|
68
|
+
900: "rgba(54, 83, 20, 1)",
|
|
69
|
+
950: "rgba(26, 46, 5, 1)",
|
|
70
|
+
},
|
|
71
|
+
green: {
|
|
72
|
+
50: "rgba(240, 253, 244, 1)",
|
|
73
|
+
100: "rgba(220, 252, 231, 1)",
|
|
74
|
+
200: "rgba(187, 247, 208, 1)",
|
|
75
|
+
300: "rgba(134, 239, 172, 1)",
|
|
76
|
+
400: "rgba(74, 222, 128, 1)",
|
|
77
|
+
500: "rgba(34, 197, 94, 1)",
|
|
78
|
+
600: "rgba(22, 163, 74, 1)",
|
|
79
|
+
700: "rgba(21, 128, 61, 1)",
|
|
80
|
+
800: "rgba(22, 101, 52, 1)",
|
|
81
|
+
900: "rgba(20, 83, 45, 1)",
|
|
82
|
+
950: "rgba(5, 46, 22, 1)",
|
|
83
|
+
},
|
|
84
|
+
emerald: {
|
|
85
|
+
50: "rgba(236, 253, 245, 1)",
|
|
86
|
+
100: "rgba(209, 250, 229, 1)",
|
|
87
|
+
200: "rgba(167, 243, 208, 1)",
|
|
88
|
+
300: "rgba(110, 231, 183, 1)",
|
|
89
|
+
400: "rgba(52, 211, 153, 1)",
|
|
90
|
+
500: "rgba(16, 185, 129, 1)",
|
|
91
|
+
600: "rgba(5, 150, 105, 1)",
|
|
92
|
+
700: "rgba(4, 120, 87, 1)",
|
|
93
|
+
800: "rgba(6, 95, 70, 1)",
|
|
94
|
+
900: "rgba(6, 78, 59, 1)",
|
|
95
|
+
950: "rgba(2, 44, 34, 1)",
|
|
96
|
+
},
|
|
97
|
+
teal: {
|
|
98
|
+
50: "rgba(240, 253, 250, 1)",
|
|
99
|
+
100: "rgba(204, 251, 241, 1)",
|
|
100
|
+
200: "rgba(153, 246, 228, 1)",
|
|
101
|
+
300: "rgba(94, 234, 212, 1)",
|
|
102
|
+
400: "rgba(45, 212, 191, 1)",
|
|
103
|
+
500: "rgba(20, 184, 166, 1)",
|
|
104
|
+
600: "rgba(13, 148, 136, 1)",
|
|
105
|
+
700: "rgba(15, 118, 110, 1)",
|
|
106
|
+
800: "rgba(17, 94, 89, 1)",
|
|
107
|
+
900: "rgba(19, 78, 74, 1)",
|
|
108
|
+
950: "rgba(4, 47, 46, 1)",
|
|
109
|
+
},
|
|
110
|
+
cyan: {
|
|
111
|
+
50: "rgba(236, 254, 255, 1)",
|
|
112
|
+
100: "rgba(207, 250, 254, 1)",
|
|
113
|
+
200: "rgba(165, 243, 252, 1)",
|
|
114
|
+
300: "rgba(103, 232, 249, 1)",
|
|
115
|
+
400: "rgba(34, 211, 238, 1)",
|
|
116
|
+
500: "rgba(6, 182, 212, 1)",
|
|
117
|
+
600: "rgba(8, 145, 178, 1)",
|
|
118
|
+
700: "rgba(14, 116, 144, 1)",
|
|
119
|
+
800: "rgba(21, 94, 117, 1)",
|
|
120
|
+
900: "rgba(22, 78, 99, 1)",
|
|
121
|
+
950: "rgba(8, 51, 68, 1)",
|
|
122
|
+
},
|
|
123
|
+
sky: {
|
|
124
|
+
50: "rgba(240, 249, 255, 1)",
|
|
125
|
+
100: "rgba(224, 242, 254, 1)",
|
|
126
|
+
200: "rgba(186, 230, 253, 1)",
|
|
127
|
+
300: "rgba(125, 211, 252, 1)",
|
|
128
|
+
400: "rgba(56, 189, 248, 1)",
|
|
129
|
+
500: "rgba(14, 165, 233, 1)",
|
|
130
|
+
600: "rgba(2, 132, 199, 1)",
|
|
131
|
+
700: "rgba(3, 105, 161, 1)",
|
|
132
|
+
800: "rgba(7, 89, 133, 1)",
|
|
133
|
+
900: "rgba(12, 74, 110, 1)",
|
|
134
|
+
950: "rgba(8, 47, 73, 1)",
|
|
135
|
+
},
|
|
136
|
+
blue: {
|
|
137
|
+
50: "rgba(239, 246, 255, 1)",
|
|
138
|
+
100: "rgba(219, 234, 254, 1)",
|
|
139
|
+
200: "rgba(191, 219, 254, 1)",
|
|
140
|
+
300: "rgba(147, 197, 253, 1)",
|
|
141
|
+
400: "rgba(96, 165, 250, 1)",
|
|
142
|
+
500: "rgba(59, 130, 246, 1)",
|
|
143
|
+
600: "rgba(37, 99, 235, 1)",
|
|
144
|
+
700: "rgba(29, 78, 216, 1)",
|
|
145
|
+
800: "rgba(30, 64, 175, 1)",
|
|
146
|
+
900: "rgba(30, 58, 138, 1)",
|
|
147
|
+
950: "rgba(23, 37, 84, 1)",
|
|
148
|
+
},
|
|
149
|
+
indigo: {
|
|
150
|
+
50: "rgba(238, 242, 255, 1)",
|
|
151
|
+
100: "rgba(224, 231, 255, 1)",
|
|
152
|
+
200: "rgba(199, 210, 254, 1)",
|
|
153
|
+
300: "rgba(165, 180, 252, 1)",
|
|
154
|
+
400: "rgba(129, 140, 248, 1)",
|
|
155
|
+
500: "rgba(99, 102, 241, 1)",
|
|
156
|
+
600: "rgba(79, 70, 229, 1)",
|
|
157
|
+
700: "rgba(67, 56, 202, 1)",
|
|
158
|
+
800: "rgba(55, 48, 163, 1)",
|
|
159
|
+
900: "rgba(49, 46, 129, 1)",
|
|
160
|
+
950: "rgba(30, 27, 75, 1)",
|
|
161
|
+
},
|
|
162
|
+
violet: {
|
|
163
|
+
50: "rgba(245, 243, 255, 1)",
|
|
164
|
+
100: "rgba(237, 233, 254, 1)",
|
|
165
|
+
200: "rgba(221, 214, 254, 1)",
|
|
166
|
+
300: "rgba(196, 181, 253, 1)",
|
|
167
|
+
400: "rgba(167, 139, 250, 1)",
|
|
168
|
+
500: "rgba(139, 92, 246, 1)",
|
|
169
|
+
600: "rgba(124, 58, 237, 1)",
|
|
170
|
+
700: "rgba(109, 40, 217, 1)",
|
|
171
|
+
800: "rgba(91, 33, 182, 1)",
|
|
172
|
+
900: "rgba(76, 29, 149, 1)",
|
|
173
|
+
950: "rgba(46, 16, 101, 1)",
|
|
174
|
+
},
|
|
175
|
+
purple: {
|
|
176
|
+
50: "rgba(250, 245, 255, 1)",
|
|
177
|
+
100: "rgba(243, 232, 255, 1)",
|
|
178
|
+
200: "rgba(233, 213, 255, 1)",
|
|
179
|
+
300: "rgba(216, 180, 254, 1)",
|
|
180
|
+
400: "rgba(192, 132, 252, 1)",
|
|
181
|
+
500: "rgba(168, 85, 247, 1)",
|
|
182
|
+
600: "rgba(147, 51, 234, 1)",
|
|
183
|
+
700: "rgba(126, 34, 206, 1)",
|
|
184
|
+
800: "rgba(107, 33, 168, 1)",
|
|
185
|
+
900: "rgba(88, 28, 135, 1)",
|
|
186
|
+
950: "rgba(59, 7, 100, 1)",
|
|
187
|
+
},
|
|
188
|
+
fuchsia: {
|
|
189
|
+
50: "rgba(253, 244, 255, 1)",
|
|
190
|
+
100: "rgba(250, 232, 255, 1)",
|
|
191
|
+
200: "rgba(245, 208, 254, 1)",
|
|
192
|
+
300: "rgba(240, 171, 252, 1)",
|
|
193
|
+
400: "rgba(232, 121, 249, 1)",
|
|
194
|
+
500: "rgba(217, 70, 239, 1)",
|
|
195
|
+
600: "rgba(192, 38, 211, 1)",
|
|
196
|
+
700: "rgba(162, 28, 175, 1)",
|
|
197
|
+
800: "rgba(134, 25, 143, 1)",
|
|
198
|
+
900: "rgba(112, 26, 117, 1)",
|
|
199
|
+
950: "rgba(74, 4, 78, 1)",
|
|
200
|
+
},
|
|
201
|
+
pink: {
|
|
202
|
+
50: "rgba(253, 242, 248, 1)",
|
|
203
|
+
100: "rgba(252, 231, 243, 1)",
|
|
204
|
+
200: "rgba(251, 207, 232, 1)",
|
|
205
|
+
300: "rgba(249, 168, 212, 1)",
|
|
206
|
+
400: "rgba(244, 114, 182, 1)",
|
|
207
|
+
500: "rgba(236, 72, 153, 1)",
|
|
208
|
+
600: "rgba(219, 39, 119, 1)",
|
|
209
|
+
700: "rgba(190, 24, 93, 1)",
|
|
210
|
+
800: "rgba(157, 23, 77, 1)",
|
|
211
|
+
900: "rgba(131, 24, 67, 1)",
|
|
212
|
+
950: "rgba(80, 7, 36, 1)",
|
|
213
|
+
},
|
|
214
|
+
rose: {
|
|
215
|
+
50: "rgba(255, 241, 242, 1)",
|
|
216
|
+
100: "rgba(255, 228, 230, 1)",
|
|
217
|
+
200: "rgba(254, 205, 211, 1)",
|
|
218
|
+
300: "rgba(253, 164, 175, 1)",
|
|
219
|
+
400: "rgba(251, 113, 133, 1)",
|
|
220
|
+
500: "rgba(244, 63, 94, 1)",
|
|
221
|
+
600: "rgba(225, 29, 72, 1)",
|
|
222
|
+
700: "rgba(190, 18, 60, 1)",
|
|
223
|
+
800: "rgba(159, 18, 57, 1)",
|
|
224
|
+
900: "rgba(136, 19, 55, 1)",
|
|
225
|
+
950: "rgba(76, 5, 25, 1)",
|
|
226
|
+
},
|
|
227
|
+
slate: {
|
|
228
|
+
50: "rgba(248, 250, 252, 1)",
|
|
229
|
+
100: "rgba(241, 245, 249, 1)",
|
|
230
|
+
200: "rgba(226, 232, 240, 1)",
|
|
231
|
+
300: "rgba(203, 213, 225, 1)",
|
|
232
|
+
400: "rgba(148, 163, 184, 1)",
|
|
233
|
+
500: "rgba(100, 116, 139, 1)",
|
|
234
|
+
600: "rgba(71, 85, 105, 1)",
|
|
235
|
+
700: "rgba(51, 65, 85, 1)",
|
|
236
|
+
800: "rgba(30, 41, 59, 1)",
|
|
237
|
+
900: "rgba(15, 23, 42, 1)",
|
|
238
|
+
950: "rgba(2, 6, 23, 1)",
|
|
239
|
+
},
|
|
240
|
+
gray: {
|
|
241
|
+
50: "rgba(249, 250, 251, 1)",
|
|
242
|
+
100: "rgba(243, 244, 246, 1)",
|
|
243
|
+
200: "rgba(229, 231, 235, 1)",
|
|
244
|
+
300: "rgba(209, 213, 219, 1)",
|
|
245
|
+
400: "rgba(156, 163, 175, 1)",
|
|
246
|
+
500: "rgba(107, 114, 128, 1)",
|
|
247
|
+
600: "rgba(75, 85, 99, 1)",
|
|
248
|
+
700: "rgba(55, 65, 81, 1)",
|
|
249
|
+
800: "rgba(31, 41, 55, 1)",
|
|
250
|
+
900: "rgba(17, 24, 39, 1)",
|
|
251
|
+
950: "rgba(3, 7, 18, 1)",
|
|
252
|
+
},
|
|
253
|
+
zinc: {
|
|
254
|
+
50: "rgba(250, 250, 250, 1)",
|
|
255
|
+
100: "rgba(244, 244, 245, 1)",
|
|
256
|
+
200: "rgba(228, 228, 231, 1)",
|
|
257
|
+
300: "rgba(212, 212, 216, 1)",
|
|
258
|
+
400: "rgba(161, 161, 170, 1)",
|
|
259
|
+
500: "rgba(113, 113, 122, 1)",
|
|
260
|
+
600: "rgba(82, 82, 91, 1)",
|
|
261
|
+
700: "rgba(63, 63, 70, 1)",
|
|
262
|
+
800: "rgba(39, 39, 42, 1)",
|
|
263
|
+
900: "rgba(24, 24, 27, 1)",
|
|
264
|
+
950: "rgba(9, 9, 11, 1)",
|
|
265
|
+
},
|
|
266
|
+
neutral: {
|
|
267
|
+
50: "rgba(250, 250, 250, 1)",
|
|
268
|
+
100: "rgba(245, 245, 245, 1)",
|
|
269
|
+
200: "rgba(229, 229, 229, 1)",
|
|
270
|
+
300: "rgba(212, 212, 212, 1)",
|
|
271
|
+
400: "rgba(163, 163, 163, 1)",
|
|
272
|
+
500: "rgba(115, 115, 115, 1)",
|
|
273
|
+
600: "rgba(82, 82, 82, 1)",
|
|
274
|
+
700: "rgba(64, 64, 64, 1)",
|
|
275
|
+
800: "rgba(38, 38, 38, 1)",
|
|
276
|
+
900: "rgba(23, 23, 23, 1)",
|
|
277
|
+
950: "rgba(10, 10, 10, 1)",
|
|
278
|
+
},
|
|
279
|
+
stone: {
|
|
280
|
+
50: "rgba(250, 250, 249, 1)",
|
|
281
|
+
100: "rgba(245, 245, 244, 1)",
|
|
282
|
+
200: "rgba(231, 229, 228, 1)",
|
|
283
|
+
300: "rgba(214, 211, 209, 1)",
|
|
284
|
+
400: "rgba(168, 162, 158, 1)",
|
|
285
|
+
500: "rgba(120, 113, 108, 1)",
|
|
286
|
+
600: "rgba(87, 83, 78, 1)",
|
|
287
|
+
700: "rgba(68, 64, 60, 1)",
|
|
288
|
+
800: "rgba(41, 37, 36, 1)",
|
|
289
|
+
900: "rgba(28, 25, 23, 1)",
|
|
290
|
+
950: "rgba(12, 10, 9, 1)",
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* THE THREE THEMEABLE ROLES are `background`, `border` and `accent` — and
|
|
296
|
+
* deliberately nothing else. See `colors.web.ts`, which redefines exactly
|
|
297
|
+
* these as CSS variables so an app can carry its own identity.
|
|
298
|
+
*
|
|
299
|
+
* The line is drawn at MEANING, not at convenience. A palette family
|
|
300
|
+
* (`red`, `amber`, `emerald`, `zinc`…) is how this system says what something
|
|
301
|
+
* IS — a status, a valence, a series — so letting an app repaint those would
|
|
302
|
+
* not restyle the product, it would make it lie. What is left over is chrome:
|
|
303
|
+
* the paper, the paper's edge, and the one hue that is the customer's rather
|
|
304
|
+
* than ours. An app can own all of that and change no claim the UI makes.
|
|
305
|
+
*
|
|
306
|
+
* This file is the CONTRACT and stays literal: TypeScript resolves `./colors`
|
|
307
|
+
* here (never the `.web` variant), so both files must carry the same keys or an
|
|
308
|
+
* app reading `colors.accent` would fail to typecheck against the platform it
|
|
309
|
+
* actually runs on. It is also what native renders, where `var()` means nothing.
|
|
310
|
+
*/
|
|
311
|
+
export const colors = {
|
|
312
|
+
...palette,
|
|
313
|
+
border: palette.zinc["200"],
|
|
314
|
+
border_shadow: `0px 1px 2px 0px ${palette.zinc["200"]}`,
|
|
315
|
+
/** A RAISED surface — Card, Drawer, Modal, the things that sit on the page. */
|
|
316
|
+
background: palette.white,
|
|
317
|
+
/**
|
|
318
|
+
* The one brand hue, and its reach is narrower than the word suggests: it
|
|
319
|
+
* paints IDENTITY marks — today the avatar's initials disc, the one place a
|
|
320
|
+
* person is rendered as a coloured shape, on every register in every app.
|
|
321
|
+
*
|
|
322
|
+
* It deliberately does NOT paint interaction chrome. The focus ring is
|
|
323
|
+
* `zinc[900]` and the primary action is near-black because those are the KIT's
|
|
324
|
+
* semantics, shared across every app so a reader learns them once; repainting
|
|
325
|
+
* them per tenant would make "what does a focus ring look like" a per-app
|
|
326
|
+
* question. And never for DATA: a series, a status and a valence all come from
|
|
327
|
+
* a palette family, so an accent can never overwrite a meaning.
|
|
328
|
+
*/
|
|
329
|
+
accent: palette.blue["600"],
|
|
330
|
+
shadow: `0px 0px 6px 1px ${palette.zinc["300"]}`,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* A low-alpha wash of a palette color (hover tints, dimmed chart segments,
|
|
335
|
+
* icon discs). Handles the palette's rgba() strings plus rgb()/#RRGGBB.
|
|
336
|
+
*/
|
|
337
|
+
export function withAlpha(color: string, alpha: number): string {
|
|
338
|
+
if (color.startsWith("rgba(")) return color.replace(/,\s*[\d.]+\s*\)\s*$/, `, ${alpha})`);
|
|
339
|
+
if (color.startsWith("rgb(")) return color.replace("rgb(", "rgba(").replace(/\)\s*$/, `, ${alpha})`);
|
|
340
|
+
if (color.startsWith("#") && color.length === 7) {
|
|
341
|
+
return `${color}${Math.round(alpha * 255)
|
|
342
|
+
.toString(16)
|
|
343
|
+
.padStart(2, "0")}`;
|
|
344
|
+
}
|
|
345
|
+
return color;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* A palette FAMILY name — the single, semantic way to reference a color across
|
|
350
|
+
* the system (Badge, status indicators, chart series, breakdown segments).
|
|
351
|
+
* Reference colors by NAME and let the component resolve the shade it needs;
|
|
352
|
+
* never thread a raw hex through props — it drifts (one call site picks 500,
|
|
353
|
+
* another 600, and the same "status" renders two greens). Excludes every
|
|
354
|
+
* non-scale role key — the four themeable roles included, since `solid("accent")`
|
|
355
|
+
* would otherwise typecheck and hand back `undefined` from a family that has no
|
|
356
|
+
* shades.
|
|
357
|
+
*/
|
|
358
|
+
export type ColorName = Exclude<
|
|
359
|
+
keyof typeof colors,
|
|
360
|
+
"border" | "border_shadow" | "background" | "accent" | "shadow" | "black" | "white"
|
|
361
|
+
>;
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* The canonical SOLID shade of a family (500) — status dots, status-grid
|
|
365
|
+
* cells, breakdown segments, chart series. Defined ONCE so every indicator of
|
|
366
|
+
* the same color agrees on the shade.
|
|
367
|
+
*/
|
|
368
|
+
export function solid(name: ColorName): string {
|
|
369
|
+
return colors[name][500];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** A low-alpha wash of a family's solid shade — cell/segment tints, dimmed
|
|
373
|
+
* states. `tint("emerald", 0.2)` === `withAlpha(solid("emerald"), 0.2)`. */
|
|
374
|
+
export function tint(name: ColorName, alpha: number): string {
|
|
375
|
+
return withAlpha(solid(name), alpha);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Dark → light stops a `ramp` spans (the usable mid range of a scale). */
|
|
379
|
+
const RAMP_STOPS = [700, 600, 500, 400, 300, 200] as const;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* `count` distinct shades of ONE family, strong → light — the monochrome
|
|
383
|
+
* "one hue family per dimension" data ramp (a Breakdown's segments, an ordered
|
|
384
|
+
* funnel, any multi-category breakdown of a single dimension). Use this for a
|
|
385
|
+
* COHERENT dimension instead of hand-picking shades or scattering hues; the
|
|
386
|
+
* segment LABEL carries identity, the shade only orders. (For semantic
|
|
387
|
+
* categories whose color carries MEANING — status — give each its own
|
|
388
|
+
* `ColorName` and `solid()` it, don't ramp.)
|
|
389
|
+
*/
|
|
390
|
+
export function ramp(name: ColorName, count: number): string[] {
|
|
391
|
+
const scale = colors[name];
|
|
392
|
+
if (count <= 1) return [scale[600]];
|
|
393
|
+
return Array.from({ length: count }, (_, i) => {
|
|
394
|
+
const idx = Math.round((i / (count - 1)) * (RAMP_STOPS.length - 1));
|
|
395
|
+
return scale[RAMP_STOPS[idx]];
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Is `value` a usable {@link ColorName} — a palette FAMILY, not a role key
|
|
401
|
+
* (`border`/`background`/…) and not `black`/`white`? A family resolves to a
|
|
402
|
+
* shade object; the role/scalar keys resolve to a string, so "value is an
|
|
403
|
+
* object" is the test (no name list to keep in sync).
|
|
404
|
+
*/
|
|
405
|
+
export function isColorName(value: unknown): value is ColorName {
|
|
406
|
+
return (
|
|
407
|
+
typeof value === "string" &&
|
|
408
|
+
value in colors &&
|
|
409
|
+
typeof (colors as Record<string, unknown>)[value] === "object"
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Coerce an arbitrary color token to a {@link ColorName}, falling back to a
|
|
415
|
+
* neutral. The single graceful-degradation point for stored option/status
|
|
416
|
+
* colors: a select option's `color` may be a token a newer table config
|
|
417
|
+
* introduced that this UI build predates, or absent entirely — either way a
|
|
418
|
+
* component renders a neutral badge instead of breaking. Used by `OptionBadge`.
|
|
419
|
+
*/
|
|
420
|
+
export function asColorName(value: unknown, fallback: ColorName = "zinc"): ColorName {
|
|
421
|
+
return isColorName(value) ? value : fallback;
|
|
422
|
+
}
|