@ahrowe/ui 0.14.1 → 0.15.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/dist/esm/common/errorBoundary/errorBoundary.mjs +4 -0
- package/dist/esm/common/errorBoundary/errorBoundary.mjs.map +1 -0
- package/dist/esm/common/errorBoundary/errorBoundary.module.mjs +2 -0
- package/dist/esm/common/errorBoundary/errorBoundary.module.mjs.map +1 -0
- package/dist/esm/common/kanbanBoard/KanbanBoard.mjs +2 -2
- package/dist/esm/common/kanbanBoard/KanbanBoard.mjs.map +1 -1
- package/dist/esm/common/kanbanBoard/KanbanColumn.mjs +1 -1
- package/dist/esm/common/kanbanBoard/KanbanColumn.mjs.map +1 -1
- package/dist/esm/common/kanbanBoard/kanbanBoard.module.mjs +1 -1
- package/dist/esm/common/kanbanBoard/kanbanBoard.module.mjs.map +1 -1
- package/dist/esm/common/timeline/timeline.mjs +2 -0
- package/dist/esm/common/timeline/timeline.mjs.map +1 -0
- package/dist/esm/common/timeline/timeline.module.mjs +2 -0
- package/dist/esm/common/timeline/timeline.module.mjs.map +1 -0
- package/dist/esm/common/timeline/timeline.types.mjs +2 -0
- package/dist/esm/common/timeline/timeline.types.mjs.map +1 -0
- package/dist/esm/index.mjs +1 -1
- package/dist/index.cjs +7 -5
- package/dist/index.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/package/common/errorBoundary/errorBoundary.d.ts +14 -0
- package/dist/types/package/common/errorBoundary/errorBoundary.types.d.ts +57 -0
- package/dist/types/package/common/errorBoundary/index.d.ts +2 -0
- package/dist/types/package/common/kanbanBoard/KanbanBoard.d.ts +1 -1
- package/dist/types/package/common/kanbanBoard/KanbanColumn.d.ts +9 -1
- package/dist/types/package/common/kanbanBoard/kanbanBoard.types.d.ts +29 -1
- package/dist/types/package/common/timeline/index.d.ts +2 -0
- package/dist/types/package/common/timeline/timeline.d.ts +2 -0
- package/dist/types/package/common/timeline/timeline.types.d.ts +51 -0
- package/dist/types/package/index.d.ts +4 -0
- package/docs/CLAUDE.md +2 -0
- package/docs/ErrorBoundary.md +93 -0
- package/docs/KanbanBoard.md +37 -1
- package/docs/Timeline.md +127 -0
- package/package.json +1 -1
|
@@ -16,6 +16,13 @@ export interface KanbanColumnDef {
|
|
|
16
16
|
* Use for a workflow rule like "cards in Done can't be moved back out."
|
|
17
17
|
*/
|
|
18
18
|
disabled?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Whether more items exist for this column beyond what's currently in `itemIds` — enables the
|
|
21
|
+
* scroll-triggered `onLoadMore` request. Only meaningful alongside `KanbanBoardProps.onLoadMore`;
|
|
22
|
+
* a column without it never asks for more regardless of this flag. Set `false` (or omit) once
|
|
23
|
+
* the column's source is exhausted to stop asking.
|
|
24
|
+
*/
|
|
25
|
+
hasMore?: boolean;
|
|
19
26
|
}
|
|
20
27
|
export type KanbanBoardSlots = 'dragOverlay';
|
|
21
28
|
export interface KanbanBoardProps<T = unknown> extends HtmlProps {
|
|
@@ -70,12 +77,33 @@ export interface KanbanBoardProps<T = unknown> extends HtmlProps {
|
|
|
70
77
|
* via `ThemeProvider`'s `variables`, or per board via `style`.
|
|
71
78
|
*/
|
|
72
79
|
collapseEmptyColumns?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Bounds each column's height (any CSS length, e.g. `'70vh'`, `'600px'`) and makes its card
|
|
82
|
+
* list scroll independently once content overflows, instead of growing the whole page. Leave
|
|
83
|
+
* unset for the default unbounded height. Required for `onLoadMore` to ever trigger, since an
|
|
84
|
+
* unbounded column never scrolls.
|
|
85
|
+
*/
|
|
86
|
+
columnMaxHeight?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Called with a column's `id` when that column has `hasMore` set and its card list is scrolled
|
|
89
|
+
* within `loadMoreThreshold` of the bottom — fetch and append the next page of items for it.
|
|
90
|
+
* `KanbanColumn` tracks its own in-flight state per column (shown as a spinner at the bottom of
|
|
91
|
+
* the list), so overlapping calls for the same column are never made concurrently. Also
|
|
92
|
+
* retried automatically if a loaded batch doesn't fill the column's visible height, since a
|
|
93
|
+
* column shorter than its own viewport never generates a scroll event to trigger a next page.
|
|
94
|
+
*/
|
|
95
|
+
onLoadMore?: (columnId: string) => void | Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Distance in px from the bottom of a column's scroll area that triggers `onLoadMore` (default
|
|
98
|
+
* `100`, matching `VirtualList`'s `loadMoreThreshold`).
|
|
99
|
+
*/
|
|
100
|
+
loadMoreThreshold?: number;
|
|
73
101
|
className?: string;
|
|
74
102
|
style?: CSSProperties;
|
|
75
103
|
classNames?: SlotClassNames<KanbanBoardSlots>;
|
|
76
104
|
styles?: SlotStyles<KanbanBoardSlots>;
|
|
77
105
|
}
|
|
78
|
-
export type KanbanColumnSlots = 'header' | 'items' | 'count' | 'placeholder' | 'actions';
|
|
106
|
+
export type KanbanColumnSlots = 'header' | 'items' | 'count' | 'placeholder' | 'actions' | 'loadingIndicator';
|
|
79
107
|
export interface KanbanColumnProps extends HtmlProps {
|
|
80
108
|
id: string;
|
|
81
109
|
title: string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
|
|
3
|
+
import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
|
|
4
|
+
/** Layout of entries relative to the connecting line. */
|
|
5
|
+
export declare enum TimelineAlignment {
|
|
6
|
+
/** Line and markers on the left, content to the right (the default). */
|
|
7
|
+
Left = "left",
|
|
8
|
+
/** Line and markers on the right, content to the left. */
|
|
9
|
+
Right = "right",
|
|
10
|
+
/** Line down the center, entries alternating left/right. */
|
|
11
|
+
Alternate = "alternate"
|
|
12
|
+
}
|
|
13
|
+
/** Semantic colour for an entry's marker, matching the theme's status colours. */
|
|
14
|
+
export declare enum TimelineItemStatus {
|
|
15
|
+
Default = "default",
|
|
16
|
+
Primary = "primary",
|
|
17
|
+
Success = "success",
|
|
18
|
+
Warn = "warn",
|
|
19
|
+
Error = "error"
|
|
20
|
+
}
|
|
21
|
+
export type TimelineSlots = 'root' | 'item' | 'marker' | 'dot' | 'connector' | 'content' | 'title' | 'description' | 'timestamp';
|
|
22
|
+
export interface TimelineItem {
|
|
23
|
+
/** Stable identity for the entry, used as its React key. Falls back to its array index when omitted; pass one whenever `items` can be reordered or have entries prepended (e.g. a live "newest first" audit log), so React doesn't misattribute row state across the shift. */
|
|
24
|
+
id?: string | number;
|
|
25
|
+
/** Primary label for the entry. */
|
|
26
|
+
title: React.ReactNode;
|
|
27
|
+
/** Secondary line under the title. */
|
|
28
|
+
description?: React.ReactNode;
|
|
29
|
+
/** Rendered next to the title, e.g. a date or relative time ("2 hours ago"). */
|
|
30
|
+
timestamp?: React.ReactNode;
|
|
31
|
+
/** FontAwesome icon shown in the marker instead of a plain dot. */
|
|
32
|
+
icon?: IconDefinition;
|
|
33
|
+
/** Semantic marker colour (default `Default`). */
|
|
34
|
+
status?: TimelineItemStatus;
|
|
35
|
+
/** Extra content rendered under the description, e.g. a diff, an attachment, an actor avatar. */
|
|
36
|
+
content?: React.ReactNode;
|
|
37
|
+
/** Makes the whole entry clickable (e.g. to open the full record), rendering it as an accessible button. */
|
|
38
|
+
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
39
|
+
/** Disables the entry: never clickable, dimmed. Only relevant alongside `onClick`. */
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface TimelineProps extends HtmlProps {
|
|
43
|
+
/** The entries to render, in display order (typically newest first for an audit log). */
|
|
44
|
+
items?: TimelineItem[];
|
|
45
|
+
/** Layout of entries relative to the connecting line (default `Left`). */
|
|
46
|
+
alignment?: TimelineAlignment;
|
|
47
|
+
className?: string;
|
|
48
|
+
style?: React.CSSProperties;
|
|
49
|
+
classNames?: SlotClassNames<TimelineSlots>;
|
|
50
|
+
styles?: SlotStyles<TimelineSlots>;
|
|
51
|
+
}
|
|
@@ -47,6 +47,8 @@ export { default as Dropdown } from './common/dropdown';
|
|
|
47
47
|
export * from './common/dropdown';
|
|
48
48
|
export { default as EmptyState } from './common/emptyState';
|
|
49
49
|
export * from './common/emptyState';
|
|
50
|
+
export { default as ErrorBoundary } from './common/errorBoundary';
|
|
51
|
+
export * from './common/errorBoundary';
|
|
50
52
|
export { default as Fab } from './common/fab';
|
|
51
53
|
export * from './common/fab';
|
|
52
54
|
export { default as FloatingMenu } from './common/floatingMenu';
|
|
@@ -117,6 +119,8 @@ export * from './common/themeProvider';
|
|
|
117
119
|
export { useTheme } from './common/themeProvider/useTheme';
|
|
118
120
|
export { default as TimeInput } from './common/timeInput';
|
|
119
121
|
export * from './common/timeInput';
|
|
122
|
+
export { default as Timeline } from './common/timeline';
|
|
123
|
+
export * from './common/timeline';
|
|
120
124
|
export { default as ToastProvider } from './common/toast';
|
|
121
125
|
export * from './common/toast';
|
|
122
126
|
export { default as Tooltip } from './common/tooltip';
|
package/docs/CLAUDE.md
CHANGED
|
@@ -117,6 +117,7 @@ Slot keys per component are documented in each component's doc file below.
|
|
|
117
117
|
@Dropdown.md
|
|
118
118
|
@DropZone.md
|
|
119
119
|
@EmptyState.md
|
|
120
|
+
@ErrorBoundary.md
|
|
120
121
|
@Fab.md
|
|
121
122
|
@FloatingMenu.md
|
|
122
123
|
@FormValidator.md
|
|
@@ -154,6 +155,7 @@ Slot keys per component are documented in each component's doc file below.
|
|
|
154
155
|
@Textarea.md
|
|
155
156
|
@ThemeProvider.md
|
|
156
157
|
@TimeInput.md
|
|
158
|
+
@Timeline.md
|
|
157
159
|
@Toast.md
|
|
158
160
|
@Tooltip.md
|
|
159
161
|
@VirtualList.md
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# ErrorBoundary
|
|
2
|
+
|
|
3
|
+
**When to use:** Wrap any subtree whose render errors shouldn't take down the rest of the app — a widget, a route, a third-party embed, a list row. It catches JavaScript errors thrown while rendering descendants (via React's `getDerivedStateFromError` / `componentDidCatch`) and swaps in a fallback UI instead of unmounting the whole tree. Only errors thrown during rendering, in lifecycle methods, or in constructors of the tree below it are caught — event handlers, async code, and errors in the boundary itself are not, same as any React error boundary.
|
|
4
|
+
|
|
5
|
+
**Import:** `import { ErrorBoundary } from '@ahrowe/ui'`
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { ErrorBoundary } from '@ahrowe/ui';
|
|
9
|
+
|
|
10
|
+
// Basic — default fallback with a "Try again" button
|
|
11
|
+
<ErrorBoundary>
|
|
12
|
+
<Widget />
|
|
13
|
+
</ErrorBoundary>
|
|
14
|
+
|
|
15
|
+
// Report to your error tracker, and re-arm whatever caused the crash on reset
|
|
16
|
+
<ErrorBoundary
|
|
17
|
+
onError={(error, errorInfo) => reportToSentry(error, errorInfo)}
|
|
18
|
+
onReset={() => setWidgetKey((k) => k + 1)}
|
|
19
|
+
>
|
|
20
|
+
<Widget />
|
|
21
|
+
</ErrorBoundary>
|
|
22
|
+
|
|
23
|
+
// Custom copy, no stack-trace section (e.g. for a small inline widget)
|
|
24
|
+
<ErrorBoundary
|
|
25
|
+
title="Widget crashed"
|
|
26
|
+
description="This panel failed to load. The rest of the page is unaffected."
|
|
27
|
+
hideStack
|
|
28
|
+
>
|
|
29
|
+
<Widget />
|
|
30
|
+
</ErrorBoundary>
|
|
31
|
+
|
|
32
|
+
// Fully custom fallback UI — render function receives the error and a reset callback
|
|
33
|
+
<ErrorBoundary
|
|
34
|
+
fallback={({ error, resetErrorBoundary }) => (
|
|
35
|
+
<div>
|
|
36
|
+
Something broke: {error.message}
|
|
37
|
+
<button onClick={resetErrorBoundary}>Retry</button>
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
<Widget />
|
|
42
|
+
</ErrorBoundary>
|
|
43
|
+
|
|
44
|
+
// Auto-reset when inputs the crash depends on change (e.g. a route or record id)
|
|
45
|
+
<ErrorBoundary resetKeys={[recordId]}>
|
|
46
|
+
<RecordDetail id={recordId} />
|
|
47
|
+
</ErrorBoundary>
|
|
48
|
+
|
|
49
|
+
// Translated strings for a non-English app — title/description are their own
|
|
50
|
+
// props (see above); labels covers everything else the built-in fallback renders
|
|
51
|
+
<ErrorBoundary
|
|
52
|
+
title="Etwas ist schiefgelaufen"
|
|
53
|
+
labels={{
|
|
54
|
+
stackTrace: 'Stapelverfolgung',
|
|
55
|
+
copy: 'Kopieren',
|
|
56
|
+
copied: 'Kopiert',
|
|
57
|
+
reset: 'Erneut versuchen',
|
|
58
|
+
copySuccessToast: 'Stapelverfolgung kopiert',
|
|
59
|
+
copyErrorToast: 'Kopieren fehlgeschlagen',
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<Widget />
|
|
63
|
+
</ErrorBoundary>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The default fallback is a centered card with a tinted icon badge, an `Accordion` for the collapsible stack trace (with a labeled copy `Button` that fires a `Toast` confirmation if `ToastProvider` is mounted), and a `Button` for "Try again".
|
|
67
|
+
|
|
68
|
+
**Key props:**
|
|
69
|
+
|
|
70
|
+
| Prop | Type | Description |
|
|
71
|
+
|------|------|-------------|
|
|
72
|
+
| `children` | `ReactNode` | The subtree to protect |
|
|
73
|
+
| `title` | `ReactNode` | Heading in the default fallback (default `"Something went wrong"`) |
|
|
74
|
+
| `description` | `ReactNode` | Overrides the message shown under the title. Defaults to `error.message` |
|
|
75
|
+
| `labels` | `ErrorBoundaryLabels` | Overrides for every other built-in string (stack trace label, copy/reset button text, copy toasts) — pass a translated set for non-English apps. See below |
|
|
76
|
+
| `fallback` | `ReactNode \| (props: ErrorBoundaryFallbackProps) => ReactNode` | Replaces the built-in fallback entirely. The render-function form receives `{ error, errorInfo, resetErrorBoundary }` |
|
|
77
|
+
| `onError` | `(error: Error, errorInfo: ErrorInfo) => void` | Called once per catch — the place to report to Sentry/Datadog/etc. |
|
|
78
|
+
| `onReset` | `() => void` | Called after the boundary is reset, whether via the built-in "Try again" button, a custom fallback calling `resetErrorBoundary`, or a `resetKeys` change. Use it to re-arm whatever state caused the crash |
|
|
79
|
+
| `resetKeys` | `unknown[]` | When any value in this array changes while the boundary is showing an error, it resets automatically — mirrors the common `react-error-boundary` pattern |
|
|
80
|
+
| `hideStack` | `boolean` | Hide the collapsible stack-trace section. Default `false` |
|
|
81
|
+
|
|
82
|
+
**`ErrorBoundaryLabels`:**
|
|
83
|
+
|
|
84
|
+
| Field | Default | Description |
|
|
85
|
+
|-------|---------|--------------|
|
|
86
|
+
| `stackTrace` | `"Stack trace"` | Accordion header for the stack trace section |
|
|
87
|
+
| `copy` | `"Copy"` | Copy button label before copying |
|
|
88
|
+
| `copied` | `"Copied"` | Copy button label right after a successful copy |
|
|
89
|
+
| `reset` | `"Try again"` | Reset button label |
|
|
90
|
+
| `copySuccessToast` | `"Stack trace copied to clipboard"` | Toast shown after a successful copy (only visible if `ToastProvider` is mounted) |
|
|
91
|
+
| `copyErrorToast` | `"Could not copy stack trace"` | Toast shown if the copy fails |
|
|
92
|
+
|
|
93
|
+
**Slots:** `root` `header` `badge` `icon` `title` `description` `stack` `stackHeader` `stackLabel` `stackTrace` `copyButton` `actions` `resetButton`
|
package/docs/KanbanBoard.md
CHANGED
|
@@ -135,6 +135,38 @@ const columnsWithLock: KanbanColumnDef[] = [
|
|
|
135
135
|
collapseEmptyColumns={false}
|
|
136
136
|
onChange={(updatedColumns) => setColumns(updatedColumns)}
|
|
137
137
|
/>
|
|
138
|
+
|
|
139
|
+
// Bound each column's height and let its card list scroll independently, instead
|
|
140
|
+
// of growing the whole page — useful once a column can hold a lot of cards
|
|
141
|
+
<KanbanBoard
|
|
142
|
+
columns={columns}
|
|
143
|
+
items={items}
|
|
144
|
+
renderItem={(task) => <div className="task-card"><strong>{task.title}</strong></div>}
|
|
145
|
+
columnMaxHeight="70vh"
|
|
146
|
+
onChange={(updatedColumns) => setColumns(updatedColumns)}
|
|
147
|
+
/>
|
|
148
|
+
|
|
149
|
+
// Paginate a column instead of loading everything up front — mark it `hasMore`
|
|
150
|
+
// and fetch/append the next page in `onLoadMore` once it's scrolled near the
|
|
151
|
+
// bottom. Requires `columnMaxHeight`, since an unbounded column never scrolls.
|
|
152
|
+
// KanbanBoard tracks its own in-flight state per column (shown as a spinner at
|
|
153
|
+
// the bottom of the list) and retries automatically if a page doesn't fill the
|
|
154
|
+
// column's visible height, so pagination doesn't dead-end on a short first batch.
|
|
155
|
+
const columnsPaged: KanbanColumnDef[] = [
|
|
156
|
+
{ id: 'done', title: 'Done', itemIds: doneTaskIds, hasMore: doneHasMore },
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
<KanbanBoard
|
|
160
|
+
columns={columnsPaged}
|
|
161
|
+
items={items}
|
|
162
|
+
renderItem={(task) => <div className="task-card"><strong>{task.title}</strong></div>}
|
|
163
|
+
columnMaxHeight="70vh"
|
|
164
|
+
onLoadMore={async (columnId) => {
|
|
165
|
+
const nextPage = await fetchNextPage(columnId);
|
|
166
|
+
appendItemsToColumn(columnId, nextPage); // your own state update
|
|
167
|
+
}}
|
|
168
|
+
onChange={(updatedColumns) => setColumns(updatedColumns)}
|
|
169
|
+
/>
|
|
138
170
|
```
|
|
139
171
|
|
|
140
172
|
**Keyboard support:** cards are focusable and can be picked up and moved with the keyboard — Space or Enter to pick up, Space or Enter again to drop, Escape to cancel (reverts to the original position). Arrow keys move the card: Up/Down reorders within its current column; Left/Right moves it into the adjacent column, landing on the nearest card there (or the column itself, if it's empty). This uses a board-aware coordinate getter rather than `dnd-kit`'s generic `sortableKeyboardCoordinates`, which — across multiple per-column `SortableContext`s — can't reliably tell "next card in this column" from "some other column's droppable" and ends up mixing up Up/Down and Left/Right. With `reorderableColumns`, a column's header is focusable the same way — Space/Enter to pick it up, Left/Right to move it, Up/Down does nothing (columns are a single row).
|
|
@@ -158,6 +190,9 @@ const columnsWithLock: KanbanColumnDef[] = [
|
|
|
158
190
|
| `renderColumnActions` | `(column: KanbanColumnDef) => ReactNode` | Custom actions rendered in a column's header, next to its title (e.g. an "add card" button, a column menu) |
|
|
159
191
|
| `reorderableColumns` | `boolean` | Allow whole columns to be reordered by dragging their header, alongside card dragging (default `false`) |
|
|
160
192
|
| `collapseEmptyColumns` | `boolean` | Shrink a column to a narrow strip once it has no cards (default `true`). Set `false` to keep every column the same width regardless of card count. The collapsed width is set via `--kanban-empty-column-width` (falls back to `140px`) — override it theme-wide via `ThemeProvider`, or per board via `style` |
|
|
193
|
+
| `columnMaxHeight` | `string` | Bounds each column's height (any CSS length, e.g. `'70vh'`) and makes its card list scroll independently once content overflows, instead of growing the whole page. Unset by default (unbounded height). Required for `onLoadMore` to ever trigger |
|
|
194
|
+
| `onLoadMore` | `(columnId: string) => void \| Promise<void>` | Called when a column with `hasMore` is scrolled within `loadMoreThreshold` of the bottom — fetch and append the next page for that column. Tracked per column, so overlapping calls for the same column are never made concurrently; retried automatically if a loaded batch doesn't fill the column's own visible height |
|
|
195
|
+
| `loadMoreThreshold` | `number` | Distance in px from the bottom of a column's scroll area that triggers `onLoadMore` (default `100`, matching `VirtualList`) |
|
|
161
196
|
| `id` / `role` / `aria-*` / `data-*` | | Passed through to the root element |
|
|
162
197
|
|
|
163
198
|
`KanbanColumn`'s own props accept the same `id` / `role` / `aria-*` / `data-*` pass-through on its root element.
|
|
@@ -171,10 +206,11 @@ const columnsWithLock: KanbanColumnDef[] = [
|
|
|
171
206
|
itemIds: string[];
|
|
172
207
|
maxItems?: number; // work-in-progress limit — display-only, see above
|
|
173
208
|
disabled?: boolean; // freezes the column — cards inside can't be dragged, nothing can drop in
|
|
209
|
+
hasMore?: boolean; // more items exist beyond itemIds — enables onLoadMore, see above
|
|
174
210
|
}
|
|
175
211
|
```
|
|
176
212
|
|
|
177
213
|
**Note on item ids:** don't give a card an id starting with `column-` — the board uses that prefix internally to identify a column's own droppable (the empty space above/below its cards) versus a specific card. A colliding card id is silently misread as a column reference, and drops involving it silently no-op instead of throwing, so the bug can be easy to miss.
|
|
178
214
|
|
|
179
215
|
**Slots (KanbanBoard):** `dragOverlay`
|
|
180
|
-
**Slots (KanbanColumn):** `header` `items` `count` (the `count/maxItems` badge, only rendered when `maxItems` is set) `placeholder` (the `renderEmptyColumn` wrapper, only rendered when the column has no cards and `renderEmptyColumn` is set) `actions` (the `renderColumnActions` wrapper, only rendered when `renderColumnActions` is set)
|
|
216
|
+
**Slots (KanbanColumn):** `header` `items` `count` (the `count/maxItems` badge, only rendered when `maxItems` is set) `placeholder` (the `renderEmptyColumn` wrapper, only rendered when the column has no cards and `renderEmptyColumn` is set) `actions` (the `renderColumnActions` wrapper, only rendered when `renderColumnActions` is set) `loadingIndicator` (the spinner shown while an `onLoadMore` request for that column is in flight)
|
package/docs/Timeline.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Timeline
|
|
2
|
+
|
|
3
|
+
**When to use:** A vertical list of dated events, connected by a line, for audit logs, activity feeds, order status history, and deploy logs. Each entry can carry a title, timestamp, description, an icon or status colour, and arbitrary extra content.
|
|
4
|
+
|
|
5
|
+
**Import:** `import { Timeline, TimelineAlignment, TimelineItemStatus } from '@ahrowe/ui'`
|
|
6
|
+
**Types:** `import type { TimelineItem, TimelineProps } from '@ahrowe/ui'`
|
|
7
|
+
|
|
8
|
+
**Enums:**
|
|
9
|
+
- `TimelineAlignment`: `Left` (default) | `Right` | `Alternate`
|
|
10
|
+
- `TimelineItemStatus`: `Default` (default) | `Primary` | `Success` | `Warn` | `Error`
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { Timeline, TimelineItemStatus } from '@ahrowe/ui';
|
|
14
|
+
import type { TimelineItem } from '@ahrowe/ui';
|
|
15
|
+
|
|
16
|
+
const items: TimelineItem[] = [
|
|
17
|
+
{ title: 'Invoice created', description: 'by Jane Doe', timestamp: '09:12' },
|
|
18
|
+
{ title: 'Invoice sent to client', description: 'via email', timestamp: '09:15' },
|
|
19
|
+
{ title: 'Payment received', status: TimelineItemStatus.Success, timestamp: '2 days later' },
|
|
20
|
+
{ title: 'Invoice closed', status: TimelineItemStatus.Success, timestamp: 'Today' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Default: left-aligned line and markers, content to the right
|
|
24
|
+
<Timeline items={items} />
|
|
25
|
+
|
|
26
|
+
// Icon markers instead of plain dots
|
|
27
|
+
import { faUserPlus, faCheck } from '@fortawesome/free-solid-svg-icons';
|
|
28
|
+
<Timeline
|
|
29
|
+
items={[
|
|
30
|
+
{ title: 'Account created', icon: faUserPlus, timestamp: 'Mon' },
|
|
31
|
+
{ title: 'Email verified', icon: faCheck, status: TimelineItemStatus.Success, timestamp: 'Wed' },
|
|
32
|
+
]}
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
// Right-aligned: line and markers on the right, content on the left
|
|
36
|
+
import { TimelineAlignment } from '@ahrowe/ui';
|
|
37
|
+
<Timeline items={items} alignment={TimelineAlignment.Right} />
|
|
38
|
+
|
|
39
|
+
// Alternate: entries alternate either side of a centered line
|
|
40
|
+
<Timeline items={items} alignment={TimelineAlignment.Alternate} />
|
|
41
|
+
|
|
42
|
+
// Extra content per entry: a diff, an attachment, an actor avatar
|
|
43
|
+
<Timeline
|
|
44
|
+
items={[
|
|
45
|
+
{
|
|
46
|
+
title: 'Deploy #128 started',
|
|
47
|
+
description: 'main @ a93d12d',
|
|
48
|
+
timestamp: '14:02',
|
|
49
|
+
content: <p>Triggered by Jon Snow.</p>,
|
|
50
|
+
},
|
|
51
|
+
]}
|
|
52
|
+
/>
|
|
53
|
+
|
|
54
|
+
// Clickable entries: e.g. open the full audit log record in a Modal.
|
|
55
|
+
// An item without onClick stays a plain, non-interactive row.
|
|
56
|
+
import { Modal } from '@ahrowe/ui';
|
|
57
|
+
|
|
58
|
+
function AuditLog() {
|
|
59
|
+
const [selected, setSelected] = useState<(typeof items)[number] | null>(null);
|
|
60
|
+
return (
|
|
61
|
+
<>
|
|
62
|
+
<Timeline
|
|
63
|
+
items={items.map((entry) => ({ ...entry, onClick: () => setSelected(entry) }))}
|
|
64
|
+
/>
|
|
65
|
+
<Modal isOpen={selected != null} title={selected?.title} onClose={() => setSelected(null)}>
|
|
66
|
+
{selected?.description}
|
|
67
|
+
</Modal>
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Lock an individual entry: dimmed, never clickable, even with onClick set
|
|
73
|
+
<Timeline
|
|
74
|
+
items={[
|
|
75
|
+
{ title: 'Archived entry', onClick: () => openDetail(), disabled: true },
|
|
76
|
+
]}
|
|
77
|
+
/>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**TimelineItem:**
|
|
81
|
+
|
|
82
|
+
| Field | Type | Description |
|
|
83
|
+
|-------|------|-------------|
|
|
84
|
+
| `id` | `string \| number` | Stable identity used as the entry's React key. Falls back to its array index when omitted; pass one whenever `items` can be reordered or have entries prepended (e.g. a live "newest first" audit log) |
|
|
85
|
+
| `title` | `ReactNode` | Primary label for the entry |
|
|
86
|
+
| `description` | `ReactNode` | Secondary line under the title |
|
|
87
|
+
| `timestamp` | `ReactNode` | Rendered next to the title, e.g. a date or relative time ("2 hours ago") |
|
|
88
|
+
| `icon` | `IconDefinition` | FontAwesome icon shown in the marker instead of a plain dot |
|
|
89
|
+
| `status` | `TimelineItemStatus` | Semantic marker colour (default `Default`) |
|
|
90
|
+
| `content` | `ReactNode` | Extra content rendered under the description, e.g. a diff, an attachment, an actor avatar |
|
|
91
|
+
| `onClick` | `(event: MouseEvent<HTMLDivElement>) => void` | Makes the whole entry clickable, rendering it as an accessible button (`role="button"`, focusable, click and Enter both fire it). Omit to render a plain, non-interactive row |
|
|
92
|
+
| `disabled` | `boolean` | Disables the entry: never clickable, dimmed. Only relevant alongside `onClick` |
|
|
93
|
+
|
|
94
|
+
**Key props:**
|
|
95
|
+
|
|
96
|
+
| Prop | Type | Description |
|
|
97
|
+
|------|------|-------------|
|
|
98
|
+
| `items` | `TimelineItem[]` | The entries to render, in display order (typically newest first for an audit log) |
|
|
99
|
+
| `alignment` | `TimelineAlignment` | Layout of entries relative to the connecting line (default `Left`) |
|
|
100
|
+
|
|
101
|
+
**Theming:** these are theme variables (typed on `ThemeVariables`). Set them theme-wide via `ThemeProvider`'s `variables`, or per instance via `style`, without fighting specificity. Each falls back to a built-in default when unset:
|
|
102
|
+
|
|
103
|
+
| Variable | Falls back to |
|
|
104
|
+
|----------|---------------|
|
|
105
|
+
| `--timeline-dot-size` | `14px` |
|
|
106
|
+
| `--timeline-dot-color` | `var(--primary-color)` |
|
|
107
|
+
| `--timeline-dot-border` | `var(--background)` |
|
|
108
|
+
| `--timeline-line-color` | `var(--border-color)` |
|
|
109
|
+
| `--timeline-line-thickness` | `2px` |
|
|
110
|
+
| `--timeline-gap` | `16px` (gap between marker column and content) |
|
|
111
|
+
| `--timeline-item-gap` | `20px` (vertical gap between entries) |
|
|
112
|
+
| `--timeline-icon-dot-size` | `28px` (marker size when an entry has `icon`) |
|
|
113
|
+
|
|
114
|
+
`TimelineItemStatus` values other than `Default` resolve directly to the matching theme colour (`--primary-color`, `--success-color`, `--warn-color`, `--error-color`) rather than a dedicated `--timeline-*` variable, so they always match the rest of the app's semantic colours.
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
// Theme-wide, via ThemeProvider: thicker line, larger dots
|
|
118
|
+
const theme: Theme = {
|
|
119
|
+
id: 'brand',
|
|
120
|
+
variables: {
|
|
121
|
+
'--timeline-line-thickness': '3px',
|
|
122
|
+
'--timeline-dot-size': '18px',
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Slots:** `root` `item` `marker` `dot` `connector` `content` `title` `description` `timestamp`
|