@unofficialbox/box-open-elements 0.5.0 → 0.6.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.
Files changed (219) hide show
  1. package/README.md +3 -3
  2. package/dist/components/collections/table.d.ts +57 -3
  3. package/dist/components/collections/table.js +339 -20
  4. package/dist/components/feedback/due-badge.d.ts +38 -0
  5. package/dist/components/feedback/due-badge.js +168 -0
  6. package/dist/components/feedback/due-types.d.ts +32 -0
  7. package/dist/components/feedback/due-types.js +91 -0
  8. package/dist/components/feedback/progress-steps.d.ts +51 -5
  9. package/dist/components/feedback/progress-steps.js +200 -22
  10. package/dist/components/feedback/stage-path.d.ts +44 -0
  11. package/dist/components/feedback/stage-path.js +235 -0
  12. package/dist/components/forms/select.d.ts +21 -0
  13. package/dist/components/forms/select.js +100 -3
  14. package/dist/components/forms/text-field.d.ts +21 -0
  15. package/dist/components/forms/text-field.js +100 -1
  16. package/dist/components/layout/nav-sidebar.d.ts +14 -0
  17. package/dist/components/layout/nav-sidebar.js +49 -0
  18. package/dist/components/layout/split-view.d.ts +14 -0
  19. package/dist/components/layout/split-view.js +80 -1
  20. package/dist/components/overlays/command-palette.d.ts +62 -0
  21. package/dist/components/overlays/command-palette.js +537 -0
  22. package/dist/components/overlays/command-types.d.ts +90 -0
  23. package/dist/components/overlays/command-types.js +259 -0
  24. package/dist/components/overlays/drawer.d.ts +18 -0
  25. package/dist/components/overlays/drawer.js +135 -9
  26. package/dist/components/overlays/shortcuts-overlay.d.ts +48 -0
  27. package/dist/components/overlays/shortcuts-overlay.js +385 -0
  28. package/dist/components/overlays/tooltip.js +1 -1
  29. package/dist/element-maps.d.ts +292 -0
  30. package/dist/element-maps.js +1 -0
  31. package/dist/entries/activity-density.d.ts +1 -0
  32. package/dist/entries/activity-density.js +1 -0
  33. package/dist/entries/agent-chat.d.ts +1 -0
  34. package/dist/entries/agent-chat.js +1 -0
  35. package/dist/entries/audit-log.d.ts +1 -0
  36. package/dist/entries/audit-log.js +1 -0
  37. package/dist/entries/command-palette.d.ts +1 -0
  38. package/dist/entries/command-palette.js +1 -0
  39. package/dist/entries/compare-view.d.ts +1 -0
  40. package/dist/entries/compare-view.js +1 -0
  41. package/dist/entries/diff-viewer.d.ts +1 -0
  42. package/dist/entries/diff-viewer.js +1 -0
  43. package/dist/entries/due-badge.d.ts +1 -0
  44. package/dist/entries/due-badge.js +1 -0
  45. package/dist/entries/form-wizard.d.ts +1 -0
  46. package/dist/entries/form-wizard.js +1 -0
  47. package/dist/entries/lineage-graph.d.ts +1 -0
  48. package/dist/entries/lineage-graph.js +1 -0
  49. package/dist/entries/notification-bell.d.ts +1 -0
  50. package/dist/entries/notification-bell.js +1 -0
  51. package/dist/entries/notification-inbox.d.ts +1 -0
  52. package/dist/entries/notification-inbox.js +1 -0
  53. package/dist/entries/provenance-strip.d.ts +1 -0
  54. package/dist/entries/provenance-strip.js +1 -0
  55. package/dist/entries/run-trace.d.ts +1 -0
  56. package/dist/entries/run-trace.js +1 -0
  57. package/dist/entries/shortcuts-overlay.d.ts +1 -0
  58. package/dist/entries/shortcuts-overlay.js +1 -0
  59. package/dist/entries/signature-ceremony.d.ts +1 -0
  60. package/dist/entries/signature-ceremony.js +1 -0
  61. package/dist/entries/stage-path.d.ts +1 -0
  62. package/dist/entries/stage-path.js +1 -0
  63. package/dist/entries/timeline.d.ts +1 -0
  64. package/dist/entries/timeline.js +1 -0
  65. package/dist/entries/version-graph.d.ts +1 -0
  66. package/dist/entries/version-graph.js +1 -0
  67. package/dist/entries/version-list.d.ts +1 -0
  68. package/dist/entries/version-list.js +1 -0
  69. package/dist/entries/wizard-summary.d.ts +1 -0
  70. package/dist/entries/wizard-summary.js +1 -0
  71. package/dist/entries/work-queue.d.ts +1 -0
  72. package/dist/entries/work-queue.js +1 -0
  73. package/dist/entries/workload-board.d.ts +1 -0
  74. package/dist/entries/workload-board.js +1 -0
  75. package/dist/index.d.ts +21 -0
  76. package/dist/index.js +21 -0
  77. package/dist/patterns/agent-chat/agent-chat.d.ts +75 -0
  78. package/dist/patterns/agent-chat/agent-chat.js +820 -0
  79. package/dist/patterns/agent-chat/controller.d.ts +27 -0
  80. package/dist/patterns/agent-chat/controller.js +182 -0
  81. package/dist/patterns/agent-chat/index.d.ts +3 -0
  82. package/dist/patterns/agent-chat/index.js +6 -0
  83. package/dist/patterns/agent-chat/types.d.ts +109 -0
  84. package/dist/patterns/agent-chat/types.js +1 -0
  85. package/dist/patterns/audit/activity-density.d.ts +68 -0
  86. package/dist/patterns/audit/activity-density.js +428 -0
  87. package/dist/patterns/audit/audit-log.d.ts +81 -0
  88. package/dist/patterns/audit/audit-log.js +794 -0
  89. package/dist/patterns/audit/index.d.ts +3 -0
  90. package/dist/patterns/audit/index.js +7 -0
  91. package/dist/patterns/audit/shared.d.ts +10 -0
  92. package/dist/patterns/audit/shared.js +50 -0
  93. package/dist/patterns/audit/types.d.ts +143 -0
  94. package/dist/patterns/audit/types.js +327 -0
  95. package/dist/patterns/content-explorer/actions/controller.js +12 -2
  96. package/dist/patterns/content-explorer/box-transport.js +117 -25
  97. package/dist/patterns/content-explorer/content-explorer.d.ts +13 -1
  98. package/dist/patterns/content-explorer/content-explorer.js +36 -3
  99. package/dist/patterns/content-explorer/contracts.d.ts +24 -1
  100. package/dist/patterns/content-explorer/contracts.js +68 -11
  101. package/dist/patterns/content-explorer/controller.d.ts +16 -1
  102. package/dist/patterns/content-explorer/controller.js +130 -4
  103. package/dist/patterns/content-explorer/navigation/controller.js +11 -9
  104. package/dist/patterns/content-explorer/types.d.ts +58 -1
  105. package/dist/patterns/content-picker/content-picker.d.ts +71 -0
  106. package/dist/patterns/content-picker/content-picker.js +747 -0
  107. package/dist/patterns/content-picker/controller.d.ts +38 -0
  108. package/dist/patterns/content-picker/controller.js +141 -0
  109. package/dist/patterns/content-picker/index.d.ts +3 -0
  110. package/dist/patterns/content-picker/index.js +6 -0
  111. package/dist/patterns/content-picker/types.d.ts +52 -0
  112. package/dist/patterns/content-picker/types.js +26 -0
  113. package/dist/patterns/content-sidebar/content-sidebar.d.ts +43 -0
  114. package/dist/patterns/content-sidebar/content-sidebar.js +293 -0
  115. package/dist/patterns/content-sidebar/index.d.ts +2 -0
  116. package/dist/patterns/content-sidebar/index.js +4 -0
  117. package/dist/patterns/content-sidebar/types.d.ts +18 -0
  118. package/dist/patterns/content-sidebar/types.js +20 -0
  119. package/dist/patterns/content-uploader/box-transport.d.ts +14 -0
  120. package/dist/patterns/content-uploader/box-transport.js +54 -0
  121. package/dist/patterns/content-uploader/content-uploader.d.ts +62 -0
  122. package/dist/patterns/content-uploader/content-uploader.js +559 -0
  123. package/dist/patterns/content-uploader/controller.d.ts +39 -0
  124. package/dist/patterns/content-uploader/controller.js +227 -0
  125. package/dist/patterns/content-uploader/index.d.ts +4 -0
  126. package/dist/patterns/content-uploader/index.js +8 -0
  127. package/dist/patterns/content-uploader/types.d.ts +120 -0
  128. package/dist/patterns/content-uploader/types.js +38 -0
  129. package/dist/patterns/diff/compare-view.d.ts +47 -0
  130. package/dist/patterns/diff/compare-view.js +276 -0
  131. package/dist/patterns/diff/diff-viewer.d.ts +42 -0
  132. package/dist/patterns/diff/diff-viewer.js +434 -0
  133. package/dist/patterns/diff/engine.d.ts +22 -0
  134. package/dist/patterns/diff/engine.js +276 -0
  135. package/dist/patterns/diff/index.d.ts +4 -0
  136. package/dist/patterns/diff/index.js +8 -0
  137. package/dist/patterns/diff/types.d.ts +69 -0
  138. package/dist/patterns/diff/types.js +24 -0
  139. package/dist/patterns/form-wizard/controller.d.ts +39 -0
  140. package/dist/patterns/form-wizard/controller.js +157 -0
  141. package/dist/patterns/form-wizard/form-wizard.d.ts +63 -0
  142. package/dist/patterns/form-wizard/form-wizard.js +468 -0
  143. package/dist/patterns/form-wizard/index.d.ts +4 -0
  144. package/dist/patterns/form-wizard/index.js +8 -0
  145. package/dist/patterns/form-wizard/types.d.ts +109 -0
  146. package/dist/patterns/form-wizard/types.js +83 -0
  147. package/dist/patterns/form-wizard/wizard-summary.d.ts +62 -0
  148. package/dist/patterns/form-wizard/wizard-summary.js +324 -0
  149. package/dist/patterns/internal/safe-href.d.ts +17 -0
  150. package/dist/patterns/internal/safe-href.js +17 -0
  151. package/dist/patterns/lineage/index.d.ts +3 -0
  152. package/dist/patterns/lineage/index.js +6 -0
  153. package/dist/patterns/lineage/lineage-graph.d.ts +30 -0
  154. package/dist/patterns/lineage/lineage-graph.js +434 -0
  155. package/dist/patterns/lineage/provenance-strip.d.ts +22 -0
  156. package/dist/patterns/lineage/provenance-strip.js +194 -0
  157. package/dist/patterns/lineage/types.d.ts +53 -0
  158. package/dist/patterns/lineage/types.js +49 -0
  159. package/dist/patterns/notifications/index.d.ts +3 -0
  160. package/dist/patterns/notifications/index.js +6 -0
  161. package/dist/patterns/notifications/notification-bell.d.ts +38 -0
  162. package/dist/patterns/notifications/notification-bell.js +198 -0
  163. package/dist/patterns/notifications/notification-inbox.d.ts +46 -0
  164. package/dist/patterns/notifications/notification-inbox.js +537 -0
  165. package/dist/patterns/notifications/types.d.ts +61 -0
  166. package/dist/patterns/notifications/types.js +108 -0
  167. package/dist/patterns/preview/annotation-inspector.d.ts +25 -6
  168. package/dist/patterns/preview/annotation-inspector.js +239 -94
  169. package/dist/patterns/preview/annotation-thread.d.ts +25 -5
  170. package/dist/patterns/preview/annotation-thread.js +233 -82
  171. package/dist/patterns/preview/annotation-toolbar.d.ts +20 -6
  172. package/dist/patterns/preview/annotation-toolbar.js +162 -96
  173. package/dist/patterns/preview/content-preview-adapter.d.ts +10 -1
  174. package/dist/patterns/preview/content-preview-adapter.js +26 -1
  175. package/dist/patterns/preview/preview-element.d.ts +15 -2
  176. package/dist/patterns/preview/preview-element.js +223 -52
  177. package/dist/patterns/preview/provider-adapter.d.ts +47 -0
  178. package/dist/patterns/preview/provider-adapter.js +17 -0
  179. package/dist/patterns/run/index.d.ts +2 -0
  180. package/dist/patterns/run/index.js +4 -0
  181. package/dist/patterns/run/run-trace.d.ts +40 -0
  182. package/dist/patterns/run/run-trace.js +441 -0
  183. package/dist/patterns/run/types.d.ts +66 -0
  184. package/dist/patterns/run/types.js +152 -0
  185. package/dist/patterns/signature/index.d.ts +2 -0
  186. package/dist/patterns/signature/index.js +4 -0
  187. package/dist/patterns/signature/signature-ceremony.d.ts +33 -0
  188. package/dist/patterns/signature/signature-ceremony.js +317 -0
  189. package/dist/patterns/signature/types.d.ts +68 -0
  190. package/dist/patterns/signature/types.js +89 -0
  191. package/dist/patterns/timeline/index.d.ts +2 -0
  192. package/dist/patterns/timeline/index.js +4 -0
  193. package/dist/patterns/timeline/timeline.d.ts +27 -0
  194. package/dist/patterns/timeline/timeline.js +430 -0
  195. package/dist/patterns/timeline/types.d.ts +36 -0
  196. package/dist/patterns/timeline/types.js +27 -0
  197. package/dist/patterns/versions/graph-layout.d.ts +43 -0
  198. package/dist/patterns/versions/graph-layout.js +159 -0
  199. package/dist/patterns/versions/index.d.ts +4 -0
  200. package/dist/patterns/versions/index.js +8 -0
  201. package/dist/patterns/versions/shared.d.ts +4 -0
  202. package/dist/patterns/versions/shared.js +13 -0
  203. package/dist/patterns/versions/types.d.ts +37 -0
  204. package/dist/patterns/versions/types.js +39 -0
  205. package/dist/patterns/versions/version-graph.d.ts +36 -0
  206. package/dist/patterns/versions/version-graph.js +438 -0
  207. package/dist/patterns/versions/version-list.d.ts +39 -0
  208. package/dist/patterns/versions/version-list.js +407 -0
  209. package/dist/patterns/work-queue/controller.d.ts +26 -0
  210. package/dist/patterns/work-queue/controller.js +150 -0
  211. package/dist/patterns/work-queue/index.d.ts +4 -0
  212. package/dist/patterns/work-queue/index.js +7 -0
  213. package/dist/patterns/work-queue/types.d.ts +128 -0
  214. package/dist/patterns/work-queue/types.js +45 -0
  215. package/dist/patterns/work-queue/work-queue.d.ts +48 -0
  216. package/dist/patterns/work-queue/work-queue.js +534 -0
  217. package/dist/patterns/work-queue/workload-board.d.ts +53 -0
  218. package/dist/patterns/work-queue/workload-board.js +608 -0
  219. package/package.json +89 -5
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  > **Community project — not affiliated with, authorized, or endorsed by Box, Inc.** “Box” is a trademark of Box, Inc. This library tracks Box’s public design language for interoperability; it ships no Box code.
12
12
 
