@crewlethq/ui 0.85.0 → 0.86.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +18 -4
  2. package/dist/DataView/index.js +2 -2
  3. package/dist/InboxList/InboxList.d.ts +14 -3
  4. package/dist/InboxList/index.js +1 -1
  5. package/dist/{InboxList-X5Q5WWAI.css → InboxList-NVPIL3WU.css} +61 -28
  6. package/dist/KeyValueRows/KeyValueRows.d.ts +125 -0
  7. package/dist/KeyValueRows/index.d.ts +3 -0
  8. package/dist/KeyValueRows/index.js +12 -0
  9. package/dist/KeyValueRows-US5KW7JK.css +44 -0
  10. package/dist/ListDetail/index.js +1 -1
  11. package/dist/{ListDetail-Q7SFVKSO.css → ListDetail-WXBCLONL.css} +7 -1
  12. package/dist/RailList/RailList.d.ts +74 -0
  13. package/dist/RailList/index.d.ts +3 -0
  14. package/dist/RailList/index.js +17 -0
  15. package/dist/RailList-N2ZBPRM2.css +97 -0
  16. package/dist/RelatedRow/index.js +2 -2
  17. package/dist/SetupWizard/index.js +2 -2
  18. package/dist/ShareDialog/index.js +1 -1
  19. package/dist/TableCells/TableCells.d.ts +60 -0
  20. package/dist/TableCells/index.d.ts +2 -2
  21. package/dist/TableCells/index.js +2 -1
  22. package/dist/{TableCells-TKSNLRK3.css → TableCells-ZALZLLEF.css} +28 -0
  23. package/dist/Text/Text.d.ts +25 -3
  24. package/dist/Text/index.js +1 -1
  25. package/dist/{chunk-AFLS26AC.js → chunk-3CALM534.js} +36 -2
  26. package/dist/{chunk-EAHEJNVJ.js → chunk-CL3FWLDO.js} +1 -1
  27. package/dist/{chunk-QP2P6GDQ.js → chunk-O5CEG7TQ.js} +14 -0
  28. package/dist/{chunk-SNJM5KPV.js → chunk-P33VNG7Q.js} +1 -1
  29. package/dist/chunk-REWKR3S4.js +73 -0
  30. package/dist/chunk-TRZS7DLB.js +159 -0
  31. package/dist/{chunk-F6ZUM66S.js → chunk-VKNZQB2G.js} +44 -23
  32. package/dist/generated/index.d.ts +2 -0
  33. package/dist/generated/index.js +65 -63
  34. package/dist/index.js +65 -63
  35. package/dist/styles.css +246 -32
  36. package/package.json +3 -3
  37. package/dist/{chunk-VIX3GZCD.js → chunk-PAGITACP.js} +0 -0
  38. package/dist/{chunk-3BVNNQZY.js → chunk-WRZPO4YL.js} +1 -1
package/README.md CHANGED
@@ -41,9 +41,9 @@ import { Button } from '@crewlethq/ui/Button'; // 8 KB of CSS, not 182 KB
41
41
  | Area | Components |
42
42
  | ---- | ---------- |
43
43
  | Actions | `Button` and `ButtonLink` (variants `primary`, `secondary`, `outline`, `ghost`, `accent`, `danger`; sizes `small`, `medium`, `large`; shapes `square`, `pill`), `IconButton`, `Menu`, `Link`, `Copyable`, `CopyButton`, `DownloadButton`, `Kbd` |
