@arbor-education/design-system.components 0.16.1 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.gather/instructions/project-overview.md +0 -4
  2. package/.gather/skills/aroo-hunni/SKILL.md +58 -0
  3. package/CHANGELOG.md +31 -0
  4. package/CONTRIBUTING.md +1 -0
  5. package/dist/components/arborLogo/ArborLogo.d.ts +9 -0
  6. package/dist/components/arborLogo/ArborLogo.d.ts.map +1 -0
  7. package/dist/components/arborLogo/ArborLogo.js +17 -0
  8. package/dist/components/arborLogo/ArborLogo.js.map +1 -0
  9. package/dist/components/arborLogo/ArborLogo.stories.d.ts +94 -0
  10. package/dist/components/arborLogo/ArborLogo.stories.d.ts.map +1 -0
  11. package/dist/components/arborLogo/ArborLogo.stories.js +418 -0
  12. package/dist/components/arborLogo/ArborLogo.stories.js.map +1 -0
  13. package/dist/components/arborLogo/ArborLogo.test.d.ts +2 -0
  14. package/dist/components/arborLogo/ArborLogo.test.d.ts.map +1 -0
  15. package/dist/components/arborLogo/ArborLogo.test.js +32 -0
  16. package/dist/components/arborLogo/ArborLogo.test.js.map +1 -0
  17. package/dist/components/dataViewCard/DataViewCard.d.ts +19 -0
  18. package/dist/components/dataViewCard/DataViewCard.d.ts.map +1 -0
  19. package/dist/components/dataViewCard/DataViewCard.js +13 -0
  20. package/dist/components/dataViewCard/DataViewCard.js.map +1 -0
  21. package/dist/components/dataViewCard/DataViewCard.stories.d.ts +100 -0
  22. package/dist/components/dataViewCard/DataViewCard.stories.d.ts.map +1 -0
  23. package/dist/components/dataViewCard/DataViewCard.stories.js +317 -0
  24. package/dist/components/dataViewCard/DataViewCard.stories.js.map +1 -0
  25. package/dist/components/dataViewCard/DataViewCard.test.d.ts +2 -0
  26. package/dist/components/dataViewCard/DataViewCard.test.d.ts.map +1 -0
  27. package/dist/components/dataViewCard/DataViewCard.test.js +67 -0
  28. package/dist/components/dataViewCard/DataViewCard.test.js.map +1 -0
  29. package/dist/components/row/Row.d.ts +2 -1
  30. package/dist/components/row/Row.d.ts.map +1 -1
  31. package/dist/components/row/Row.js +2 -2
  32. package/dist/components/row/Row.js.map +1 -1
  33. package/dist/components/treeRow/TreeRow.d.ts +32 -0
  34. package/dist/components/treeRow/TreeRow.d.ts.map +1 -0
  35. package/dist/components/treeRow/TreeRow.js +19 -0
  36. package/dist/components/treeRow/TreeRow.js.map +1 -0
  37. package/dist/components/treeRow/TreeRow.stories.d.ts +13 -0
  38. package/dist/components/treeRow/TreeRow.stories.d.ts.map +1 -0
  39. package/dist/components/treeRow/TreeRow.stories.js +774 -0
  40. package/dist/components/treeRow/TreeRow.stories.js.map +1 -0
  41. package/dist/components/treeRow/TreeRow.test.d.ts +2 -0
  42. package/dist/components/treeRow/TreeRow.test.d.ts.map +1 -0
  43. package/dist/components/treeRow/TreeRow.test.js +262 -0
  44. package/dist/components/treeRow/TreeRow.test.js.map +1 -0
  45. package/dist/components/treeRow/TreeRowSection.d.ts +12 -0
  46. package/dist/components/treeRow/TreeRowSection.d.ts.map +1 -0
  47. package/dist/components/treeRow/TreeRowSection.js +20 -0
  48. package/dist/components/treeRow/TreeRowSection.js.map +1 -0
  49. package/dist/index.css +146 -1
  50. package/dist/index.css.map +1 -1
  51. package/dist/index.d.ts +4 -1
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +4 -1
  54. package/dist/index.js.map +1 -1
  55. package/package.json +2 -1
  56. package/src/components/arborLogo/ArborLogo.stories.tsx +663 -0
  57. package/src/components/arborLogo/ArborLogo.test.tsx +36 -0
  58. package/src/components/arborLogo/ArborLogo.tsx +92 -0
  59. package/src/components/arborLogo/__snapshots__/ArborLogo.test.tsx.snap +424 -0
  60. package/src/components/dataViewCard/DataViewCard.stories.tsx +464 -0
  61. package/src/components/dataViewCard/DataViewCard.test.tsx +127 -0
  62. package/src/components/dataViewCard/DataViewCard.tsx +62 -0
  63. package/src/components/dataViewCard/dataViewCard.scss +25 -0
  64. package/src/components/row/Row.tsx +4 -1
  65. package/src/components/row/row.scss +9 -1
  66. package/src/components/treeRow/TreeRow.stories.tsx +870 -0
  67. package/src/components/treeRow/TreeRow.test.tsx +371 -0
  68. package/src/components/treeRow/TreeRow.tsx +85 -0
  69. package/src/components/treeRow/TreeRowSection.tsx +56 -0
  70. package/src/components/treeRow/treeRow.scss +134 -0
  71. package/src/docs/Contributing.mdx +1 -0
  72. package/src/index.scss +2 -0
  73. package/src/index.ts +4 -1
