@arbor-education/design-system.components 0.25.3 → 0.25.5
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/.gather/skills/write-stories/README.md +1 -1
- package/.gather/skills/write-stories/SKILL.md +1 -1
- package/.gather/skills/write-stories/meta.md +1 -1
- package/CHANGELOG.md +16 -0
- package/component-library.md +26 -1
- package/dist/components/breadcrumbs/BreadcrumbCopyButton.d.ts +3 -0
- package/dist/components/breadcrumbs/BreadcrumbCopyButton.d.ts.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbCopyButton.js +13 -0
- package/dist/components/breadcrumbs/BreadcrumbCopyButton.js.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbDropdown.d.ts +3 -0
- package/dist/components/breadcrumbs/BreadcrumbDropdown.d.ts.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbDropdown.js +8 -0
- package/dist/components/breadcrumbs/BreadcrumbDropdown.js.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbEllipsis.d.ts +3 -0
- package/dist/components/breadcrumbs/BreadcrumbEllipsis.d.ts.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbEllipsis.js +7 -0
- package/dist/components/breadcrumbs/BreadcrumbEllipsis.js.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbItemSeparator.d.ts +2 -0
- package/dist/components/breadcrumbs/BreadcrumbItemSeparator.d.ts.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbItemSeparator.js +5 -0
- package/dist/components/breadcrumbs/BreadcrumbItemSeparator.js.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbLink.d.ts +6 -0
- package/dist/components/breadcrumbs/BreadcrumbLink.d.ts.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbLink.js +9 -0
- package/dist/components/breadcrumbs/BreadcrumbLink.js.map +1 -0
- package/dist/components/breadcrumbs/Breadcrumbs.d.ts +21 -0
- package/dist/components/breadcrumbs/Breadcrumbs.d.ts.map +1 -0
- package/dist/components/breadcrumbs/Breadcrumbs.js +51 -0
- package/dist/components/breadcrumbs/Breadcrumbs.js.map +1 -0
- package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts +81 -0
- package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts.map +1 -0
- package/dist/components/breadcrumbs/Breadcrumbs.stories.js +681 -0
- package/dist/components/breadcrumbs/Breadcrumbs.stories.js.map +1 -0
- package/dist/components/breadcrumbs/Breadcrumbs.test.d.ts +2 -0
- package/dist/components/breadcrumbs/Breadcrumbs.test.d.ts.map +1 -0
- package/dist/components/breadcrumbs/Breadcrumbs.test.js +160 -0
- package/dist/components/breadcrumbs/Breadcrumbs.test.js.map +1 -0
- package/dist/components/breadcrumbs/types.d.ts +27 -0
- package/dist/components/breadcrumbs/types.d.ts.map +1 -0
- package/dist/components/breadcrumbs/types.js +2 -0
- package/dist/components/breadcrumbs/types.js.map +1 -0
- package/dist/components/fileUpload/FileUpload.d.ts +21 -0
- package/dist/components/fileUpload/FileUpload.d.ts.map +1 -0
- package/dist/components/fileUpload/FileUpload.js +23 -0
- package/dist/components/fileUpload/FileUpload.js.map +1 -0
- package/dist/components/fileUpload/FileUpload.stories.d.ts +71 -0
- package/dist/components/fileUpload/FileUpload.stories.d.ts.map +1 -0
- package/dist/components/fileUpload/FileUpload.stories.js +736 -0
- package/dist/components/fileUpload/FileUpload.stories.js.map +1 -0
- package/dist/components/fileUpload/FileUpload.test.d.ts +2 -0
- package/dist/components/fileUpload/FileUpload.test.d.ts.map +1 -0
- package/dist/components/fileUpload/FileUpload.test.js +99 -0
- package/dist/components/fileUpload/FileUpload.test.js.map +1 -0
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.js +14 -15
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.js.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.test.js +11 -0
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.test.js.map +1 -1
- package/dist/components/icon/allowedIcons.d.ts +1 -0
- package/dist/components/icon/allowedIcons.d.ts.map +1 -1
- package/dist/components/icon/allowedIcons.js +2 -1
- package/dist/components/icon/allowedIcons.js.map +1 -1
- package/dist/components/skeleton/Skeleton.d.ts +10 -0
- package/dist/components/skeleton/Skeleton.d.ts.map +1 -0
- package/dist/components/skeleton/Skeleton.js +9 -0
- package/dist/components/skeleton/Skeleton.js.map +1 -0
- package/dist/components/skeleton/Skeleton.stories.d.ts +99 -0
- package/dist/components/skeleton/Skeleton.stories.d.ts.map +1 -0
- package/dist/components/skeleton/Skeleton.stories.js +539 -0
- package/dist/components/skeleton/Skeleton.stories.js.map +1 -0
- package/dist/components/skeleton/Skeleton.test.d.ts +2 -0
- package/dist/components/skeleton/Skeleton.test.d.ts.map +1 -0
- package/dist/components/skeleton/Skeleton.test.js +49 -0
- package/dist/components/skeleton/Skeleton.test.js.map +1 -0
- package/dist/index.css +225 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/components/breadcrumbs/BreadcrumbCopyButton.tsx +27 -0
- package/src/components/breadcrumbs/BreadcrumbDropdown.tsx +27 -0
- package/src/components/breadcrumbs/BreadcrumbEllipsis.tsx +16 -0
- package/src/components/breadcrumbs/BreadcrumbItemSeparator.tsx +3 -0
- package/src/components/breadcrumbs/BreadcrumbLink.tsx +29 -0
- package/src/components/breadcrumbs/Breadcrumbs.stories.tsx +870 -0
- package/src/components/breadcrumbs/Breadcrumbs.test.tsx +185 -0
- package/src/components/breadcrumbs/Breadcrumbs.tsx +92 -0
- package/src/components/breadcrumbs/breadcrumbs.scss +158 -0
- package/src/components/breadcrumbs/types.ts +31 -0
- package/src/components/button/button.scss +2 -0
- package/src/components/dropdown/dropdown.scss +1 -0
- package/src/components/fileUpload/FileUpload.stories.tsx +920 -0
- package/src/components/fileUpload/FileUpload.test.tsx +172 -0
- package/src/components/fileUpload/FileUpload.tsx +144 -0
- package/src/components/fileUpload/fileUpload.scss +74 -0
- package/src/components/formField/inputs/selectDropdown/SelectDropdown.stories.tsx +15 -16
- package/src/components/formField/inputs/selectDropdown/SelectDropdown.test.tsx +19 -0
- package/src/components/formField/inputs/selectDropdown/SelectDropdown.tsx +1 -1
- package/src/components/icon/allowedIcons.tsx +2 -0
- package/src/components/progress/progress.scss +1 -0
- package/src/components/skeleton/Skeleton.stories.tsx +748 -0
- package/src/components/skeleton/Skeleton.test.tsx +55 -0
- package/src/components/skeleton/Skeleton.tsx +36 -0
- package/src/components/skeleton/skeleton.scss +23 -0
- package/src/index.scss +3 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import {
|
|
3
|
+
Controls,
|
|
4
|
+
Heading as DocHeading,
|
|
5
|
+
Markdown,
|
|
6
|
+
Primary as DocPrimary,
|
|
7
|
+
Stories,
|
|
8
|
+
Subtitle,
|
|
9
|
+
Title,
|
|
10
|
+
} from '@storybook/addon-docs/blocks';
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import { Skeleton } from './Skeleton.js';
|
|
13
|
+
import { Avatar } from 'Components/avatar/Avatar.js';
|
|
14
|
+
import { Button } from 'Components/button/Button.js';
|
|
15
|
+
import { Card } from 'Components/card/Card.js';
|
|
16
|
+
import { Heading } from 'Components/heading/Heading.js';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Docs page content
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
const DESCRIPTION_INTRO = [
|
|
23
|
+
'Skeleton is an animated pulsing placeholder that communicates to users that content is loading.',
|
|
24
|
+
'Wrap your real content as `children` — the skeleton matches its dimensions automatically.',
|
|
25
|
+
'When data arrives, flip `loading` to `false` and the real content appears.',
|
|
26
|
+
].join(' ');
|
|
27
|
+
|
|
28
|
+
const USAGE_GUIDANCE = [
|
|
29
|
+
'### When to use',
|
|
30
|
+
'',
|
|
31
|
+
'- **While fetching data** for a named region of the UI — a student profile, a report table, a list of events',
|
|
32
|
+
'- **Wrapping real content** — pass `children` and toggle `loading`; the skeleton sizes itself to match the wrapped element automatically',
|
|
33
|
+
'- **Multiple independent placeholders** in the same region — compose separate Skeleton instances, one per content element',
|
|
34
|
+
'',
|
|
35
|
+
'---',
|
|
36
|
+
'',
|
|
37
|
+
'### When NOT to use',
|
|
38
|
+
'',
|
|
39
|
+
'| Situation | Use instead |',
|
|
40
|
+
'|---|---|',
|
|
41
|
+
'| Measurable async operation (file upload, multi-step wizard) | [`Progress`](?path=/docs/components-progress--docs) |',
|
|
42
|
+
'| Unknown-duration background process | Spinner (Icon `name="loader"` with CSS rotation animation) |',
|
|
43
|
+
'| Content that loads instantly from cache | Nothing — a flash of skeleton is jarring |',
|
|
44
|
+
'| Content that is purely dynamic in size (infinite scroll, variable-length tables) | Nothing — a mismatched skeleton causes layout shift |',
|
|
45
|
+
].join('\n');
|
|
46
|
+
|
|
47
|
+
const DEVELOPER_NOTES = [
|
|
48
|
+
'### Critical usage patterns',
|
|
49
|
+
'',
|
|
50
|
+
'**Wrap the real content as children** — the skeleton sizes itself to match automatically:',
|
|
51
|
+
'',
|
|
52
|
+
'```tsx',
|
|
53
|
+
'<Skeleton loading={isLoading}>',
|
|
54
|
+
' <span>{student.name}</span>',
|
|
55
|
+
'</Skeleton>',
|
|
56
|
+
'```',
|
|
57
|
+
'',
|
|
58
|
+
'**`width` and `height` are escape hatches** for cases where you cannot wrap the real element',
|
|
59
|
+
'(e.g. approximating the width of a text line whose content is unknown):',
|
|
60
|
+
'',
|
|
61
|
+
'```tsx',
|
|
62
|
+
'<Skeleton loading={isLoading} width="160px" height="20px" />',
|
|
63
|
+
'```',
|
|
64
|
+
'',
|
|
65
|
+
'**`loading={false}` with no children renders an empty fragment** — nothing in the DOM at all.',
|
|
66
|
+
'This is intentional and useful for conditional rendering; the component unmounts cleanly.',
|
|
67
|
+
'',
|
|
68
|
+
'**Circle shapes** require `style={{ borderRadius: \'50%\' }}` — there is no `shape` prop.',
|
|
69
|
+
'',
|
|
70
|
+
'---',
|
|
71
|
+
'',
|
|
72
|
+
'### Accessibility',
|
|
73
|
+
'',
|
|
74
|
+
'`aria-hidden="true"` is hardcoded on the pulsing placeholder span — screen readers skip it entirely. This is correct behaviour.',
|
|
75
|
+
'This attribute is set explicitly after the `{...props}` spread, so consumers **cannot** override it.',
|
|
76
|
+
'',
|
|
77
|
+
'When `loading` flips to `false` and meaningful content appears, screen readers will not announce the change automatically.',
|
|
78
|
+
'If that announcement matters, wrap the containing region in `aria-live="polite"`:',
|
|
79
|
+
'',
|
|
80
|
+
'```tsx',
|
|
81
|
+
'<section aria-live="polite" aria-busy={isLoading}>',
|
|
82
|
+
' <Skeleton loading={isLoading}>',
|
|
83
|
+
' <span>{student.name}</span>',
|
|
84
|
+
' </Skeleton>',
|
|
85
|
+
'</section>',
|
|
86
|
+
'```',
|
|
87
|
+
'',
|
|
88
|
+
'**Design tokens:**',
|
|
89
|
+
'',
|
|
90
|
+
'| Token | Value | Used for |',
|
|
91
|
+
'|---|---|---|',
|
|
92
|
+
'| `--color-grey-200` | `#dfdfdf` | Resting background colour |',
|
|
93
|
+
'| `--color-grey-100` | `#efefef` | Animation pulse peak (lighter) |',
|
|
94
|
+
'| `--border-radius-xsmall` | `4px` | Corner radius |',
|
|
95
|
+
'',
|
|
96
|
+
'---',
|
|
97
|
+
'',
|
|
98
|
+
'### TypeScript types',
|
|
99
|
+
'',
|
|
100
|
+
'```ts',
|
|
101
|
+
"import { Skeleton } from '@arbor-education/design-system.components';",
|
|
102
|
+
'',
|
|
103
|
+
'function MyComponent(props: Skeleton.Props) { ... }',
|
|
104
|
+
'```',
|
|
105
|
+
'',
|
|
106
|
+
'| Type | Description |',
|
|
107
|
+
'|---|---|',
|
|
108
|
+
'| `Skeleton.Props` | Full props interface |',
|
|
109
|
+
].join('\n');
|
|
110
|
+
|
|
111
|
+
const RELATED_COMPONENTS = [
|
|
112
|
+
'## Related components',
|
|
113
|
+
'',
|
|
114
|
+
'[Progress](?path=/docs/components-progress--docs) · [Avatar](?path=/docs/components-avatar--docs) · [Card](?path=/docs/components-card--docs)',
|
|
115
|
+
].join('\n');
|
|
116
|
+
|
|
117
|
+
const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
|
|
118
|
+
|
|
119
|
+
function SkeletonDocsPage() {
|
|
120
|
+
return (
|
|
121
|
+
<>
|
|
122
|
+
<Title />
|
|
123
|
+
<Subtitle />
|
|
124
|
+
<Markdown>{DESCRIPTION_INTRO}</Markdown>
|
|
125
|
+
<DocHeading>Interactive example</DocHeading>
|
|
126
|
+
<Markdown>{PROPS_INTRO}</Markdown>
|
|
127
|
+
<DocPrimary />
|
|
128
|
+
<Controls />
|
|
129
|
+
<DocHeading>Usage guidance</DocHeading>
|
|
130
|
+
<Markdown>{USAGE_GUIDANCE}</Markdown>
|
|
131
|
+
<DocHeading>Developer notes</DocHeading>
|
|
132
|
+
<Markdown>{DEVELOPER_NOTES}</Markdown>
|
|
133
|
+
<DocHeading>Examples</DocHeading>
|
|
134
|
+
<Stories title="" />
|
|
135
|
+
<Markdown>{RELATED_COMPONENTS}</Markdown>
|
|
136
|
+
</>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Meta
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
const meta = {
|
|
145
|
+
title: 'Components/Skeleton',
|
|
146
|
+
component: Skeleton,
|
|
147
|
+
tags: ['autodocs'],
|
|
148
|
+
parameters: {
|
|
149
|
+
layout: 'padded',
|
|
150
|
+
docs: {
|
|
151
|
+
page: SkeletonDocsPage,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
argTypes: {
|
|
155
|
+
loading: {
|
|
156
|
+
control: 'boolean',
|
|
157
|
+
description: [
|
|
158
|
+
'Controls whether the pulsing placeholder is shown.',
|
|
159
|
+
'`true` (default) renders the animated `<span>`. `false` renders `children` directly — or an empty fragment if no children are provided.',
|
|
160
|
+
].join(' '),
|
|
161
|
+
table: {
|
|
162
|
+
type: { summary: 'boolean' },
|
|
163
|
+
defaultValue: { summary: 'true' },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
width: {
|
|
167
|
+
control: 'text',
|
|
168
|
+
description: [
|
|
169
|
+
'Override the skeleton width. Accepts any CSS value (`"200px"`, `"100%"`, `"12rem"`) or a number (converted to pixels).',
|
|
170
|
+
'Only needed when you cannot wrap the real element as `children` — the skeleton normally sizes itself to its children.',
|
|
171
|
+
].join(' '),
|
|
172
|
+
table: {
|
|
173
|
+
type: { summary: 'string | number' },
|
|
174
|
+
defaultValue: { summary: 'undefined' },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
height: {
|
|
178
|
+
control: 'text',
|
|
179
|
+
description: [
|
|
180
|
+
'Override the skeleton height. Same rules as `width`.',
|
|
181
|
+
'Only needed when you cannot wrap the real element as `children`.',
|
|
182
|
+
].join(' '),
|
|
183
|
+
table: {
|
|
184
|
+
type: { summary: 'string | number' },
|
|
185
|
+
defaultValue: { summary: 'undefined' },
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
style: {
|
|
189
|
+
control: false,
|
|
190
|
+
description: [
|
|
191
|
+
'Additional inline styles applied to the skeleton span.',
|
|
192
|
+
'Primary use case: `style={{ borderRadius: \'50%\' }}` for a circular placeholder.',
|
|
193
|
+
'⚠️ Spreads LAST in the internal style object (`{ width, height, ...style }`),',
|
|
194
|
+
'so it overrides `width`/`height` props for the same CSS property. Avoid setting width or height in both places.',
|
|
195
|
+
].join(' '),
|
|
196
|
+
table: {
|
|
197
|
+
type: { summary: 'React.CSSProperties' },
|
|
198
|
+
defaultValue: { summary: 'undefined' },
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
className: {
|
|
202
|
+
control: false,
|
|
203
|
+
description: 'Additional CSS class names merged with `ds-skeleton`. Only applied when `loading={true}`.',
|
|
204
|
+
table: {
|
|
205
|
+
type: { summary: 'string' },
|
|
206
|
+
defaultValue: { summary: 'undefined' },
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
children: {
|
|
210
|
+
control: false,
|
|
211
|
+
description: [
|
|
212
|
+
'The real content the skeleton is standing in for.',
|
|
213
|
+
'While `loading={true}` the children are rendered but hidden — they determine the skeleton\'s size.',
|
|
214
|
+
'When `loading={false}` the skeleton disappears and children render normally.',
|
|
215
|
+
'When no children are provided and `loading={false}`, the component returns an empty React fragment.',
|
|
216
|
+
].join(' '),
|
|
217
|
+
table: {
|
|
218
|
+
type: { summary: 'React.ReactNode' },
|
|
219
|
+
defaultValue: { summary: 'undefined' },
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
} satisfies Meta<typeof Skeleton>;
|
|
224
|
+
|
|
225
|
+
export default meta;
|
|
226
|
+
type Story = StoryObj<typeof meta>;
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Helper: attach a per-story description to docs
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
const withDescription = (story: Story, description: string): Story => ({
|
|
233
|
+
...story,
|
|
234
|
+
parameters: {
|
|
235
|
+
...story.parameters,
|
|
236
|
+
docs: {
|
|
237
|
+
...story.parameters?.docs,
|
|
238
|
+
description: {
|
|
239
|
+
story: description,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Template components for stateful and composed stories.
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
const MultipleLinesTmpl = () => (
|
|
250
|
+
<div
|
|
251
|
+
style={{
|
|
252
|
+
display: 'flex',
|
|
253
|
+
flexDirection: 'column',
|
|
254
|
+
gap: 'var(--spacing-xsmall)',
|
|
255
|
+
width: '360px',
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
<Skeleton width="100%" height="1em" />
|
|
259
|
+
<Skeleton width="85%" height="1em" />
|
|
260
|
+
<Skeleton width="60%" height="1em" />
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const LoadingToLoadedTmpl = () => {
|
|
265
|
+
const [loading, setLoading] = useState(true);
|
|
266
|
+
return (
|
|
267
|
+
<div
|
|
268
|
+
style={{
|
|
269
|
+
display: 'flex',
|
|
270
|
+
flexDirection: 'column',
|
|
271
|
+
gap: 'var(--spacing-large)',
|
|
272
|
+
alignItems: 'flex-start',
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
<Button onClick={() => setLoading(prev => !prev)}>
|
|
276
|
+
{loading ? 'Show content' : 'Show skeleton'}
|
|
277
|
+
</Button>
|
|
278
|
+
<Skeleton loading={loading}>
|
|
279
|
+
<span className="ds-text">Maya Patel — Year 9</span>
|
|
280
|
+
</Skeleton>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const CircleShapeTmpl = () => (
|
|
286
|
+
<div style={{ display: 'flex', gap: 'var(--spacing-large)', alignItems: 'center' }}>
|
|
287
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)', alignItems: 'center' }}>
|
|
288
|
+
<Skeleton style={{ borderRadius: '50%' }}>
|
|
289
|
+
<Avatar size="medium" initials="JC" alt="Joe Carter" />
|
|
290
|
+
</Skeleton>
|
|
291
|
+
<span className="ds-text" style={{ color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }}>medium</span>
|
|
292
|
+
</div>
|
|
293
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)', alignItems: 'center' }}>
|
|
294
|
+
<Skeleton style={{ borderRadius: '50%' }}>
|
|
295
|
+
<Avatar size="large" initials="JC" alt="Joe Carter" />
|
|
296
|
+
</Skeleton>
|
|
297
|
+
<span className="ds-text" style={{ color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }}>large</span>
|
|
298
|
+
</div>
|
|
299
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)', alignItems: 'center' }}>
|
|
300
|
+
<Skeleton style={{ borderRadius: '50%' }}>
|
|
301
|
+
<Avatar size="extra-large" initials="JC" alt="Joe Carter" />
|
|
302
|
+
</Skeleton>
|
|
303
|
+
<span className="ds-text" style={{ color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }}>extra-large</span>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const FullWidthTmpl = () => (
|
|
309
|
+
<div style={{ width: '360px' }}>
|
|
310
|
+
<Skeleton width="100%" height="20px" />
|
|
311
|
+
</div>
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const AvatarCircleTmpl = () => {
|
|
315
|
+
const [loading, setLoading] = useState(true);
|
|
316
|
+
return (
|
|
317
|
+
<div
|
|
318
|
+
style={{
|
|
319
|
+
display: 'flex',
|
|
320
|
+
flexDirection: 'column',
|
|
321
|
+
gap: 'var(--spacing-large)',
|
|
322
|
+
alignItems: 'flex-start',
|
|
323
|
+
}}
|
|
324
|
+
>
|
|
325
|
+
<Button onClick={() => setLoading(prev => !prev)}>
|
|
326
|
+
{loading ? 'Show content' : 'Show skeleton'}
|
|
327
|
+
</Button>
|
|
328
|
+
<Skeleton loading={loading} style={{ borderRadius: '50%' }}>
|
|
329
|
+
<Avatar size="large" initials="JC" alt="Joe Carter" />
|
|
330
|
+
</Skeleton>
|
|
331
|
+
</div>
|
|
332
|
+
);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const CardLayoutTmpl = () => {
|
|
336
|
+
const [loading, setLoading] = useState(true);
|
|
337
|
+
return (
|
|
338
|
+
<div
|
|
339
|
+
style={{
|
|
340
|
+
width: '360px',
|
|
341
|
+
display: 'flex',
|
|
342
|
+
flexDirection: 'column',
|
|
343
|
+
gap: 'var(--spacing-large)',
|
|
344
|
+
}}
|
|
345
|
+
>
|
|
346
|
+
<Button onClick={() => setLoading(prev => !prev)}>
|
|
347
|
+
{loading ? 'Show content' : 'Show skeleton'}
|
|
348
|
+
</Button>
|
|
349
|
+
<Card>
|
|
350
|
+
<div style={{ display: 'flex', gap: 'var(--spacing-large)', alignItems: 'flex-start' }}>
|
|
351
|
+
<Skeleton loading={loading} style={{ borderRadius: '50%' }}>
|
|
352
|
+
<Avatar size="large" initials="JC" alt="Joe Carter" />
|
|
353
|
+
</Skeleton>
|
|
354
|
+
<div
|
|
355
|
+
style={{
|
|
356
|
+
display: 'flex',
|
|
357
|
+
flexDirection: 'column',
|
|
358
|
+
gap: 'var(--spacing-xsmall)',
|
|
359
|
+
flex: 1,
|
|
360
|
+
}}
|
|
361
|
+
>
|
|
362
|
+
<Skeleton loading={loading}>
|
|
363
|
+
<Heading level={2}>Joe Carter</Heading>
|
|
364
|
+
</Skeleton>
|
|
365
|
+
<Skeleton loading={loading}>
|
|
366
|
+
<Heading level={4}>Form 10B</Heading>
|
|
367
|
+
</Skeleton>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
</Card>
|
|
371
|
+
</div>
|
|
372
|
+
);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const ExplicitDimensionsTmpl = () => (
|
|
376
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
377
|
+
<div>
|
|
378
|
+
<p className="ds-text" style={{ margin: '0 0 var(--spacing-xsmall)' }}>
|
|
379
|
+
Approximate text line (unknown content, known approximate width)
|
|
380
|
+
</p>
|
|
381
|
+
<Skeleton width="200px" height="1em" />
|
|
382
|
+
</div>
|
|
383
|
+
<div>
|
|
384
|
+
<p className="ds-text" style={{ margin: '0 0 var(--spacing-xsmall)' }}>
|
|
385
|
+
Approximate paragraph block (multiple lines, varying width)
|
|
386
|
+
</p>
|
|
387
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }}>
|
|
388
|
+
<Skeleton width="100%" height="1em" />
|
|
389
|
+
<Skeleton width="85%" height="1em" />
|
|
390
|
+
<Skeleton width="60%" height="1em" />
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
<div>
|
|
394
|
+
<p className="ds-text" style={{ margin: '0 0 var(--spacing-xsmall)' }}>
|
|
395
|
+
Full-width block (e.g. a banner or table row)
|
|
396
|
+
</p>
|
|
397
|
+
<Skeleton width="100%" height="48px" />
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
const AccessibilityPatternTmpl = () => {
|
|
403
|
+
const [loading, setLoading] = useState(true);
|
|
404
|
+
return (
|
|
405
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', alignItems: 'flex-start' }}>
|
|
406
|
+
<Button onClick={() => setLoading(prev => !prev)}>
|
|
407
|
+
{loading ? 'Reveal content' : 'Reset to loading'}
|
|
408
|
+
</Button>
|
|
409
|
+
<section aria-live="polite" aria-busy={loading}>
|
|
410
|
+
<Heading level={3} style={{ marginBottom: 'var(--spacing-xsmall)' }}>Student profile</Heading>
|
|
411
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }}>
|
|
412
|
+
<Skeleton loading={loading} width="180px" height="16px">
|
|
413
|
+
<span className="ds-text">Maya Patel — Year 10</span>
|
|
414
|
+
</Skeleton>
|
|
415
|
+
<Skeleton loading={loading} width="140px" height="16px">
|
|
416
|
+
<span className="ds-text">Form: 10B/Tu</span>
|
|
417
|
+
</Skeleton>
|
|
418
|
+
</div>
|
|
419
|
+
</section>
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
// Stories
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
export const Default: Story = withDescription(
|
|
429
|
+
{
|
|
430
|
+
args: {
|
|
431
|
+
loading: true,
|
|
432
|
+
},
|
|
433
|
+
render: args => (
|
|
434
|
+
<Skeleton {...args}>
|
|
435
|
+
<span className="ds-text">Joe Carter — Year 9</span>
|
|
436
|
+
</Skeleton>
|
|
437
|
+
),
|
|
438
|
+
},
|
|
439
|
+
[
|
|
440
|
+
'Toggle `loading` in the Controls panel to see the skeleton replaced by the real content.',
|
|
441
|
+
'No `width` or `height` is set — the skeleton sizes itself to match the wrapped `<span>`.',
|
|
442
|
+
].join(' '),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
export const CircleShape: Story = withDescription(
|
|
446
|
+
{
|
|
447
|
+
render: CircleShapeTmpl,
|
|
448
|
+
parameters: {
|
|
449
|
+
controls: { disable: true },
|
|
450
|
+
docs: {
|
|
451
|
+
source: {
|
|
452
|
+
language: 'tsx',
|
|
453
|
+
code: `
|
|
454
|
+
import { Avatar, Skeleton } from '@arbor-education/design-system.components';
|
|
455
|
+
|
|
456
|
+
function AvatarSkeletonExample() {
|
|
457
|
+
return (
|
|
458
|
+
// Wrap the Avatar — the skeleton sizes itself automatically.
|
|
459
|
+
// style={{ borderRadius: '50%' }} rounds the skeleton to match the Avatar's shape.
|
|
460
|
+
<Skeleton style={{ borderRadius: '50%' }}>
|
|
461
|
+
<Avatar size="large" initials="JC" alt="Joe Carter" />
|
|
462
|
+
</Skeleton>
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
export default AvatarSkeletonExample;
|
|
466
|
+
`.trim(),
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
[
|
|
472
|
+
'Wrap the real Avatar and add `style={{ borderRadius: \'50%\' }}` — the skeleton matches the Avatar\'s dimensions automatically.',
|
|
473
|
+
'There is no `shape` prop; `style` is the escape hatch for corner radius.',
|
|
474
|
+
].join(' '),
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
export const FullWidth: Story = withDescription(
|
|
478
|
+
{
|
|
479
|
+
render: FullWidthTmpl,
|
|
480
|
+
parameters: {
|
|
481
|
+
controls: { disable: true },
|
|
482
|
+
docs: {
|
|
483
|
+
source: {
|
|
484
|
+
language: 'tsx',
|
|
485
|
+
code: `
|
|
486
|
+
import { Skeleton } from '@arbor-education/design-system.components';
|
|
487
|
+
|
|
488
|
+
function FullWidthSkeletonExample() {
|
|
489
|
+
return (
|
|
490
|
+
// width="100%" fills the parent container.
|
|
491
|
+
// Control the actual rendered size by sizing the wrapper.
|
|
492
|
+
<div style={{ width: '360px' }}>
|
|
493
|
+
<Skeleton width="100%" height="20px" />
|
|
494
|
+
</div>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
export default FullWidthSkeletonExample;
|
|
498
|
+
`.trim(),
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
[
|
|
504
|
+
'Percentage widths work — `width="100%"` fills the parent container.',
|
|
505
|
+
'Use this for full-bleed block placeholders like a report title, a section header, or a table cell.',
|
|
506
|
+
'Control the actual rendered width by sizing the wrapper element.',
|
|
507
|
+
].join(' '),
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
export const MultipleLines: Story = withDescription(
|
|
511
|
+
{
|
|
512
|
+
render: MultipleLinesTmpl,
|
|
513
|
+
parameters: {
|
|
514
|
+
controls: { disable: true },
|
|
515
|
+
docs: {
|
|
516
|
+
source: {
|
|
517
|
+
language: 'tsx',
|
|
518
|
+
code: `
|
|
519
|
+
import { Skeleton } from '@arbor-education/design-system.components';
|
|
520
|
+
|
|
521
|
+
function MultipleLinesExample() {
|
|
522
|
+
return (
|
|
523
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)', width: '360px' }}>
|
|
524
|
+
<Skeleton width="100%" height="1em" />
|
|
525
|
+
<Skeleton width="85%" height="1em" />
|
|
526
|
+
<Skeleton width="60%" height="1em" />
|
|
527
|
+
</div>
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
export default MultipleLinesExample;
|
|
531
|
+
`.trim(),
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
[
|
|
537
|
+
'When you are loading a block of text whose content is unknown, use explicit `width` and `height` to approximate the paragraph shape.',
|
|
538
|
+
'Use `height="1em"` to match the line height and vary widths to mimic natural text breaks.',
|
|
539
|
+
'Prefer wrapping real content as `children` when the content is known.',
|
|
540
|
+
].join(' '),
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
export const LoadingToLoaded: Story = withDescription(
|
|
544
|
+
{
|
|
545
|
+
render: LoadingToLoadedTmpl,
|
|
546
|
+
parameters: {
|
|
547
|
+
controls: { disable: true },
|
|
548
|
+
docs: {
|
|
549
|
+
source: {
|
|
550
|
+
language: 'tsx',
|
|
551
|
+
code: `
|
|
552
|
+
import { useState } from 'react';
|
|
553
|
+
import { Button, Skeleton } from '@arbor-education/design-system.components';
|
|
554
|
+
|
|
555
|
+
function LoadingToLoadedExample() {
|
|
556
|
+
const [loading, setLoading] = useState(true);
|
|
557
|
+
|
|
558
|
+
return (
|
|
559
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', alignItems: 'flex-start' }}>
|
|
560
|
+
<Button onClick={() => setLoading(prev => !prev)}>
|
|
561
|
+
{loading ? 'Show content' : 'Show skeleton'}
|
|
562
|
+
</Button>
|
|
563
|
+
<Skeleton loading={loading}>
|
|
564
|
+
<span>Maya Patel — Year 9</span>
|
|
565
|
+
</Skeleton>
|
|
566
|
+
</div>
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
export default LoadingToLoadedExample;
|
|
570
|
+
`.trim(),
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
[
|
|
576
|
+
'The canonical wrap-and-reveal pattern: pass the real content as `children` and drive `loading` from your async state.',
|
|
577
|
+
'When the data arrives, set `loading={false}` — the pulsing placeholder disappears and the children appear in its place with no additional wrappers or state.',
|
|
578
|
+
].join(' '),
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
export const AvatarCircle: Story = withDescription(
|
|
582
|
+
{
|
|
583
|
+
name: 'Avatar circle',
|
|
584
|
+
render: AvatarCircleTmpl,
|
|
585
|
+
parameters: {
|
|
586
|
+
controls: { disable: true },
|
|
587
|
+
docs: {
|
|
588
|
+
source: {
|
|
589
|
+
language: 'tsx',
|
|
590
|
+
code: `
|
|
591
|
+
import { useState } from 'react';
|
|
592
|
+
import { Avatar, Button, Skeleton } from '@arbor-education/design-system.components';
|
|
593
|
+
|
|
594
|
+
function AvatarRevealExample() {
|
|
595
|
+
const [loading, setLoading] = useState(true);
|
|
596
|
+
|
|
597
|
+
return (
|
|
598
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', alignItems: 'flex-start' }}>
|
|
599
|
+
<Button onClick={() => setLoading(prev => !prev)}>
|
|
600
|
+
{loading ? 'Show content' : 'Show skeleton'}
|
|
601
|
+
</Button>
|
|
602
|
+
<Skeleton loading={loading} style={{ borderRadius: '50%' }}>
|
|
603
|
+
<Avatar size="large" initials="JC" alt="Joe Carter" />
|
|
604
|
+
</Skeleton>
|
|
605
|
+
</div>
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
export default AvatarRevealExample;
|
|
609
|
+
`.trim(),
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
[
|
|
615
|
+
'A circular Skeleton wrapping an Avatar — no explicit `width` or `height` needed.',
|
|
616
|
+
'The skeleton auto-sizes to the wrapped Avatar\'s dimensions.',
|
|
617
|
+
'Add `style={{ borderRadius: \'50%\' }}` to match the Avatar\'s circular shape.',
|
|
618
|
+
'When `loading` flips to `false`, the Avatar appears in place of the circle.',
|
|
619
|
+
].join(' '),
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
export const CardLayout: Story = withDescription(
|
|
623
|
+
{
|
|
624
|
+
name: 'Card layout',
|
|
625
|
+
render: CardLayoutTmpl,
|
|
626
|
+
parameters: {
|
|
627
|
+
controls: { disable: true },
|
|
628
|
+
docs: {
|
|
629
|
+
source: {
|
|
630
|
+
language: 'tsx',
|
|
631
|
+
code: `
|
|
632
|
+
import { useState } from 'react';
|
|
633
|
+
import { Avatar, Button, Card, Heading, Skeleton } from '@arbor-education/design-system.components';
|
|
634
|
+
|
|
635
|
+
function StudentCardSkeleton() {
|
|
636
|
+
const [loading, setLoading] = useState(true);
|
|
637
|
+
|
|
638
|
+
return (
|
|
639
|
+
<div style={{ width: '360px', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
640
|
+
<Button onClick={() => setLoading(prev => !prev)}>
|
|
641
|
+
{loading ? 'Show content' : 'Show skeleton'}
|
|
642
|
+
</Button>
|
|
643
|
+
<Card>
|
|
644
|
+
<div style={{ display: 'flex', gap: 'var(--spacing-large)', alignItems: 'flex-start' }}>
|
|
645
|
+
<Skeleton loading={loading} style={{ borderRadius: '50%' }}>
|
|
646
|
+
<Avatar size="large" initials="JC" alt="Joe Carter" />
|
|
647
|
+
</Skeleton>
|
|
648
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)', flex: 1 }}>
|
|
649
|
+
<Skeleton loading={loading}>
|
|
650
|
+
<Heading level={2}>Joe Carter</Heading>
|
|
651
|
+
</Skeleton>
|
|
652
|
+
<Skeleton loading={loading}>
|
|
653
|
+
<Heading level={4}>Form 10B</Heading>
|
|
654
|
+
</Skeleton>
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
</Card>
|
|
658
|
+
</div>
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
export default StudentCardSkeleton;
|
|
662
|
+
`.trim(),
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
[
|
|
668
|
+
'A composed Card layout with a circular Avatar skeleton alongside stacked Heading skeletons —',
|
|
669
|
+
'mirroring the shape and position of the real content.',
|
|
670
|
+
'All Skeleton instances share a single `loading` state and reveal their children together when the data arrives.',
|
|
671
|
+
].join(' '),
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
export const ExplicitDimensions: Story = withDescription(
|
|
675
|
+
{
|
|
676
|
+
name: 'Explicit dimensions',
|
|
677
|
+
render: ExplicitDimensionsTmpl,
|
|
678
|
+
parameters: {
|
|
679
|
+
controls: { disable: true },
|
|
680
|
+
docs: {
|
|
681
|
+
source: {
|
|
682
|
+
language: 'tsx',
|
|
683
|
+
code: `
|
|
684
|
+
import { Skeleton } from '@arbor-education/design-system.components';
|
|
685
|
+
|
|
686
|
+
// Use explicit width/height when you cannot wrap the real element —
|
|
687
|
+
// for example, when approximating text whose content is unknown at load time.
|
|
688
|
+
function TextLineSkeleton() {
|
|
689
|
+
return (
|
|
690
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }}>
|
|
691
|
+
<Skeleton width="100%" height="1em" />
|
|
692
|
+
<Skeleton width="85%" height="1em" />
|
|
693
|
+
<Skeleton width="60%" height="1em" />
|
|
694
|
+
</div>
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
export default TextLineSkeleton;
|
|
698
|
+
`.trim(),
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
[
|
|
704
|
+
'When you cannot wrap the real element — for example, when loading a paragraph whose text is unknown — use `width` and `height` to approximate the shape.',
|
|
705
|
+
'Pass `height="1em"` to match the current line height, and vary widths to suggest natural text breaks.',
|
|
706
|
+
].join(' '),
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
export const AccessibilityPattern: Story = withDescription(
|
|
710
|
+
{
|
|
711
|
+
name: 'Accessibility pattern',
|
|
712
|
+
render: AccessibilityPatternTmpl,
|
|
713
|
+
parameters: {
|
|
714
|
+
controls: { disable: true },
|
|
715
|
+
docs: {
|
|
716
|
+
source: {
|
|
717
|
+
language: 'tsx',
|
|
718
|
+
code: `
|
|
719
|
+
import { useState } from 'react';
|
|
720
|
+
import { Skeleton } from '@arbor-education/design-system.components';
|
|
721
|
+
|
|
722
|
+
function StudentProfileSection({ isLoading }: { isLoading: boolean }) {
|
|
723
|
+
return (
|
|
724
|
+
<section aria-live="polite" aria-busy={isLoading}>
|
|
725
|
+
<h3>Student profile</h3>
|
|
726
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }}>
|
|
727
|
+
<Skeleton loading={isLoading} width="180px" height="16px">
|
|
728
|
+
<span>Maya Patel — Year 10</span>
|
|
729
|
+
</Skeleton>
|
|
730
|
+
<Skeleton loading={isLoading} width="140px" height="16px">
|
|
731
|
+
<span>Form: 10B/Tu</span>
|
|
732
|
+
</Skeleton>
|
|
733
|
+
</div>
|
|
734
|
+
</section>
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
export default StudentProfileSection;
|
|
738
|
+
`.trim(),
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
[
|
|
744
|
+
'Screen readers do not announce when `loading` flips to `false` — the pulsing placeholder is `aria-hidden="true"` and unmounts silently.',
|
|
745
|
+
'Wrap the region in `aria-live="polite"` with `aria-busy` so assistive technology announces when content becomes available.',
|
|
746
|
+
'Click the button to see the loading-to-loaded transition.',
|
|
747
|
+
].join(' '),
|
|
748
|
+
);
|