44
- | Content and text | `Text` (the eight registers and `inherit`), `Prose`, `Statement`, `Document`, `DocumentTitle` and `DocumentTitleField`, the Markdown grammar (`parseMarkdown`, `parseMarkdownSpans`, `markdownInline`, `safeHref` and the line readers, with no React in it), `Highlight`, `InlineCode`, `List` and `ListItem`, `DescriptionList`, `Timeline`, `RelativeTime`, `Note`, `Divider` |
45
- | Forms | `Input`, `Textarea`, `Label`, `FormField`, `FormRow`, `Checkbox`, `CheckboxGroup`, `ConfirmField`, `Select`, `Tag`, `TagsInput`, `DateTimePicker`, `TimeWindowPicker`, `TimeRangePicker`, `ImageUpload`, `CharacterPicker`, `HuePicker`, `AvatarPicker`, `FactList`, `ChoiceList`, `ChoiceCards`, `MemberPicker` (groups granted whole, with members left out or chosen on their own, and `chosenMembers`), `ChipField` (chosen items as chips inside the field), `PersonRow` (a person or an agent as one row of a list), `Composer`, `MarkdownEditor`, `RichEditor` (with `preloadRichEditor`, and the round trip `roundTripMarkdown` and `canonicalMarkdown` from `@crewlethq/ui/RichEditorMarkdown`) |
46
- | Data display | `DataGrid` (with `useDataGridDriving`, `useAnyDataGridDriving` and `countDataGridRows`), `DataTable`, `CopyableCell`, `Table`, the read-only table cells (`TableKeyCell`, `TableNumberCell`, `TableDateCell`, `TableEntityCell`, `TableStatusCell`, `TableTagsCell`, `TableTextCell`), `StatCard`, `Meter`, `SegmentedMeter`, `Stepper`, `PricingCard`, `Avatar`, `AvatarStack`, `EntityChip`, `CodeBlock`, `QrCode`, `ObjectHeader`, `FactLine`, `SetByLine`, `Skeleton`, `Eyebrow`, `StatGroup`, `PinTile`, `IconTile`, `SettingsTable` (with `SettingsTable.Row`), `CatalogTile` |
44
+ | Content and text | `Text` (the eight registers and `inherit`; a line cut by `truncate` or `clamp` shows its whole words on hover), `Prose`, `Statement`, `Document`, `DocumentTitle` and `DocumentTitleField`, the Markdown grammar (`parseMarkdown`, `parseMarkdownSpans`, `markdownInline`, `safeHref` and the line readers, with no React in it), `Highlight`, `InlineCode`, `List` and `ListItem`, `DescriptionList`, `Timeline`, `RelativeTime`, `Note`, `Divider` |
45
+ | Forms | `Input`, `Textarea`, `Label`, `FormField`, `FormRow`, `Checkbox`, `CheckboxGroup`, `ConfirmField`, `Select`, `Tag`, `TagsInput`, `DateTimePicker`, `TimeWindowPicker`, `TimeRangePicker`, `ImageUpload`, `CharacterPicker`, `HuePicker`, `AvatarPicker`, `FactList`, `ChoiceList`, `ChoiceCards`, `MemberPicker` (groups granted whole, with members left out or chosen on their own, and `chosenMembers`), `KeyValueRows` (keys and their values as rows under one column head, with `keyValueRowsLabels`), `ChipField` (chosen items as chips inside the field), `PersonRow` (a person or an agent as one row of a list), `Composer`, `MarkdownEditor`, `RichEditor` (with `preloadRichEditor`, and the round trip `roundTripMarkdown` and `canonicalMarkdown` from `@crewlethq/ui/RichEditorMarkdown`) |
46
+ | Data display | `DataGrid` (with `useDataGridDriving`, `useAnyDataGridDriving` and `countDataGridRows`), `DataTable`, `CopyableCell`, `Table`, the read-only table cells (`TableKeyCell`, `TableNumberCell`, `TableDateCell`, `TableEntityCell`, `TableStatusCell`, `TableTagsCell`, `TableTextCell`, `TableTwoLineCell`), `StatCard`, `Meter`, `SegmentedMeter`, `Stepper`, `PricingCard`, `Avatar`, `AvatarStack`, `EntityChip`, `CodeBlock`, `QrCode`, `ObjectHeader`, `FactLine`, `SetByLine`, `Skeleton`, `Eyebrow`, `StatGroup`, `PinTile`, `IconTile`, `SettingsTable` (with `SettingsTable.Row`), `CatalogTile` |
47
47
  | List screens | `DataView`, and its parts on their own: `DataViewToolbar`, `FilterAxisBar`, `FilterAxisChip`, `FilterPill`, plus the filter model (`filterDefsFromColumns`, `applyColumnFilters`, `filterPredicate`, `blankFilterValue`) |
48
48
  | Feedback and overlays | `Callout`, `AttentionList` (what needs a person, as rows in one card), `ConflictNote`, `EditingStatus`, `Toaster`, `Modal` (with `variant="sheet"`), `ConfirmModal`, `CommandPalette`, `Tooltip`, `Popover`, `Announcer` |
49
49
  | Documents and review | `DiffView` and `DiffSummary` with the line diff they draw (`diffLines`, `diffStat`, `collapseDiff`, `firstChangedLines`, `splitLines`, `MAX_DIFF_LINES`, with no React in it), `DiffList`, `VersionCompare`, `CommentThread` |
@@ -51,7 +51,7 @@ import { Button } from '@crewlethq/ui/Button'; // 8 KB of CSS, not 182 KB
51
51
  | Charts | `Legend`, `BarList`, `StackedBar`, `StackedColumns`, `ActivityStrip`, `TimeSeries`, `Sparkline`, the reading layer they share (`ChartTooltip`, `useChartHover`), and the ramp (`DATA_COLORS`, `DATA_COLOR_OTHER`, `dataColor`) |
52
52
  | Marks | `Count` (with `tone="attention"`), `StatusDot`, `Spinner`, `EmptyValue`, `VisuallyHidden` |
53
53
  | Hooks and seams | `useModalLayer`, `usePopupLayer`, `LayerHost`, `useListbox`, `useOptionKeys`, `useHighlightRest` and `HIGHLIGHT_REST_MS`, `useClipboard`, `useDownload`, `downloadText`, `downloadBlob`, `safeFilename`, `toCsv`, `elementToPng` and `ElementToPngError`, `useOutcome`, `useAnnouncer`, `useNow`, `formatRelative`, `HeadingLevelProvider`, `cx`, the node hues (`NODE_HUES`, `NODE_HUE_NAMES`, `isNodeHue`) |
