@sckyzo/cosmos-react 0.3.0 → 0.4.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/CHANGELOG.md +128 -0
- package/README.md +916 -19
- package/dist/CodeBlock-CY18_3tZ.d.cts +52 -0
- package/dist/CodeBlock-CY18_3tZ.d.ts +52 -0
- package/dist/code/index.cjs +331 -0
- package/dist/code/index.cjs.map +1 -0
- package/dist/code/index.d.cts +29 -0
- package/dist/code/index.d.ts +29 -0
- package/dist/code/index.js +326 -0
- package/dist/code/index.js.map +1 -0
- package/dist/editor/index.cjs +879 -0
- package/dist/editor/index.cjs.map +1 -0
- package/dist/editor/index.d.cts +100 -0
- package/dist/editor/index.d.ts +100 -0
- package/dist/editor/index.js +872 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/fieldRecipe-Cao5uUIK.d.cts +3 -0
- package/dist/fieldRecipe-Cao5uUIK.d.ts +3 -0
- package/dist/index.cjs +18158 -12012
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1631 -243
- package/dist/index.d.ts +1631 -243
- package/dist/index.js +18113 -12005
- package/dist/index.js.map +1 -1
- package/dist/intent-D0bANgYJ.d.cts +12 -0
- package/dist/intent-D0bANgYJ.d.ts +12 -0
- package/dist/map/index.cjs +559 -0
- package/dist/map/index.cjs.map +1 -0
- package/dist/map/index.d.cts +95 -0
- package/dist/map/index.d.ts +95 -0
- package/dist/map/index.js +556 -0
- package/dist/map/index.js.map +1 -0
- package/package.json +82 -3
package/README.md
CHANGED
|
@@ -125,6 +125,12 @@ chat messages stay a plain `<a>`, and a provider inside another one replaces it
|
|
|
125
125
|
- `Input`, `Select` and `Textarea` `success="Username is available."`: the message under a field whose value is valid, with a green border (and a check icon on `Input` and `Select`). It is linked as the description and announced politely; `error` wins when both are set. `OtpInput success`: a green border on every cell, the message is yours.
|
|
126
126
|
- `SegmentedControl` options take a `badge` (a count in a small pill, part of the radio's accessible name).
|
|
127
127
|
- `PageHeader icon={Plug} iconIntent="brand"`: a decorative icon tile left of the title.
|
|
128
|
+
- `PageHeader breadcrumb={…} description="…"`: the breadcrumb sits above the title and the description under it; both can be passed together. Below `sm` the `actions` wrap under the meta, at the start of the line, and the icon tile stays beside the text column.
|
|
129
|
+
- `ErrorState onRetry={reload} retryLabel="Reload" action={<Button>Back to tickets</Button>}`: `retryLabel` renames the retry button (default "Try again"); `action` is a way out that is not a retry, next to it (the retry first), or alone without `onRetry`.
|
|
130
|
+
- `EmptyState` `children`: rendered under the description and before the `action`, start-aligned and up to 28rem wide: a short numbered list of setup steps, for instance.
|
|
131
|
+
- `FormRow`: label and description at the start, the control at the end. Below `sm` they stack, the label and its description above the control, which takes the row's width (a `Select` with `fullWidth={false}` stays at its own width, at the start).
|
|
132
|
+
- `StickyScrollNav sections={[{ id, label }]}` (a long page with anchors, such as a settings page): below `sm` the row of buttons folds into a native select, named "Jump to section" (`jumpLabel`), that shows the section being read and scrolls to the one picked, below the nav like the buttons do. From `sm` up nothing changes. `VerticalNavigation` is a list of links to pages, not anchors, and keeps its look.
|
|
133
|
+
- `KpiCard value="€12.4M" exactValue="€12,418,906.40"`: the exact figure behind an abbreviated value. A tooltip shows it on hover and focus, and it is read as part of the card's name ("…€12.4M, exact value €12,418,906.40"). On a plain card the value becomes a tab stop to reach the tooltip; a link or button card shows it when the card takes keyboard focus, and stays one tab stop.
|
|
128
134
|
- `Rating value={4.5} allowHalf readOnly showValue count="(128 reviews)"`: stars on the warning hue with an outlined empty star, announced as `role="img"` ("Rated 4.5 out of 5"). Without `readOnly` it is a `radiogroup` (one tab stop, arrows, Home/End, hover preview) with `defaultValue`/`value` and `onChange(value)`; `0` is "no rating".
|
|
129
135
|
- `Carousel.Thumbnails`: a scrolling row of buttons under the slides, one child per slide, named "Go to slide N" with `aria-current` on the current one and scrolled into view as it changes.
|
|
130
136
|
- `ProfileHeader`: the top of a profile page. A `cover` (a gradient of the accent's tints by default), an `avatar` overlapping its bottom edge, the `name` with `badges`, a `headline` and `meta`, `actions`, `stats` read as a `<dl>`, and a `tabs` slot for a `Tabs.List` under a border. Below `sm` it stacks and the actions share the width.
|
|
@@ -186,8 +192,8 @@ checkbox, the action column, a link or button in another cell) never counts as a
|
|
|
186
192
|
row click. `RowActionsMenu` is the ⋮ button and menu of a row, for
|
|
187
193
|
`renderRowAction`: give it `Menu.Item`s, a destructive one with
|
|
188
194
|
`intent="danger"`. On a narrow screen the edge of a table that scrolls sideways
|
|
189
|
-
fades, and `stickyFirstColumn` pins its first column (
|
|
190
|
-
`
|
|
195
|
+
fades, and `Table`'s `stickyFirstColumn` pins its first column (`DataTable` pins
|
|
196
|
+
columns with `pin`, below). `TableIdentityCell`, `TableIdCell`, `TableDescriptionCell` and
|
|
191
197
|
`TableAmountCell` are the cells tables keep rebuilding.
|
|
192
198
|
|
|
193
199
|
`DataTable`'s column filters can be controlled: `filters`, `defaultFilters` and
|
|
@@ -201,11 +207,137 @@ filters bar; the counts come from you, the table never computes them.
|
|
|
201
207
|
instead of scrolling: the checkbox, the column marked `mobile: 'title'` and the
|
|
202
208
|
row action on the first line, then a label/value pair per other column
|
|
203
209
|
(`mobileLabel` renames a label, `mobile: 'hidden'` drops a column). The toolbar
|
|
204
|
-
gets a "Sort by" select for the sortable columns
|
|
205
|
-
the
|
|
210
|
+
gets a "Sort by" select for the sortable columns and, when a column is
|
|
211
|
+
filterable, a "Filters" button with the number of values filtered: it opens a
|
|
212
|
+
bottom sheet with the filter of each filterable column, "Clear all" and "Show n
|
|
213
|
+
rows", and sets the same `filters` as the column headers, controlled or not. The
|
|
214
|
+
default, `scroll`, keeps the table.
|
|
215
|
+
|
|
216
|
+
With `selectable`, a selection puts a tinted bar on one row: the count, "Clear"
|
|
217
|
+
and the bulk actions on the start side, the search and the toolbar on the end
|
|
218
|
+
side. `bulkActionItems` takes the actions as data (`{ key, label, icon,
|
|
219
|
+
intent, disabled, onClick(selectedRows) }`) so the bar can adapt: the first is a
|
|
220
|
+
button and, under 1152px of bar, the others fold into an "Actions" menu; the
|
|
221
|
+
toolbar buttons go icon-only and the search narrows under 1360px; under 640px
|
|
222
|
+
the bar wraps to two lines; with `mobileLayout="cards"` on a phone each action is
|
|
223
|
+
an icon button named by its `label`. `bulkActions(selectedRows)` still renders
|
|
224
|
+
any node after them, as given at every width. While rows are selected the
|
|
225
|
+
toolbar's Export follows them ("Export 3" downloads those rows, in the table's
|
|
226
|
+
sort order, and `onExport` gets their keys as `selection`); with none selected it
|
|
227
|
+
exports the filtered rows.
|
|
228
|
+
|
|
229
|
+
`emptyAction` puts a call to action under the empty state ("Create the first
|
|
230
|
+
order"). When a search or filters hide every row it gives way to a "Clear
|
|
231
|
+
filters" button, which clears both.
|
|
232
|
+
|
|
233
|
+
`DataTable` rows can expand: `renderExpandedRow(row)` adds a first column of
|
|
234
|
+
chevron buttons (`aria-expanded`, `aria-controls` the detail row while it is
|
|
235
|
+
open, named "Show details for <row key>") and draws what it returns in a row
|
|
236
|
+
below, spanning every column. The open rows are a set of row keys, like the
|
|
237
|
+
selection: `expandedRows`, `defaultExpandedRows` and
|
|
238
|
+
`onExpandedRowsChange(expanded)`. A toggle never selects a row nor counts as a
|
|
239
|
+
click on it. Limit: with `virtualize` nothing expands (its rows have one fixed
|
|
240
|
+
height), and the prop is ignored. With `mobileLayout="cards"` the chevron sits on
|
|
241
|
+
the card's title line and the content opens inside the card, under its fields.
|
|
242
|
+
|
|
243
|
+
`pin: 'start' | 'end'` on a column keeps it in place while the table scrolls
|
|
244
|
+
sideways, several per side: list the `start` ones first and the `end` ones last
|
|
245
|
+
in `columns`. A pinned cell sticks after the widths of the pinned columns before
|
|
246
|
+
it, measured, so percentage widths, a resize and the text size keep it right.
|
|
247
|
+
The selection and toggle columns are pinned with the start columns, the action
|
|
248
|
+
column with the end ones. A shadow marks an edge only while content scrolls
|
|
249
|
+
under it, and the scroll fade leaves a pinned side alone. `pin` replaces
|
|
250
|
+
`DataTable`'s `stickyFirstColumn` (removed): put `pin: 'start'` on the first
|
|
251
|
+
column. With `rowHref`, the link covers its own column when that is pinned and
|
|
252
|
+
a click on the rest of the row follows it from a handler. The cards of
|
|
253
|
+
`mobileLayout="cards"` ignore `pin`.
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
<DataTable
|
|
257
|
+
rows={hosts}
|
|
258
|
+
rowKey={(r) => r.host}
|
|
259
|
+
selectable
|
|
260
|
+
renderExpandedRow={(r) => <HostDetail host={r} />}
|
|
261
|
+
columns={[
|
|
262
|
+
{ key: 'host', label: 'Host', pin: 'start', render: (r) => r.host },
|
|
263
|
+
{ key: 'rack', label: 'Rack', render: (r) => r.rack },
|
|
264
|
+
// …
|
|
265
|
+
{
|
|
266
|
+
key: 'status',
|
|
267
|
+
label: 'Status',
|
|
268
|
+
pin: 'end',
|
|
269
|
+
render: (r) => <StatusBadge status={r.status} />,
|
|
270
|
+
},
|
|
271
|
+
]}
|
|
272
|
+
/>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Inline editing: `edit` on a column is `{ initialValue(row), render(editor) }`,
|
|
276
|
+
and `render` draws an existing form field from the editor: `editor.value` (the
|
|
277
|
+
draft, a string: parse it yourself), `setValue`, `commit`, `cancel`, `saving`
|
|
278
|
+
and `fieldProps` (the field's name, "Rack for node-a02", its error tie, `disabled`
|
|
279
|
+
while saving and a red border on `aria-invalid`), which an `Input` or a `Select`
|
|
280
|
+
takes as it is. `editTrigger` picks how an editable cell opens, one per table:
|
|
281
|
+
|
|
282
|
+
- `'cell'` (default): the cell's content is a button that fills it; a click,
|
|
283
|
+
Enter or F2 opens the editor in place, focused. Keep that content to text and
|
|
284
|
+
badges, with no link or button in it.
|
|
285
|
+
- `'button'`: a pencil button in each editable cell ("Edit Rack for node-a02"),
|
|
286
|
+
shown on the hovered or focused row and always on a screen with no hover; the
|
|
287
|
+
editor has Save and Cancel buttons beside the field.
|
|
288
|
+
- `'row'`: an Edit button in the row's action column turns every editable cell
|
|
289
|
+
of the row into its field, with Cancel and Save in place of the button.
|
|
290
|
+
|
|
291
|
+
In `'cell'` and `'button'`, Enter or blur saves and Escape cancels, putting the
|
|
292
|
+
focus back on the cell's trigger. `onCellEdit(row, columnKey, value)` is called
|
|
293
|
+
only when the draft changed, and may return a promise: the cell is `aria-busy`
|
|
294
|
+
with a spinner until it settles. If it rejects (or throws), the editor opens
|
|
295
|
+
again with what was typed, and the error's message shows under the cell, tied to
|
|
296
|
+
the field by `aria-describedby`. In `'row'`, Save (or Enter in a field) calls
|
|
297
|
+
`onRowEdit(row, changes)` once, `changes` being column `key` to draft for the
|
|
298
|
+
cells that changed only; a promise makes the row `aria-busy` and the fields
|
|
299
|
+
disabled, and a rejection keeps the fields open with the error in a row under
|
|
300
|
+
the row. Escape or Cancel drops the changes. Only one cell or row edits at a
|
|
301
|
+
time, and nothing opens while a save is pending. The table never changes
|
|
302
|
+
`rows`: update them from the handler. Limits: editing is off with `virtualize`
|
|
303
|
+
(a fixed-height cell would clip an editor and its error) and in the cards of
|
|
304
|
+
`mobileLayout="cards"`.
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
<DataTable
|
|
308
|
+
rows={hosts}
|
|
309
|
+
rowKey={(r) => r.host}
|
|
310
|
+
editTrigger="cell"
|
|
311
|
+
onCellEdit={async (row, key, value) => {
|
|
312
|
+
await api.patchHost(row.host, { [key]: value }); // a rejection shows under the cell
|
|
313
|
+
setHosts((all) => all.map((h) => (h.host === row.host ? { ...h, [key]: value } : h)));
|
|
314
|
+
}}
|
|
315
|
+
columns={[
|
|
316
|
+
{ key: 'host', label: 'Host', render: (r) => r.host },
|
|
317
|
+
{
|
|
318
|
+
key: 'rack',
|
|
319
|
+
label: 'Rack',
|
|
320
|
+
render: (r) => r.rack,
|
|
321
|
+
edit: {
|
|
322
|
+
initialValue: (r) => r.rack,
|
|
323
|
+
render: (e) => (
|
|
324
|
+
<Input
|
|
325
|
+
{...e.fieldProps}
|
|
326
|
+
size="sm"
|
|
327
|
+
value={e.value}
|
|
328
|
+
onChange={(ev) => e.setValue(ev.target.value)}
|
|
329
|
+
/>
|
|
330
|
+
),
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
]}
|
|
334
|
+
/>
|
|
335
|
+
```
|
|
206
336
|
|
|
207
337
|
`DataTable`'s `density` is controllable like the rest: `density`, `defaultDensity`
|
|
208
|
-
and `onDensityChange(density)
|
|
338
|
+
and `onDensityChange(density)`: `compact`, `default` or `comfortable`. Without
|
|
339
|
+
`density` and `defaultDensity` the table follows the page's `data-density`; set,
|
|
340
|
+
it is a local override of the page. As with `pageSize`, a `density` passed without
|
|
209
341
|
`onDensityChange` is a controlled value that never moves, so a saved view
|
|
210
342
|
reports its density and the table keeps the one you pass.
|
|
211
343
|
|
|
@@ -222,7 +354,8 @@ mark (U+FEFF), so that spreadsheets read accents. Fields follow RFC 4180
|
|
|
222
354
|
header or string value that starts with `=`, `+`, `-`, `@`, a tab or a carriage
|
|
223
355
|
return gets a `'` prefix so a spreadsheet never runs it as a formula; a
|
|
224
356
|
`number` is left alone. To export on the server, `onExport(query)` receives `{ search, filters,
|
|
225
|
-
sort, columns }` (the visible column keys, in order
|
|
357
|
+
sort, columns }` (the visible column keys, in order, plus `selection`, the
|
|
358
|
+
selected keys, while rows are selected) and builds no file.
|
|
226
359
|
`toCsv(rows, columns)` gives the same text, without the byte order mark, for an
|
|
227
360
|
export outside a table (prepend U+FEFF for a file meant for spreadsheets).
|
|
228
361
|
|
|
@@ -332,13 +465,13 @@ scrolling. Its limits:
|
|
|
332
465
|
|
|
333
466
|
- The table needs a height (`className="max-h-96"`): its own area is what
|
|
334
467
|
scrolls, and without a height every row is in view.
|
|
335
|
-
- Every row has one fixed height, from
|
|
336
|
-
`comfortable`, at the default text size, and growing with the text size
|
|
468
|
+
- Every row has one fixed height, from the density: 41px when `compact`, 49px,
|
|
469
|
+
or 61px when `comfortable`, at the default text size, and growing with the text size
|
|
337
470
|
setting. Content taller than that is clipped, so keep a cell to a line of text
|
|
338
471
|
or a control, as in a standard row.
|
|
339
472
|
- It applies to the table layout only: with `mobileLayout="cards"` below `sm`
|
|
340
473
|
the cards keep the pagination.
|
|
341
|
-
- It is not for expandable rows.
|
|
474
|
+
- It is not for expandable rows or inline editing: both are off with it.
|
|
342
475
|
- On paper only the drawn rows print.
|
|
343
476
|
|
|
344
477
|
```tsx
|
|
@@ -352,11 +485,12 @@ scrolling. Its limits:
|
|
|
352
485
|
/>
|
|
353
486
|
```
|
|
354
487
|
|
|
355
|
-
`Table` takes `density` (`default` or `comfortable`, as on `DataTable
|
|
488
|
+
`Table` takes `density` (`compact`, `default` or `comfortable`, as on `DataTable`; the
|
|
489
|
+
page's by default),
|
|
356
490
|
`striped`, `stickyHeader` (give the wrapper a height with
|
|
357
491
|
`containerClassName`, e.g. `max-h-80`) and `stickyFirstColumn`. While data
|
|
358
492
|
loads, `TableSkeletonRows` fills the `TableBody` under the header row. Cells take `align` and `numeric`
|
|
359
|
-
(
|
|
493
|
+
(`start`, `center` or `end`; `numeric` aligns to the end, with tabular figures). `TableHeaderCell` sets `scope` to `col` in the
|
|
360
494
|
head and `row` elsewhere. The wrapper scrolls horizontally, so a wide table
|
|
361
495
|
stays inside the page on small screens.
|
|
362
496
|
|
|
@@ -487,6 +621,144 @@ the locale nor the zone, and keep the room of their text until then.
|
|
|
487
621
|
Every one of them takes `now`, which freezes it at that moment and sets no timer: use it for
|
|
488
622
|
stories, screenshots and tests.
|
|
489
623
|
|
|
624
|
+
## Health checks
|
|
625
|
+
|
|
626
|
+
`HealthSummary`, `HealthCheckList` and `HealthMatrix` show whether a system and its parts are
|
|
627
|
+
healthy, and why not. They build on `HealthStatus` (`OK`, `WARN`, `CRIT`, `UNKNOWN`, `INFO`) and on
|
|
628
|
+
the dots and badges of the same folder.
|
|
629
|
+
|
|
630
|
+
A check is plain data. A group holds checks and, below them, other groups: one level per dependency
|
|
631
|
+
reads as a tree. A group has no status of its own, it is the worst status under it.
|
|
632
|
+
|
|
633
|
+
```tsx
|
|
634
|
+
const checks: HealthCheck[] = [
|
|
635
|
+
{
|
|
636
|
+
id: 'queue',
|
|
637
|
+
name: 'Job queue',
|
|
638
|
+
status: 'WARN',
|
|
639
|
+
target: 'redis://queue:6379/0',
|
|
640
|
+
message: '312 jobs waiting (limit 200)',
|
|
641
|
+
latency: 4, // milliseconds
|
|
642
|
+
checkedAt: '2026-01-15T09:59:30Z', // a Date, a timestamp or an ISO string
|
|
643
|
+
output: 'queue=default depth=312 oldest=94s', // shown in a code block, on demand
|
|
644
|
+
},
|
|
645
|
+
];
|
|
646
|
+
const groups: HealthCheckGroup[] = [{ id: 'queue', name: 'Queue', checks, groups: [] }];
|
|
647
|
+
|
|
648
|
+
<HealthSummary checks={flattenChecks(groups)} refresh={{ onRefresh, autoRefreshMs, onIntervalChange }} />
|
|
649
|
+
<HealthCheckList groups={groups} sort="severity" />
|
|
650
|
+
<HealthMatrix ariaLabel="Services by node" columns={nodes} rows={services} />
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Everything that sorts or rolls up uses one **severity order**, worst first: `CRIT`, `WARN`,
|
|
654
|
+
`UNKNOWN`, `INFO`, `OK`. A check nobody could run weighs more than one that passes. `SEVERITY_ORDER`,
|
|
655
|
+
`compareSeverity` and `worstStatus` are exported, as are `flattenChecks` and `groupWorst`. A set with
|
|
656
|
+
nothing in it is `UNKNOWN`.
|
|
657
|
+
|
|
658
|
+
- `HealthSummary` takes the worst status of its `checks`, and `status` overrides it when you weigh the
|
|
659
|
+
checks differently (the counts still come from `checks`). The headlines are "All systems
|
|
660
|
+
operational", "Degraded performance", "Major outage", "Status unknown" and "Operational, with
|
|
661
|
+
notices"; `labels` replaces any of them. It also shows the count per status, the last check
|
|
662
|
+
(`RelativeTime`, from the latest `checkedAt`, or `checkedAt`) and, with `refresh` (the props of
|
|
663
|
+
`RefreshButton`), a refresh action. `variant="banner"` is full width and tinted by the status,
|
|
664
|
+
`variant="compact"` is one line for a card or a topbar.
|
|
665
|
+
- `HealthCheckList` draws rows with the status in words, the name, target, message, latency and last
|
|
666
|
+
check. A check with an `output` has a toggle on its name. Pass `checks`, `groups` or both. A group
|
|
667
|
+
header shows the worst status and the counts under it and collapses
|
|
668
|
+
(`collapsedGroups` / `defaultCollapsedGroups` / `onCollapsedGroupsChange`, by group id).
|
|
669
|
+
`sort="severity"` puts the failing checks, and the groups holding them, first. `variant="list"` is
|
|
670
|
+
rows, `variant="grid"` one small card per check, for a wall screen.
|
|
671
|
+
- `HealthMatrix` is one cell per service (`rows`) and per environment, node or region (`columns`),
|
|
672
|
+
with sticky headers, in a `role="grid"` that takes one tab stop: arrow keys move between cells,
|
|
673
|
+
Home and End go to the ends of the row. A cell opens its detail in a popover (`message`, `latency`,
|
|
674
|
+
`checkedAt`, and a `detail` node of your own) with Enter, Space or a click; Escape closes it and the
|
|
675
|
+
focus returns to the cell. Each cell is named like "API on par-2b: Warning", and a column with no
|
|
676
|
+
entry is an empty cell. `variant="dots"` is dense, and every dot has a shape as well as a colour: OK a
|
|
677
|
+
filled circle, warning a diamond, critical a square, unknown a hollow ring, info a triangle. The
|
|
678
|
+
legend above the grid shows the same shapes. `variant="pills"` writes the status in each cell. The
|
|
679
|
+
grid scrolls in its own box when it is wider than the screen; `maxHeight` also scrolls it
|
|
680
|
+
vertically. `rowHeader` names the first header cell ("Service" by default; a cluster says "Rack").
|
|
681
|
+
`attentionStrip` adds a "Needs attention" strip above the grid: one chip per row that holds a
|
|
682
|
+
warning or a critical cell, with its counts in words ("2 Critical, 1 Warning"), the worst rows
|
|
683
|
+
first. Picking a chip scrolls its row into view, in the grid's own box when `maxHeight` is set,
|
|
684
|
+
and moves the focus to its worst cell; on a phone the strip scrolls sideways.
|
|
685
|
+
|
|
686
|
+
`HealthSummary`, `HealthMatrix` and `MonitorRow` (see Uptime monitoring) take `stale`, the time of the
|
|
687
|
+
last data, for a source that has stopped answering. The status colours turn grey, the component says
|
|
688
|
+
"Stale · 4 minutes ago" and the stale state is in the accessible text (the headline, each matrix
|
|
689
|
+
cell, the monitor's state chip). The statuses shown stay those of the last data.
|
|
690
|
+
|
|
691
|
+
Every one takes `now`, which freezes the relative times, for stories and tests. One check shown as a
|
|
692
|
+
card, and a dependency tree, need no component of their own: see the stories of the Monitoring group.
|
|
693
|
+
|
|
694
|
+
## Uptime monitoring
|
|
695
|
+
|
|
696
|
+
`HeartbeatBar`, `UptimeBars`, `UptimeStat` and `MonitorRow` draw what a monitor found: its recent
|
|
697
|
+
checks, its uptime over days and periods, and one monitor as a line or a tile. A monitor checks a
|
|
698
|
+
target every N seconds; these show the result, and the host brings the data.
|
|
699
|
+
|
|
700
|
+
A **beat** is one check, and a **day** is one day of history. `time` and `date` take a `Date`, a
|
|
701
|
+
timestamp in milliseconds or an ISO 8601 string (a bare `YYYY-MM-DD` names that calendar day for every
|
|
702
|
+
reader, whatever their time zone).
|
|
703
|
+
|
|
704
|
+
```tsx
|
|
705
|
+
const beats: Beat[] = [
|
|
706
|
+
{ time: '2026-01-15T06:41:10Z', status: 'up', latency: 182 },
|
|
707
|
+
{ time: '2026-01-15T06:42:10Z', status: 'down', latency: 5003, message: 'Timeout after 5 s' },
|
|
708
|
+
]; // oldest first; status: 'up' | 'down' | 'degraded' | 'maintenance' | 'pending'
|
|
709
|
+
const days: UptimeDay[] = [
|
|
710
|
+
{ date: '2026-01-14', uptime: 0.9995, incidents: 1 }, // 0 to 1; null is a day with no data
|
|
711
|
+
{ date: '2026-01-15', uptime: null },
|
|
712
|
+
];
|
|
713
|
+
|
|
714
|
+
<HeartbeatBar beats={beats} />
|
|
715
|
+
<UptimeBars days={days} title="API" legend />
|
|
716
|
+
<UptimeStat periods={[{ label: '30 days', uptime: 0.9991 }]} responseTime={182} certificate={{ daysLeft: 12 }} />
|
|
717
|
+
<MonitorRow name="API" type="HTTP" beats={beats} uptime={0.9992} />
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
**Thresholds.** An uptime is coloured by one shared rule: 99.9 % and up is green, 99 % up to 99.9 %
|
|
721
|
+
is amber, below 99 % is red, and `null` is grey ("No data"). Every component that colours by uptime
|
|
722
|
+
(`UptimeBars`, `UptimeStat`, `MonitorRow`) takes `thresholds`, a partial `{ good, warning }` in shares
|
|
723
|
+
from 0 to 1 (`{ good: 0.9999, warning: 0.999 }`), and the legend of `UptimeBars` follows it. A
|
|
724
|
+
`warning` limit above `good` is lowered to it. `DEFAULT_THRESHOLDS`, `resolveThresholds` and
|
|
725
|
+
`uptimeLevel` are exported, to colour your own figures the same way. The status colours are the
|
|
726
|
+
theme's (`--color-status-ok`, `-warn`, `-crit`), so a theme or an accent repaints them.
|
|
727
|
+
|
|
728
|
+
- `HeartbeatBar` draws one mark per beat, the newest at the end. `variant` is `bars` (default),
|
|
729
|
+
`blocks` or `dots`; `size` is `md` (32px, default) or `sm` (24px, the height inside a `MonitorRow`).
|
|
730
|
+
It holds as many beats as its width allows and drops the oldest, so a 375px screen still shows the
|
|
731
|
+
latest history: give it the width of its container. A `down` beat differs by shape as well as
|
|
732
|
+
colour (a taller bar, a block crossed by a diagonal, a square dot) and every beat is named in words
|
|
733
|
+
(status, time, latency, message). The bar is one tab stop on the newest beat; the arrow keys, Home
|
|
734
|
+
and End move between beats, and hovering or focusing one shows its tooltip (Escape hides it).
|
|
735
|
+
`legend` writes the key of the statuses under the bar.
|
|
736
|
+
- `UptimeBars` draws one bar per day over `period` 30, 60 or 90 days (default 90), coloured by the
|
|
737
|
+
thresholds. The header gives `title` and the uptime of the period (the mean of the days with data,
|
|
738
|
+
or `uptime` when you have the exact figure), the captions read "90 days ago" and "Today". When the
|
|
739
|
+
width cannot hold every day, the oldest are dropped and the caption and the figure describe what is
|
|
740
|
+
drawn. Days are named ("Jan 15, 2026: 99.95% uptime, 1 incident"), reached with the same keyboard
|
|
741
|
+
model as `HeartbeatBar`, and `legend` writes the thresholds. `compact` is the form for a card: lower
|
|
742
|
+
bars, no header, captions or legend.
|
|
743
|
+
- `UptimeStat` has two variants. `row` (default) puts the uptime of each of your `periods` side by
|
|
744
|
+
side (`{ label: '24 hours', uptime }`: you name the periods), then `responseTime` and the
|
|
745
|
+
`certificate`: days left, a warning (an icon, and the amber colour) under `warnBelow` days, 14 by
|
|
746
|
+
default. `card` is one figure for a dashboard tile: the first period, and the downtime it stands for
|
|
747
|
+
("39 min down") when the period has a `duration` in milliseconds, with `days` drawn as compact bars.
|
|
748
|
+
It is a variant of its own, not a `KpiCard` preset.
|
|
749
|
+
- `MonitorRow` shows the state (the status of the last beat, `status` to force it, `paused` to
|
|
750
|
+
pause it), the name (a link with `href`) and `type` (any string: `HTTP`, `TCP`, `DNS`, `ping`,
|
|
751
|
+
`keyword`), a 24px `HeartbeatBar`, the uptime over 24 hours, and the current latency (default: the
|
|
752
|
+
last beat's). `variant="row"` is a line of a list, which the host draws: it stacks on a narrow
|
|
753
|
+
screen. `variant="card"` is a tile for a grid. `stale` (with `now` to freeze its text) greys the
|
|
754
|
+
chip, the beats and the uptime and adds a "Stale · 4 minutes ago" tag under the name.
|
|
755
|
+
|
|
756
|
+
The response time (a `LineAreaChart` from `@sckyzo/cosmos-charts`, its `referenceBands` shading the
|
|
757
|
+
down periods; the stories draw a plain SVG, since this package has no chart dependency), the timeline
|
|
758
|
+
of an incident (`Timeline`) and a maintenance notice (`AlertBanner`) need no component of their own:
|
|
759
|
+
see the stories of the Monitoring group, and the two example pages (a public status page and a
|
|
760
|
+
monitors dashboard).
|
|
761
|
+
|
|
490
762
|
## Auth layouts
|
|
491
763
|
|
|
492
764
|
`AuthLayout` wraps the sign-in, sign-up, reset and two-step forms. `variant` picks the page:
|
|
@@ -656,17 +928,229 @@ only http(s) URLs become links.
|
|
|
656
928
|
Cancel and Escape return the focus to the button that opened it. Delete calls back at once:
|
|
657
929
|
confirm in the host if needed.
|
|
658
930
|
- Replies are one level, `Comment`s passed as `replies`, folded beyond `maxReplies` (default
|
|
659
|
-
`2`) behind a "Show 3 more replies" button. A reply has no Reply of its own.
|
|
931
|
+
`2`) behind a "Show 3 more replies" button. A reply has no Reply of its own. For a ticket, where
|
|
932
|
+
the latest exchanges matter, `keepReplies="last"` keeps the last `maxReplies` visible and folds
|
|
933
|
+
the older ones behind a button above them ("Show 3 earlier replies", `labels.showEarlierReplies`).
|
|
660
934
|
- `variant="internal"` tints a note and labels it "Internal note" with a lock, so it reads as one
|
|
661
935
|
without the color.
|
|
662
936
|
- `CommentComposer` is a `Textarea` with `autoResize`: Enter adds a line, Ctrl or Cmd+Enter and the
|
|
663
937
|
submit button send, Escape cancels when `onCancel` is given. Submit is disabled while the text
|
|
664
938
|
is empty or `submitting`. `value` / `defaultValue` / `onChange` work as on other fields.
|
|
939
|
+
- `modes` gives the composer a segmented control above the field, for what is being written
|
|
940
|
+
(`{ value: 'reply', label: 'Reply to customer' }`, `{ value: 'note', label: 'Internal note',
|
|
941
|
+
internal: true }`). `onSubmit(text, mode)` receives the selected mode's `value`; without `modes`
|
|
942
|
+
it gets the text alone, as before. `mode` / `defaultMode` / `onModeChange` work as on other
|
|
943
|
+
fields, and a mode may carry its own `placeholder` and `submitLabel`. An `internal` mode tints
|
|
944
|
+
the field and the submit button like `variant="internal"` tints a comment, with a lock and the
|
|
945
|
+
line `internalNotice` ("Internal note: only your team sees this."), announced when the mode
|
|
946
|
+
changes. `modeLabel` names the control.
|
|
665
947
|
- `time` is a node, as in `Timeline`: a `RelativeTime`, a `<time>` or your own text.
|
|
666
948
|
- Every text can be translated: `labels` on `CommentThread` reaches all its comments (a
|
|
667
949
|
comment's own `labels` win), `emptyTitle` and `label` on the thread, and the composer's own
|
|
668
950
|
`label`, `placeholder`, `submitLabel` and `cancelLabel`.
|
|
669
951
|
|
|
952
|
+
## Log views
|
|
953
|
+
|
|
954
|
+
`LogViewer`, `LogToolbar`, `LogLevelBadge` and `LogEntryDetail` read a log: the live tail, its
|
|
955
|
+
controls, a level label and one record in full. The host owns the data: it fetches the snapshot,
|
|
956
|
+
streams the new lines (SSE, WebSocket), trims the buffer and decides what "clear" and "download"
|
|
957
|
+
do. The components draw what they are given.
|
|
958
|
+
|
|
959
|
+
```tsx
|
|
960
|
+
const [lines, setLines] = useState<LogLine[]>([]); // oldest first, ids unique and stable
|
|
961
|
+
const [follow, setFollow] = useState(true);
|
|
962
|
+
const [levels, setLevels] = useState<readonly LogLevelKey[]>(['debug', 'info', 'warn', 'error']);
|
|
963
|
+
const [search, setSearch] = useState('');
|
|
964
|
+
const [active, setActive] = useState(0);
|
|
965
|
+
const [matches, setMatches] = useState(0);
|
|
966
|
+
|
|
967
|
+
<LogToolbar
|
|
968
|
+
live={follow}
|
|
969
|
+
onLiveChange={setFollow}
|
|
970
|
+
levels={['debug', 'info', 'warn', 'error']}
|
|
971
|
+
levelCounts={countLogLevels(lines)}
|
|
972
|
+
activeLevels={levels}
|
|
973
|
+
onActiveLevelsChange={setLevels}
|
|
974
|
+
search={search}
|
|
975
|
+
onSearchChange={(text) => {
|
|
976
|
+
setSearch(text);
|
|
977
|
+
setActive(0); // a new search starts at its first match
|
|
978
|
+
}}
|
|
979
|
+
matchCount={matches}
|
|
980
|
+
activeMatch={active}
|
|
981
|
+
onActiveMatchChange={setActive}
|
|
982
|
+
onClear={() => setLines([])}
|
|
983
|
+
/>
|
|
984
|
+
<LogViewer
|
|
985
|
+
aria-label="Backend logs"
|
|
986
|
+
lines={lines}
|
|
987
|
+
follow={follow}
|
|
988
|
+
onFollowChange={setFollow}
|
|
989
|
+
activeLevels={levels}
|
|
990
|
+
search={search}
|
|
991
|
+
activeMatch={active}
|
|
992
|
+
onMatchCountChange={setMatches}
|
|
993
|
+
/>
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
**Data model.** A `LogLine` is `{ id, time, level?, source?, message, fields?, stack? }`. `time` is
|
|
997
|
+
a `Date`, a timestamp in milliseconds or an ISO string, shown with milliseconds. `level` is
|
|
998
|
+
`trace`, `debug`, `info`, `warn`, `error` or `fatal` in any case; anything else (`warning`,
|
|
999
|
+
`notice`, nothing) is `unknown`. `lines` is always oldest first, whatever the `order` it is drawn
|
|
1000
|
+
in, and each `id` must stay the same from one render to the next: the viewer uses it to key rows
|
|
1001
|
+
and to count the lines that arrived while it was paused.
|
|
1002
|
+
|
|
1003
|
+
**`LogViewer`**
|
|
1004
|
+
|
|
1005
|
+
- `tone`: `terminal` (default) is a dark panel in every theme, as a console is; `surface` follows
|
|
1006
|
+
the theme, for a card on a dashboard. `density`: `compact` (default) is one line per record,
|
|
1007
|
+
`comfortable` puts the message under its time, level and source. `columns` hides parts of the
|
|
1008
|
+
row (`['time']`), `lineNumbers` numbers the lines in the order of `lines`.
|
|
1009
|
+
- `follow` / `defaultFollow` / `onFollowChange`: it stays at the newest line while lines arrive.
|
|
1010
|
+
Scrolling away from the end pauses it and shows "Jump to latest (12 new)"; the button, or
|
|
1011
|
+
scrolling back to the end, resumes it; a filter that shortens the list does not. `order` puts
|
|
1012
|
+
the newest line last (`newest-last`, the default, a tail) or first.
|
|
1013
|
+
- The row follows the width of the viewer, not the screen's: the viewer is a container. Under
|
|
1014
|
+
28rem the source column is hidden; without `wrap` the row stays on one line and the viewer
|
|
1015
|
+
scrolls sideways inside itself, with `wrap` the message goes on a line of its own under the
|
|
1016
|
+
time and the level. Give the viewer a width (it fills a block parent; in a flex row, a `flex-1`
|
|
1017
|
+
or a width class), since a container takes none from its content.
|
|
1018
|
+
- `activeLevels` and `activeSources` filter the lines (left out or, for sources, empty, shows
|
|
1019
|
+
all). `search` highlights a case-insensitive term in the messages and sources, `searchRegex`
|
|
1020
|
+
reads it as a pattern (one that does not compile finds nothing). `activeMatch` is the active
|
|
1021
|
+
match, counted from 0 among the lines that match; it is drawn stronger and scrolled into view
|
|
1022
|
+
when it changes. `onMatchCountChange` reports how many lines match. Reset `activeMatch` to 0
|
|
1023
|
+
when the search changes, as the example does; `LogToolbar` steps it with previous and next
|
|
1024
|
+
(and Enter / Shift+Enter), wrapping around the ends.
|
|
1025
|
+
- A row with `fields` or a `stack` has a button that expands it in place
|
|
1026
|
+
(`expandedIds` / `defaultExpandedIds` / `onExpandedIdsChange`). `onOpenLine` adds an "Open
|
|
1027
|
+
details" button to the expanded row, to open a `LogEntryDetail` in a drawer.
|
|
1028
|
+
- Messages, sources, fields and stacks are text, never HTML. A bidirectional control character
|
|
1029
|
+
stays inside its own message.
|
|
1030
|
+
|
|
1031
|
+
**`LogToolbar`.** Each control is there when its props are given: Live / Paused
|
|
1032
|
+
(`onLiveChange`), level toggles with counts (`levels`), search (`onSearchChange`) with a regex
|
|
1033
|
+
toggle (`onSearchRegexChange`) and previous / next (`matchCount` and `onActiveMatchChange`), a
|
|
1034
|
+
source filter on `MultiSelect` (`sources`), `onWrapChange`, `onOrderChange`, `onDownload` and
|
|
1035
|
+
`onClear`. From `sm` up it is one bar. Below, Live / Paused and the search stay in the bar and
|
|
1036
|
+
the levels, sources and view options move into a "Filters" popover; its button shows how many
|
|
1037
|
+
filters are active (levels narrowed, sources chosen). The state pairs (`live`, `activeLevels`,
|
|
1038
|
+
`search`, `searchRegex`, `activeSources`, `wrap`, `order`) work controlled or not, as on other
|
|
1039
|
+
fields. It holds none of the log: pass the same filters to the viewer.
|
|
1040
|
+
|
|
1041
|
+
**`LogLevelBadge`** is the level as a word in its colour (`ERROR`, `WARN`), on the `surface` tone
|
|
1042
|
+
by default or the `terminal` one. Use it in a table cell, a card or a detail.
|
|
1043
|
+
|
|
1044
|
+
**`LogEntryDetail`** shows one record in full for a drawer or a side panel: level, time, source
|
|
1045
|
+
and message, the fields as key / value pairs with a copy button each, the stack trace, the raw
|
|
1046
|
+
JSON, and "Show context", the lines around it (a `context` prop, or fetched through
|
|
1047
|
+
`onShowContext`). `onFilterValue` and `onExcludeValue` add "Filter on this value" and "Exclude
|
|
1048
|
+
this value" buttons to each field. A structured table is a `DataTable` with a `LogLevelBadge`
|
|
1049
|
+
column and, once rows can expand, a `LogEntryDetail` in them.
|
|
1050
|
+
|
|
1051
|
+
**Accessibility.** The viewer is `role="log"` with `aria-live="off"`: a fast stream read aloud
|
|
1052
|
+
line by line cannot be followed, so new lines are not announced. `announceErrors` reads the
|
|
1053
|
+
error and fatal lines that arrive (three at most, then the count of the rest) in a polite live
|
|
1054
|
+
region of its own. The scroller takes a tab stop, so the arrow keys scroll it, and every level is
|
|
1055
|
+
a word, not only a colour. Each level colour holds 4.5:1 on both tones in the four themes.
|
|
1056
|
+
|
|
1057
|
+
**Limits.**
|
|
1058
|
+
|
|
1059
|
+
- The default mode is windowed: only the rows in view are drawn, so tens of thousands of lines
|
|
1060
|
+
stay smooth. The row height is measured, so it follows the text size, and a row that scrolls out
|
|
1061
|
+
of view and back is redrawn: a button on it loses the focus when it leaves the window.
|
|
1062
|
+
- An expanded row takes a fixed room of up to 12 rows (a row per field and per stack line); a
|
|
1063
|
+
longer detail scrolls inside it.
|
|
1064
|
+
- `wrap` lines have no fixed height, so they cannot be windowed: all of them are drawn (the
|
|
1065
|
+
browser skips the ones out of view with `content-visibility`). Keep the buffer to a few
|
|
1066
|
+
thousand lines with `wrap` on, as rackscope does with 2,000. Newest first and paused, wrapped
|
|
1067
|
+
lines do not stay put when new ones arrive.
|
|
1068
|
+
- Without `wrap` a long line scrolls sideways: the scroll width is that of the widest row drawn.
|
|
1069
|
+
- The labels ("Jump to latest", "Filters"…) are English.
|
|
1070
|
+
- Not here: the log volume chart (stacked `BarChart` of `@sckyzo/cosmos-charts`, as the lines per
|
|
1071
|
+
level over time) and a structured logs table. The output of a job is `JobLog`, below.
|
|
1072
|
+
|
|
1073
|
+
## Job logs
|
|
1074
|
+
|
|
1075
|
+
`JobLog` is the output of a CI job or a build: collapsible steps, each with its status and
|
|
1076
|
+
duration, and the lines of each step numbered and coloured. It is read differently from a live
|
|
1077
|
+
tail, hence a component of its own; it shares the tones, the clock format and the windowing of
|
|
1078
|
+
`LogViewer`. The host owns the data and the clock: it passes the steps, their durations (`JobLog`
|
|
1079
|
+
runs no timer) and the new lines as they come.
|
|
1080
|
+
|
|
1081
|
+
```tsx
|
|
1082
|
+
const steps: JobLogStep[] = [
|
|
1083
|
+
{ id: 'install', name: 'Install dependencies', status: 'passed', duration: 41_000, lines: [...] },
|
|
1084
|
+
{
|
|
1085
|
+
id: 'run-tests',
|
|
1086
|
+
name: 'Run tests',
|
|
1087
|
+
status: 'failed',
|
|
1088
|
+
duration: 72_000,
|
|
1089
|
+
startedAt: '2026-01-15T09:30:44Z',
|
|
1090
|
+
// A string, or { text, timestamp } when the line has a time.
|
|
1091
|
+
lines: ['$ pnpm test', '\x1b[31m✗ sortRows keeps empty values last\x1b[0m', ...],
|
|
1092
|
+
},
|
|
1093
|
+
{ id: 'build', name: 'Build', status: 'skipped', lines: [] },
|
|
1094
|
+
];
|
|
1095
|
+
|
|
1096
|
+
<JobLog title="Build #1287" meta="main · 4f2c1a9" status="failed" duration={123_000} steps={steps} />
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
- **Steps.** `status` is `queued`, `running`, `passed`, `failed` or `skipped`. `duration` is in
|
|
1100
|
+
milliseconds; `startedAt` is the origin of the elapsed times (it defaults to the first
|
|
1101
|
+
timestamp of the step). `id` is unique and stays the same from one render to the next. The
|
|
1102
|
+
optional header (`title`, `status`, `meta`, `duration`) is drawn when any of them is given.
|
|
1103
|
+
- **Open steps.** The failed and the running steps start open, the others closed, and a step that
|
|
1104
|
+
starts running later opens. `expandedSteps` / `defaultExpandedSteps` / `onExpandedStepsChange`
|
|
1105
|
+
work as on other controls. The header of an open step sticks to the top of the log while its
|
|
1106
|
+
lines scroll, and each step is windowed on its own, so a step of tens of thousands of lines
|
|
1107
|
+
stays smooth.
|
|
1108
|
+
- **Line numbers.** They restart at 1 in each step and are links: `href="#<idPrefix><step id>-L<n>"`.
|
|
1109
|
+
A click calls `onCurrentLineChange({ step, line })` and does not touch `location`; a click with a
|
|
1110
|
+
modifier key is the browser's own, so "open in a new tab" works. `currentLine` /
|
|
1111
|
+
`defaultCurrentLine` is the line in focus: when it changes its step opens, the line scrolls into
|
|
1112
|
+
view and takes `aria-current="location"`. One line at a time. To make the links shareable, sync
|
|
1113
|
+
it with the URL hash in the host: read `parseJobLogHash(location.hash, idPrefix)` into
|
|
1114
|
+
`currentLine` on load and on `hashchange`, and write `jobLogLineHref(idPrefix, line)` back from
|
|
1115
|
+
`onCurrentLineChange` (the `Monitoring/JobLog` "Synced with the URL hash" story does).
|
|
1116
|
+
- **Toolbar.** Search across all the steps (a match in a closed step opens it) with the count and
|
|
1117
|
+
previous / next, a timestamps toggle, wrap, expand or collapse all, and download as plain text
|
|
1118
|
+
(ANSI codes stripped, `fileName`). `controls` picks which are drawn (`['search']`; `[]` leaves
|
|
1119
|
+
the toolbar out). Below `md` of the log's own width, as a container, the search stays and the
|
|
1120
|
+
rest moves into a "More options" menu. `timestamps`, `wrap` and `search` each come with a
|
|
1121
|
+
`default*` and an `on*Change`.
|
|
1122
|
+
- **Timestamps.** Off by default. `timestampFormat="clock"` (default) is the time of each line, as
|
|
1123
|
+
`LogViewer` writes it (`locale`, `timeZone`); `"elapsed"` is the time since its step started,
|
|
1124
|
+
`+0:04.2`.
|
|
1125
|
+
- **Following.** While a step runs the log stays at the end, pauses when the reader scrolls up,
|
|
1126
|
+
and has a footer button: Pause, then "Jump to latest (12 new)". `follow` / `defaultFollow` /
|
|
1127
|
+
`onFollowChange`. Moving to a line (a `currentLine`, a search match) leaves the end too.
|
|
1128
|
+
- **Tone.** `terminal` (default) is a dark panel in every theme, `surface` follows the theme.
|
|
1129
|
+
|
|
1130
|
+
**ANSI.** Colour codes are read into styled spans, never HTML. Kept: the 16 colours and their
|
|
1131
|
+
bright variants as a foreground and as a background, bold, dim, italic, underline and the resets.
|
|
1132
|
+
A 256-colour or truecolor code lands on the nearest of the 16. Every other escape sequence (cursor
|
|
1133
|
+
moves, window titles, links) and control character is dropped, and a carriage return keeps only the
|
|
1134
|
+
last segment of the line, so a progress bar reads as its last state. Black and white are mapped to
|
|
1135
|
+
what reads on the tone (a black text on the terminal panel is the muted gray). Each colour holds
|
|
1136
|
+
4.5:1 on both tones in the four themes. On a background colour, the text takes the colour that
|
|
1137
|
+
reads on that background and its own is dropped (`31;47` is dark text on a light fill), and dim
|
|
1138
|
+
mutes only the default text: a coloured text keeps its colour.
|
|
1139
|
+
|
|
1140
|
+
**Accessibility.** The log is a named `region` with a tab stop. A step header is a button with
|
|
1141
|
+
`aria-expanded` and `aria-controls`, its status an image with a text name (never a colour alone),
|
|
1142
|
+
and the lines of an open step a named group. A line number is a link named "Line 6".
|
|
1143
|
+
|
|
1144
|
+
**Limits.**
|
|
1145
|
+
|
|
1146
|
+
- The windowing needs a row of a fixed height, so it is off with `wrap`: all the lines of an open
|
|
1147
|
+
step are then drawn (the browser skips the ones out of view). Keep a step to a few thousand lines
|
|
1148
|
+
with `wrap` on. A line link that scrolls out of the window loses the focus.
|
|
1149
|
+
- Without `wrap`, the lines of a step scroll sideways inside the step, under its header.
|
|
1150
|
+
- A search compiles the plain text of every line of every step: for logs of hundreds of thousands
|
|
1151
|
+
of lines, filter on the host.
|
|
1152
|
+
- The labels ("Jump to latest", "More options"…) are English.
|
|
1153
|
+
|
|
670
1154
|
## Slider
|
|
671
1155
|
|
|
672
1156
|
`Slider` is a position on a scale, or a range of it. One `value` (with `defaultValue` and
|
|
@@ -729,10 +1213,11 @@ the Sidebar does with `collapseControl="sidebar"`.
|
|
|
729
1213
|
Below `lg` the sidebar leaves the layout and the Topbar's hamburger opens it as a 288px
|
|
730
1214
|
drawer over a scrim (`defaultSidebarOpen`, or `sidebarOpen` and `onSidebarOpenChange`
|
|
731
1215
|
to control it). It is always expanded there, with a close button in `Sidebar.Brand`, and it
|
|
732
|
-
closes on the scrim, Escape, a swipe
|
|
1216
|
+
closes on the scrim, Escape, a swipe toward its edge, a click on a `Sidebar.Item` and when the
|
|
733
1217
|
viewport grows past `lg`. Pass the product logo as the Topbar's `brand` to keep it in
|
|
734
1218
|
view while the sidebar is off screen. Any `Drawer` can close on a swipe too, with
|
|
735
|
-
`swipeToClose`: toward its edge on `
|
|
1219
|
+
`swipeToClose`: toward its edge on `side="start"` and `side="end"` (the left and right, swapped
|
|
1220
|
+
under `dir="rtl"`), downward on `side="bottom"`, the
|
|
736
1221
|
bottom sheet (full width, as tall as its content up to 85% of the viewport, a grab `handle`
|
|
737
1222
|
you can turn off with `handle={false}`, and a `Drawer.Body` that scrolls past that height).
|
|
738
1223
|
|
|
@@ -750,7 +1235,7 @@ drawer, and it does not touch the collapsed state: `onCollapsedChange` is not ca
|
|
|
750
1235
|
toggles keep their state.
|
|
751
1236
|
|
|
752
1237
|
`SidebarToggle` is that toggle, to place anywhere else in the Shell (a header of
|
|
753
|
-
your own, the Topbar's `
|
|
1238
|
+
your own, the Topbar's `startActions`, the Sidebar's `footer`) with no props: it
|
|
754
1239
|
reads the Shell's state, and every toggle stays in sync. Outside a Shell, pass it
|
|
755
1240
|
`collapsed` and `onCollapsedChange`.
|
|
756
1241
|
|
|
@@ -763,6 +1248,78 @@ reads the Shell's state, and every toggle stays in sync. Outside a Shell, pass i
|
|
|
763
1248
|
</Shell>
|
|
764
1249
|
```
|
|
765
1250
|
|
|
1251
|
+
### Search in the Topbar
|
|
1252
|
+
|
|
1253
|
+
`CommandPaletteTrigger` is a button drawn like a search field (a magnifier, a placeholder and the
|
|
1254
|
+
hotkey as a key hint, `⌘K` on Apple platforms and `Ctrl K` elsewhere) that opens a
|
|
1255
|
+
`CommandPalette`. It is not an input: the palette holds the field, so a screen reader announces a
|
|
1256
|
+
button that opens a dialog. Put it in the Topbar's `center` slot. Below `sm` it is a square icon
|
|
1257
|
+
button, and the placeholder stays as its accessible name. `size` takes the form fields' `sm`, `md`
|
|
1258
|
+
and `lg`.
|
|
1259
|
+
|
|
1260
|
+
The palette owns the hotkey (`hotkey`, `mod+k` by default) and the open state; the button
|
|
1261
|
+
registers no listener of its own. Open the palette from its `onClick`, and pass the same hotkey
|
|
1262
|
+
as `shortcut` so the hint is the right one (`shortcut={null}` for a palette without a hotkey).
|
|
1263
|
+
|
|
1264
|
+
```tsx
|
|
1265
|
+
const [open, setOpen] = useState(false);
|
|
1266
|
+
|
|
1267
|
+
<Topbar
|
|
1268
|
+
pageTitle="Projects"
|
|
1269
|
+
center={<CommandPaletteTrigger placeholder="Search projects" onClick={() => setOpen(true)} />}
|
|
1270
|
+
/>
|
|
1271
|
+
<CommandPalette open={open} onOpenChange={setOpen}>
|
|
1272
|
+
<CommandPalette.Input placeholder="Search…" />
|
|
1273
|
+
<CommandPalette.List>{/* items */}</CommandPalette.List>
|
|
1274
|
+
</CommandPalette>
|
|
1275
|
+
```
|
|
1276
|
+
|
|
1277
|
+
### Layout modes
|
|
1278
|
+
|
|
1279
|
+
`Sidebar variant="compact"` is a fixed 88px rail with the label under each icon, on two lines
|
|
1280
|
+
at most (a tooltip carries the full name of a label that is cut). A `Sidebar.SubMenu` opens in a
|
|
1281
|
+
flyout, as in the collapsed rail. It has no collapsed state, so `collapsed`, `expandOnHover` and
|
|
1282
|
+
the bottom button do nothing: inside a Shell, set `collapseControl="sidebar"` so the Topbar shows
|
|
1283
|
+
no hamburger that would do nothing. Below `lg` the drawer shows the default sidebar.
|
|
1284
|
+
|
|
1285
|
+
`Shell container="boxed"` centres the topbar, the sidebar and the page in a column of at most
|
|
1286
|
+
1440px, with the page background on both sides. `fluid`, the default, spans the window.
|
|
1287
|
+
|
|
1288
|
+
A top navigation is a composition: a `Shell` with no `sidebar` and a `Navbar` in `topbar`. Its
|
|
1289
|
+
`Navbar.Items` take `Navbar.Item` and `Navbar.Menu`, a dropdown group of links (`Menu.Item` with
|
|
1290
|
+
an `href`, a `Menu.Group`, a `Menu.Separator`) that follows the menu button pattern: Enter, Space
|
|
1291
|
+
and the arrows open it, the arrows move between the links, Escape closes it and gives the focus
|
|
1292
|
+
back to the trigger, Tab leaves it. Mark the group `active` while one of its links is the current
|
|
1293
|
+
page, and that link with `aria-current="page"`. Items that do not fit move, from the last, into a
|
|
1294
|
+
"More" menu (`moreLabel` renames it) and come back when there is room. Only `Navbar.Item` and
|
|
1295
|
+
`Navbar.Menu` placed directly in `Navbar.Items` move; `Navbar.Items` inside a `Navbar.Brand` takes
|
|
1296
|
+
the room the bar leaves, and anywhere else it needs a bounded width (`min-w-0 flex-1`) to measure
|
|
1297
|
+
against. Below `md` the items leave the bar and `Navbar.MobileMenuButton` opens your own menu.
|
|
1298
|
+
|
|
1299
|
+
```tsx
|
|
1300
|
+
<Shell
|
|
1301
|
+
container="boxed"
|
|
1302
|
+
topbar={
|
|
1303
|
+
<Navbar>
|
|
1304
|
+
<Navbar.Brand>
|
|
1305
|
+
<Navbar.MobileMenuButton />
|
|
1306
|
+
<Navbar.Items>
|
|
1307
|
+
<Navbar.Item href="/" active>
|
|
1308
|
+
Dashboard
|
|
1309
|
+
</Navbar.Item>
|
|
1310
|
+
<Navbar.Menu label="Settings">
|
|
1311
|
+
<Menu.Item href="/profile">Profile</Menu.Item>
|
|
1312
|
+
<Menu.Item href="/team">Team</Menu.Item>
|
|
1313
|
+
</Navbar.Menu>
|
|
1314
|
+
</Navbar.Items>
|
|
1315
|
+
</Navbar.Brand>
|
|
1316
|
+
</Navbar>
|
|
1317
|
+
}
|
|
1318
|
+
>
|
|
1319
|
+
{/* page */}
|
|
1320
|
+
</Shell>
|
|
1321
|
+
```
|
|
1322
|
+
|
|
766
1323
|
## Split panes
|
|
767
1324
|
|
|
768
1325
|
`SplitView` puts two or three `SplitView.Pane` side by side in one bordered frame (mail,
|
|
@@ -784,10 +1341,294 @@ place.
|
|
|
784
1341
|
</SplitView>
|
|
785
1342
|
```
|
|
786
1343
|
|
|
1344
|
+
## Code
|
|
1345
|
+
|
|
1346
|
+
`CodeBlock` shows a snippet with a copy button. It needs no highlighter, and takes a few variants:
|
|
1347
|
+
`lineNumbers`, `highlightLines={[3, '5-7']}`, `filename` (a header with the name, the language and
|
|
1348
|
+
the copy button), `wrap`, `maxHeight` (the block scrolls inside, takes a Tab stop and scrolls with the
|
|
1349
|
+
arrow keys), `diff` (the first column of each line is its sign, `+`, `-` or a space; changes are
|
|
1350
|
+
tinted and marked, the sign column is not copied) and `variant="terminal"` (a line starting with
|
|
1351
|
+
`$` and a space is a command, the others are its output; only the commands are copied). A block that a long line
|
|
1352
|
+
makes scroll sideways is a Tab stop too. The copy button copies the code, never the line numbers, the
|
|
1353
|
+
signs or the prompts.
|
|
1354
|
+
|
|
1355
|
+
To colour it, use the `/code` entry. It needs [Shiki](https://shiki.style), an optional peer
|
|
1356
|
+
dependency that only the apps using this entry install:
|
|
1357
|
+
|
|
1358
|
+
```bash
|
|
1359
|
+
pnpm add shiki
|
|
1360
|
+
```
|
|
1361
|
+
|
|
1362
|
+
```tsx
|
|
1363
|
+
import { CodeBlock } from '@sckyzo/cosmos-react/code';
|
|
1364
|
+
|
|
1365
|
+
<CodeBlock language="yaml" filename="deployment.yaml" code={manifest} lineNumbers />;
|
|
1366
|
+
```
|
|
1367
|
+
|
|
1368
|
+
The colours are the `--color-code-*` tokens of the theme, so they follow the theme, accent and text
|
|
1369
|
+
size. The JavaScript regex engine is used (no WebAssembly), and each language is its own chunk,
|
|
1370
|
+
loaded when a block shows it: until then, and for a language Cosmos has no grammar for, the block
|
|
1371
|
+
shows plain text. The languages are shell (`bash`, `sh`, `zsh`), `shellsession`, `yaml`, `json`,
|
|
1372
|
+
`toml`, `ini`, `dockerfile`, `sql`, `typescript`, `javascript`, `tsx`, `jsx`, `python`, `go`, `html`,
|
|
1373
|
+
`xml`, `css`, `diff`, `nginx`, `hcl`, `terraform`, `rust`, `java`, `php`, `markdown` and `promql`.
|
|
1374
|
+
A `highlight` prop still replaces the built-in highlighter. The main entry never imports Shiki:
|
|
1375
|
+
an app that does not use `/code` ships none of it. Imported without `shiki` installed, a block
|
|
1376
|
+
throws an error that names the package.
|
|
1377
|
+
|
|
1378
|
+
## Editor
|
|
1379
|
+
|
|
1380
|
+
`RichTextEditor` is a formatted text field: bold, italic, strike, headings, bulleted and numbered
|
|
1381
|
+
lists, quotes, code, links, undo and redo. It lives in the `/editor` entry and is built on
|
|
1382
|
+
[Tiptap](https://tiptap.dev) (MIT), whose packages are optional peer dependencies that only the apps
|
|
1383
|
+
using this entry install:
|
|
1384
|
+
|
|
1385
|
+
```bash
|
|
1386
|
+
pnpm add @tiptap/react @tiptap/core @tiptap/pm @tiptap/starter-kit @tiptap/extensions
|
|
1387
|
+
```
|
|
1388
|
+
|
|
1389
|
+
```tsx
|
|
1390
|
+
import { RichTextEditor } from '@sckyzo/cosmos-react/editor';
|
|
1391
|
+
|
|
1392
|
+
<RichTextEditor
|
|
1393
|
+
label="Reply"
|
|
1394
|
+
placeholder="Write a reply…"
|
|
1395
|
+
value={html}
|
|
1396
|
+
onChange={setHtml}
|
|
1397
|
+
maxLength={2000}
|
|
1398
|
+
/>;
|
|
1399
|
+
```
|
|
1400
|
+
|
|
1401
|
+
`value`, `defaultValue` and `onChange` carry HTML by default, or the ProseMirror JSON document with
|
|
1402
|
+
`format="json"`; an empty HTML document is `''`. `variant` picks the chrome: `toolbar` (default,
|
|
1403
|
+
buttons on top; `toolbar={['bold', 'italic', 'link']}` picks them), `bubble` (a small toolbar over
|
|
1404
|
+
the selection; Alt+F10 moves the focus into it, Escape gives it back) or `minimal` (no toolbar, the
|
|
1405
|
+
shortcuts and Markdown-like input rules only: `##` and a space, `-` and a space, `**bold**`).
|
|
1406
|
+
`labels` replaces every text the editor draws, tooltips included. `readOnly`, `disabled`, `rows`,
|
|
1407
|
+
`autoFocus` and `name` (a hidden input, for a native form) work as on a Textarea, and so do `label`,
|
|
1408
|
+
`description`, `error` and `required`: the label and the error are linked to the editable text.
|
|
1409
|
+
`maxLength` counts the text characters and stops typing and pasting at the limit.
|
|
1410
|
+
|
|
1411
|
+
Links accept `http(s):`, `mailto:` and relative URLs only: a
|
|
1412
|
+
`javascript:` link is refused whether it is typed in the link field, typed as Markdown, pasted or
|
|
1413
|
+
passed in `value`. The schema holds paragraphs, headings 2 and 3, lists, quotes, hard breaks and the
|
|
1414
|
+
marks above; images, tables and anything else are dropped on paste. **That is the editor's
|
|
1415
|
+
guarantee, not yours: sanitise the stored HTML again on the server before you render it elsewhere.**
|
|
1416
|
+
The main entry never imports Tiptap: an app that does not use `/editor` ships none of it.
|
|
1417
|
+
|
|
1418
|
+
## Map
|
|
1419
|
+
|
|
1420
|
+
`MapView` draws places on a map: pins in the theme's colours with an icon or a number, a popup per
|
|
1421
|
+
pin, groups of close pins as a counted circle, zoom buttons and a fit to the markers. It lives in
|
|
1422
|
+
the `/map` entry and needs [MapLibre GL JS](https://maplibre.org) (BSD-3), an optional peer
|
|
1423
|
+
dependency that only the apps using this entry install:
|
|
1424
|
+
|
|
1425
|
+
```bash
|
|
1426
|
+
pnpm add maplibre-gl
|
|
1427
|
+
```
|
|
1428
|
+
|
|
1429
|
+
MapLibre draws in a web worker. Since `maplibre-gl` 6, an app built with a bundler tells it where
|
|
1430
|
+
the worker is, once, before the first map; without that the map stays empty. With Vite:
|
|
1431
|
+
|
|
1432
|
+
```ts
|
|
1433
|
+
import { setWorkerUrl } from 'maplibre-gl';
|
|
1434
|
+
import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url';
|
|
1435
|
+
|
|
1436
|
+
setWorkerUrl(workerUrl);
|
|
1437
|
+
```
|
|
1438
|
+
|
|
1439
|
+
MapLibre's [installation guide](https://maplibre.org/maplibre-gl-js/docs/#installation) gives the
|
|
1440
|
+
line for webpack, Rollup, esbuild and Next.js, and the extra setting for server rendering.
|
|
1441
|
+
`maplibre-gl` 5 needs none of this.
|
|
1442
|
+
|
|
1443
|
+
Load MapLibre's stylesheet once in the app, and import the component from `/map`:
|
|
1444
|
+
|
|
1445
|
+
```tsx
|
|
1446
|
+
import { useState } from 'react';
|
|
1447
|
+
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
1448
|
+
import { MapMarkerList, MapView, type MapMarker } from '@sckyzo/cosmos-react/map';
|
|
1449
|
+
|
|
1450
|
+
const stores: MapMarker[] = [
|
|
1451
|
+
{ id: 'harbour', label: 'Harbour', position: [4.81, 45.71], description: '12 quay street' },
|
|
1452
|
+
{ id: 'station', label: 'Station', position: [4.9, 45.75], intent: 'success', icon: 3 },
|
|
1453
|
+
];
|
|
1454
|
+
|
|
1455
|
+
export const Stores = () => {
|
|
1456
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
1457
|
+
return (
|
|
1458
|
+
<>
|
|
1459
|
+
<MapMarkerList markers={stores} selectedId={selected} onSelect={setSelected} />
|
|
1460
|
+
<MapView
|
|
1461
|
+
markers={stores}
|
|
1462
|
+
style="https://tiles.example.com/styles/light.json"
|
|
1463
|
+
darkStyle="https://tiles.example.com/styles/dark.json"
|
|
1464
|
+
ariaLabel="Stores"
|
|
1465
|
+
selectedId={selected}
|
|
1466
|
+
onSelectedIdChange={setSelected}
|
|
1467
|
+
/>
|
|
1468
|
+
</>
|
|
1469
|
+
);
|
|
1470
|
+
};
|
|
1471
|
+
```
|
|
1472
|
+
|
|
1473
|
+
- **The base map is yours.** Cosmos ships no tiles: a tile server has its own terms and sees every
|
|
1474
|
+
visitor. `style` is a MapLibre style (a URL or an object) and its attribution stays visible.
|
|
1475
|
+
`darkStyle` is optional: under the dark themes (`.dark`, `.deep-blue`) the map switches to it,
|
|
1476
|
+
and without it keeps `style`. Pass constants, not a new object on each render. A markers array
|
|
1477
|
+
is the same: define it once, or the pins are redrawn each time.
|
|
1478
|
+
- **Pins, popups and buttons are React and HTML**, coloured by `intent` with the theme's classes,
|
|
1479
|
+
so a theme, accent or text size switch repaints them with no code. A title or description with
|
|
1480
|
+
markup shows as text.
|
|
1481
|
+
- **`variant="static"`** is a fixed view for a card or a detail page: no gesture, no zoom buttons,
|
|
1482
|
+
no focus stop, no popup, and the pins are pictures.
|
|
1483
|
+
- **Keyboard and screen readers.** The interactive map is one focus stop with the name given by
|
|
1484
|
+
`ariaLabel`: the arrow keys pan, `+` and `-` zoom. A map alone cannot be used with a screen
|
|
1485
|
+
reader, so show the same `markers` as a `MapMarkerList` next to it: choosing a row opens the
|
|
1486
|
+
popup on the map. On a static map, the list is the only way to the places.
|
|
1487
|
+
- `cluster` (default on) groups close pins; a click on a group zooms in to split it. `fit` frames
|
|
1488
|
+
the markers on the first draw and when the set of markers changes; otherwise `initialCenter` and
|
|
1489
|
+
`initialZoom` give the view. `onViewChange` reports each pan or zoom.
|
|
1490
|
+
- A browser without WebGL shows `unavailableMessage` in place of the map. The main entry never
|
|
1491
|
+
imports MapLibre: an app that does not use `/map` ships none of it. Imported without
|
|
1492
|
+
`maplibre-gl` installed, a map throws an error that names the package.
|
|
1493
|
+
|
|
1494
|
+
## Right to left
|
|
1495
|
+
|
|
1496
|
+
Set the native `dir` attribute on `<html>`, or on any ancestor of the components, and
|
|
1497
|
+
they follow it. There is no prop and no `data-` attribute:
|
|
1498
|
+
|
|
1499
|
+
```html
|
|
1500
|
+
<html lang="ar" dir="rtl"></html>
|
|
1501
|
+
```
|
|
1502
|
+
|
|
1503
|
+
What mirrors is what follows the reading direction: the side an icon, a ribbon, a floating
|
|
1504
|
+
panel or a status dot sits on, the padding and margins, the corners of a button group, a
|
|
1505
|
+
chevron that points forward, the `start` and `end` alignments of panels. A prop that names
|
|
1506
|
+
a side of the line takes `start` or `end`, never `left` or `right` (`Ribbon`'s
|
|
1507
|
+
`position="top-end"`, `BackToTopFab`'s `position="bottom-end"`, `iconPosition="start"`,
|
|
1508
|
+
`align="end"`), so the side swaps under `dir="rtl"`. `top` and `bottom` stay as they are.
|
|
1509
|
+
Left to right renders as it always did.
|
|
1510
|
+
|
|
1511
|
+
Text that comes from you and sits inside a label or a sentence (a name, a value, a key) is
|
|
1512
|
+
wrapped in `<bdi>`, so a Latin name in an Arabic page, or the reverse, keeps its own order
|
|
1513
|
+
and does not drag the punctuation around it. The package's own copy stays in English, as it
|
|
1514
|
+
is everywhere else. A Latin text cut short by an ellipsis in a right-to-left page (a long file
|
|
1515
|
+
name in a narrow tile) shows its end rather than its beginning: the line ends on the left, where
|
|
1516
|
+
Latin text starts.
|
|
1517
|
+
|
|
1518
|
+
What you put in a slot (a table cell, a card's title, a list item, a toast's message) is drawn
|
|
1519
|
+
as you pass it. A Latin value that starts with a number reorders in a right-to-left line, "248 W"
|
|
1520
|
+
showing as "W 248": wrap it in `<bdi>` yourself.
|
|
1521
|
+
|
|
1522
|
+
```tsx
|
|
1523
|
+
<TableCell>
|
|
1524
|
+
<bdi>248 W</bdi>
|
|
1525
|
+
</TableCell>
|
|
1526
|
+
```
|
|
1527
|
+
|
|
1528
|
+
A text field (`Input`, `SearchInput`, `Textarea`, the inputs of `Combobox`, `MultiSelect` and
|
|
1529
|
+
`TagInput`) follows the direction of what is typed in it: Arabic aligns and flows right to
|
|
1530
|
+
left, a URL left to right, each paragraph of a `Textarea` on its own. It does that with
|
|
1531
|
+
`unicode-bidi: plaintext` rather than `dir="auto"`, which would also turn the field's own
|
|
1532
|
+
direction around for a Latin text and leave the room reserved for a trailing icon or button
|
|
1533
|
+
on the wrong side. A `PasswordInput` does the same once its value is shown; hidden, it keeps
|
|
1534
|
+
the page's direction, as HTML leaves password fields out of `dir="auto"`, so how it aligns
|
|
1535
|
+
says nothing of what is typed. A value that is a code reads left to right in any page: the cells of
|
|
1536
|
+
`OtpInput`, the country button and number of `PhoneInput`, and the swatch and hex of
|
|
1537
|
+
`ColorInput` are `dir="ltr"` islands. `Slider` and `Calendar` hand the direction to Radix and
|
|
1538
|
+
react-day-picker, which lay out the handles and read the arrow keys from a `dir` prop and not
|
|
1539
|
+
from the page.
|
|
1540
|
+
|
|
1541
|
+
Tailwind's `rtl:` variant (redefined by the theme to follow the nearest `dir`) is the tool
|
|
1542
|
+
for what has no logical form, such as `rtl:-scale-x-100` on an arrow. In your own markup,
|
|
1543
|
+
prefer the logical utilities (`ms-`, `pe-`, `text-start`, `border-s`, `rounded-e`, `inset-s-`)
|
|
1544
|
+
to the physical ones.
|
|
1545
|
+
|
|
1546
|
+
A component draws the icon you pass it as it is. An icon that points along the line (a
|
|
1547
|
+
forward arrow, a back chevron, undo) turns around through its own class:
|
|
1548
|
+
|
|
1549
|
+
```tsx
|
|
1550
|
+
import { clsx } from 'clsx';
|
|
1551
|
+
import { ArrowRight, type LucideProps } from 'lucide-react';
|
|
1552
|
+
|
|
1553
|
+
const Forward = ({ className, ...props }: LucideProps) => (
|
|
1554
|
+
<ArrowRight {...props} className={clsx(className, 'rtl:-scale-x-100')} />
|
|
1555
|
+
);
|
|
1556
|
+
|
|
1557
|
+
<Button icon={Forward} iconPosition="end">
|
|
1558
|
+
Continue
|
|
1559
|
+
</Button>;
|
|
1560
|
+
```
|
|
1561
|
+
|
|
1562
|
+
The overlays name their sides `start` and `end`, which follow the reading direction: left and
|
|
1563
|
+
right under `dir="ltr"`, right and left under `dir="rtl"`. That is `Drawer`'s `side`
|
|
1564
|
+
(`start` | `end` | `bottom`, default `end`), the `align` of `Menu`, `Dropdown`, `UserMenu` and
|
|
1565
|
+
`NotificationsPanel` (`start` | `end`), and the `position` of `Tooltip` and `Popover.Content`
|
|
1566
|
+
(`top` | `bottom` | `start` | `end`). A panel that opens next to a trigger takes the direction of
|
|
1567
|
+
that trigger, even though it renders at the end of `<body>`, and flips to the other side as before
|
|
1568
|
+
when the screen would cut it. A `Drawer`, a `Modal`, a `ConfirmationModal` and a `CommandPalette` are
|
|
1569
|
+
not next to anything: they read the `dir` of `<html>`, so set it there rather than on a wrapper.
|
|
1570
|
+
`Toaster` stacks its toasts at the bottom corner at the end of the line (bottom left under
|
|
1571
|
+
`dir="rtl"`, from its `dir` prop or `<html>`); sonner's own `position` is a corner of the screen,
|
|
1572
|
+
so one you pass is kept as it is. A keyboard shortcut (the `shortcut` of a `Menu.Item`,
|
|
1573
|
+
`CommandPalette.Item` or `Tooltip`, the keys of a `HelpOverlay.Shortcut`) keeps the order the keys
|
|
1574
|
+
are pressed in.
|
|
1575
|
+
|
|
1576
|
+
Two things stay as they are on purpose. A chevron that points down, or turns over when a
|
|
1577
|
+
panel opens (`Accordion`), and a trend arrow that points up or down, are not turned around.
|
|
1578
|
+
And the arrow keys that move a card across a `KanbanBoard` follow where the columns are, not
|
|
1579
|
+
the reading direction: with the first column on the right, ArrowLeft takes the card to the
|
|
1580
|
+
next one.
|
|
1581
|
+
|
|
1582
|
+
The page layout follows the line too. A `Shell` puts its `Sidebar` at the start (on the right
|
|
1583
|
+
under `dir="rtl"`, with its edge, its collapse toggle and the drawer of the phone layout on that
|
|
1584
|
+
side) and its `secondary` panel at the end; `SplitView` runs its panes from the start. `Tabs`
|
|
1585
|
+
and the settings pickers swap ArrowLeft and ArrowRight with the direction, and a scrolling strip
|
|
1586
|
+
fades the side with more to see. The pinned columns of `DataTable` (`pin: 'start'` | `'end'`)
|
|
1587
|
+
and `Table`'s `stickyFirstColumn` stick to the start or end edge, with their shadow on the side
|
|
1588
|
+
that faces the columns scrolling under them. In a `Chat`, a person's own messages sit at the end
|
|
1589
|
+
of the line and the others' at the start, and what was typed in a message or the composer reads in its own script. The `hotkeys` of a
|
|
1590
|
+
`RecordPager` are literal keys and do not follow the direction: give Previous the arrow that
|
|
1591
|
+
points toward it where the page reads right to left.
|
|
1592
|
+
|
|
1593
|
+
Output keeps its order. Code (`CodeBlock`), logs (`LogViewer`, `JobLog`, and the message, fields
|
|
1594
|
+
and stack of `LogEntryDetail`) and keyboard keys (`Kbd`) read left to right in any page: they are
|
|
1595
|
+
`dir="ltr"` islands, so a number gutter stays at the left of its lines and a log scrolls from the
|
|
1596
|
+
left. What surrounds them follows the page: the title bar and copy button of a code block, the
|
|
1597
|
+
toolbar and the "Jump to latest" button of a log, the step headers of a `JobLog` (their chevron
|
|
1598
|
+
turns around). Time keeps its order too, as the axis of a chart does: `HeartbeatBar` and
|
|
1599
|
+
`UptimeBars` draw their oldest mark on the left and their newest on the right in both directions,
|
|
1600
|
+
with "N days ago" at the left and "Today" at the right, and ArrowLeft moves to the older mark. A
|
|
1601
|
+
percentage, a latency or a count the components write (`99.95%`, `182 ms`, `3 of 12`) is wrapped in
|
|
1602
|
+
`<bdi>`, so it reads in order inside an Arabic line.
|
|
1603
|
+
|
|
1604
|
+
`Carousel` hands the direction to Embla, which lays the slides out from the end of the line: the
|
|
1605
|
+
first slide is at the start, the next one comes in from the end, a swipe follows, and the arrows
|
|
1606
|
+
and their chevrons sit on their sides. The play triangle of a `VideoEmbed` points right in both
|
|
1607
|
+
directions, as the time of a video runs from the left. `MapView` does not mirror the map; its zoom
|
|
1608
|
+
buttons and the popup of a pin follow the page. Arabic and Hebrew labels in the tiles of a map
|
|
1609
|
+
need MapLibre's RTL text plugin (`setRTLTextPlugin` in `maplibre-gl`), which a host sets once
|
|
1610
|
+
for the page: Cosmos adds no dependency for it. In `RichTextEditor`, each block follows what is
|
|
1611
|
+
typed in it (the same `unicode-bidi: plaintext` as a `Textarea`), lists are indented and quotes
|
|
1612
|
+
edged on the start side, the arrow keys along the toolbar swap, and the link field reads left to
|
|
1613
|
+
right.
|
|
1614
|
+
|
|
1615
|
+
The pages follow the same rules. The back arrow of `ResetPasswordForm` and `TwoStepVerificationForm`
|
|
1616
|
+
turns around, the address or number a form says it sent the code to is wrapped in `<bdi>`, and
|
|
1617
|
+
`AuthLayout`'s `asidePosition` (`start` | `end`) puts the panel at the start or the end of the line.
|
|
1618
|
+
|
|
1619
|
+
Every group of the Storybook sidebar is converted: **Atoms**, **Actions**, **Forms**, **Data**,
|
|
1620
|
+
**Lists**, **Feedback**, **Overlays**, **Navigation**, **Layout**, **Chat**, **Tables**,
|
|
1621
|
+
**Settings**, **Monitoring**, **Media**, **Status**, **Typography**, **Foundations** and
|
|
1622
|
+
**Examples**. A check in the repository refuses a physical inline utility (`ml-`, `pr-`, `left-`,
|
|
1623
|
+
`text-left`…) in any source file of the package, and every story is rendered right to left in CI.
|
|
1624
|
+
In Storybook, the **Direction** menu of the toolbar (`&globals=direction:rtl`) renders any story
|
|
1625
|
+
right to left.
|
|
1626
|
+
|
|
787
1627
|
## Themes and accents
|
|
788
1628
|
|
|
789
|
-
The theme is a class on `<html>`; the accent, the font
|
|
790
|
-
the `data-accent`, `data-font
|
|
1629
|
+
The theme is a class on `<html>`; the accent, the font, the text size, the
|
|
1630
|
+
density and the corner radius are the `data-accent`, `data-font`,
|
|
1631
|
+
`data-text-size`, `data-density` and `data-radius` attributes, see the
|
|
791
1632
|
[theme README](../theme#themes--accents). `AppearanceSettings` renders a picker
|
|
792
1633
|
for each, and `applyAppearance` sets them on the document:
|
|
793
1634
|
|
|
@@ -801,6 +1642,7 @@ function Appearance() {
|
|
|
801
1642
|
accent: 'blue',
|
|
802
1643
|
font: 'outfit',
|
|
803
1644
|
textSize: 'md',
|
|
1645
|
+
density: 'default',
|
|
804
1646
|
});
|
|
805
1647
|
useEffect(() => applyAppearance(appearance), [appearance]);
|
|
806
1648
|
const update = (patch: Partial<CosmosAppearance>) => setAppearance((a) => ({ ...a, ...patch }));
|
|
@@ -811,17 +1653,18 @@ function Appearance() {
|
|
|
811
1653
|
onAccentChange={(accent) => update({ accent })}
|
|
812
1654
|
onFontChange={(font) => update({ font })}
|
|
813
1655
|
onTextSizeChange={(textSize) => update({ textSize })}
|
|
1656
|
+
onDensityChange={(density) => update({ density })}
|
|
814
1657
|
/>
|
|
815
1658
|
);
|
|
816
1659
|
}
|
|
817
1660
|
```
|
|
818
1661
|
|
|
819
|
-
The font
|
|
1662
|
+
The font, text size and density groups show only when their handlers are set. Pass
|
|
820
1663
|
`fonts` to offer only the fonts whose CSS the app imports
|
|
821
1664
|
(`@sckyzo/cosmos-theme/fonts/<name>`).
|
|
822
1665
|
|
|
823
1666
|
For a top bar, `AppearanceMenu` takes the same props and puts the pickers in a
|
|
824
|
-
panel behind an icon button, for example in `Topbar`'s `
|
|
1667
|
+
panel behind an icon button, for example in `Topbar`'s `endActions`. Set `onReset`
|
|
825
1668
|
to add a Reset button under the groups.
|
|
826
1669
|
|
|
827
1670
|
To offer a **System** choice that follows the device's `prefers-color-scheme`,
|
|
@@ -844,9 +1687,63 @@ useEffect(() => {
|
|
|
844
1687
|
|
|
845
1688
|
Saving the choice (local storage, user profile) is left to the app.
|
|
846
1689
|
|
|
1690
|
+
`applyAppearance` also takes a `direction` (`'ltr'` or `'rtl'`): it sets `dir="rtl"` on
|
|
1691
|
+
`<html>`, and removes `dir` for `ltr`, like the other defaults.
|
|
1692
|
+
|
|
847
1693
|
Charts live in a separate package, [`@sckyzo/cosmos-charts`](../charts), so apps
|
|
848
1694
|
that don't render charts don't ship a chart library.
|
|
849
1695
|
|
|
1696
|
+
### Customizer
|
|
1697
|
+
|
|
1698
|
+
`Customizer` is a panel on the end side (a `Drawer`) that puts the choices above in one place,
|
|
1699
|
+
plus the shell's layout, sidebar mode and container, with optional presets, a Reset and a
|
|
1700
|
+
"Copy config" that copies the value as JSON. It only **holds** the choice: it is controlled
|
|
1701
|
+
(`value`, `onChange`, with the whole new value on each click) and applies nothing itself. The
|
|
1702
|
+
host applies it on every change:
|
|
1703
|
+
|
|
1704
|
+
```tsx
|
|
1705
|
+
const [value, setValue] = useState<CustomizerValue>(CUSTOMIZER_DEFAULT_VALUE);
|
|
1706
|
+
const [open, setOpen] = useState(false);
|
|
1707
|
+
useEffect(() => applyAppearance(value), [value]);
|
|
1708
|
+
const { layout, sidebar, container } = { ...CUSTOMIZER_DEFAULT_VALUE, ...value };
|
|
1709
|
+
|
|
1710
|
+
<Shell
|
|
1711
|
+
container={container}
|
|
1712
|
+
sidebarCollapsed={sidebar === 'collapsed' || sidebar === 'hover'}
|
|
1713
|
+
topbar={layout === 'horizontal' ? <Navbar>…</Navbar> : <Topbar />}
|
|
1714
|
+
sidebar={
|
|
1715
|
+
layout === 'horizontal' ? undefined : (
|
|
1716
|
+
<Sidebar
|
|
1717
|
+
variant={sidebar === 'compact' ? 'compact' : 'default'}
|
|
1718
|
+
expandOnHover={sidebar === 'hover' ? 'overlay' : false}
|
|
1719
|
+
>
|
|
1720
|
+
…
|
|
1721
|
+
</Sidebar>
|
|
1722
|
+
)
|
|
1723
|
+
}
|
|
1724
|
+
>
|
|
1725
|
+
…
|
|
1726
|
+
</Shell>
|
|
1727
|
+
<Customizer open={open} onClose={() => setOpen(false)} value={value} onChange={setValue} />
|
|
1728
|
+
```
|
|
1729
|
+
|
|
1730
|
+
| Value | What the host does |
|
|
1731
|
+
| ----------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
1732
|
+
| `theme`, `accent`, `font`, `textSize`, `density`, `radius`, `direction` | `applyAppearance(value)` |
|
|
1733
|
+
| `container` | `Shell`'s `container` |
|
|
1734
|
+
| `sidebar: 'collapsed'` | `Shell`'s `sidebarCollapsed` |
|
|
1735
|
+
| `sidebar: 'hover'` | collapsed, plus `Sidebar`'s `expandOnHover="overlay"` |
|
|
1736
|
+
| `sidebar: 'compact'` | `Sidebar`'s `variant="compact"` (and `collapseControl="sidebar"` on the `Shell`) |
|
|
1737
|
+
| `layout: 'horizontal'` | a `Navbar` in `Shell`'s `topbar` in place of the `Sidebar` |
|
|
1738
|
+
|
|
1739
|
+
Keep the `Customizer` outside what `layout` swaps, so the panel keeps its focus as the shell
|
|
1740
|
+
re-renders. Cosmos ships no presets: pass `presets` (`{ id, label, value }`, the value being
|
|
1741
|
+
a part of the whole) to show the section. `sections` lists the sections to show (a host with a
|
|
1742
|
+
fixed layout leaves out `layout`, `sidebar` and `container`), `defaultValue` is what Reset
|
|
1743
|
+
restores, and `themes`, `accents`, `fonts`, `textSizes`, `densities` and `radii` narrow the choices as
|
|
1744
|
+
they do on `AppearanceSettings`. The `Settings/Customizer` stories wire all of it, with local
|
|
1745
|
+
storage.
|
|
1746
|
+
|
|
850
1747
|
## License
|
|
851
1748
|
|
|
852
1749
|
MIT — see [LICENSE](../../LICENSE).
|