@@ -0,0 +1,663 @@
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 { ArborLogo } from './ArborLogo';
12
+
13
+ const DESCRIPTION_INTRO = `
14
+ ArborLogo renders the Arbor Education brand mark as an inline SVG with a built-in \`<title>\` element for accessibility.
15
+
16
+ Reach for it whenever the Arbor identity needs to appear on screen — application headers, login and splash screens, homepage links, email and report headers, and any dark hero panel that calls for the white wordmark variant.
17
+ `.trim();
18
+
19
+ const USAGE_GUIDANCE = `
20
+ ### When to use
21
+
22
+ - **Application headers** — the icon-only mark fits compact top-nav bars and rails where horizontal space is precious.
23
+ - **Login, onboarding, and splash screens** — the full wordmark establishes brand context the moment a user lands.
24
+ - **Homepage links** — wrap the logo in an anchor and pass \`aria-label="Arbor home"\` so assistive tech announces the destination clearly.
25
+ - **Email and report headers** — the dark wordmark on a light background is the canonical lockup for printed and emailed artefacts.
26
+ - **Dark hero panels and sidebars** — switch to \`textColor="white"\` so the wordmark stays legible.
27
+
28
+ ---
29
+
30
+ ### When NOT to use
31
+
32
+ | Scenario | Use instead |
33
+ |---|---|
34
+ | Generic decorative iconography | [\`Icon\`](?path=/docs/components-icon--docs) |
35
+ | Arbitrary image asset | A plain \`<img>\` with the hosted asset URL |
36
+ | A school's crest or logo | A separate, school-supplied image — ArborLogo is the *Arbor Education* mark only |
37
+ | Repeating decorative flourish where any abstract shape would do | Don't dilute the brand — pick a neutral shape or pattern instead |
38
+ `.trim();
39
+
40
+ const DEVELOPER_NOTES = `
41
+ ### Critical usage patterns
42
+
43
+ **\`textColor\` is silently ignored when \`showText\` is falsy.** The icon-only mark uses fixed brand colours that cannot be customised — \`textColor\` only affects the wordmark.
44
+
45
+ **\`aria-label\` becomes the \`<title>\` text, not an \`aria-label\` attribute.** If you inspect the DOM you'll find \`aria-labelledby="..."\` pointing to a \`<title>\` element, not \`aria-label="..."\` on the SVG itself. The two are functionally equivalent for screen readers but visually different in DevTools.
46
+
47
+ **\`aria-hidden\` strips ALL accessibility attributes.** When set, the SVG drops \`role\`, \`aria-labelledby\`, and the inner \`<title>\` element entirely — and any \`aria-label\` you pass is silently ignored. Don't combine the two.
48
+
49
+ **The "black" wordmark is \`#2F2F2F\`, not pure black.** That's a brand decision, equivalent to \`--color-grey-900\`. If a designer asks why it isn't \`#000\`, that's the answer.
50
+
51
+ **SVG dimensions are hardcoded.** The icon-only variant is 25×25 px and the full logo is 66×26 px. To resize, apply CSS via \`className\` (for example \`width: 120px; height: auto\`). Setting width without \`height: auto\` distorts the aspect ratio. There are no \`width\` or \`height\` props.
52
+
53
+ **Multiple ArborLogos on one page won't clash.** Internal \`<clipPath>\` and \`<title>\` IDs are generated with React's \`useId()\`, so duplicate instances stay isolated.
54
+
55
+ ---
56
+
57
+ ### Accessibility
58
+
59
+ - By default the SVG has \`role="img"\` and the inner \`<title>\` provides the accessible name (\`"Arbor"\` unless overridden).
60
+ - For brand context override, pass \`aria-label\` — for example \`aria-label="Arbor home"\` when the logo links to the homepage.
61
+ - For decorative use alongside visible "Arbor" text, pass \`aria-hidden\` so screen readers don't announce the brand twice.
62
+ - Never combine \`aria-label\` and \`aria-hidden\` — \`aria-label\` is ignored when the SVG is hidden.
63
+
64
+ ---
65
+
66
+ ### TypeScript types
67
+
68
+ \`\`\`ts
69
+ import type { ArborLogoProps } from '@arbor-education/design-system.components';
70
+ \`\`\`
71
+
72
+ | Prop | Type | Default |
73
+ |---|---|---|
74
+ | \`showText\` | \`boolean\` | \`undefined\` (icon-only) |
75
+ | \`textColor\` | \`'white' \\| 'black'\` | \`'black'\` |
76
+ | \`aria-label\` | \`string\` | \`'Arbor'\` |
77
+ | \`aria-hidden\` | \`boolean\` | \`undefined\` |
78
+ | \`className\` | \`string\` | \`undefined\` |
79
+ `.trim();
80
+
81
+ const RELATED_COMPONENTS = `
82
+ ## Related components
83
+
84
+ [Icon](?path=/docs/components-icon--docs)
85
+ `.trim();
86
+
87
+ const PROPS_INTRO
88
+ = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
89
+
90
+ function ArborLogoDocsPage() {
91
+ return (
92
+ <>
93
+ <Title />
94
+ <Subtitle />
95
+ <Markdown>{DESCRIPTION_INTRO}</Markdown>
96
+ <DocHeading>Interactive example</DocHeading>
97
+ <Markdown>{PROPS_INTRO}</Markdown>
98
+ <DocPrimary />
99
+ <Controls />
100
+ <DocHeading>Usage guidance</DocHeading>
101
+ <Markdown>{USAGE_GUIDANCE}</Markdown>
102
+ <DocHeading>Developer notes</DocHeading>
103
+ <Markdown>{DEVELOPER_NOTES}</Markdown>
104
+ <DocHeading>Examples</DocHeading>
105
+ <Stories title="" />
106
+ <Markdown>{RELATED_COMPONENTS}</Markdown>
107
+ </>
108
+ );
109
+ }
110
+
111
+ const meta = {
112
+ title: 'Components/ArborLogo',
113
+ component: ArborLogo,
114
+ tags: ['autodocs'],
115
+ parameters: {
116
+ layout: 'padded',
117
+ docs: {
118
+ page: ArborLogoDocsPage,
119
+ },
120
+ },
121
+ argTypes: {
122
+ 'showText': {
123
+ description:
124
+ 'When true, renders the full 66×26 px logo with wordmark. When falsy, renders the 25×25 px icon-only mark.',
125
+ control: { type: 'boolean' },
126
+ table: {
127
+ type: { summary: 'boolean' },
128
+ defaultValue: { summary: 'undefined' },
129
+ },
130
+ },
131
+ 'textColor': {
132
+ description:
133
+ 'Wordmark colour. Only takes effect when `showText` is true. The "black" value renders as `#2F2F2F` (equivalent to `--color-grey-900`).',
134
+ control: { type: 'radio' },
135
+ options: ['white', 'black'],
136
+ table: {
137
+ type: { summary: '\'white\' | \'black\'' },
138
+ defaultValue: { summary: '\'black\'' },
139
+ },
140
+ },
141
+ 'aria-label': {
142
+ description:
143
+ 'Accessible name. Rendered as the SVG\'s inner `<title>` text and referenced via `aria-labelledby`. Ignored when `aria-hidden` is set.',
144
+ control: { type: 'text' },
145
+ table: {
146
+ type: { summary: 'string' },
147
+ defaultValue: { summary: '\'Arbor\'' },
148
+ },
149
+ },
150
+ 'aria-hidden': {
151
+ description:
152
+ 'When truthy, marks the SVG as decorative — strips `role`, `aria-labelledby`, and the inner `<title>`. Use when adjacent visible text already conveys the brand name.',
153
+ control: { type: 'boolean' },
154
+ table: {
155
+ type: { summary: 'boolean' },
156
+ defaultValue: { summary: 'undefined' },
157
+ },
158
+ },
159
+ 'className': {
160
+ description:
161
+ 'Class applied to the root SVG. Use this to resize via CSS (for example `width: 120px; height: auto`) — there are no width/height props.',
162
+ control: { type: 'text' },
163
+ table: {
164
+ type: { summary: 'string' },
165
+ },
166
+ },
167
+ },
168
+ } satisfies Meta<typeof ArborLogo>;
169
+
170
+ export default meta;
171
+
172
+ // Use StoryObj<typeof ArborLogo> (not typeof meta) so render-only stories are
173
+ // not forced to provide every required arg — template components handle their own instances.
174
+ type Story = StoryObj<typeof ArborLogo>;
175
+
176
+ const withDescription = (story: Story, description: string): Story => ({
177
+ ...story,
178
+ parameters: {
179
+ ...story.parameters,
180
+ docs: {
181
+ ...story.parameters?.docs,
182
+ description: {
183
+ story: description,
184
+ },
185
+ },
186
+ },
187
+ });
188
+
189
+ export const Default: Story = withDescription(
190
+ {
191
+ args: {
192
+ 'showText': true,
193
+ 'textColor': 'black',
194
+ 'aria-label': 'Arbor',
195
+ 'aria-hidden': false,
196
+ },
197
+ decorators: [
198
+ (StoryFn, context) => {
199
+ const isDark = context.args.textColor === 'white';
200
+ return (
201
+ <div
202
+ style={{
203
+ alignItems: 'center',
204
+ backgroundColor: isDark
205
+ ? 'var(--color-grey-800)'
206
+ : 'var(--color-grey-050)',
207
+ borderRadius: 'var(--border-radius-xsmall)',
208
+ display: 'flex',
209
+ justifyContent: 'center',
210
+ padding: 'var(--spacing-xlarge)',
211
+ }}
212
+ >
213
+ <StoryFn />
214
+ </div>
215
+ );
216
+ },
217
+ ],
218
+ render: args => <ArborLogo {...args} />,
219
+ },
220
+ 'Interactive playground. Toggle `showText` to switch between the icon-only mark and the full wordmark, then flip `textColor` — the wrapper background swaps automatically so the white variant stays visible.',
221
+ );
222
+
223
+ export const IconOnly: Story = withDescription(
224
+ {
225
+ parameters: {
226
+ controls: { disable: true },
227
+ docs: {
228
+ source: {
229
+ language: 'tsx',
230
+ code: `
231
+ import { ArborLogo } from '@arbor-education/design-system.components';
232
+
233
+ function ArborLogoIconOnlyExample() {
234
+ return <ArborLogo aria-label="Arbor" />;
235
+ }
236
+ export default ArborLogoIconOnlyExample;
237
+ `.trim(),
238
+ },
239
+ },
240
+ },
241
+ render: () => (
242
+ <div
243
+ style={{
244
+ alignItems: 'center',
245
+ backgroundColor: 'var(--color-grey-050)',
246
+ borderRadius: 'var(--border-radius-xsmall)',
247
+ display: 'flex',
248
+ justifyContent: 'center',
249
+ padding: 'var(--spacing-xlarge)',
250
+ }}
251
+ >
252
+ <ArborLogo aria-label="Arbor" />
253
+ </div>
254
+ ),
255
+ },
256
+ 'The 25×25 px icon-only mark. Reach for this in compact spaces — top-nav rails, app icons, favourited shortcuts — where the wordmark would crowd the layout.',
257
+ );
258
+
259
+ export const WithWordmark: Story = withDescription(
260
+ {
261
+ parameters: {
262
+ controls: { disable: true },
263
+ docs: {
264
+ source: {
265
+ language: 'tsx',
266
+ code: `
267
+ import { ArborLogo } from '@arbor-education/design-system.components';
268
+
269
+ function ArborLogoWithWordmarkExample() {
270
+ return <ArborLogo showText aria-label="Arbor" />;
271
+ }
272
+ export default ArborLogoWithWordmarkExample;
273
+ `.trim(),
274
+ },
275
+ },
276
+ },
277
+ render: () => (
278
+ <div
279
+ style={{
280
+ alignItems: 'center',
281
+ backgroundColor: 'var(--color-grey-050)',
282
+ borderRadius: 'var(--border-radius-xsmall)',
283
+ display: 'flex',
284
+ justifyContent: 'center',
285
+ padding: 'var(--spacing-xlarge)',
286
+ }}
287
+ >
288
+ <ArborLogo showText aria-label="Arbor" />
289
+ </div>
290
+ ),
291
+ },
292
+ 'The canonical brand lockup — full 66×26 px logo with the dark `#2F2F2F` wordmark on a light surface. Use this for login screens, email headers, and printed reports.',
293
+ );
294
+
295
+ const WhiteWordmarkTemplate = () => (
296
+ <div
297
+ style={{
298
+ display: 'flex',
299
+ flexDirection: 'column',
300
+ gap: 'var(--spacing-large)',
301
+ }}
302
+ >
303
+ <div
304
+ style={{
305
+ alignItems: 'center',
306
+ backgroundColor: 'var(--color-grey-800)',
307
+ borderRadius: 'var(--border-radius-xsmall)',
308
+ display: 'flex',
309
+ justifyContent: 'center',
310
+ padding: 'var(--spacing-xlarge)',
311
+ }}
312
+ >
313
+ <ArborLogo showText textColor="white" aria-label="Arbor" />
314
+ </div>
315
+ <span
316
+ className="ds-text"
317
+ style={{
318
+ color: 'var(--color-grey-600)',
319
+ fontStyle: 'italic',
320
+ }}
321
+ >
322
+ The dark background is part of the story, not the component — pair the
323
+ white wordmark with a sufficiently dark surface in your own layout.
324
+ </span>
325
+ </div>
326
+ );
327
+
328
+ export const WhiteWordmark: Story = withDescription(
329
+ {
330
+ parameters: {
331
+ controls: { disable: true },
332
+ docs: {
333
+ source: {
334
+ language: 'tsx',
335
+ code: `
336
+ import { ArborLogo } from '@arbor-education/design-system.components';
337
+
338
+ function ArborLogoWhiteWordmarkExample() {
339
+ return (
340
+ <div style={{ background: 'var(--color-grey-800)', padding: 'var(--spacing-xlarge)', display: 'inline-flex' }}>
341
+ <ArborLogo showText textColor="white" aria-label="Arbor" />
342
+ </div>
343
+ );
344
+ }
345
+ export default ArborLogoWhiteWordmarkExample;
346
+ `.trim(),
347
+ },
348
+ },
349
+ },
350
+ render: WhiteWordmarkTemplate,
351
+ },
352
+ 'Switch the wordmark to white for dark hero panels, marketing splash screens, and inverted sidebars. The component itself does nothing to the surrounding background — that is the consumer\'s responsibility.',
353
+ );
354
+
355
+ const DecorativeTemplate = () => (
356
+ <div
357
+ style={{
358
+ display: 'flex',
359
+ flexDirection: 'column',
360
+ gap: 'var(--spacing-xxlarge)',
361
+ }}
362
+ >
363
+ <div
364
+ style={{
365
+ display: 'flex',
366
+ flexDirection: 'column',
367
+ gap: 'var(--spacing-large)',
368
+ }}
369
+ >
370
+ <div
371
+ style={{
372
+ alignItems: 'center',
373
+ backgroundColor: 'var(--color-grey-050)',
374
+ borderRadius: 'var(--border-radius-xsmall)',
375
+ display: 'flex',
376
+ gap: 'var(--spacing-small)',
377
+ padding: 'var(--spacing-xlarge)',
378
+ }}
379
+ >
380
+ <ArborLogo aria-hidden />
381
+ <span className="ds-text">Arbor</span>
382
+ </div>
383
+ <span
384
+ className="ds-text"
385
+ style={{
386
+ color: 'var(--color-grey-600)',
387
+ fontStyle: 'italic',
388
+ }}
389
+ >
390
+ Screen reader announces:
391
+ {' '}
392
+ <strong>&ldquo;Arbor&rdquo;</strong>
393
+ {' '}
394
+ — once, from the visible text.
395
+ </span>
396
+ </div>
397
+
398
+ <div
399
+ style={{
400
+ display: 'flex',
401
+ flexDirection: 'column',
402
+ gap: 'var(--spacing-large)',
403
+ }}
404
+ >
405
+ <div
406
+ style={{
407
+ alignItems: 'center',
408
+ backgroundColor: 'var(--color-grey-050)',
409
+ borderRadius: 'var(--border-radius-xsmall)',
410
+ display: 'flex',
411
+ gap: 'var(--spacing-small)',
412
+ padding: 'var(--spacing-xlarge)',
413
+ }}
414
+ >
415
+ <ArborLogo showText aria-hidden />
416
+ <span className="ds-text">— Dashboard</span>
417
+ </div>
418
+ <span
419
+ className="ds-text"
420
+ style={{
421
+ color: 'var(--color-grey-600)',
422
+ fontStyle: 'italic',
423
+ }}
424
+ >
425
+ Screen reader announces:
426
+ {' '}
427
+ <strong>&ldquo;— Dashboard&rdquo;</strong>
428
+ {' '}
429
+ — the logo is skipped because the wordmark is already visible.
430
+ </span>
431
+ </div>
432
+ </div>
433
+ );
434
+
435
+ export const Decorative: Story = withDescription(
436
+ {
437
+ parameters: {
438
+ controls: { disable: true },
439
+ docs: {
440
+ source: {
441
+ language: 'tsx',
442
+ code: `
443
+ import { ArborLogo } from '@arbor-education/design-system.components';
444
+
445
+ function ArborLogoDecorativeExample() {
446
+ return (
447
+ <header style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
448
+ <ArborLogo aria-hidden />
449
+ <span>Arbor</span>
450
+ </header>
451
+ );
452
+ }
453
+ export default ArborLogoDecorativeExample;
454
+ `.trim(),
455
+ },
456
+ },
457
+ },
458
+ render: DecorativeTemplate,
459
+ },
460
+ 'Use `aria-hidden` whenever an adjacent visible "Arbor" wordmark already announces the brand — repeating it via the SVG just doubles the screen-reader output. Both examples below show the same content with the logo correctly hidden from assistive tech.',
461
+ );
462
+
463
+ const CustomAccessibleNameTemplate = () => (
464
+ <div
465
+ style={{
466
+ display: 'flex',
467
+ flexDirection: 'column',
468
+ gap: 'var(--spacing-large)',
469
+ }}
470
+ >
471
+ <div
472
+ style={{
473
+ alignItems: 'center',
474
+ backgroundColor: 'var(--color-grey-050)',
475
+ borderRadius: 'var(--border-radius-xsmall)',
476
+ display: 'flex',
477
+ justifyContent: 'center',
478
+ padding: 'var(--spacing-xlarge)',
479
+ }}
480
+ >
481
+ <a
482
+ href="/"
483
+ style={{
484
+ alignItems: 'center',
485
+ display: 'inline-flex',
486
+ padding: 'var(--spacing-small)',
487
+ textDecoration: 'none',
488
+ }}
489
+ >
490
+ <ArborLogo showText aria-label="Arbor home" />
491
+ </a>
492
+ </div>
493
+ <span
494
+ className="ds-text"
495
+ style={{
496
+ color: 'var(--color-grey-600)',
497
+ fontStyle: 'italic',
498
+ }}
499
+ >
500
+ Screen reader announces:
501
+ {' '}
502
+ <strong>&ldquo;Arbor home, link&rdquo;</strong>
503
+ {' '}
504
+ — the accessible name clarifies that the brand mark is also the homepage destination.
505
+ </span>
506
+ </div>
507
+ );
508
+
509
+ export const CustomAccessibleName: Story = withDescription(
510
+ {
511
+ parameters: {
512
+ controls: { disable: true },
513
+ docs: {
514
+ source: {
515
+ language: 'tsx',
516
+ code: `
517
+ import { ArborLogo } from '@arbor-education/design-system.components';
518
+
519
+ function ArborLogoCustomAccessibleNameExample() {
520
+ return (
521
+ <a href="/">
522
+ <ArborLogo showText aria-label="Arbor home" />
523
+ </a>
524
+ );
525
+ }
526
+ export default ArborLogoCustomAccessibleNameExample;
527
+ `.trim(),
528
+ },
529
+ },
530
+ },
531
+ render: CustomAccessibleNameTemplate,
532
+ },
533
+ 'When the logo is the homepage link, override `aria-label` so screen-reader users hear the destination, not just the brand name. The default `"Arbor"` label is fine for purely decorative brand placement, but link targets deserve more context.',
534
+ );
535
+
536
+ // Inline <style> tags inside the template inject the CSS rules referenced by
537
+ // the className examples below. In a real app these rules would live in your
538
+ // stylesheet — Storybook can't resize the SVG without them because width/height
539
+ // are baked into the SVG attributes.
540
+ const CustomSizingTemplate = () => (
541
+ <>
542
+ <style>
543
+ {`
544
+ .my-arbor-logo--favicon { width: 16px; height: auto; }
545
+ .my-arbor-logo--hero { width: 240px; height: auto; }
546
+ `}
547
+ </style>
548
+ <div
549
+ style={{
550
+ display: 'flex',
551
+ flexDirection: 'column',
552
+ gap: 'var(--spacing-large)',
553
+ }}
554
+ >
555
+ <div
556
+ style={{
557
+ alignItems: 'flex-end',
558
+ backgroundColor: 'var(--color-grey-050)',
559
+ borderRadius: 'var(--border-radius-xsmall)',
560
+ display: 'flex',
561
+ gap: 'var(--spacing-xxlarge)',
562
+ justifyContent: 'center',
563
+ padding: 'var(--spacing-xlarge)',
564
+ }}
565
+ >
566
+ <div
567
+ style={{
568
+ alignItems: 'center',
569
+ display: 'flex',
570
+ flexDirection: 'column',
571
+ gap: 'var(--spacing-small)',
572
+ }}
573
+ >
574
+ <ArborLogo showText aria-label="Arbor" className="my-arbor-logo--favicon" />
575
+ <span
576
+ className="ds-text"
577
+ style={{ color: 'var(--color-grey-600)' }}
578
+ >
579
+ 16 px (favicon)
580
+ </span>
581
+ </div>
582
+ <div
583
+ style={{
584
+ alignItems: 'center',
585
+ display: 'flex',
586
+ flexDirection: 'column',
587
+ gap: 'var(--spacing-small)',
588
+ }}
589
+ >
590
+ <ArborLogo showText aria-label="Arbor" />
591
+ <span
592
+ className="ds-text"
593
+ style={{ color: 'var(--color-grey-600)' }}
594
+ >
595
+ 66 px (default)
596
+ </span>
597
+ </div>
598
+ <div
599
+ style={{
600
+ alignItems: 'center',
601
+ display: 'flex',
602
+ flexDirection: 'column',
603
+ gap: 'var(--spacing-small)',
604
+ }}
605
+ >
606
+ <ArborLogo showText aria-label="Arbor" className="my-arbor-logo--hero" />
607
+ <span
608
+ className="ds-text"
609
+ style={{ color: 'var(--color-grey-600)' }}
610
+ >
611
+ 240 px (hero / login)
612
+ </span>
613
+ </div>
614
+ </div>
615
+ <span
616
+ className="ds-text"
617
+ style={{
618
+ color: 'var(--color-grey-600)',
619
+ fontStyle: 'italic',
620
+ }}
621
+ >
622
+ Resizing happens via CSS in your own stylesheet — for example
623
+ {' '}
624
+ <code>.my-arbor-logo--hero &#123; width: 240px; height: auto; &#125;</code>
625
+ . Always set
626
+ {' '}
627
+ <code>height: auto</code>
628
+ {' '}
629
+ alongside
630
+ {' '}
631
+ <code>width</code>
632
+ {' '}
633
+ to preserve the 66:26 aspect ratio.
634
+ </span>
635
+ </div>
636
+ </>
637
+ );
638
+
639
+ export const CustomSizing: Story = withDescription(
640
+ {
641
+ parameters: {
642
+ controls: { disable: true },
643
+ docs: {
644
+ source: {
645
+ language: 'tsx',
646
+ code: `
647
+ import { ArborLogo } from '@arbor-education/design-system.components';
648
+
649
+ // In your stylesheet:
650
+ // .my-arbor-logo--hero { width: 240px; height: auto; }
651
+
652
+ function ArborLogoCustomSizingExample() {
653
+ return <ArborLogo showText aria-label="Arbor" className="my-arbor-logo--hero" />;
654
+ }
655
+ export default ArborLogoCustomSizingExample;
656
+ `.trim(),
657
+ },
658
+ },
659
+ },
660
+ render: CustomSizingTemplate,
661
+ },
662
+ 'SVG width and height are baked into the component — to resize, target the SVG with CSS via `className`. Always pair `width` with `height: auto` to preserve the 66:26 aspect ratio. The three sizes shown here cover the realistic range: favicon, default, and login hero.',
663
+ );
@@ -0,0 +1,36 @@
1
+ import { expect, describe, it } from 'vitest';
2
+ import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/vitest';
4
+ import { ArborLogo } from './ArborLogo';
5
+
6
+ describe('ArborLogo', () => {
7
+ it('renders the icon-only variant', () => {
8
+ const { container } = render(<ArborLogo />);
9
+ expect(container.firstChild).toMatchSnapshot();
10
+ });
11
+
12
+ it('renders the icon-only variant with aria-hidden', () => {
13
+ const { container } = render(<ArborLogo aria-hidden />);
14
+ expect(container.firstChild).toMatchSnapshot();
15
+ });
16
+
17
+ it('renders the icon-only variant with a custom aria-label', () => {
18
+ const { container } = render(<ArborLogo aria-label="Arbor Education logo" />);
19
+ expect(container.firstChild).toMatchSnapshot();
20
+ });
21
+
22
+ it('renders the text variant with black text', () => {
23
+ const { container } = render(<ArborLogo showText />);
24
+ expect(container.firstChild).toMatchSnapshot();
25
+ });
26
+
27
+ it('renders the text variant with white text', () => {
28
+ const { container } = render(<ArborLogo showText textColor="white" />);
29
+ expect(container.firstChild).toMatchSnapshot();
30
+ });
31
+
32
+ it('renders the text variant with aria-hidden', () => {
33
+ const { container } = render(<ArborLogo showText aria-hidden />);
34
+ expect(container.firstChild).toMatchSnapshot();
35
+ });
36
+ });