13
- It is a ground-up rebuild of [`box-open-web-components`](https://github.com/unofficialbox/box-open-web-components), carrying forward that repo's validated architecture, docs, and research — without its accumulated baggage — under a new industry-standard taxonomy:
13
+ The library is organized around three layers:
14
14
 
15
15
  - **Foundations** — design decisions as data: tokens, color, typography, iconography, accessibility, theming
16
16
  - **Components** — accessible Web Components for single controls, organized by category
@@ -18,7 +18,7 @@ It is a ground-up rebuild of [`box-open-web-components`](https://github.com/unof
18
18
 
19
19
  See [docs/taxonomy.md](./docs/taxonomy.md) for the canonical model.
20
20
 
21
- The guiding idea is unchanged: predictable, extensible building blocks that are easy for both human developers and AI coding assistants to compose.
21
+ Core implementation principles:
22
22
 
23
23
  - plain TypeScript modules, no React requirement in the core package
24
24
  - state and business logic separate from rendering: controllers and stores, not framework components
@@ -73,7 +73,7 @@ Full catalog parity with the reference repo, plus every scoped gap the research
73
73
  - `src/patterns/content-explorer` — the full headless explorer stack (collection, navigation, selection, actions, facade controller, data-source contracts, Box transport, wire schemas) plus the `box-explorer-*` presentation adapters and the composed `box-content-explorer` surface
74
74
  - `src/patterns/{search,item,metadata,share,preview,file-request,task,governance,insights}` — all nine composition/workflow areas, including contracts and wire schemas for metadata and share, the provider-neutral preview adapter stack, the pluggable `box-preview-element`, and the share workflows (`box-presence`, `box-invite-collaborators-modal`, `box-unified-share-modal`, `box-access-stats`, `box-collaborator-avatars`)
75
75
  - `packages/box-server` — a dependency-free server-side adapter: CCG auth + REST client, Box-backed explorer/share/metadata data sources, DTO mappers, and framework-neutral route handlers (see [docs/integration/box-server.md](./docs/integration/box-server.md))
76
- - `packages/react` — optional React wrappers (`@box-open-elements/react`); the validated surface ships `Button`, `TextField`, and `Select`. React, Angular, Vue, and Svelte progress is tracked in [docs/integration/framework-adapters.md](./docs/integration/framework-adapters.md).
76
+ - `packages/{react,angular,vue,svelte}` — lockstep `0.1.0` framework adapters for `Button`, `TextField`, `Select`, `Dialog`, and explorer selection composition. Progress is tracked in [docs/integration/framework-adapters.md](./docs/integration/framework-adapters.md).
77
77
  - `storybook/` — a Bun-native workshop: typed stories → identity-guarded extracted JSON → a self-contained, separately-deployable static site, with no Storybook/Vite runtime or consumer dependency (see [storybook/README.md](./storybook/README.md))
78
78
  - Docs site + CI — live GitHub Pages deploy, Storybook-backed variant dropdown, and a strict pixel-diff visual-regression gate in CI
79
79
 
@@ -7,12 +7,47 @@ export interface TableColumn {
7
7
  /** Mark the column sortable — clicking its header emits `sort`. */
8
8
  sortable?: boolean;
9
9
  }
10
+ export type TableCellTone = "neutral" | "brand" | "success" | "warning" | "error";
11
+ /**
12
+ * Declarative cell content beyond plain text. Deliberately a descriptor the
13
+ * table renders itself, never an HTML string or a render callback: cell data
14
+ * is server-supplied and stays escaped — a string renderer would reopen the
15
+ * injection hole the escaping exists to close.
16
+ */
17
+ export interface TableCellDescriptor {
18
+ kind: "text" | "badge" | "link";
19
+ text: string;
20
+ /** Badge colour; the text still carries the meaning. */
21
+ tone?: TableCellTone;
22
+ /** Link target; non-http(s) schemes render as plain text. */
23
+ href?: string;
24
+ }
25
+ export type TableCellValue = string | TableCellDescriptor;
10
26
  export interface TableRow {
11
27
  id: string;
12
- /** Cell text keyed by column key, or positional strings matching `columns`. */
13
- cells: Record<string, string> | string[];
28
+ /** Cell values keyed by column key, or positional, matching `columns`. */
29
+ cells: Record<string, TableCellValue> | TableCellValue[];
30
+ /**
31
+ * Expandable detail. Renders a per-row toggle and a full-width detail row
32
+ * holding this text plus a `detail-<id>` slot for rich host content.
33
+ */
34
+ detail?: string;
14
35
  }
15
36
  export type TableSelectionMode = "none" | "single" | "multiple";
37
+ /** Detail of the `sort` event, for typed listeners. */
38
+ export interface TableSortDetail {
39
+ key: string;
40
+ direction: "ascending" | "descending";
41
+ }
42
+ /** Detail of the `selection-changed` event, for typed listeners. */
43
+ export interface TableSelectionChangedDetail {
44
+ selectedIds: string[];
45
+ }
46
+ /** Detail of the `row-toggled` event, for typed listeners. */
47
+ export interface TableRowToggledDetail {
48
+ rowId: string;
49
+ expanded: boolean;
50
+ }
16
51
  /**
17
52
  * A semantic, selectable data table — box-ui-elements `Table` + `makeSelectable`.
18
53
  * Rows and columns come from JSON. Selection supports single / Ctrl-click toggle
@@ -27,6 +62,18 @@ export declare class Table extends BaseElement {
27
62
  private headEl;
28
63
  private selected;
29
64
  private anchorIndex;
65
+ private readonly expandedIds;
66
+ /** True while rows are being fetched: the body states "Loading rows…". */
67
+ get loading(): boolean;
68
+ set loading(value: boolean);
69
+ /** What the body says when there are no rows and nothing is loading. */
70
+ get emptyText(): string;
71
+ set emptyText(value: string);
72
+ /** Non-empty puts the table in an error state stating this text. */
73
+ get errorText(): string;
74
+ set errorText(value: string);
75
+ /** Ids of the rows whose detail is expanded (host-readable). */
76
+ get expandedRows(): string[];
30
77
  get columns(): TableColumn[];
31
78
  set columns(value: TableColumn[]);
32
79
  get rows(): TableRow[];
@@ -38,10 +85,17 @@ export declare class Table extends BaseElement {
38
85
  get label(): string;
39
86
  set label(value: string);
40
87
  private parseJson;
41
- private cellText;
88
+ private cellValue;
89
+ /**
90
+ * Descriptor rendering. Everything is escaped; a link with a non-http(s)
91
+ * href renders as plain text; a badge's tone colours a word that already
92
+ * carries the meaning.
93
+ */
94
+ private cellMarkup;
42
95
  protected renderTemplate(): void;
43
96
  protected setupListeners(): void;
44
97
  private rowElements;
98
+ private toggleRowDetail;
45
99
  private onBodyKeydown;
46
100
  private toggleSort;
47
101
  private activateRow;
@@ -1,6 +1,15 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
2
  import { boePanel, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
3
+ import { isSafeHref } from "../../patterns/internal/safe-href.js";
3
4
  const DEFAULT_TAG_NAME = "box-table";
5
+ const CELL_TONES = new Set(["neutral", "brand", "success", "warning", "error"]);
6
+ const CELL_ALIGNS = new Set(["start", "end", "center"]);
7
+ /** `align` is author input from a JSON attribute like everything else — an
8
+ * unvalidated value would close the attribute and inject markup. */
9
+ const alignAttr = (align) => align && CELL_ALIGNS.has(align) ? ` data-align="${align}"` : "";
10
+ const isCellDescriptor = (value) => typeof value === "object" && value !== null && typeof value.text === "string";
11
+ /** Escape a value for use inside a double-quoted attribute selector. */
12
+ const cssAttrValue = (value) => value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
4
13
  const escapeHtml = (value) => value
5
14
  .replaceAll("&", "&amp;")
6
15
  .replaceAll("<", "&lt;")
@@ -47,8 +56,35 @@ const tableStyles = `
47
56
  th[part="sortable"] {
48
57
  cursor: pointer;
49
58
  user-select: none;
59
+ /* The button carries the padding so its hit area fills the cell. */
60
+ padding: 0;
50
61
  }
51
62
  th[part="sortable"]:hover { color: var(--boe-token-text-text, #222222); }
63
+
64
+ /* A real button, so sorting is reachable by keyboard: focus, Enter and
65
+ Space come from the platform instead of a keydown reimplementation. */
66
+ [part="sort-button"] {
67
+ appearance: none;
68
+ display: block;
69
+ width: 100%;
70
+ padding: ${boeSpace[2]} ${boeSpace[3]};
71
+ border: 0;
72
+ background: transparent;
73
+ font: inherit;
74
+ letter-spacing: inherit;
75
+ text-transform: inherit;
76
+ text-align: inherit;
77
+ color: inherit;
78
+ cursor: pointer;
79
+ }
80
+
81
+ th[data-align="end"] [part="sort-button"] { text-align: end; }
82
+ th[data-align="center"] [part="sort-button"] { text-align: center; }
83
+
84
+ [part="sort-button"]:focus-visible {
85
+ outline: none;
86
+ box-shadow: inset 0 0 0 2px var(--boe-token-surface-surface-brand, #0061d5);
87
+ }
52
88
  th[aria-sort] .boe-sort-arrow::after { content: " ↕"; opacity: 0.5; }
53
89
  th[aria-sort="ascending"] .boe-sort-arrow::after { content: " ↑"; opacity: 1; }
54
90
  th[aria-sort="descending"] .boe-sort-arrow::after { content: " ↓"; opacity: 1; }
@@ -76,7 +112,143 @@ const tableStyles = `
76
112
  tbody tr[part="row"]:focus-visible td {
77
113
  box-shadow: inset 0 0 0 2px var(--boe-token-surface-surface-brand, #0061d5);
78
114
  }
115
+
116
+ [part="cell-badge"] {
117
+ display: inline-block;
118
+ padding: 0.08rem 0.5rem;
119
+ border-radius: 999px;
120
+ font-size: 0.74rem;
121
+ font-weight: 700;
122
+ color: color-mix(in srgb, var(--cell-tone, var(--boe-token-text-text-secondary, #6f6f6f)) 76%, black 24%);
123
+ background: color-mix(in srgb, var(--cell-tone, var(--boe-token-text-text-secondary, #6f6f6f)) 14%, transparent);
124
+ border: 1px solid color-mix(in srgb, var(--cell-tone, var(--boe-token-text-text-secondary, #6f6f6f)) 30%, transparent);
125
+ }
126
+
127
+ [part="cell-link"] {
128
+ color: var(--boe-token-surface-surface-brand, #0061d5);
129
+ text-decoration: none;
130
+ }
131
+
132
+ [part="cell-link"]:hover {
133
+ text-decoration: underline;
134
+ }
135
+
136
+ [part="expander"] {
137
+ appearance: none;
138
+ display: inline-grid;
139
+ place-items: center;
140
+ inline-size: 1.4rem;
141
+ block-size: 1.4rem;
142
+ border: 0;
143
+ border-radius: 4px;
144
+ background: transparent;
145
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
146
+ font: inherit;
147
+ font-size: 0.72rem;
148
+ cursor: pointer;
149
+ }
150
+
151
+ [part="expander"]:hover {
152
+ background: var(--boe-token-surface-surface-hover, #f4f4f4);
153
+ }
154
+
155
+ [part="expander"]:focus-visible {
156
+ outline: 2px solid color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 34%, transparent);
157
+ outline-offset: 1px;
158
+ }
159
+
160
+ [part="expander"]::before {
161
+ content: "▸";
162
+ transition: transform 120ms ease;
163
+ }
164
+
165
+ [part="expander"][aria-expanded="true"]::before {
166
+ transform: rotate(90deg);
167
+ }
168
+
169
+ tr[part="detail-row"] td {
170
+ background: color-mix(in srgb, var(--boe-token-surface-surface-secondary, #fbfbfb) 80%, transparent);
171
+ font-size: 0.84rem;
172
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
173
+ }
174
+
175
+ tr[part="state-row"] td {
176
+ padding: 1.1rem ${boeSpace[3]};
177
+ text-align: center;
178
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
179
+ }
180
+
181
+ tr[part="state-row"][data-state="error"] td {
182
+ color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 76%, black 24%);
183
+ }
184
+
185
+ [part="state-spinner"] {
186
+ display: inline-block;
187
+ vertical-align: -0.15rem;
188
+ margin-inline-end: 0.45rem;
189
+ inline-size: 0.95rem;
190
+ block-size: 0.95rem;
191
+ border-radius: 999px;
192
+ border: 2px solid color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 20%, transparent);
193
+ border-top-color: var(--boe-token-surface-surface-brand, #0061d5);
194
+ animation: boe-table-spin 0.9s linear infinite;
195
+ }
196
+
197
+ @keyframes boe-table-spin {
198
+ to { transform: rotate(360deg); }
199
+ }
200
+
201
+ @media (prefers-reduced-motion: reduce) {
202
+ [part="state-spinner"] { animation-duration: 1.6s; }
203
+ }
204
+
205
+ /* Stacked rows: each row becomes a card, each cell states its column via
206
+ data-label. Roles are explicit on rows and cells, so the grid semantics
207
+ survive the display change. stacked="always" forces it; stacked="auto"
208
+ stacks when the host container narrows. */
209
+ :host {
210
+ container-type: inline-size;
211
+ }
212
+ ` +
213
+ (() => {
214
+ const rules = (scope) => `
215
+ ${scope} thead { display: none; }
216
+ ${scope} tr[part="row"] {
217
+ display: block;
218
+ padding: ${boeSpace[2]} 0;
219
+ }
220
+ ${scope} tbody td {
221
+ display: grid;
222
+ grid-template-columns: minmax(7.5rem, 9rem) 1fr;
223
+ gap: 0.5rem;
224
+ border-bottom: none;
225
+ padding-block: 0.18rem;
226
+ text-align: start;
227
+ }
228
+ ${scope} tbody td[data-label]::before {
229
+ content: attr(data-label);
230
+ font-size: 0.74rem;
231
+ font-weight: 700;
232
+ letter-spacing: 0.05em;
233
+ text-transform: uppercase;
234
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
235
+ align-self: center;
236
+ }
237
+ ${scope} tr[part="row"]:not(:last-child) {
238
+ border-bottom: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
239
+ }
240
+ ${scope} tr[part="detail-row"] td,
241
+ ${scope} tr[part="state-row"] td {
242
+ display: block;
243
+ }
79
244
  `;
245
+ return `
246
+ ${rules(':host([stacked]:not([stacked="auto"]))')}
247
+ @container (max-width: 560px) {
248
+ ${rules(':host([stacked="auto"])')}
249
+ }
250
+ `;
251
+ })();
80
252
  /**
81
253
  * A semantic, selectable data table — box-ui-elements `Table` + `makeSelectable`.
82
254
  * Rows and columns come from JSON. Selection supports single / Ctrl-click toggle
@@ -87,12 +259,54 @@ const tableStyles = `
87
259
  export class Table extends BaseElement {
88
260
  static tagName = DEFAULT_TAG_NAME;
89
261
  static get observedAttributes() {
90
- return ["columns", "rows", "selection-mode", "label", "sort-key", "sort-direction"];
262
+ return [
263
+ "columns",
264
+ "empty-text",
265
+ "error-text",
266
+ "label",
267
+ "loading",
268
+ "rows",
269
+ "selection-mode",
270
+ "sort-direction",
271
+ "sort-key",
272
+ "stacked",
273
+ ];
91
274
  }
92
275
  bodyEl;
93
276
  headEl;
94
277
  selected = new Set();
95
278
  anchorIndex = -1;
279
+ expandedIds = new Set();
280
+ /** True while rows are being fetched: the body states "Loading rows…". */
281
+ get loading() {
282
+ return this.hasAttribute("loading");
283
+ }
284
+ set loading(value) {
285
+ this.toggleAttribute("loading", Boolean(value));
286
+ }
287
+ /** What the body says when there are no rows and nothing is loading. */
288
+ get emptyText() {
289
+ return this.getAttribute("empty-text") ?? "No rows";
290
+ }
291
+ set emptyText(value) {
292
+ this.setAttribute("empty-text", value);
293
+ }
294
+ /** Non-empty puts the table in an error state stating this text. */
295
+ get errorText() {
296
+ return this.getAttribute("error-text") ?? "";
297
+ }
298
+ set errorText(value) {
299
+ if (value) {
300
+ this.setAttribute("error-text", value);
301
+ }
302
+ else {
303
+ this.removeAttribute("error-text");
304
+ }
305
+ }
306
+ /** Ids of the rows whose detail is expanded (host-readable). */
307
+ get expandedRows() {
308
+ return [...this.expandedIds];
309
+ }
96
310
  get columns() {
97
311
  return this.parseJson("columns", []);
98
312
  }
@@ -139,12 +353,35 @@ export class Table extends BaseElement {
139
353
  return fallback;
140
354
  }
141
355
  }
142
- cellText(row, column, index) {
356
+ cellValue(row, column, index) {
143
357
  if (Array.isArray(row.cells)) {
144
358
  return row.cells[index] ?? "";
145
359
  }
146
360
  return row.cells[column.key] ?? "";
147
361
  }
362
+ /**
363
+ * Descriptor rendering. Everything is escaped; a link with a non-http(s)
364
+ * href renders as plain text; a badge's tone colours a word that already
365
+ * carries the meaning.
366
+ */
367
+ cellMarkup(value) {
368
+ if (!isCellDescriptor(value)) {
369
+ return escapeHtml(typeof value === "string" ? value : "");
370
+ }
371
+ if (value.kind === "badge") {
372
+ const tone = value.tone && CELL_TONES.has(value.tone) ? value.tone : "neutral";
373
+ const toneVar = tone === "neutral"
374
+ ? "var(--boe-token-text-text-secondary, #6f6f6f)"
375
+ : tone === "brand"
376
+ ? "var(--boe-token-surface-surface-brand, #0061d5)"
377
+ : `var(--boe-token-surface-status-surface-${tone}, #6f6f6f)`;
378
+ return `<span part="cell-badge" style="--cell-tone:${toneVar};">${escapeHtml(value.text)}</span>`;
379
+ }
380
+ if (value.kind === "link" && value.href && isSafeHref(value.href)) {
381
+ return `<a part="cell-link" href="${escapeHtml(value.href)}">${escapeHtml(value.text)}</a>`;
382
+ }
383
+ return escapeHtml(value.text);
384
+ }
148
385
  renderTemplate() {
149
386
  if (!this.shadowRoot)
150
387
  return;
@@ -168,6 +405,11 @@ export class Table extends BaseElement {
168
405
  this.toggleSort(th.dataset.key ?? "");
169
406
  });
170
407
  this.bodyEl.addEventListener("click", event => {
408
+ const expander = event.target.closest('[part="expander"]');
409
+ if (expander) {
410
+ this.toggleRowDetail(expander.dataset.rowId ?? "");
411
+ return; // expanding is not selecting
412
+ }
171
413
  const row = event.target.closest('[part="row"]');
172
414
  if (!row)
173
415
  return;
@@ -179,7 +421,33 @@ export class Table extends BaseElement {
179
421
  rowElements() {
180
422
  return Array.from(this.bodyEl.querySelectorAll('[part="row"]'));
181
423
  }
424
+ toggleRowDetail(rowId) {
425
+ if (!rowId)
426
+ return;
427
+ const expanded = !this.expandedIds.has(rowId);
428
+ if (expanded) {
429
+ this.expandedIds.add(rowId);
430
+ }
431
+ else {
432
+ this.expandedIds.delete(rowId);
433
+ }
434
+ this.dispatchEvent(new CustomEvent("row-toggled", {
435
+ bubbles: true,
436
+ composed: true,
437
+ detail: { rowId, expanded },
438
+ }));
439
+ this.update();
440
+ // update() rewrote the body, destroying the button that was activated;
441
+ // put focus on its replacement so a keyboard user is not dropped.
442
+ this.bodyEl
443
+ .querySelector(`[part="expander"][data-row-id="${cssAttrValue(rowId)}"]`)
444
+ ?.focus();
445
+ }
182
446
  onBodyKeydown(event) {
447
+ // Keys aimed at the expander button (Enter/Space activate it natively)
448
+ // must not double as row selection.
449
+ if (event.target.closest('[part="expander"]'))
450
+ return;
183
451
  const rows = this.rowElements();
184
452
  if (rows.length === 0)
185
453
  return;
@@ -226,7 +494,11 @@ export class Table extends BaseElement {
226
494
  const currentKey = this.getAttribute("sort-key");
227
495
  const currentDir = this.getAttribute("sort-direction");
228
496
  const direction = currentKey === key && currentDir === "ascending" ? "descending" : "ascending";
229
- this.dispatchEvent(new CustomEvent("sort", { bubbles: true, composed: true, detail: { key, direction } }));
497
+ this.dispatchEvent(new CustomEvent("sort", {
498
+ bubbles: true,
499
+ composed: true,
500
+ detail: { key, direction },
501
+ }));
230
502
  }
231
503
  activateRow(index, options = {}) {
232
504
  const rows = this.rows;
@@ -284,32 +556,79 @@ export class Table extends BaseElement {
284
556
  const selectable = this.selectionMode !== "none";
285
557
  const sortKey = this.getAttribute("sort-key");
286
558
  const sortDir = this.getAttribute("sort-direction");
287
- this.shadowRoot?.querySelector('[part="table"]')?.setAttribute("aria-label", this.label);
288
- this.shadowRoot?.querySelector('[part="table"]')?.setAttribute("role", selectable ? "grid" : "table");
289
- this.headEl.innerHTML = columns
290
- .map(column => {
291
- const align = column.align ? ` data-align="${column.align}"` : "";
292
- if (column.sortable) {
293
- const sort = sortKey === column.key ? ` aria-sort="${sortDir === "descending" ? "descending" : "ascending"}"` : ' aria-sort="none"';
294
- return `<th part="sortable" scope="col" data-key="${escapeHtml(column.key)}"${align}${sort}>${escapeHtml(column.label)}<span class="boe-sort-arrow" aria-hidden="true"></span></th>`;
295
- }
296
- return `<th scope="col"${align}>${escapeHtml(column.label)}</th>`;
297
- })
298
- .join("");
559
+ // A non-string detail is malformed input, not a crash: it gets no expander.
560
+ const expandable = rows.some(row => typeof row.detail === "string");
561
+ const columnCount = columns.length + (expandable ? 1 : 0);
562
+ const tableEl = this.shadowRoot?.querySelector('[part="table"]');
563
+ tableEl?.setAttribute("aria-label", this.label);
564
+ tableEl?.setAttribute("role", selectable ? "grid" : "table");
565
+ if (this.loading) {
566
+ tableEl?.setAttribute("aria-busy", "true");
567
+ }
568
+ else {
569
+ tableEl?.removeAttribute("aria-busy");
570
+ }
571
+ this.headEl.innerHTML =
572
+ (expandable ? `<th scope="col" aria-label="Row details"></th>` : "") +
573
+ columns
574
+ .map(column => {
575
+ const align = alignAttr(column.align);
576
+ if (column.sortable) {
577
+ const sorted = sortKey === column.key;
578
+ const sort = sorted
579
+ ? ` aria-sort="${sortDir === "descending" ? "descending" : "ascending"}"`
580
+ : ' aria-sort="none"';
581
+ // The state, in words as well as the arrow glyph.
582
+ const stateLabel = sorted
583
+ ? `, sorted ${sortDir === "descending" ? "descending" : "ascending"}`
584
+ : ", not sorted";
585
+ return `<th part="sortable" scope="col" data-key="${escapeHtml(column.key)}"${align}${sort}><button type="button" part="sort-button" aria-label="Sort by ${escapeHtml(column.label)}${stateLabel}">${escapeHtml(column.label)}<span class="boe-sort-arrow" aria-hidden="true"></span></button></th>`;
586
+ }
587
+ return `<th scope="col"${align}>${escapeHtml(column.label)}</th>`;
588
+ })
589
+ .join("");
590
+ // Loading, error, and empty are stated in words in the body — a blank
591
+ // grid reads as broken. Loading wins; a stale error must not outlive a
592
+ // retry that is visibly in flight.
593
+ if (this.loading) {
594
+ this.bodyEl.innerHTML = `<tr part="state-row" data-state="loading" role="row"><td role="${selectable ? "gridcell" : "cell"}" colspan="${String(columnCount)}"><span part="state-spinner" aria-hidden="true"></span>Loading rows…</td></tr>`;
595
+ return;
596
+ }
597
+ if (this.errorText) {
598
+ this.bodyEl.innerHTML = `<tr part="state-row" data-state="error" role="row"><td role="${selectable ? "gridcell" : "cell"}" colspan="${String(columnCount)}">${escapeHtml(this.errorText)}</td></tr>`;
599
+ return;
600
+ }
601
+ if (rows.length === 0) {
602
+ this.bodyEl.innerHTML = `<tr part="state-row" data-state="empty" role="row"><td role="${selectable ? "gridcell" : "cell"}" colspan="${String(columnCount)}">${escapeHtml(this.emptyText)}</td></tr>`;
603
+ return;
604
+ }
299
605
  this.bodyEl.innerHTML = rows
300
606
  .map((row, index) => {
301
607
  const selected = this.selected.has(row.id);
608
+ const cellRole = selectable ? ' role="gridcell"' : ' role="cell"';
302
609
  const rowAttrs = selectable
303
610
  ? ` part="row" role="row" tabindex="${index === 0 ? "0" : "-1"}" aria-selected="${selected}"`
304
611
  : ' part="row" role="row"';
612
+ const expanded = this.expandedIds.has(row.id);
613
+ const expanderCell = expandable
614
+ ? `<td${cellRole} part="expander-cell">${typeof row.detail === "string"
615
+ ? `<button type="button" part="expander" data-row-id="${escapeHtml(row.id)}" aria-expanded="${String(expanded)}" aria-label="${expanded ? "Hide" : "Show"} details for row ${escapeHtml(row.id)}"></button>`
616
+ : ""}</td>`
617
+ : "";
305
618
  const cells = columns
306
- .map(column => {
307
- const align = column.align ? ` data-align="${column.align}"` : "";
308
- const role = selectable ? ' role="gridcell"' : ' role="cell"';
309
- return `<td${align}${role}>${escapeHtml(this.cellText(row, column, columns.indexOf(column)))}</td>`;
619
+ .map((column, columnIndex) => {
620
+ const align = alignAttr(column.align);
621
+ const value = this.cellValue(row, column, columnIndex);
622
+ return `<td${align}${cellRole} data-label="${escapeHtml(column.label)}">${this.cellMarkup(value)}</td>`;
310
623
  })
311
624
  .join("");
312
- return `<tr${rowAttrs} data-index="${index}" data-id="${escapeHtml(row.id)}">${cells}</tr>`;
625
+ const rowMarkup = `<tr${rowAttrs} data-index="${index}" data-id="${escapeHtml(row.id)}">${expanderCell}${cells}</tr>`;
626
+ // The detail row renders only while expanded, keeping the roving
627
+ // tabindex and shift-range math on visible [part="row"] rows only.
628
+ const detailRow = typeof row.detail === "string" && expanded
629
+ ? `<tr part="detail-row" role="row" data-for="${escapeHtml(row.id)}"><td${cellRole} colspan="${String(columnCount)}">${escapeHtml(row.detail)}<slot name="detail-${escapeHtml(row.id)}"></slot></td></tr>`
630
+ : "";
631
+ return rowMarkup + detailRow;
313
632
  })
314
633
  .join("");
315
634
  }
@@ -0,0 +1,38 @@
1
+ import type { DueBucket } from "./due-types.js";
2
+ import { BaseElement } from "../../core/index.js";
3
+ /**
4
+ * SLA / aging urgency for a due date — the badge the work-queue rows and
5
+ * record headers were specified to share.
6
+ *
7
+ * The label states the urgency in words ("Overdue by 3 days"), so colour is
8
+ * never the only signal, and the day count answers the question a due badge
9
+ * exists for: *how late is this?* A bare date makes the reader do that
10
+ * arithmetic themselves.
11
+ *
12
+ * `reference-time` pins "now" so rendering is deterministic — the same
13
+ * inputs always produce the same badge, in tests and in screenshots.
14
+ */
15
+ export declare class DueBadge extends BaseElement {
16
+ static readonly tagName: string;
17
+ static get observedAttributes(): string[];
18
+ private badgeEl;
19
+ private textEl;
20
+ /** ISO timestamp the badge describes. Absent renders the no-due-date state. */
21
+ get dueAt(): string;
22
+ set dueAt(value: string);
23
+ /** Overrides the derived wording; the bucket still drives the tone. */
24
+ get label(): string;
25
+ set label(value: string);
26
+ get compact(): boolean;
27
+ set compact(value: boolean);
28
+ get referenceTime(): string;
29
+ set referenceTime(value: string);
30
+ private now;
31
+ /** The urgency bucket currently rendered. */
32
+ get bucket(): DueBucket;
33
+ /** The rendered wording, derived unless `label` overrides it. */
34
+ get resolvedLabel(): string;
35
+ protected renderTemplate(): void;
36
+ protected setupListeners(): void;
37
+ protected update(): void;
38
+ }