@tristan2828/ui-foundation 4.61.1 → 4.63.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/conventions/docs/cell-patterns.md +16 -11
- package/conventions/docs/design-language.md +132 -23
- package/dist/components/app/app-shell.js +28 -2
- package/dist/components/app/board.js +1 -1
- package/dist/components/app/data-table.js +1 -1
- package/dist/components/app/foundation-providers.d.ts +14 -1
- package/dist/components/app/foundation-providers.js +4 -3
- package/dist/components/app/markdown.d.ts +7 -2
- package/dist/components/app/markdown.js +15 -20
- package/dist/components/app/milkdown-editor.d.ts +1 -1
- package/dist/components/app/milkdown-editor.js +58 -17
- package/dist/components/app/module-card.js +1 -1
- package/dist/components/app/palette-provider.d.ts +14 -0
- package/dist/components/app/palette-provider.js +35 -0
- package/dist/components/app/prose-look.d.ts +7 -0
- package/dist/components/app/rich-text-editor.d.ts +8 -1
- package/dist/components/app/rich-text-editor.js +6 -0
- package/dist/components/app/rich-text-format-items.js +1 -1
- package/dist/components/app/rich-text-node-views.d.ts +9 -3
- package/dist/components/app/rich-text-node-views.js +12 -8
- package/dist/components/app/rich-text-table-grips.d.ts +31 -0
- package/dist/components/app/rich-text-table-grips.js +216 -0
- package/dist/components/app/rich-text-table.d.ts +19 -2
- package/dist/components/app/rich-text-table.js +232 -1
- package/dist/components/app/rich-text-toolbar.d.ts +4 -9
- package/dist/components/app/rich-text-toolbar.js +9 -11
- package/dist/components/app/rich-text-widgets.js +2 -2
- package/dist/components/app/widget-card.js +2 -2
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/badge.js +5 -2
- package/dist/components/ui/button.js +10 -10
- package/dist/components/ui/card.js +3 -3
- package/dist/components/ui/checkbox.js +1 -1
- package/dist/components/ui/combobox.js +1 -1
- package/dist/components/ui/dialog.js +1 -1
- package/dist/components/ui/input-group.js +1 -1
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/progress.d.ts +7 -0
- package/dist/components/ui/progress.js +19 -0
- package/dist/components/ui/select.js +1 -1
- package/dist/components/ui/sheet.js +1 -1
- package/dist/components/ui/slider.d.ts +3 -0
- package/dist/components/ui/slider.js +12 -0
- package/dist/components/ui/textarea.js +1 -1
- package/dist/hooks/use-palette.d.ts +14 -0
- package/dist/hooks/use-palette.js +7 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/lib/markdown-table.d.ts +28 -0
- package/dist/lib/markdown-table.js +106 -0
- package/dist/lib/palettes.d.ts +21 -0
- package/dist/lib/palettes.js +74 -0
- package/package.json +1 -1
- package/styles/index.css +152 -0
- package/styles/theme.css +321 -11
- package/vite/index.d.ts +7 -1
- package/vite/pwa.js +21 -7
- package/dist/components/app/rich-text-table-menu.d.ts +0 -20
- package/dist/components/app/rich-text-table-menu.js +0 -53
|
@@ -26,15 +26,15 @@
|
|
|
26
26
|
- [A variant: a pressed icon inside another value's cell](#a-variant-a-pressed-icon-inside-another-values-cell) · lines 565–648
|
|
27
27
|
- [16. Title linking to the record's view](#16-title-linking-to-the-records-view) · lines 650–685
|
|
28
28
|
- [17. Quick action on the view](#17-quick-action-on-the-view) · lines 687–760
|
|
29
|
-
- [18. Editable value](#18-editable-value) · lines 762–
|
|
30
|
-
- [A list of sub-items](#a-list-of-sub-items) · lines
|
|
31
|
-
- [In a table's rows](#in-a-tables-rows) · lines
|
|
32
|
-
- [A table of a few short columns: `width="content"`](#a-table-of-a-few-short-columns-widthcontent) · lines
|
|
33
|
-
- [19. Stage circle for an ordered status](#19-stage-circle-for-an-ordered-status) · lines
|
|
34
|
-
- [20. Count linking to the related records](#20-count-linking-to-the-related-records) · lines
|
|
35
|
-
- [21. Rows grouped under headings, and a pill filter](#21-rows-grouped-under-headings-and-a-pill-filter) · lines
|
|
36
|
-
- [22. A highlighted row, and rows that are done](#22-a-highlighted-row-and-rows-that-are-done) · lines
|
|
37
|
-
- [23. A Group menu, and a record read beside the list](#23-a-group-menu-and-a-record-read-beside-the-list) · lines
|
|
29
|
+
- [18. Editable value](#18-editable-value) · lines 762–1226
|
|
30
|
+
- [A list of sub-items](#a-list-of-sub-items) · lines 1042–1127
|
|
31
|
+
- [In a table's rows](#in-a-tables-rows) · lines 1129–1214
|
|
32
|
+
- [A table of a few short columns: `width="content"`](#a-table-of-a-few-short-columns-widthcontent) · lines 1216–1226
|
|
33
|
+
- [19. Stage circle for an ordered status](#19-stage-circle-for-an-ordered-status) · lines 1228–1294
|
|
34
|
+
- [20. Count linking to the related records](#20-count-linking-to-the-related-records) · lines 1296–1360
|
|
35
|
+
- [21. Rows grouped under headings, and a pill filter](#21-rows-grouped-under-headings-and-a-pill-filter) · lines 1362–1430
|
|
36
|
+
- [22. A highlighted row, and rows that are done](#22-a-highlighted-row-and-rows-that-are-done) · lines 1432–1460
|
|
37
|
+
- [23. A Group menu, and a record read beside the list](#23-a-group-menu-and-a-record-read-beside-the-list) · lines 1462–1538
|
|
38
38
|
|
|
39
39
|
<!-- /index -->
|
|
40
40
|
|
|
@@ -1002,8 +1002,13 @@ const price = editInPlace({
|
|
|
1002
1002
|
is GitHub's `> [!NOTE]` (note, tip, important, warning, caution), its
|
|
1003
1003
|
icon a menu of kinds. In a table, Tab in the last cell adds a row,
|
|
1004
1004
|
Enter goes down a column (and out from the last row), bars under and
|
|
1005
|
-
beside it add a row or a column, and the
|
|
1006
|
-
|
|
1005
|
+
beside it add a row or a column, and a grip on the hovered column and
|
|
1006
|
+
row opens its menu: insert, move and delete, align, sort (rewriting the
|
|
1007
|
+
rows), a header column (its first cells bold) and cell colours (a
|
|
1008
|
+
cell's whole text in a highlight); a right-click or Shift+F10 in a cell
|
|
1009
|
+
opens the cell's. Columns fit their words. `tableLook="boxed"` (on the
|
|
1010
|
+
editor and on `<Markdown>`) gives a roomier boxed table with zebra rows
|
|
1011
|
+
instead of the compact grid. Selected words can be
|
|
1007
1012
|
highlighted in four colours (amber, yellow, green, red) from the
|
|
1008
1013
|
toolbars' Highlight menu or with "/highlight", and un-highlighted;
|
|
1009
1014
|
they're saved as `==words==` (`=={green}words==` for the others), plain
|
|
@@ -3,25 +3,28 @@
|
|
|
3
3
|
<!-- index: written by `npm run docs:index` in the package from the headings below; never edit it by hand -->
|
|
4
4
|
## Index
|
|
5
5
|
|
|
6
|
-
- [
|
|
7
|
-
- [
|
|
8
|
-
- [
|
|
9
|
-
- [
|
|
10
|
-
- [
|
|
11
|
-
- [
|
|
12
|
-
- [
|
|
13
|
-
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
|
|
20
|
-
- [
|
|
21
|
-
- [A
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
6
|
+
- [Palettes](#palettes) · lines 37–95
|
|
7
|
+
- [Glow](#glow) · lines 97–115
|
|
8
|
+
- [Semantic tones](#semantic-tones) · lines 117–145
|
|
9
|
+
- [The three badge styles](#the-three-badge-styles) · lines 147–164
|
|
10
|
+
- [Mapping an enum to tones](#mapping-an-enum-to-tones) · lines 166–195
|
|
11
|
+
- [Glyphs and icons: 3:1](#glyphs-and-icons-31) · lines 197–210
|
|
12
|
+
- [Logos and brand marks](#logos-and-brand-marks) · lines 212–256
|
|
13
|
+
- [Categorical colour](#categorical-colour) · lines 258–328
|
|
14
|
+
- [Shapes and sizes](#shapes-and-sizes) · lines 330–351
|
|
15
|
+
- [Typography](#typography) · lines 353–447
|
|
16
|
+
- [Written text (notes)](#written-text-notes) · lines 387–447
|
|
17
|
+
- [Density](#density) · lines 449–506
|
|
18
|
+
- [Widget cards (hub pages)](#widget-cards-hub-pages) · lines 508–622
|
|
19
|
+
- [Module cards (a hub's places)](#module-cards-a-hubs-places) · lines 624–693
|
|
20
|
+
- [Notices](#notices) · lines 695–723
|
|
21
|
+
- [A value over time](#a-value-over-time) · lines 725–803
|
|
22
|
+
- [Several values over time](#several-values-over-time) · lines 766–803
|
|
23
|
+
- [A list as a board](#a-list-as-a-board) · lines 805–856
|
|
24
|
+
- [A list as cards](#a-list-as-cards) · lines 858–904
|
|
25
|
+
- [Key numbers on a record](#key-numbers-on-a-record) · lines 906–928
|
|
26
|
+
- [Picking at random](#picking-at-random) · lines 930–961
|
|
27
|
+
- [Filling in what's missing](#filling-in-whats-missing) · lines 963–1030
|
|
25
28
|
|
|
26
29
|
<!-- /index -->
|
|
27
30
|
|
|
@@ -31,6 +34,86 @@ Tokens and styles here **grow freely**, in the foundation package, so every
|
|
|
31
34
|
app gets them: they do not wait for a second app to need them. New
|
|
32
35
|
composites don't grow that way; they are built against a real app's screen.
|
|
33
36
|
|
|
37
|
+
## Palettes
|
|
38
|
+
|
|
39
|
+
A palette is an app's own dark look: its surfaces, text, accent and a
|
|
40
|
+
second colour, chosen once for every app the foundation serves. There
|
|
41
|
+
are four, in the order they were ranked:
|
|
42
|
+
|
|
43
|
+
| Name | `palette` | Accent | Second colour |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| Indigo and Cyan | `indigo-cyan` | indigo, the only accent that passes 4.5:1 on cards | cyan |
|
|
46
|
+
| Indigo and Rose | `indigo-rose` | indigo-violet | rose |
|
|
47
|
+
| Graphite and Periwinkle | `graphite-periwinkle` | periwinkle on neutral grey | lilac |
|
|
48
|
+
| Violet | `violet` | violet | magenta |
|
|
49
|
+
|
|
50
|
+
**How an app picks one.** `<FoundationProviders palette="indigo-cyan">`
|
|
51
|
+
names the app's default. Add `palettePicker` and the app shell's header
|
|
52
|
+
offers all four (with swatches); the one picked is remembered on that
|
|
53
|
+
device, and a new device starts on the default. `usePalette()` reads the
|
|
54
|
+
palette on screen. Without `palette`, nothing changes: light and dark
|
|
55
|
+
follow the system, with the toggle in the header.
|
|
56
|
+
|
|
57
|
+
**A palette is dark only.** With one, the theme is forced dark and the
|
|
58
|
+
shell has no light/dark toggle. The light theme stays for apps without a
|
|
59
|
+
palette. Printed notes still print light: the print override beats a
|
|
60
|
+
palette's block.
|
|
61
|
+
|
|
62
|
+
**Where it lives.** `styles/theme.css`: each palette's colours as
|
|
63
|
+
primitives (`--palette-<name>-*`, ratios beside them), and one block per
|
|
64
|
+
palette, `.dark[data-palette="<name>"]`, that sets only what differs from
|
|
65
|
+
`.dark`. `data-palette` goes on `<html>`, so popups portaled to `<body>`
|
|
66
|
+
wear it too. An installed app's colours follow it with
|
|
67
|
+
`pwa({ palette })`.
|
|
68
|
+
|
|
69
|
+
**Why some tokens aren't the guide's colour as written.**
|
|
70
|
+
|
|
71
|
+
- **The focus ring is the accent** (4.24 to 5.25:1 on the page), not the
|
|
72
|
+
guide's accent deep, which measured 2.30 to 2.91, under the 3:1 a focus
|
|
73
|
+
indicator needs.
|
|
74
|
+
- **Links are accent light** (`--link`, 8.5:1 or more). The accent fails
|
|
75
|
+
4.5:1 as small text on Indigo and Rose and on Violet, and on every
|
|
76
|
+
palette's cards but Indigo and Cyan, so it is never small text: it is
|
|
77
|
+
the ring, the primary fill and the active item's tint. The `link`
|
|
78
|
+
Button variant is `text-link` for the same reason.
|
|
79
|
+
- **The status colours are text shades.** The guide's success, warning
|
|
80
|
+
and danger are `--success-text`, `--warning-text` and
|
|
81
|
+
`--destructive-text` (soft badges, icons, coloured words). Solid fills
|
|
82
|
+
keep the deeper shades with white text: white on the guide's success
|
|
83
|
+
green measures 1.80. Info keeps its blue.
|
|
84
|
+
- **Violet's text on its accent** is `#110823`, a shade darker than the
|
|
85
|
+
guide's `#130925`, which measured 4.496:1 (under 4.5).
|
|
86
|
+
- **Danger text** is `#df7a72`, a shade lighter than the guide's
|
|
87
|
+
`#dc726a`, which measured 4.37 on Graphite's raised surface and 4.46 on
|
|
88
|
+
an outline button's fill (an invalid image field's message).
|
|
89
|
+
- **Field borders** (`--input`, the guide's strong border) measure 2.5 to
|
|
90
|
+
2.9 against the page, under 3:1. It is already the lightest grey each
|
|
91
|
+
palette has, so it is kept as designed; axe doesn't check borders.
|
|
92
|
+
|
|
93
|
+
The package's `e2e/fixtures-visual.spec.ts` runs every primitive,
|
|
94
|
+
composite and tone story through axe in each palette, and measures every
|
|
95
|
+
glyph and categorical slot against each palette's page and card.
|
|
96
|
+
|
|
97
|
+
## Glow
|
|
98
|
+
|
|
99
|
+
Under a palette only, from `--primary` so each palette glows in its own
|
|
100
|
+
accent; an app without a palette never gets it. Everything else stays
|
|
101
|
+
flat: no glow at the top of the page.
|
|
102
|
+
|
|
103
|
+
- **The primary button** (Button's default variant) on hover, keyboard
|
|
104
|
+
focus and press: one soft layer of the accent (`--primary-glow`: 38px
|
|
105
|
+
blur, 2px spread, 38%), and on hover a shine sweeping across once
|
|
106
|
+
(650ms). Under reduced motion, no shine and no shrink. Secondary,
|
|
107
|
+
outline, ghost and destructive buttons never glow.
|
|
108
|
+
- **A hovered card**: its edge turns the accent with a soft halo
|
|
109
|
+
(`--edge-glow`), through the `glow-edge` utility. `ModuleCard`,
|
|
110
|
+
`WidgetCard` and `Board`'s cards carry it, and a `DataTable` card
|
|
111
|
+
layout's `Card`s; an app's own card takes the class.
|
|
112
|
+
- **A slider's thumb** (`ui/slider`): a small halo (`--thumb-glow`).
|
|
113
|
+
|
|
114
|
+
For anything else of an app's own, `palette:` is the variant
|
|
115
|
+
(`palette:hover:shadow-(--primary-glow)`).
|
|
116
|
+
|
|
34
117
|
## Semantic tones
|
|
35
118
|
|
|
36
119
|
Four tones carry meaning: **success**, **warning**, **destructive** and
|
|
@@ -244,6 +327,29 @@ If you find yourself wanting a thirteenth, that is a sign the column should
|
|
|
244
327
|
be showing a shape or a label rather than more colours: there is no more
|
|
245
328
|
room that stays readable.
|
|
246
329
|
|
|
330
|
+
## Shapes and sizes
|
|
331
|
+
|
|
332
|
+
The shared style guide's, the same in every app with or without a
|
|
333
|
+
palette:
|
|
334
|
+
|
|
335
|
+
- **Radius 9px** (`--radius-base`): buttons and cards on `--radius`,
|
|
336
|
+
fields on `--radius-md` (about 7px).
|
|
337
|
+
- **Buttons** 36px tall with 16px sides, semibold (`sm` 32px, `lg` 40px,
|
|
338
|
+
the icon sizes to match). A press shrinks the button to 96%, not under
|
|
339
|
+
reduced motion.
|
|
340
|
+
- **Fields** (input, select, input group, a multi-pick's box) 36px,
|
|
341
|
+
outlined. **Checkbox** 18px, a 5px radius, solid when checked, a small
|
|
342
|
+
press pop.
|
|
343
|
+
- **Cards** a hairline border with a soft shadow (`--card-shadow`: 30% in
|
|
344
|
+
dark, lighter on white), 16px padding, 14px between their parts. A
|
|
345
|
+
card's header, when it comes first, is a tinted bar flush with the top
|
|
346
|
+
(`--card-header`), as on every `WidgetCard` and `EntityView` section.
|
|
347
|
+
- **Badges** are pills; the default is a soft tint of the accent
|
|
348
|
+
(`variant="solid"` is the filled one). The tone styles are unchanged.
|
|
349
|
+
- **Behind a dialog or sheet**, the page is dimmed and blurred.
|
|
350
|
+
- **Motion** is quick: 120ms with the standard easing, for every
|
|
351
|
+
transition that doesn't name its own.
|
|
352
|
+
|
|
247
353
|
## Typography
|
|
248
354
|
|
|
249
355
|
Six roles cover every screen. Each is one class that sets size, line
|
|
@@ -251,13 +357,16 @@ height and weight together, from tokens in the package's `styles/theme.css`:
|
|
|
251
357
|
|
|
252
358
|
| Class | For | Size / weight |
|
|
253
359
|
|---|---|---|
|
|
254
|
-
| `type-page-title` | the screen's one `<h1>` (a table's title, a form's title) |
|
|
360
|
+
| `type-page-title` | the screen's one `<h1>` (a table's title, a form's title) | 28px / 600, line 1.2 |
|
|
255
361
|
| `type-section-title` | a heading inside a screen (a form section, a card group) | 16px / 600 |
|
|
256
|
-
| `type-body` | running text, table cells, form text, the pagination summary | 14px / 400 |
|
|
362
|
+
| `type-body` | running text, table cells, form text, the pagination summary | 14px / 400, line 1.5 |
|
|
257
363
|
| `type-label` | column headers, field labels, a small heading over a group | 14px / 500 |
|
|
258
364
|
| `type-caption` | counts, metadata, helper text under a field | 12px / 400 |
|
|
259
365
|
| `type-numeric` | the one live number on a card (`ModuleCard`'s count), in tabular figures | 22px / 500 |
|
|
260
366
|
|
|
367
|
+
- **Headings draw in a little:** the page and section titles, and written
|
|
368
|
+
text's `#` and `##`, take -0.02em letter spacing. Labels stay normal
|
|
369
|
+
case at 14px medium (small caps at 12px read too small).
|
|
261
370
|
- **Use a role instead of size and weight classes,** not as well as them.
|
|
262
371
|
`type-page-title` replaces `text-lg font-semibold`. Combining a role with
|
|
263
372
|
`text-sm`, `font-medium` or `leading-*` makes two classes set the same
|
|
@@ -351,8 +460,8 @@ set, not a `DataTable` prop:
|
|
|
351
460
|
|
|
352
461
|
| `data-density` | Changes | Table cell padding (vertical) | Header height | For |
|
|
353
462
|
|---|---|---|---|---|
|
|
354
|
-
|
|
|
355
|
-
|
|
|
463
|
+
| *(unset)* | — | 4px | 32px | the default: compact rows, as the shared style guide has them |
|
|
464
|
+
| `compact` | a hub's cards (`WidgetCard`, `WidgetList` rows, `ModuleCard`); table rows stay the default's | 4px | 32px | a dense hub |
|
|
356
465
|
| `comfortable` | table rows only | 12px | 48px | short tables, or rows people read rather than scan |
|
|
357
466
|
| `large` | **everything**: type, spacing, control heights, icons, the sidebar, table rows | 12px | 56px | a screen read from a distance: a TV used as the desktop monitor, with a mouse and keyboard |
|
|
358
467
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ChevronRightIcon, LogOutIcon, MoonIcon, SunIcon } from 'lucide-react';
|
|
2
|
+
import { ChevronRightIcon, LogOutIcon, MoonIcon, PaletteIcon, SunIcon } from 'lucide-react';
|
|
3
3
|
import { useTheme } from 'next-themes';
|
|
4
4
|
import { useEffect, useId, useState } from 'react';
|
|
5
5
|
import { Link, Navigate, Outlet, useLocation } from 'react-router';
|
|
@@ -11,8 +11,11 @@ import { ErrorState } from './error-state.js';
|
|
|
11
11
|
import { useErrorReporter } from '../../error-reporting/error-reporting-context.js';
|
|
12
12
|
import { ReportRoutePattern } from '../../error-reporting/error-reporting-provider.js';
|
|
13
13
|
import { useShellDocumentTitle } from '../../hooks/use-document-title.js';
|
|
14
|
+
import { usePalette } from '../../hooks/use-palette.js';
|
|
15
|
+
import { isPaletteName, PALETTE_LABELS, PALETTES } from '../../lib/palettes.js';
|
|
14
16
|
import { cn } from 'cn';
|
|
15
17
|
import { Button } from '../ui/button.js';
|
|
18
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, } from '../ui/dropdown-menu.js';
|
|
16
19
|
import { Skeleton } from '../ui/skeleton.js';
|
|
17
20
|
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarTrigger, useSidebar, } from '../ui/sidebar.js';
|
|
18
21
|
import { Toaster } from '../ui/sonner.js';
|
|
@@ -36,6 +39,29 @@ function CloseMobileSidebarOnNavigate() {
|
|
|
36
39
|
}, [key, setOpenMobile]);
|
|
37
40
|
return null;
|
|
38
41
|
}
|
|
42
|
+
// The header's look control: an app with a palette is always dark, so it
|
|
43
|
+
// has no light/dark toggle, and with the picker on it gets the palette
|
|
44
|
+
// menu in its place (design-language.md "Palettes").
|
|
45
|
+
function LookControl() {
|
|
46
|
+
const { palette, picker } = usePalette();
|
|
47
|
+
if (!palette)
|
|
48
|
+
return _jsx(ThemeToggle, {});
|
|
49
|
+
return picker ? _jsx(PalettePicker, { palette: palette }) : null;
|
|
50
|
+
}
|
|
51
|
+
// Each palette's swatch is its own background with its accent and second
|
|
52
|
+
// colour on it, read from the palette primitives, so the menu shows all
|
|
53
|
+
// four whichever is on screen.
|
|
54
|
+
function PaletteSwatch({ name }) {
|
|
55
|
+
const colour = (part) => `var(--palette-${name}-${part})`;
|
|
56
|
+
return (_jsxs("span", { "aria-hidden": true, className: "flex h-4 w-7 shrink-0 items-center justify-center gap-0.5 rounded-sm border border-border", style: { background: colour('background') }, children: [_jsx("span", { className: "size-2 rounded-full", style: { background: colour('accent') } }), _jsx("span", { className: "size-2 rounded-full", style: { background: colour('second') } })] }));
|
|
57
|
+
}
|
|
58
|
+
function PalettePicker({ palette }) {
|
|
59
|
+
const { setPalette } = usePalette();
|
|
60
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": `Palette: ${PALETTE_LABELS[palette]}` }), children: _jsx(PaletteIcon, {}) }), _jsx(DropdownMenuContent, { align: "end", className: "w-60", children: _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Palette on this device" }), _jsx(DropdownMenuRadioGroup, { value: palette, onValueChange: (next) => {
|
|
61
|
+
if (isPaletteName(next))
|
|
62
|
+
setPalette(next);
|
|
63
|
+
}, children: PALETTES.map((name) => (_jsxs(DropdownMenuRadioItem, { value: name, children: [_jsx(PaletteSwatch, { name: name }), PALETTE_LABELS[name]] }, name))) })] }) })] }));
|
|
64
|
+
}
|
|
39
65
|
function ThemeToggle() {
|
|
40
66
|
const { resolvedTheme, setTheme } = useTheme();
|
|
41
67
|
const isDark = resolvedTheme === 'dark';
|
|
@@ -160,5 +186,5 @@ export function AppShell({ title, logo: Logo, nav, sidebarExtra, defaultSidebarO
|
|
|
160
186
|
// (AuthProvider), so this stays a deliberate logout: a
|
|
161
187
|
// clean /login, never the previous user's page as `from`.
|
|
162
188
|
logout().catch(() => { });
|
|
163
|
-
}, children: _jsx(LogOutIcon, {}) })] })] })] }) }), _jsxs(SidebarInset, { className: "min-w-0", children: [_jsxs("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border px-4", children: [_jsx(SidebarTrigger, {}), _jsx("div", { className: "flex-1" }), _jsx(
|
|
189
|
+
}, children: _jsx(LogOutIcon, {}) })] })] })] }) }), _jsxs(SidebarInset, { className: "min-w-0", children: [_jsxs("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border px-4", children: [_jsx(SidebarTrigger, {}), _jsx("div", { className: "flex-1" }), _jsx(LookControl, {})] }), _jsx("div", { className: cn('flex-1 p-6', bottomBar?.length && BOTTOM_BAR_PADDING), children: _jsx(Outlet, {}) })] }), bottomBar && bottomBar.length > 0 && _jsx(BottomBar, { items: bottomBar, nav: nav, activeTo: activeTo })] }), _jsx(Toaster, { position: "top-center" })] }));
|
|
164
190
|
}
|
|
@@ -101,7 +101,7 @@ export function Board({ 'aria-label': ariaLabel, columns, items, getId, getColum
|
|
|
101
101
|
event.dataTransfer.setData(DRAG_TYPE, String(getId(item)));
|
|
102
102
|
event.dataTransfer.effectAllowed = 'move';
|
|
103
103
|
}
|
|
104
|
-
: undefined, className: cn('gap-2 px-3 py-3', onMove && 'cursor-grab active:cursor-grabbing'), children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-2", children: renderCard(item) }), onMove && (_jsx(MoveMenu, { id: String(getId(item)), name: getName(item), value: getColumn(item), targets: targets, onMove: (value) => move(String(getId(item)), value, 'menu') }))] }) }, getId(item)))), cards.length === 0 && (_jsx("p", { className: "px-1 py-6 text-center type-caption text-muted-foreground", children: column.empty ?? 'Nothing here' })), column.footer && _jsx("div", { className: "px-1 type-caption text-muted-foreground", children: column.footer })] }, column.value));
|
|
104
|
+
: undefined, className: cn('glow-edge gap-2 px-3 py-3', onMove && 'cursor-grab active:cursor-grabbing'), children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-2", children: renderCard(item) }), onMove && (_jsx(MoveMenu, { id: String(getId(item)), name: getName(item), value: getColumn(item), targets: targets, onMove: (value) => move(String(getId(item)), value, 'menu') }))] }) }, getId(item)))), cards.length === 0 && (_jsx("p", { className: "px-1 py-6 text-center type-caption text-muted-foreground", children: column.empty ?? 'Nothing here' })), column.footer && _jsx("div", { className: "px-1 type-caption text-muted-foreground", children: column.footer })] }, column.value));
|
|
105
105
|
}), _jsx("span", { role: "status", className: "sr-only", children: announcement })] }));
|
|
106
106
|
}
|
|
107
107
|
function BoardColumnView({ column, count, headingLevel, onDrop, children, }) {
|
|
@@ -559,7 +559,7 @@ function GroupLabel({ run, icon, isFolded, onToggle, }) {
|
|
|
559
559
|
return (_jsxs("button", { type: "button", "aria-expanded": !isFolded, onClick: onToggle, className: GROUP_TOGGLE_CLASS, children: [_jsx(ChevronDownIcon, { "aria-hidden": "true", className: cn('size-4 self-center text-muted-foreground transition-transform', isFolded && '-rotate-90') }), label, _jsx("span", { className: "type-caption text-muted-foreground tabular-nums", children: run.rows.length })] }));
|
|
560
560
|
}
|
|
561
561
|
function Cards({ layout, runs, rows, folded, onToggleFold, groupSummary, groupIcons, }) {
|
|
562
|
-
const list = (items) => (_jsx("ul", { role: "list", className: cn(layout.arrange === 'list' ? CARD_LIST_CLASS : ['grid', CARD_GRID_CLASS[layout.size ?? 'md'], CARD_GRID_GAP_CLASS]), children: items.map((row) => (_jsx("li", { "data-slot": "data-table-card", className: "min-w-0", children: layout.renderCard(row.original) }, row.id))) }));
|
|
562
|
+
const list = (items) => (_jsx("ul", { role: "list", className: cn(layout.arrange === 'list' ? CARD_LIST_CLASS : ['grid', CARD_GRID_CLASS[layout.size ?? 'md'], CARD_GRID_GAP_CLASS]), children: items.map((row) => (_jsx("li", { "data-slot": "data-table-card", className: "min-w-0 *:data-[slot=card]:glow-edge", children: layout.renderCard(row.original) }, row.id))) }));
|
|
563
563
|
return (_jsx("div", { className: "@container/data-table-cards flex flex-col gap-6", "data-state": "success", "data-slot": "data-table-cards", children: runs
|
|
564
564
|
? runs.map((run, runIndex) => {
|
|
565
565
|
const isFolded = folded.has(run.key);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type QueryClient } from '@tanstack/react-query';
|
|
2
2
|
import { type ReactNode } from 'react';
|
|
3
|
+
import type { PaletteName } from '../../lib/palettes.js';
|
|
3
4
|
import { type ErrorReportingOptions } from '../../error-reporting/reporter.js';
|
|
4
|
-
export declare function FoundationProviders({ mockMode, queryClient, errorReporting, children, }: {
|
|
5
|
+
export declare function FoundationProviders({ mockMode, queryClient, errorReporting, palette, palettePicker, children, }: {
|
|
5
6
|
/** Whether the API is served by MSW — the app's own IS_MOCK_MODE. */
|
|
6
7
|
mockMode: boolean;
|
|
7
8
|
/** Defaults to createQueryClient(); pass one to share it with code outside React. */
|
|
@@ -13,5 +14,17 @@ export declare function FoundationProviders({ mockMode, queryClient, errorReport
|
|
|
13
14
|
* report holds and never holds. `{ release }` names the build.
|
|
14
15
|
*/
|
|
15
16
|
errorReporting?: boolean | ErrorReportingOptions;
|
|
17
|
+
/**
|
|
18
|
+
* The app's palette (design-language.md "Palettes"). With one, the app
|
|
19
|
+
* is always dark in that palette and the shell has no light/dark
|
|
20
|
+
* toggle; printed notes still print light. Without, light and dark
|
|
21
|
+
* follow the system, as before.
|
|
22
|
+
*/
|
|
23
|
+
palette?: PaletteName;
|
|
24
|
+
/**
|
|
25
|
+
* With `palette`: the shell offers all four palettes, and the one picked
|
|
26
|
+
* is remembered on this device. A new device starts on `palette`.
|
|
27
|
+
*/
|
|
28
|
+
palettePicker?: boolean;
|
|
16
29
|
children: ReactNode;
|
|
17
30
|
}): import("react").JSX.Element;
|
|
@@ -5,20 +5,21 @@ import { createQueryClient } from '../../api/query-client.js';
|
|
|
5
5
|
import { AuthProvider } from '../../auth/use-auth.js';
|
|
6
6
|
import { MockModeBanner } from './mock-mode-banner.js';
|
|
7
7
|
import { ThemeProvider } from '../theme-provider.js';
|
|
8
|
+
import { PaletteProvider } from './palette-provider.js';
|
|
8
9
|
import { ErrorReportingProvider } from '../../error-reporting/error-reporting-provider.js';
|
|
9
10
|
import { resolveErrorReporting } from '../../error-reporting/reporter.js';
|
|
10
11
|
import { browserStaleChunkReload } from '../../lib/stale-chunk-reload.js';
|
|
11
12
|
// Everything an app's root needs around its router, in the order that
|
|
12
|
-
// works: the theme outermost, the mock-mode banner above the router so it
|
|
13
|
+
// works: the theme outermost (and the palette, which forces it dark), the mock-mode banner above the router so it
|
|
13
14
|
// shows on every route (/login included), and AuthProvider inside
|
|
14
15
|
// QueryClientProvider because it queries /auth/me through TanStack Query.
|
|
15
16
|
// An app's DataEnvironmentBanner goes first among the children, before its
|
|
16
17
|
// router: it queries too, and shows on every route like the mock banner.
|
|
17
18
|
// It also reloads the page once when a lazy chunk is gone after a deploy
|
|
18
19
|
// (lib/stale-chunk-reload.ts), so no app adds its own listener.
|
|
19
|
-
export function FoundationProviders({ mockMode, queryClient, errorReporting, children, }) {
|
|
20
|
+
export function FoundationProviders({ mockMode, queryClient, errorReporting, palette, palettePicker = false, children, }) {
|
|
20
21
|
const [client] = useState(() => queryClient ?? createQueryClient());
|
|
21
22
|
const reporting = resolveErrorReporting(errorReporting);
|
|
22
23
|
useEffect(() => browserStaleChunkReload().install(window), []);
|
|
23
|
-
return (
|
|
24
|
+
return (_jsx(ThemeProvider, { attribute: "class", defaultTheme: "system", enableSystem: true, forcedTheme: palette ? 'dark' : undefined, children: _jsxs(PaletteProvider, { palette: palette, picker: palettePicker, children: [_jsx(MockModeBanner, { active: mockMode }), _jsx(QueryClientProvider, { client: client, children: _jsx(AuthProvider, { children: reporting ? _jsx(ErrorReportingProvider, { options: reporting, children: children }) : children }) })] }) }));
|
|
24
25
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ComponentProps } from 'react';
|
|
2
|
-
import { type HeadingFrom, type HeadingLevel } from './prose-look.js';
|
|
2
|
+
import { type HeadingFrom, type HeadingLevel, type TableLook } from './prose-look.js';
|
|
3
3
|
export type MarkdownProps = {
|
|
4
4
|
/** The Markdown source. Raw HTML in it is shown as text, never rendered. */
|
|
5
5
|
children: string;
|
|
@@ -44,6 +44,11 @@ export type MarkdownProps = {
|
|
|
44
44
|
* are pictures and the text stays read-only.
|
|
45
45
|
*/
|
|
46
46
|
onTasksChange?: (markdown: string) => void | Promise<unknown>;
|
|
47
|
+
/**
|
|
48
|
+
* How tables look: `'compact'` (the default) or `'boxed'`, as on
|
|
49
|
+
* RichTextEditor. Either way columns fit their words.
|
|
50
|
+
*/
|
|
51
|
+
tableLook?: TableLook;
|
|
47
52
|
className?: string;
|
|
48
53
|
} & Omit<ComponentProps<'div'>, 'children'>;
|
|
49
|
-
export declare function Markdown({ children, headingLevel, headingFrom, foldKey: foldKeyProp, foldable, onTasksChange: onTasksChangeProp, className, ...rest }: MarkdownProps): import("react").JSX.Element;
|
|
54
|
+
export declare function Markdown({ children, headingLevel, headingFrom, foldKey: foldKeyProp, foldable, onTasksChange: onTasksChangeProp, tableLook, className, ...rest }: MarkdownProps): import("react").JSX.Element;
|
|
@@ -23,6 +23,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
23
23
|
// sit: `#` and `##` on a tinted band, `###` a small uppercase label, and
|
|
24
24
|
// the text under them lined up with the words in the band (prose-look.ts).
|
|
25
25
|
//
|
|
26
|
+
// A table's columns fit their words (a cell wraps at 18rem; a right-
|
|
27
|
+
// aligned column, numbers, never wraps), in one of two looks
|
|
28
|
+
// (`tableLook`, as on RichTextEditor: styles/index.css "Tables in written
|
|
29
|
+
// text"). A cell whose whole text is one highlight is painted in it, and
|
|
30
|
+
// a first column of bold cells is row headers (src/lib/markdown-table.ts).
|
|
26
31
|
// A table wider than the text scrolls sideways, and while it does, its
|
|
27
32
|
// box is a tab stop, named for its columns, that the arrow keys scroll
|
|
28
33
|
// (prose-table-scroll.ts). Its cells wrap first, so that's only a table
|
|
@@ -60,11 +65,11 @@ import remarkGfm from 'remark-gfm';
|
|
|
60
65
|
import { cn } from 'cn';
|
|
61
66
|
import { ChevronDownIcon, ExternalLinkIcon, SquareCheckIcon, SquareIcon } from 'lucide-react';
|
|
62
67
|
import { Checkbox } from '../ui/checkbox.js';
|
|
63
|
-
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table.js';
|
|
64
68
|
import { useFolds } from '../../hooks/fold-store.js';
|
|
65
69
|
import { CALLOUT_KINDS, remarkCallouts } from '../../lib/markdown-callout.js';
|
|
66
70
|
import { rehypeFold, toggleFold } from '../../lib/markdown-fold.js';
|
|
67
71
|
import { isHighlightColor, remarkHighlights } from '../../lib/markdown-highlight.js';
|
|
72
|
+
import { rehypeTables } from '../../lib/markdown-table.js';
|
|
68
73
|
import { toggleTaskAt } from '../../lib/markdown-tasks.js';
|
|
69
74
|
import { textTopHeading } from '../../lib/markdown-top-heading.js';
|
|
70
75
|
import { CALLOUT_BOX, CALLOUT_LOOK, DEFAULT_HEADING_LEVEL, FOLD_BUTTON, FOLD_HEADING, headingBand, headingClass, headingTag, PROSE, PROSE_INSET, } from './prose-look.js';
|
|
@@ -87,10 +92,9 @@ function omitNode(props) {
|
|
|
87
92
|
delete rest.node;
|
|
88
93
|
return rest;
|
|
89
94
|
}
|
|
90
|
-
// A GFM table in
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// renames it.
|
|
95
|
+
// A GFM table in its box that scrolls sideways: a tab stop and a named
|
|
96
|
+
// region only while the table overflows it. Watched again after each
|
|
97
|
+
// render, so a header row with new words renames it.
|
|
94
98
|
function NotesTable({ hidden, ...props }) {
|
|
95
99
|
const boxRef = useRef(null);
|
|
96
100
|
useEffect(() => {
|
|
@@ -101,9 +105,7 @@ function NotesTable({ hidden, ...props }) {
|
|
|
101
105
|
const region = watchTableScroll(box, table, headerTexts(table));
|
|
102
106
|
return () => region.destroy();
|
|
103
107
|
});
|
|
104
|
-
return (_jsx(
|
|
105
|
-
// Folded away, the box goes with the table.
|
|
106
|
-
containerClassName: cn('rounded-lg border border-border', TABLE_SCROLL_FOCUS, hidden && 'hidden') }));
|
|
108
|
+
return (_jsx("div", { ref: boxRef, "data-slot": "table-container", className: cn('prose-table', TABLE_SCROLL_FOCUS, hidden && 'hidden'), children: _jsx("table", { ...props }) }));
|
|
107
109
|
}
|
|
108
110
|
// Ticks a task item by where it starts in the text: given only with
|
|
109
111
|
// `onTasksChange`. Each task item hands its box its own start and words.
|
|
@@ -211,16 +213,9 @@ const COMPONENTS = {
|
|
|
211
213
|
const { alt, ...rest } = omitNode(props);
|
|
212
214
|
return _jsx("img", { ...rest, alt: alt ?? '', loading: "lazy", className: "h-auto max-w-full rounded-md" });
|
|
213
215
|
},
|
|
214
|
-
// GFM tables
|
|
215
|
-
//
|
|
216
|
+
// GFM tables: drawn by styles/index.css "Tables in written text"
|
|
217
|
+
// (rehypeTables marks coloured cells and row headers).
|
|
216
218
|
table: (props) => _jsx(NotesTable, { ...omitNode(props) }),
|
|
217
|
-
thead: (props) => _jsx(TableHeader, { ...omitNode(props), className: "bg-muted" }),
|
|
218
|
-
tbody: (props) => _jsx(TableBody, { ...omitNode(props) }),
|
|
219
|
-
tr: (props) => _jsx(TableRow, { ...omitNode(props) }),
|
|
220
|
-
// Notes' cells wrap (a cell can hold a sentence) and have a line
|
|
221
|
-
// between columns, so a wide table still reads column by column.
|
|
222
|
-
th: (props) => _jsx(TableHead, { ...omitNode(props), className: "border-r border-border whitespace-normal last:border-r-0" }),
|
|
223
|
-
td: (props) => _jsx(TableCell, { ...omitNode(props), className: "border-r border-border whitespace-normal last:border-r-0" }),
|
|
224
219
|
};
|
|
225
220
|
// The components for each `headingLevel` and level that takes it (`#`,
|
|
226
221
|
// or the text's highest with `headingFrom="text"`), made once each.
|
|
@@ -236,7 +231,7 @@ function componentsFor(top, textTop) {
|
|
|
236
231
|
}
|
|
237
232
|
return components;
|
|
238
233
|
}
|
|
239
|
-
export function Markdown({ children, headingLevel = DEFAULT_HEADING_LEVEL, headingFrom = 'hash', foldKey: foldKeyProp, foldable = true, onTasksChange: onTasksChangeProp, className, ...rest }) {
|
|
234
|
+
export function Markdown({ children, headingLevel = DEFAULT_HEADING_LEVEL, headingFrom = 'hash', foldKey: foldKeyProp, foldable = true, onTasksChange: onTasksChangeProp, tableLook = 'compact', className, ...rest }) {
|
|
240
235
|
// On paper (EntityView's Print notes): every heading plain and open,
|
|
241
236
|
// every task box a picture.
|
|
242
237
|
const printing = usePrinting();
|
|
@@ -245,7 +240,7 @@ export function Markdown({ children, headingLevel = DEFAULT_HEADING_LEVEL, headi
|
|
|
245
240
|
const textTop = useMemo(() => (headingFrom === 'text' ? textTopHeading(children) : 1), [headingFrom, children]);
|
|
246
241
|
const components = useMemo(() => componentsFor(headingLevel, textTop), [headingLevel, textTop]);
|
|
247
242
|
const [folded, setFolded] = useFolds(foldKey);
|
|
248
|
-
const rehypePlugins = useMemo(() => (foldKey === undefined ? [] : [[rehypeFold, { folded }]]), [foldKey, folded]);
|
|
243
|
+
const rehypePlugins = useMemo(() => (foldKey === undefined ? [rehypeTables] : [rehypeTables, [rehypeFold, { folded }]]), [foldKey, folded]);
|
|
249
244
|
const toggle = useCallback((id) => setFolded(toggleFold(folded, id)), [folded, setFolded]);
|
|
250
245
|
// Ticks not yet back as `children`: shown, and built on by the next
|
|
251
246
|
// tick, so two quick ticks on a slow save both land. Kept while
|
|
@@ -261,5 +256,5 @@ export function Markdown({ children, headingLevel = DEFAULT_HEADING_LEVEL, headi
|
|
|
261
256
|
if (saving && typeof saving.then === 'function')
|
|
262
257
|
saving.then(undefined, () => setPending((current) => (current?.sent.includes(next) ? null : current)));
|
|
263
258
|
}, [onTasksChange, children, shown, live]);
|
|
264
|
-
return (_jsx("div", { "data-slot": "markdown", "data-fold-scope": foldKey, ...rest, className: cn('flex min-w-0 flex-col gap-3 type-body text-foreground break-words', PROSE_INSET, className), children: _jsx(FoldContext.Provider, { value: foldKey === undefined ? null : toggle, children: _jsx(TaskToggleContext.Provider, { value: onTasksChange ? tickTask : null, children: _jsx(ReactMarkdown, { remarkPlugins: REMARK_PLUGINS, rehypePlugins: rehypePlugins, components: components, children: onTasksChange ? shown : children }) }) }) }));
|
|
259
|
+
return (_jsx("div", { "data-slot": "markdown", "data-table-look": tableLook, "data-fold-scope": foldKey, ...rest, className: cn('flex min-w-0 flex-col gap-3 type-body text-foreground break-words', PROSE_INSET, className), children: _jsx(FoldContext.Provider, { value: foldKey === undefined ? null : toggle, children: _jsx(TaskToggleContext.Provider, { value: onTasksChange ? tickTask : null, children: _jsx(ReactMarkdown, { remarkPlugins: REMARK_PLUGINS, rehypePlugins: rehypePlugins, components: components, children: onTasksChange ? shown : children }) }) }) }));
|
|
265
260
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { RichTextEditorProps } from './rich-text-editor.js';
|
|
2
|
-
export default function MilkdownEditor({ id, defaultValue, onChange, onProblem, readOnly, autoFocus, placeholder, maxLength, toolbar, inline, upload, imageAccept, imageMaxSize, headingLevel, headingFrom, foldKey, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, }: RichTextEditorProps): import("react").JSX.Element;
|
|
2
|
+
export default function MilkdownEditor({ id, defaultValue, onChange, onProblem, readOnly, autoFocus, placeholder, maxLength, toolbar, inline, upload, imageAccept, imageMaxSize, headingLevel, headingFrom, foldKey, tableLook, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, }: RichTextEditorProps): import("react").JSX.Element;
|