54
- | Layout | `Container`, `Section`, `Card`, `TileGrid`, `ListDetail`, `AsideLayout`, `Tabs`, `Accordion`, `Disclosure`, `Outline` (with `useActiveHeading` and `scrollToHeading`), `TreeGrid` |
54
+ | Layout | `Container`, `Section`, `Card`, `TileGrid`, `ListDetail`, `RailList` (a directory's groups as a rail beside its list, folding into a picker), `AsideLayout`, `Tabs`, `Accordion`, `Disclosure`, `Outline` (with `useActiveHeading` and `scrollToHeading`), `TreeGrid` |
55
55
  | Arranging a page | `ArrangeBoard`, `ArrangeGrid`, `ArrangeFrame` (with `useGridResize`), `ArrangeGroup`, `ArrangeList`, and the reorder hooks under them (`useReorder`, `useReorderLists`, `moveKey`) |
56
56
  | Application shell | `AppShell` with `AppShell.Rail`, the compact rail (`rail="compact"`, the one fold control from `onRailChange`, and `useAppShell().rail` to pick the shape) and `fill="flush"`, the `aside` track and the `peek` slot (`useAppShellAside`, `useAppShellPeek`), `AppShell.ResizeHandle` (also `ResizeHandle`) and `useStoredWidth`, `SidePanel` and `SidePanelFrame`, `PeekRail` and `PeekHost` (with `PeekNeighbours`, `usePeekNeighbours`, `usePeekStep`, `peekClickHandler`, `peekRowActivation`), `SectionFrame` (with `SectionFrame.Nav` and `SectionFrame.Body`), `DockColumn` and `usePresence`, `LockedLabel`, `AppShell.Topbar`, `AppShell.AccountRow` and `useAppShell`, `AuthLayout`, `SidebarNav` and `NavItem` (with `dense` lists, `unread` and `muted` rows and `collapsible` groups), `CompactNav` with `CompactNavItem`, `CompactNavGroup` and `CompactNavSeparator` (the rail folded to its marks, fed from the same places), `DirectoryList`, `BrandLockup`, `SearchTrigger`, `PageHeader`, `Toolbar`, `Stack`, `Inline`, `Spacer`, `AutoGrid`, `ErrorBoundary`, `useShortcut`, `useFullscreen`, `useIdle`, `useMediaMatch` |
57
57
  | Preferences | `ThemeToggle`, `ThemeSwitcher`, `DensitySwitcher`, `ThemeGallery`, `ThemeSwatch`, `useThemePreference`, `useDensityPreference`, `useSystemTheme`, `resolveTheme`, `applyStoredPreferences`, and the framework-free `applyTheme`, `applyPalette`, `applyDensity` with `THEME_PREFERENCES`, `DENSITY_PREFERENCES` and their guards |
@@ -205,6 +205,20 @@ places:
205
205
  - A surface that pans or zooms dispatches `LAYER_REPOSITION_EVENT` on its host
206
206
  when the content beneath it moves, and every panel anchored inside follows.
207
207
 
208
+ ## Changes in 0.86.0
209
+
210
+ ### Forms, tables and directories
211
+
212
+ **`KeyValueRows` is new**: keys and their values edited as rows under one column head, for request headers, environment variables and any list of names and what each is set to. The key and value columns are named once (`keyLabel`, `valueLabel`), and each field is named on its own for a screen reader ("Header 2", "Value for Authorization"), so every row is the same row. Every row stands on its fields' top edge, so a refused key or value (`keyError`, `valueError`) is said under its own field, linked to it, and the controls beside it stay level. The remove button is a square the fields' height, and Add is a flush quiet action under the rows. Add puts the focus in the new row's key; Remove moves it to the row that took that row's place, else the one before, else Add, and says what went. Keys take the `reference` appearance and are never spell-checked; `secret` hides each value and marks it `new-password`, so no browser offers a saved password into it. `keyReadOnly` holds a row's key, and `valuePlaceholder` may differ per row. Controlled: every edit hands back a new list with each untouched row as it was. Rows may carry fields of their own (`KeyValueRows<T extends KeyValueRow>`), and then `createRow` makes a blank one, which the type requires. The group is a fieldset named by `label` (`hideLabel` keeps it for assistive technology only); `empty` is what an empty list says; `helper`, `error` and `disabled` are as on a field. Every word is the caller's (`labels`), with English defaults made from the column names by `keyValueRowsLabels(keyLabel, valueLabel)`. Types: `KeyValueRow`, `KeyValueRowsLabels`, `KeyValueRowsProps`. Stories: `KeyValueRows`.
213
+
214
+ **`TableTwoLineCell` is new**: a value over a quieter line, each cut on one line, among the read-only table cells. The `value` is set in the cell's own register and the `detail` under it is a caption in the secondary ink, so a row of these is exactly two lines tall whatever they hold. `mono` (`TableTwoLineCellMono`: `value`, `detail` or `both`) sets a literal in the mono face. A mark stands centred against both lines, as a column's hidden `icon` or a row's read `mark`, and in a grid's stacked rows the lines give the row its baseline. A cut line shows its whole words on hover; `title` speaks for the whole cell instead, and `titleWhenCut={false}` leaves the words to a `Tooltip` around the cell. Types: `TableTwoLineCellProps`, `TableTwoLineCellMono`. Stories: `TableCells` Two Line Cells, and its right to left, stacked and both themes variants.
215
+
216
+ **`RailList` is new**: a directory in a card, the groups its items are filed in as a rail beside the chosen group's list, so a long catalogue reads as a few short lists. The rail is a dense `SidebarNav`, each group (`RailListItem`) a row with its `count`, read as part of its name with `labels.counted` ("Chat, 6 apps"), the chosen one (`value`) raised and `aria-current`, with an optional quiet `note` under the rows. A group with an `href` is a link a reader can open in a new tab, and a plain click chooses it in place (`onChange`); without one it is a button. The chosen group's `description` stands over the list on a hairline, and a grid at the top of the list draws no second rule. Under 44rem of the card's own width the rail goes and a `Select` of the same groups, named by the same `label`, joins the bar across the top, so the same directory in a page and in a sheet needs nothing set. `toolbar` is the bar's content: given as a function, it is handed the picker to place among its own controls; given as content, the picker leads it. `renderLink` draws each row's link for a router. Every word is the caller's (`labels`, with `RAIL_LIST_LABELS` as the defaults). Types: `RailListItem`, `RailListLabels`, `RailListProps`. Stories: `RailList`.
217
+
218
+ ### Text
219
+
220
+ **A `Text` cut by `truncate` or `clamp` shows its whole words on hover.** While the words are cut, the element's title carries them whole, read from the element as it is shown; while they fit, it carries none. The cut is measured when the pointer arrives, so the title never goes stale after a resize or a new value, and nothing is measured until then. A `title` the caller passes always wins, and `titleWhenCut={false}` leaves the title to whatever already shows the words, such as a `Tooltip`. Story: `Text` Whole Words When Cut.
221
+
208
222
  ## Changes in 0.85.0
209
223
 
210
224
  ### What needs a person, and who gets something
@@ -4,11 +4,11 @@ import '../chunk-K4LZXSKZ.js';
4
4
  import '../chunk-7K6LVM5I.js';
5
5
  import '../chunk-WANKINXG.js';
6
6
  import '../chunk-ZXXZESCD.js';
7
+ import '../chunk-YGRTX67J.js';
7
8
  import '../chunk-STJ7QBC3.js';
8
9
  import '../chunk-DTIW4CHZ.js';
9
- import '../chunk-YGRTX67J.js';
10
- import '../chunk-EE3DJMSQ.js';
11
10
  import '../chunk-NXFTCPF6.js';
11
+ import '../chunk-EE3DJMSQ.js';
12
12
  import '../chunk-EC6EUIAD.js';
13
13
  import '../chunk-EJNJMHQG.js';
14
14
  import '../chunk-RE3FBGQQ.js';
@@ -73,13 +73,23 @@ export type InboxRowTone = 'default' | 'danger';
73
73
  export interface InboxRowProps extends Omit<LiHTMLAttributes<HTMLLIElement>, 'onClick' | 'title'> {
74
74
  /** Who the row is from: a seat's or a person's name. */
75
75
  title: ReactNode;
76
- /** A word after the name where the row is not a plain question or update: "coding run", "stopped". */
76
+ /**
77
+ * After the name, in the quiet ink: a word where the row is not a plain
78
+ * question or update ("coding run", "stopped"), or what the person did
79
+ * ("replied to a thread in Design"). The name and the qualifier read as one
80
+ * run of words that wraps to a second line before it stops, so a long
81
+ * qualifier never hides the name.
82
+ */
77
83
  qualifier?: ReactNode;
78
84
  /** `danger` sets the qualifier in the danger ink, for a seat that has stopped. */
79
85
  qualifierTone?: InboxRowTone | undefined;
80
86
  /** At the end of the first line: when it arrived. */
81
87
  meta?: ReactNode;
82
- /** Before the words: an avatar, drawn at the target size. */
88
+ /**
89
+ * Before the words: an avatar or a mark, at the target size. Its slot is at
90
+ * least a target square and grows to fit a larger mark, such as a 32px face,
91
+ * with the name's line beside its top.
92
+ */
83
93
  leading?: ReactNode;
84
94
  /** What the row says, in up to three lines. */
85
95
  children?: ReactNode;
@@ -159,7 +169,8 @@ export interface InboxRowProps extends Omit<LiHTMLAttributes<HTMLLIElement>, 'on
159
169
  * caller names in the head (`status`): every word stays legible, and the
160
170
  * state is never carried by a decoration alone.
161
171
  *
162
- * NOTHING IS CUT AT THE EDGE. The name ellipsizes, the time never wraps, and
172
+ * NOTHING IS CUT AT THE EDGE. The name and its qualifier wrap to two lines
173
+ * before they stop, the time keeps a column of its own and never wraps, and
163
174
  * the words take up to three lines before they stop, so a row says what it is
164
175
  * rather than its first few words.
165
176
  */
@@ -1,4 +1,4 @@
1
- export { INBOX_ROW_LEAVE, InboxGroup, InboxList, InboxMore, InboxRow, InboxToggle } from '../chunk-F6ZUM66S.js';
1
+ export { INBOX_ROW_LEAVE, InboxGroup, InboxList, InboxMore, InboxRow, InboxToggle } from '../chunk-VKNZQB2G.js';
2
2
  import '../chunk-RK4XPHVG.js';
3
3
  import '../chunk-6C7GQOBV.js';
4
4
  import '../chunk-PCAT2EFB.js';
@@ -223,19 +223,36 @@
223
223
  }
224
224
 
225
225
  /*
226
- * The avatar is centred on the head's line, the name it belongs to: its box
227
- * is one line tall and the avatar, a target taller than the line, overflows
228
- * it evenly into the control's top pad. Hung from the line's top by a pixel
229
- * it stood 3.6px under the name's middle, its top a step further from the
230
- * row's edge than its side.
226
+ * THE LEADING SLOT FITS WHAT IT HOLDS. It is at least a target square (and at
227
+ * least a line tall), with its mark centred in it, and it grows to a larger
228
+ * mark, such as a 32px face, rather than letting it spill: a fixed one-line
229
+ * box let a 32px face overflow it by 8px, up and to the left, until it all but
230
+ * touched the row's edge.
231
+ *
232
+ * THE NAME'S LINE IS CENTRED ON A TARGET-SIZED MARK by moving the words down
233
+ * rather than the mark up: the words start half the difference between the
234
+ * target and a line lower, and the control's top pad is that much shorter, so
235
+ * a row with a target-sized avatar keeps its height and its name where they
236
+ * were, with the avatar inside the control instead of over its pad. A larger
237
+ * mark hangs from the same top, beside the name's line and the one under it.
231
238
  */
239
+ .crewlet-inbox-row__control--leading {
240
+ --crewlet-inbox-row-lift: max(0px, (var(--size-target-min) - 1lh) / 2);
241
+
242
+ padding-top: calc(var(--spacing-2) + var(--spacing-1) / 2 - var(--crewlet-inbox-row-lift));
243
+ }
244
+
245
+ .crewlet-inbox-row__control--leading > .crewlet-inbox-row__main {
246
+ padding-top: var(--crewlet-inbox-row-lift);
247
+ }
248
+
232
249
  .crewlet-inbox-row__leading {
233
250
  display: inline-flex;
234
251
  flex: none;
235
252
  align-items: center;
236
253
  justify-content: center;
237
- width: var(--size-target-min);
238
- height: 1lh;
254
+ min-width: var(--size-target-min);
255
+ min-height: max(var(--size-target-min), 1lh);
239
256
  }
240
257
 
241
258
  .crewlet-inbox-row__main {
@@ -245,24 +262,49 @@
245
262
  min-width: 0;
246
263
  }
247
264
 
265
+ /*
266
+ * THE HEAD IS ONE RUN OF WORDS: who, then what they did, wrapping as words do
267
+ * and stopping at two lines with an ellipsis. The name comes first, so it is
268
+ * the last thing a long qualifier can push out of sight. As one line of boxes
269
+ * the name was the only one allowed to shrink, so a qualifier longer than a
270
+ * word ("replied to a thread in Product Manager") squeezed the name to
271
+ * nothing and pushed the time past the row's edge.
272
+ *
273
+ * THE TIME FLOATS AT THE END OF THE FIRST LINE, so only that line gives up
274
+ * room to it and the second runs the row's full width: "replied to a thread
275
+ * in #engineering" keeps the room's name, the part of the line most worth
276
+ * reading, where a column of its own took the time's width from both lines.
277
+ * The float sits in a block of its own inside the clamp, which is where every
278
+ * engine lays a float out and still counts the clamped lines.
279
+ */
248
280
  .crewlet-inbox-row__head {
249
- display: flex;
250
- align-items: baseline;
251
- gap: var(--spacing-1);
281
+ display: -webkit-box;
282
+ -webkit-box-orient: vertical;
283
+ -webkit-line-clamp: 2;
284
+ line-clamp: 2;
252
285
  min-width: 0;
286
+ overflow: hidden;
287
+ overflow-wrap: anywhere;
288
+ color: var(--color-text-tertiary);
253
289
  }
254
290
 
255
- .crewlet-inbox-row__title {
256
- min-width: 0;
257
- overflow: hidden;
258
- text-overflow: ellipsis;
291
+ .crewlet-inbox-row__head-text {
292
+ display: block;
293
+ }
294
+
295
+ /* The time (and any status), in the head's own line box, so the time sits on the name's baseline. */
296
+ .crewlet-inbox-row__head-end {
297
+ float: inline-end;
298
+ margin-inline-start: var(--spacing-2);
259
299
  white-space: nowrap;
300
+ }
301
+
302
+ .crewlet-inbox-row__title {
260
303
  font-weight: var(--font-weight-medium);
261
304
  color: var(--color-text-primary);
262
305
  }
263
306
 
264
307
  .crewlet-inbox-row__qualifier {
265
- flex: none;
266
308
  font-size: var(--font-size-xs);
267
309
  color: var(--color-text-tertiary);
268
310
  }
@@ -272,8 +314,6 @@
272
314
  }
273
315
 
274
316
  .crewlet-inbox-row__meta {
275
- flex: none;
276
- margin-inline-start: auto;
277
317
  font-size: var(--font-size-xs);
278
318
  font-variant-numeric: tabular-nums;
279
319
  white-space: nowrap;
@@ -321,21 +361,12 @@
321
361
  color: var(--color-text-secondary);
322
362
  }
323
363
 
364
+ /* At the end of the first line, before the time when the row has both. */
324
365
  .crewlet-inbox-row__status {
325
366
  display: inline-flex;
326
- flex: none;
327
367
  align-items: center;
328
368
  }
329
369
 
330
- /* With a status, the time stays at the end and the status sits beside it. */
331
- .crewlet-inbox-row__status + .crewlet-inbox-row__meta {
332
- margin-inline-start: 0;
333
- }
334
-
335
- .crewlet-inbox-row__head > .crewlet-inbox-row__status {
336
- margin-inline-start: auto;
337
- }
338
-
339
370
  /*
340
371
  * What came of the row: a glyph in the tone's ink, then words in the
341
372
  * secondary ink, on one line that wraps rather than cuts.
@@ -391,7 +422,9 @@
391
422
 
392
423
  /*
393
424
  * The task, under the words and lined up with them: the control's start pad,
394
- * the leading slot and the gap after it.
425
+ * a target-sized leading slot and the gap after it. The task sits outside the
426
+ * control, so it lines up with the words for a mark of the target size, the
427
+ * size `leading` asks for.
395
428
  */
396
429
  .crewlet-inbox-row__aside {
397
430
  min-width: 0;
@@ -0,0 +1,125 @@
1
+ import { type ReactNode } from 'react';
2
+ /** One key and its value: a header and what it carries, a variable and what it is set to. */
3
+ export interface KeyValueRow {
4
+ /**
5
+ * Stable across edits, and unique in the list: the row's identity while its
6
+ * key is typed, emptied or repeated. Two rows may share a key; never an id.
7
+ */
8
+ id: string;
9
+ key: string;
10
+ value: string;
11
+ }
12
+ /** Every word the rows say, beyond the names of their two columns. */
13
+ export interface KeyValueRowsLabels {
14
+ /** Names a row's key field: "Header 2". */
15
+ key: (position: number) => string;
16
+ /** Names a row's value field by its key: "Value for Authorization", or "Value for header 2" before one is typed. */
17
+ value: (key: string, position: number) => string;
18
+ /** Names a row's remove button: "Remove Authorization". */
19
+ remove: (key: string, position: number) => string;
20
+ /** The add action's words: "Add header". */
21
+ add: string;
22
+ /** Said once a row is removed, since the row's going is otherwise silent: "Removed Authorization". */
23
+ removed: (key: string, position: number) => string;
24
+ }
25
+ /**
26
+ * The rows' words in English, made from the two column names: `Header` and
27
+ * `Value` give "Header 2", "Value for Authorization", "Remove Authorization"
28
+ * and "Add header". A caller whose key column is an acronym (`URL`) or not in
29
+ * English passes its own `labels`.
30
+ */
31
+ export declare function keyValueRowsLabels(keyLabel: string, valueLabel?: string): KeyValueRowsLabels;
32
+ interface KeyValueRowsBaseProps<T extends KeyValueRow> {
33
+ /**
34
+ * What the list is: "Headers", "Environment variables". The group's legend,
35
+ * read on entering it.
36
+ */
37
+ label: string;
38
+ /**
39
+ * The legend is kept for assistive technology and drawn for nobody, where
40
+ * the column head or a heading right above already names the list.
41
+ */
42
+ hideLabel?: boolean | undefined;
43
+ /** The key column's head, and the name each key field is called by: "Header", "Variable". */
44
+ keyLabel: string;
45
+ /** The value column's head. "Value" by default. */
46
+ valueLabel?: string | undefined;
47
+ value: readonly T[];
48
+ onChange: (next: T[]) => void;
49
+ keyPlaceholder?: string | undefined;
50
+ /** The value field's placeholder, the same for every row or per row (a stored secret says so). */
51
+ valuePlaceholder?: string | ((row: T) => string | undefined) | undefined;
52
+ /**
53
+ * The values are secrets (a token, a password): each value field hides what
54
+ * is typed, and no browser offers a saved password for it.
55
+ */
56
+ secret?: boolean | undefined;
57
+ /**
58
+ * A row whose key may not change, such as one already saved: its key field
59
+ * is read only, and its value and its remove button still work.
60
+ */
61
+ keyReadOnly?: ((row: T) => boolean) | undefined;
62
+ /** Why a row's key was refused, said under that key and linked to it. */
63
+ keyError?: ((row: T, index: number) => ReactNode) | undefined;
64
+ /** Why a row's value was refused, said under that value and linked to it. */
65
+ valueError?: ((row: T, index: number) => ReactNode) | undefined;
66
+ /**
67
+ * What the list says while it holds no row, in place of the rows. Left out,
68
+ * an empty list is its legend and its add action.
69
+ */
70
+ empty?: ReactNode;
71
+ /** Where the keys come from, or what shape they take, under the add action. */
72
+ helper?: ReactNode;
73
+ /** Why the list as a whole was refused. */
74
+ error?: ReactNode;
75
+ disabled?: boolean | undefined;
76
+ /** Words in place of the ones `keyValueRowsLabels` makes from `keyLabel` and `valueLabel`. */
77
+ labels?: Partial<KeyValueRowsLabels> | undefined;
78
+ className?: string | undefined;
79
+ }
80
+ /**
81
+ * A blank row for the add action. Optional while a row is nothing but its id,
82
+ * key and value, when the list makes `{ id, key: '', value: '' }` itself;
83
+ * required once the caller's rows carry a field of their own that a blank row
84
+ * must set.
85
+ *
86
+ * AN EMPTY LIST NAMES NO ROW TYPE. `value={[]}` infers `never`, which no row
87
+ * is assignable to, so a test of assignability alone asked an empty list for
88
+ * a `createRow` it has no use for. A list whose row type was never inferred
89
+ * makes its own blank rows too.
90
+ */
91
+ type RowMaker<T extends KeyValueRow> = [T] extends [never] ? {
92
+ createRow?: (() => T) | undefined;
93
+ } : KeyValueRow extends T ? {
94
+ createRow?: (() => T) | undefined;
95
+ } : {
96
+ createRow: () => T;
97
+ };
98
+ export type KeyValueRowsProps<T extends KeyValueRow = KeyValueRow> = KeyValueRowsBaseProps<T> & RowMaker<T>;
99
+ /**
100
+ * Keys and their values, edited as rows under one column head: request
101
+ * headers, environment variables, any list of names and what each is set to.
102
+ *
103
+ * ONE HEAD OVER THE COLUMNS, SO EVERY ROW IS THE SAME ROW. The key and value
104
+ * columns are named once, above the first row, and each field's own name is
105
+ * read rather than drawn ("Header 2", "Value for Authorization"). A label
106
+ * drawn on the first row only stood that row taller than the rest, and the
107
+ * value and the remove button beside it lost the line they shared.
108
+ *
109
+ * AN ERROR MOVES NOTHING BESIDE IT. Every row stands on its fields' top edge,
110
+ * so a refused key grows its own cell downward and the value and the remove
111
+ * button stay level with the key's field.
112
+ *
113
+ * THE REMOVE BUTTON IS THE FIELDS' HEIGHT, a square at the control step, so a
114
+ * row is one line of three controls. Add is a flush quiet action under the
115
+ * rows, its words on the fields' own edge.
116
+ *
117
+ * FOCUS NEVER FALLS TO THE PAGE. Add puts it in the new row's key; Remove
118
+ * moves it to the row that took that row's place, else the one before, else
119
+ * Add, and says what went.
120
+ *
121
+ * Controlled: the rows are the caller's, and every edit hands back a new
122
+ * list with each untouched row as it was.
123
+ */
124
+ export declare function KeyValueRows<T extends KeyValueRow = KeyValueRow>(props: KeyValueRowsProps<T>): import("react").JSX.Element;
125
+ export {};
@@ -0,0 +1,3 @@
1
+ import './KeyValueRows.css';
2
+ export { KeyValueRows, keyValueRowsLabels } from './KeyValueRows.js';
3
+ export type { KeyValueRow, KeyValueRowsLabels, KeyValueRowsProps } from './KeyValueRows.js';
@@ -0,0 +1,12 @@
1
+ export { KeyValueRows, keyValueRowsLabels } from '../chunk-TRZS7DLB.js';
2
+ import '../chunk-O7N6W52M.js';
3
+ import '../chunk-PGCLHYVP.js';
4
+ import '../chunk-DGXZ635L.js';
5
+ import '../chunk-XSPCITPN.js';
6
+ import '../chunk-TUEM2XIW.js';
7
+ import '../chunk-4ITETIGZ.js';
8
+ import '../chunk-UAUP6XGK.js';
9
+ import '../chunk-PQADV6P6.js';
10
+ import '../chunk-2G355KDJ.js';
11
+ import '../chunk-UT6CW3C7.js';
12
+ import '../chunk-ODEQTPFM.js';
@@ -0,0 +1,44 @@
1
+ /*
2
+ * KeyValueRows: keys and their values as rows under one column head
3
+ * (KeyValueRows.tsx).
4
+ */
5
+
6
+ /* The rows, then Add under them, a row's gap apart. */
7
+ .crewlet-key-value-rows__body,
8
+ .crewlet-key-value-rows__table {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: var(--spacing-2);
12
+ min-width: 0;
13
+ }
14
+
15
+ /*
16
+ * EVERY ROW STANDS ON ITS FIELDS' TOP EDGE, so a key's error grows its own
17
+ * cell downward and nothing beside it moves. The last track is the remove
18
+ * button's, a square the fields' height, and the head keeps it empty so the
19
+ * value's name stands over the value column.
20
+ */
21
+ .crewlet-key-value-rows__head,
22
+ .crewlet-key-value-rows__row {
23
+ display: grid;
24
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) var(--size-control-md);
25
+ align-items: start;
26
+ gap: var(--spacing-2);
27
+ }
28
+
29
+ /* The head stands over the first row at a field label's distance from its field. */
30
+ .crewlet-key-value-rows__head {
31
+ margin-block-end: calc(var(--spacing-1) - var(--spacing-2));
32
+ }
33
+
34
+ .crewlet-key-value-rows__empty {
35
+ margin: 0;
36
+ font-size: var(--font-size-compact);
37
+ line-height: var(--font-line-height-normal);
38
+ color: var(--color-text-secondary);
39
+ }
40
+
41
+ /* Add hugs its words, on the fields' start edge, rather than spanning the list. */
42
+ .crewlet-key-value-rows__add {
43
+ display: flex;
44
+ }
@@ -1,2 +1,2 @@
1
- export { ListDetail } from '../chunk-SNJM5KPV.js';
1
+ export { ListDetail } from '../chunk-P33VNG7Q.js';
2
2
  import '../chunk-ODEQTPFM.js';
@@ -65,6 +65,12 @@
65
65
  min-height: 0;
66
66
  }
