@tribepad/themis 1.6.12 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/elements/Tabs/Tabs.d.ts +1 -1
- package/dist/elements/Tabs/Tabs.d.ts.map +1 -1
- package/dist/elements/Tabs/Tabs.types.d.ts +7 -0
- package/dist/elements/Tabs/Tabs.types.d.ts.map +1 -1
- package/dist/elements/Tabs/index.js +1 -1
- package/dist/elements/Tabs/index.js.map +1 -1
- package/dist/elements/Tabs/index.mjs +1 -1
- package/dist/elements/Tabs/index.mjs.map +1 -1
- package/dist/elements/Timeline/Timeline.d.ts +35 -0
- package/dist/elements/Timeline/Timeline.d.ts.map +1 -0
- package/dist/elements/Timeline/Timeline.styles.d.ts +41 -0
- package/dist/elements/Timeline/Timeline.styles.d.ts.map +1 -0
- package/dist/elements/Timeline/Timeline.types.d.ts +177 -0
- package/dist/elements/Timeline/Timeline.types.d.ts.map +1 -0
- package/dist/elements/Timeline/TimelineContext.d.ts +25 -0
- package/dist/elements/Timeline/TimelineContext.d.ts.map +1 -0
- package/dist/elements/Timeline/TimelineItem.d.ts +22 -0
- package/dist/elements/Timeline/TimelineItem.d.ts.map +1 -0
- package/dist/elements/Timeline/index.d.ts +14 -0
- package/dist/elements/Timeline/index.d.ts.map +1 -0
- package/dist/elements/Timeline/index.js +3 -0
- package/dist/elements/Timeline/index.js.map +1 -0
- package/dist/elements/Timeline/index.mjs +3 -0
- package/dist/elements/Timeline/index.mjs.map +1 -0
- package/dist/elements/index.d.ts +2 -0
- package/dist/elements/index.d.ts.map +1 -1
- package/dist/elements/index.js +1 -1
- package/dist/elements/index.js.map +1 -1
- package/dist/elements/index.mjs +1 -1
- package/dist/elements/index.mjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/elements/Tabs/Tabs.stories.tsx +63 -0
- package/src/elements/Timeline/Timeline.stories.tsx +244 -0
package/package.json
CHANGED
|
@@ -423,6 +423,69 @@ export const StepperVerticalNonLinear: Story = {
|
|
|
423
423
|
),
|
|
424
424
|
};
|
|
425
425
|
|
|
426
|
+
/**
|
|
427
|
+
* StepperWithError: a step flagged with `errorDescription` (here, a failed
|
|
428
|
+
* validation on the Shipping step). The errored circle turns destructive and
|
|
429
|
+
* swaps its number for an alert icon — taking visual precedence over the
|
|
430
|
+
* active/completed treatment — and the description appears as a tooltip on
|
|
431
|
+
* hover or keyboard focus of the tab. The tooltip is WCAG 2.2 AAA-compliant
|
|
432
|
+
* (React Aria: Escape-dismissible, hoverable, persistent) and the error state
|
|
433
|
+
* is also announced to screen readers via the tab's `aria-describedby`, so the
|
|
434
|
+
* information never relies on hover or colour alone.
|
|
435
|
+
*/
|
|
436
|
+
export const StepperWithError: Story = {
|
|
437
|
+
render: () => (
|
|
438
|
+
<Tabs variant="stepper" defaultSelectedKey="shipping">
|
|
439
|
+
<TabList aria-label="Checkout steps">
|
|
440
|
+
<Tab id="cart">Cart</Tab>
|
|
441
|
+
<Tab id="shipping" errorDescription="Postcode is invalid">Shipping</Tab>
|
|
442
|
+
<Tab id="payment">Payment</Tab>
|
|
443
|
+
</TabList>
|
|
444
|
+
<TabPanel id="cart">
|
|
445
|
+
<div className="p-4">Cart — completed.</div>
|
|
446
|
+
</TabPanel>
|
|
447
|
+
<TabPanel id="shipping">
|
|
448
|
+
<div className="p-4">Shipping — fix the highlighted error to continue.</div>
|
|
449
|
+
</TabPanel>
|
|
450
|
+
<TabPanel id="payment">
|
|
451
|
+
<div className="p-4">Payment — upcoming.</div>
|
|
452
|
+
</TabPanel>
|
|
453
|
+
</Tabs>
|
|
454
|
+
),
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* StepperVerticalWithError: the same error treatment in the vertical (Wizard)
|
|
459
|
+
* stepper — destructive circle, alert icon, and a tooltip above the step.
|
|
460
|
+
*/
|
|
461
|
+
export const StepperVerticalWithError: Story = {
|
|
462
|
+
render: () => (
|
|
463
|
+
<Tabs
|
|
464
|
+
variant="stepper"
|
|
465
|
+
orientation="vertical"
|
|
466
|
+
defaultSelectedKey="workflow"
|
|
467
|
+
className="w-[240px]"
|
|
468
|
+
>
|
|
469
|
+
<TabList aria-label="Edit blueprint steps">
|
|
470
|
+
<Tab id="details">Position Details</Tab>
|
|
471
|
+
<Tab id="workflow" errorDescription="At least one stage is required">
|
|
472
|
+
Candidate Workflow
|
|
473
|
+
</Tab>
|
|
474
|
+
<Tab id="pipeline">Pipeline & Journeys</Tab>
|
|
475
|
+
</TabList>
|
|
476
|
+
<TabPanel id="details">
|
|
477
|
+
<div className="p-4">Position Details — completed.</div>
|
|
478
|
+
</TabPanel>
|
|
479
|
+
<TabPanel id="workflow">
|
|
480
|
+
<div className="p-4">Candidate Workflow — has an error.</div>
|
|
481
|
+
</TabPanel>
|
|
482
|
+
<TabPanel id="pipeline">
|
|
483
|
+
<div className="p-4">Pipeline & Journeys — upcoming.</div>
|
|
484
|
+
</TabPanel>
|
|
485
|
+
</Tabs>
|
|
486
|
+
),
|
|
487
|
+
};
|
|
488
|
+
|
|
426
489
|
/**
|
|
427
490
|
* StepperFirstStep: Stepper with the first step active (no completed steps).
|
|
428
491
|
*/
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline Component Storybook Stories
|
|
3
|
+
*
|
|
4
|
+
* Covers the six core variants (orientation × marker), the mobile collapse, server-fed
|
|
5
|
+
* "Show more" pagination, empty/loading states, an activity-log composition, and a
|
|
6
|
+
* filter-responsiveness pattern.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
10
|
+
import { useDeferredValue, useState } from 'react';
|
|
11
|
+
import { FileText, MessageSquare, UserCheck, CheckCircle2 } from 'lucide-react';
|
|
12
|
+
import { Timeline } from './Timeline';
|
|
13
|
+
import { Avatar } from '../Avatar';
|
|
14
|
+
import { Badge } from '../Badge';
|
|
15
|
+
|
|
16
|
+
const meta = {
|
|
17
|
+
title: 'Elements/Timeline',
|
|
18
|
+
component: Timeline,
|
|
19
|
+
parameters: {
|
|
20
|
+
layout: 'padded',
|
|
21
|
+
docs: {
|
|
22
|
+
description: {
|
|
23
|
+
component:
|
|
24
|
+
'Accessible, chronological feed of events (WCAG 2.2 AAA). Rendered as a semantic ' +
|
|
25
|
+
'`<ol>`; vertical or horizontal; dot / basic / icon markers. Horizontal collapses to ' +
|
|
26
|
+
'vertical on narrow containers. Data enters via `<Timeline.Item>` children.',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
tags: ['autodocs'],
|
|
31
|
+
argTypes: {
|
|
32
|
+
orientation: {
|
|
33
|
+
control: 'select',
|
|
34
|
+
options: ['vertical', 'horizontal'],
|
|
35
|
+
description: 'Layout direction. Horizontal collapses to vertical on narrow containers.',
|
|
36
|
+
},
|
|
37
|
+
marker: {
|
|
38
|
+
control: 'select',
|
|
39
|
+
options: ['dot', 'basic', 'icon'],
|
|
40
|
+
description: 'Marker style for each event.',
|
|
41
|
+
},
|
|
42
|
+
titleHeadingLevel: {
|
|
43
|
+
control: 'select',
|
|
44
|
+
options: ['p', 'h2', 'h3', 'h4', 'h5', 'h6'],
|
|
45
|
+
description: 'Heading level for item titles (default `p`).',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
} satisfies Meta<typeof Timeline>;
|
|
49
|
+
|
|
50
|
+
export default meta;
|
|
51
|
+
type Story = StoryObj<typeof meta>;
|
|
52
|
+
|
|
53
|
+
const SUBTEXT = 'Timeline subtext — usually a longer piece of text for contextualising the timeline event.';
|
|
54
|
+
|
|
55
|
+
/** A small set of representative events. */
|
|
56
|
+
function basicItems() {
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<Timeline.Item date="2025-02-01" title="Application received" description={SUBTEXT} />
|
|
60
|
+
<Timeline.Item date="2025-03-01" title="Screening" description={SUBTEXT} />
|
|
61
|
+
<Timeline.Item date="2025-04-01" title="Interview" description={SUBTEXT} />
|
|
62
|
+
<Timeline.Item date="2025-05-01" title="Offer" description={SUBTEXT} />
|
|
63
|
+
</>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const VerticalDot: Story = {
|
|
68
|
+
args: { orientation: 'vertical', marker: 'dot', 'aria-label': 'Application history' },
|
|
69
|
+
render: (args) => <Timeline {...args}>{basicItems()}</Timeline>,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const VerticalBasic: Story = {
|
|
73
|
+
args: { orientation: 'vertical', marker: 'basic', 'aria-label': 'Application history' },
|
|
74
|
+
render: (args) => <Timeline {...args}>{basicItems()}</Timeline>,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const VerticalIcon: Story = {
|
|
78
|
+
args: { orientation: 'vertical', marker: 'icon', 'aria-label': 'Application history' },
|
|
79
|
+
render: (args) => (
|
|
80
|
+
<Timeline {...args}>
|
|
81
|
+
<Timeline.Item date="2025-02-01" title="Document uploaded" description={SUBTEXT} icon={<FileText />} />
|
|
82
|
+
<Timeline.Item date="2025-03-01" title="Message sent" description={SUBTEXT} icon={<MessageSquare />} />
|
|
83
|
+
<Timeline.Item date="2025-04-01" title="Approved" description={SUBTEXT} icon={<CheckCircle2 />} />
|
|
84
|
+
</Timeline>
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const HorizontalDot: Story = {
|
|
89
|
+
args: { orientation: 'horizontal', marker: 'dot', 'aria-label': 'Process', collapseBelow: false },
|
|
90
|
+
render: (args) => <Timeline {...args}>{basicItems()}</Timeline>,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const HorizontalBasic: Story = {
|
|
94
|
+
args: { orientation: 'horizontal', marker: 'basic', 'aria-label': 'Process', collapseBelow: false },
|
|
95
|
+
render: (args) => <Timeline {...args}>{basicItems()}</Timeline>,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const HorizontalIcon: Story = {
|
|
99
|
+
args: { orientation: 'horizontal', marker: 'icon', 'aria-label': 'Process', collapseBelow: false },
|
|
100
|
+
render: (args) => (
|
|
101
|
+
<Timeline {...args}>
|
|
102
|
+
<Timeline.Item date="2025-02-01" title="Uploaded" description={SUBTEXT} icon={<FileText />} />
|
|
103
|
+
<Timeline.Item date="2025-03-01" title="Reviewed" description={SUBTEXT} icon={<UserCheck />} />
|
|
104
|
+
<Timeline.Item date="2025-04-01" title="Approved" description={SUBTEXT} icon={<CheckCircle2 />} />
|
|
105
|
+
</Timeline>
|
|
106
|
+
),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Horizontal timelines collapse to vertical below `collapseBelow` (default 640px). Resize the frame. */
|
|
110
|
+
export const MobileCollapse: Story = {
|
|
111
|
+
args: { orientation: 'horizontal', marker: 'dot', 'aria-label': 'Process', collapseBelow: 640 },
|
|
112
|
+
render: (args) => (
|
|
113
|
+
<div style={{ maxWidth: 420, border: '1px dashed var(--border)', padding: 16 }}>
|
|
114
|
+
<Timeline {...args}>{basicItems()}</Timeline>
|
|
115
|
+
</div>
|
|
116
|
+
),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** Date granularity is controlled with `dateFormatOptions` (Intl.DateTimeFormatOptions). */
|
|
120
|
+
export const DateGranularity: Story = {
|
|
121
|
+
render: () => (
|
|
122
|
+
<div style={{ display: 'grid', gap: 32 }}>
|
|
123
|
+
<Timeline aria-label="Month + year" dateFormatOptions={{ year: 'numeric', month: 'long' }}>
|
|
124
|
+
<Timeline.Item date="2025-04-02T09:30:00" title="Month + year" description="{ year, month: 'long' }" />
|
|
125
|
+
</Timeline>
|
|
126
|
+
<Timeline aria-label="Full date" dateFormatOptions={{ dateStyle: 'full' }}>
|
|
127
|
+
<Timeline.Item date="2025-04-02T09:30:00" title="Full date" description="{ dateStyle: 'full' }" />
|
|
128
|
+
</Timeline>
|
|
129
|
+
<Timeline aria-label="Date + time" dateFormatOptions={{ dateStyle: 'medium', timeStyle: 'short' }}>
|
|
130
|
+
<Timeline.Item date="2025-04-02T09:30:00" title="Date + time" description="{ dateStyle, timeStyle }" />
|
|
131
|
+
</Timeline>
|
|
132
|
+
</div>
|
|
133
|
+
),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** Interactive items become links with a 44px target and a visible focus ring. */
|
|
137
|
+
export const InteractiveItems: Story = {
|
|
138
|
+
render: () => (
|
|
139
|
+
<Timeline aria-label="Recent activity">
|
|
140
|
+
<Timeline.Item href="#1" date="2025-04-01" title="Candidate applied" description={SUBTEXT} aria-label="View application" />
|
|
141
|
+
<Timeline.Item href="#2" date="2025-04-05" title="Recruiter responded" description={SUBTEXT} aria-label="View response" />
|
|
142
|
+
</Timeline>
|
|
143
|
+
),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** Server-fed pagination via `onLoadMore`/`hasMore`. Focus moves to the first new item on the final load. */
|
|
147
|
+
export const LoadMore: Story = {
|
|
148
|
+
render: () => {
|
|
149
|
+
const Demo = () => {
|
|
150
|
+
const [count, setCount] = useState(3);
|
|
151
|
+
const [loading, setLoading] = useState(false);
|
|
152
|
+
const max = 9;
|
|
153
|
+
const load = () => {
|
|
154
|
+
setLoading(true);
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
setCount((c) => Math.min(c + 3, max));
|
|
157
|
+
setLoading(false);
|
|
158
|
+
}, 600);
|
|
159
|
+
};
|
|
160
|
+
return (
|
|
161
|
+
<Timeline aria-label="Activity" hasMore={count < max} isLoadingMore={loading} onLoadMore={load}>
|
|
162
|
+
{Array.from({ length: count }, (_, i) => (
|
|
163
|
+
<Timeline.Item key={i} date={`2025-0${(i % 9) + 1}-01`} title={`Event ${i + 1}`} description={SUBTEXT} />
|
|
164
|
+
))}
|
|
165
|
+
</Timeline>
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
return <Demo />;
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/** Zero items render the `emptyState` slot — no orphaned connector rail. */
|
|
173
|
+
export const Empty: Story = {
|
|
174
|
+
render: () => (
|
|
175
|
+
<Timeline
|
|
176
|
+
aria-label="Activity"
|
|
177
|
+
emptyState={<p style={{ color: 'var(--muted-foreground)' }}>No activity yet.</p>}
|
|
178
|
+
/>
|
|
179
|
+
),
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Activity-log composition — Timeline ships the 6 core variants; richer item content is
|
|
184
|
+
* composed via the `icon`/avatar marker and the `children` body slot (no special variant).
|
|
185
|
+
*/
|
|
186
|
+
export const ActivityLogComposition: Story = {
|
|
187
|
+
render: () => (
|
|
188
|
+
<Timeline aria-label="Activity log" marker="icon">
|
|
189
|
+
<Timeline.Item date="2025-04-01T10:00:00" icon={<UserCheck />}>
|
|
190
|
+
<span className="text-sm">
|
|
191
|
+
<strong>Jordan Lee</strong> moved the candidate to <Badge>Interview</Badge>
|
|
192
|
+
</span>
|
|
193
|
+
</Timeline.Item>
|
|
194
|
+
<Timeline.Item date="2025-04-01T11:30:00" icon={<MessageSquare />}>
|
|
195
|
+
<div className="flex items-start gap-2">
|
|
196
|
+
<Avatar name="Sam Patel" size="sm" />
|
|
197
|
+
<div className="rounded-md bg-[var(--muted)] p-3 text-sm">
|
|
198
|
+
“Thanks — looking forward to the interview next week.”
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</Timeline.Item>
|
|
202
|
+
</Timeline>
|
|
203
|
+
),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Filtering large feeds: keep input responsive by deferring the filtered value
|
|
208
|
+
* (`useDeferredValue`) so typing isn't blocked by the re-render. Stable item ids keep
|
|
209
|
+
* reconciliation cheap.
|
|
210
|
+
*/
|
|
211
|
+
export const FilterResponsiveness: Story = {
|
|
212
|
+
render: () => {
|
|
213
|
+
const Demo = () => {
|
|
214
|
+
const ALL = Array.from({ length: 40 }, (_, i) => ({
|
|
215
|
+
id: `e${i}`,
|
|
216
|
+
date: `2025-${String((i % 12) + 1).padStart(2, '0')}-01`,
|
|
217
|
+
title: `Event ${i + 1}`,
|
|
218
|
+
}));
|
|
219
|
+
const [query, setQuery] = useState('');
|
|
220
|
+
const deferred = useDeferredValue(query);
|
|
221
|
+
const filtered = ALL.filter((e) => e.title.toLowerCase().includes(deferred.toLowerCase()));
|
|
222
|
+
const isStale = query !== deferred;
|
|
223
|
+
return (
|
|
224
|
+
<div style={{ display: 'grid', gap: 12 }}>
|
|
225
|
+
<input
|
|
226
|
+
type="search"
|
|
227
|
+
placeholder="Filter events…"
|
|
228
|
+
value={query}
|
|
229
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
230
|
+
style={{ padding: 8, border: '1px solid var(--input-border)', borderRadius: 6 }}
|
|
231
|
+
/>
|
|
232
|
+
<div style={{ opacity: isStale ? 0.6 : 1, transition: 'opacity 150ms' }}>
|
|
233
|
+
<Timeline aria-label="Filtered events" marker="dot">
|
|
234
|
+
{filtered.map((e) => (
|
|
235
|
+
<Timeline.Item key={e.id} id={e.id} date={e.date} title={e.title} />
|
|
236
|
+
))}
|
|
237
|
+
</Timeline>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
return <Demo />;
|
|
243
|
+
},
|
|
244
|
+
};
|