67
67
 
68
+ /* A LIST THAT GROWS SCROLLS IN ITS OWN COLUMN, whatever it holds. The pane
69
+ left the scrolling to its content, which a DirectoryList does for its
70
+ groups and an InboxList does not: an activity list taller than the screen
71
+ ran past the pane's end, where the screen clipped its later rows and its
72
+ "older" link, with nothing to scroll them into view. A list that scrolls
73
+ itself fills the pane and never makes it overflow. */
68
74
  .crewlet-list-detail__list {
69
75
  display: flex;
70
76
  flex-direction: column;
@@ -72,6 +78,7 @@
72
78
  min-height: 0;
73
79
  padding-right: var(--crewlet-list-detail-gutter);
74
80
  border-right: 1px solid var(--color-border-default);
81
+ overflow-y: auto;
75
82
  }
76
83
 
77
84
  /* A DIRECTORY RUNS TO THE HAIRLINE. A DirectoryList is navigation: its lines
@@ -106,7 +113,6 @@
106
113
  .crewlet-list-detail__list {
107
114
  padding-right: 0;
108
115
  border-right: 0;
109
- overflow-y: auto;
110
116
  }
111
117
 
112
118
  /* The list is the pane that scrolls now, and takes the detail's end where
@@ -0,0 +1,74 @@
1
+ import type { ReactNode } from 'react';
2
+ import { type NavItemProps } from '../SidebarNav/index.js';
3
+ /** One of the groups a directory files its items in, and the rail's row for it. */
4
+ export interface RailListItem {
5
+ /** Unique among the items, and what `value` names. An empty key is a fine "All". */
6
+ key: string;
7
+ label: string;
8
+ /** How many items the group holds: a quiet figure at the row's end, and after the label in the folded picker. */
9
+ count?: number | undefined;
10
+ /**
11
+ * The group's own address, where the chosen group is kept in the page's
12
+ * address: the row is then a link a reader can open in a new tab, and a
13
+ * plain click still chooses it in place.
14
+ */
15
+ href?: string | undefined;
16
+ /** One quiet line about the group, said over the list while it is chosen: what the group holds. */
17
+ description?: ReactNode;
18
+ }
19
+ /** Every word the rail and its picker say, beyond the items' own labels. */
20
+ export interface RailListLabels {
21
+ /** What a count counts, read after the figure as part of the row's name: "apps" makes "Chat, 6 apps". */
22
+ counted: string;
23
+ /** A group as the folded picker names it: "Chat (6)". */
24
+ option: (label: string, count: number | undefined) => string;
25
+ }
26
+ export declare const RAIL_LIST_LABELS: RailListLabels;
27
+ export interface RailListProps {
28
+ /** Names the rail's navigation and the picker it folds into: "Categories", "Template kinds". */
29
+ label: string;
30
+ /** The groups, in the order the rail lists them; an "All" group is one of them, usually the first. */
31
+ items: readonly RailListItem[];
32
+ /** The chosen group's key. */
33
+ value: string;
34
+ onChange: (key: string) => void;
35
+ /**
36
+ * The bar across the top of the card, over the rail and the list: a search
37
+ * and filter chips. Where the card is too narrow for the rail, the picker
38
+ * it folds into joins this bar. Given as a function, it is handed the
39
+ * picker to place among its own controls; given as content, the picker
40
+ * leads the bar. Left out, the bar is drawn only to hold the picker.
41
+ */
42
+ toolbar?: ReactNode | ((picker: ReactNode) => ReactNode);
43
+ /** One quiet fact about the whole directory, under the rail's rows: what every item in it has in common. */
44
+ note?: ReactNode;
45
+ /** The list the chosen group shows: a grid, a list of tiles. */
46
+ children: ReactNode;
47
+ /** Draws each rail row's link, for a router that owns navigation (`SidebarNav`'s `renderLink`). */
48
+ renderLink?: NavItemProps['renderLink'];
49
+ labels?: Partial<RailListLabels> | undefined;
50
+ className?: string | undefined;
51
+ }
52
+ /**
53
+ * A directory in a card: a rail of the groups its items are filed in, beside
54
+ * the list of the chosen group, so a long catalogue reads as a few short
55
+ * lists: apps in what they are for, templates in their kinds, reports in
56
+ * the teams that read them.
57
+ *
58
+ * THE RAIL IS THE APPLICATION'S RAIL, DENSER (`SidebarNav` `dense`), each
59
+ * group a row with its count, the chosen one raised and `aria-current`, on a
60
+ * hairline of its own beside the list, with an optional quiet note under its
61
+ * rows. The chosen group's description, where it has one, stands over the
62
+ * list on a hairline, so a reader knows what the short list is.
63
+ *
64
+ * IT FOLDS BY ITS OWN WIDTH, NOT THE WINDOW'S. Under 44rem of the card's
65
+ * width the rail does not leave the list a readable column, so the rail goes
66
+ * and a `Select` of the same groups, named the same, joins the bar across the
67
+ * top. The same directory in a page and in a 560px sheet is one component
68
+ * with nothing to set: the sheet is simply narrow.
69
+ *
70
+ * Controlled: the chosen group is the caller's, often kept in the page's
71
+ * address (`href` on each group), so a group opened in a new tab and Back
72
+ * both keep it.
73
+ */
74
+ export declare function RailList({ label, items, value, onChange, toolbar, note, children, renderLink, labels: overrides, className, }: RailListProps): import("react").JSX.Element;
@@ -0,0 +1,3 @@
1
+ import './RailList.css';
2
+ export { RAIL_LIST_LABELS, RailList } from './RailList.js';
3
+ export type { RailListItem, RailListLabels, RailListProps } from './RailList.js';
@@ -0,0 +1,17 @@
1
+ export { RAIL_LIST_LABELS, RailList } from '../chunk-REWKR3S4.js';
2
+ import '../chunk-BRK3HXGZ.js';
3
+ import '../chunk-3IWZVEXA.js';
4
+ import '../chunk-O5CEG7TQ.js';
5
+ import '../chunk-YGRTX67J.js';
6
+ import '../chunk-TEV5REGB.js';
7
+ import '../chunk-HBDA76EZ.js';
8
+ import '../chunk-U2IQNDDR.js';
9
+ import '../chunk-AV2DXAEL.js';
10
+ import '../chunk-OMV5W53A.js';
11
+ import '../chunk-2YTOOH3A.js';
12
+ import '../chunk-TUEM2XIW.js';
13
+ import '../chunk-4ITETIGZ.js';
14
+ import '../chunk-D2GT3CR4.js';
15
+ import '../chunk-PQADV6P6.js';
16
+ import '../chunk-UT6CW3C7.js';
17
+ import '../chunk-ODEQTPFM.js';