@arbor-education/design-system.components 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/.gather/skills/write-stories/README.md +8 -4
  2. package/.gather/skills/write-stories/SKILL.md +59 -5
  3. package/CHANGELOG.md +24 -0
  4. package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts +1 -0
  5. package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts.map +1 -1
  6. package/dist/components/breadcrumbs/Breadcrumbs.stories.js +103 -21
  7. package/dist/components/breadcrumbs/Breadcrumbs.stories.js.map +1 -1
  8. package/dist/components/card/Card.stories.d.ts.map +1 -1
  9. package/dist/components/card/Card.stories.js +46 -41
  10. package/dist/components/card/Card.stories.js.map +1 -1
  11. package/dist/components/editableText/EditableText.stories.d.ts.map +1 -1
  12. package/dist/components/editableText/EditableText.stories.js +19 -6
  13. package/dist/components/editableText/EditableText.stories.js.map +1 -1
  14. package/dist/components/filterBar/FilterBar.d.ts +2 -2
  15. package/dist/components/filterBar/FilterBar.d.ts.map +1 -1
  16. package/dist/components/filterBar/FilterBar.js +7 -3
  17. package/dist/components/filterBar/FilterBar.js.map +1 -1
  18. package/dist/components/filterBar/FilterBar.stories.d.ts +16 -0
  19. package/dist/components/filterBar/FilterBar.stories.d.ts.map +1 -1
  20. package/dist/components/filterBar/FilterBar.stories.js +101 -7
  21. package/dist/components/filterBar/FilterBar.stories.js.map +1 -1
  22. package/dist/components/filterBar/FilterBar.test.js +30 -0
  23. package/dist/components/filterBar/FilterBar.test.js.map +1 -1
  24. package/dist/components/formField/fieldset/Fieldset.stories.d.ts.map +1 -1
  25. package/dist/components/formField/fieldset/Fieldset.stories.js +5 -3
  26. package/dist/components/formField/fieldset/Fieldset.stories.js.map +1 -1
  27. package/dist/components/row/Row.stories.d.ts.map +1 -1
  28. package/dist/components/row/Row.stories.js +22 -13
  29. package/dist/components/row/Row.stories.js.map +1 -1
  30. package/dist/components/sidebar/Sidebar.stories.d.ts.map +1 -1
  31. package/dist/components/sidebar/Sidebar.stories.js +19 -24
  32. package/dist/components/sidebar/Sidebar.stories.js.map +1 -1
  33. package/dist/components/table/Table.d.ts.map +1 -1
  34. package/dist/components/table/Table.js +2 -1
  35. package/dist/components/table/Table.js.map +1 -1
  36. package/dist/components/table/Table.stories.d.ts +7 -0
  37. package/dist/components/table/Table.stories.d.ts.map +1 -1
  38. package/dist/components/table/Table.stories.js +60 -10
  39. package/dist/components/table/Table.stories.js.map +1 -1
  40. package/dist/components/table/Table.test.js +23 -0
  41. package/dist/components/table/Table.test.js.map +1 -1
  42. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.d.ts.map +1 -1
  43. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js +8 -5
  44. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js.map +1 -1
  45. package/dist/components/table/tableControls/TableControls.d.ts +2 -0
  46. package/dist/components/table/tableControls/TableControls.d.ts.map +1 -1
  47. package/dist/components/table/tableControls/TableControls.js +2 -2
  48. package/dist/components/table/tableControls/TableControls.js.map +1 -1
  49. package/dist/components/table/tableControls/TableControls.stories.d.ts +26 -1
  50. package/dist/components/table/tableControls/TableControls.stories.d.ts.map +1 -1
  51. package/dist/components/table/tableControls/TableControls.stories.js +50 -21
  52. package/dist/components/table/tableControls/TableControls.stories.js.map +1 -1
  53. package/dist/components/table/tableControls/TableControls.test.js +16 -0
  54. package/dist/components/table/tableControls/TableControls.test.js.map +1 -1
  55. package/dist/components/table/useTableSettings.d.ts +2 -0
  56. package/dist/components/table/useTableSettings.d.ts.map +1 -1
  57. package/dist/components/table/useTableSettings.js +12 -7
  58. package/dist/components/table/useTableSettings.js.map +1 -1
  59. package/dist/components/widget/Widget.stories.d.ts +13 -0
  60. package/dist/components/widget/Widget.stories.d.ts.map +1 -1
  61. package/dist/components/widget/Widget.stories.js +45 -6
  62. package/dist/components/widget/Widget.stories.js.map +1 -1
  63. package/dist/components/widgetCalendar/WidgetCalendar.stories.d.ts +88 -16
  64. package/dist/components/widgetCalendar/WidgetCalendar.stories.d.ts.map +1 -1
  65. package/dist/components/widgetCalendar/WidgetCalendar.stories.js +443 -30
  66. package/dist/components/widgetCalendar/WidgetCalendar.stories.js.map +1 -1
  67. package/dist/index.css +1 -4
  68. package/dist/index.css.map +1 -1
  69. package/package.json +1 -1
  70. package/src/components/breadcrumbs/Breadcrumbs.stories.tsx +112 -21
  71. package/src/components/card/Card.stories.tsx +46 -41
  72. package/src/components/datePicker/datePicker.scss +0 -5
  73. package/src/components/editableText/EditableText.stories.tsx +26 -29
  74. package/src/components/filterBar/FilterBar.stories.tsx +126 -4
  75. package/src/components/filterBar/FilterBar.test.tsx +51 -0
  76. package/src/components/filterBar/FilterBar.tsx +18 -12
  77. package/src/components/formField/fieldset/Fieldset.stories.tsx +5 -3
  78. package/src/components/row/Row.stories.tsx +20 -14
  79. package/src/components/sidebar/Sidebar.stories.tsx +61 -112
  80. package/src/components/table/Table.stories.tsx +82 -10
  81. package/src/components/table/Table.test.tsx +42 -0
  82. package/src/components/table/Table.tsx +2 -0
  83. package/src/components/table/cellRenderers/SelectDropdownCellRenderer.stories.tsx +8 -5
  84. package/src/components/table/tableControls/TableControls.stories.tsx +54 -21
  85. package/src/components/table/tableControls/TableControls.test.tsx +20 -0
  86. package/src/components/table/tableControls/TableControls.tsx +6 -0
  87. package/src/components/table/useTableSettings.ts +20 -6
  88. package/src/components/widget/Widget.stories.tsx +61 -6
  89. package/src/components/widgetCalendar/WidgetCalendar.stories.tsx +547 -74
  90. package/src/tokens.scss +1 -0
@@ -1,4 +1,5 @@
1
1
  import type { StoryObj } from '@storybook/react-vite';
2
+ import { WidgetCalendar } from './WidgetCalendar.js';
2
3
  declare function WidgetCalendarDocsPage(): import("react").JSX.Element;
3
4
  declare const meta: {
4
5
  title: string;
@@ -33,28 +34,99 @@ declare const meta: {
33
34
  };
34
35
  };
35
36
  tags: string[];
36
- args: {
37
- heading: string;
38
- isFloating: true;
37
+ argTypes: {
38
+ heading: {
39
+ control: "text";
40
+ description: string;
41
+ table: {
42
+ type: {
43
+ summary: string;
44
+ };
45
+ };
46
+ };
47
+ tooltip: {
48
+ control: "text";
49
+ description: string;
50
+ table: {
51
+ type: {
52
+ summary: string;
53
+ };
54
+ };
55
+ };
56
+ headingHref: {
57
+ control: "text";
58
+ description: string;
59
+ table: {
60
+ type: {
61
+ summary: string;
62
+ };
63
+ };
64
+ };
39
65
  items: {
40
- id: string;
41
- eventType?: import("./WidgetCalendarItem.js").WidgetCalendarEventType;
42
- eventPrimaryTerm: React.ReactNode;
43
- eventPrimaryDefinition: React.ReactNode;
44
- eventSecondaryTerm: React.ReactNode;
45
- eventDateTime: string;
46
- eventTimeLabel: string;
47
- eventLocation?: string;
48
- eventSecondaryDefinition?: React.ReactNode;
49
- buttonA11yText?: string;
50
- }[];
51
- onItemClick: import("vitest").Mock<(...args: any[]) => any>;
66
+ control: false;
67
+ description: string;
68
+ table: {
69
+ type: {
70
+ summary: string;
71
+ };
72
+ };
73
+ };
74
+ isFloating: {
75
+ control: "boolean";
76
+ description: string;
77
+ table: {
78
+ type: {
79
+ summary: string;
80
+ };
81
+ defaultValue: {
82
+ summary: string;
83
+ };
84
+ };
85
+ };
86
+ onItemClick: {
87
+ control: false;
88
+ description: string;
89
+ table: {
90
+ type: {
91
+ summary: string;
92
+ };
93
+ };
94
+ };
95
+ emptyState: {
96
+ control: false;
97
+ description: string;
98
+ table: {
99
+ type: {
100
+ summary: string;
101
+ };
102
+ defaultValue: {
103
+ summary: string;
104
+ };
105
+ };
106
+ };
107
+ className: {
108
+ control: false;
109
+ description: string;
110
+ table: {
111
+ type: {
112
+ summary: string;
113
+ };
114
+ };
115
+ };
52
116
  };
53
117
  };
54
118
  export default meta;
55
- type Story = StoryObj<typeof meta>;
119
+ type Story = StoryObj<typeof WidgetCalendar>;
56
120
  export declare const Default: Story;
121
+ export declare const Interactive: Story;
122
+ export declare const PrimaryEventType: Story;
123
+ export declare const SecondaryEventType: Story;
124
+ export declare const NonInteractiveItems: Story;
125
+ export declare const WithoutLocation: Story;
126
+ export declare const CustomSecondaryContent: Story;
127
+ export declare const WithTooltip: Story;
57
128
  export declare const Empty: Story;
129
+ export declare const CustomEmptyState: Story;
58
130
  export declare const FixedHeightOverflow: Story;
59
131
  export declare const Composable: Story;
60
132
  //# sourceMappingURL=WidgetCalendar.stories.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"WidgetCalendar.stories.d.ts","sourceRoot":"","sources":["../../../src/components/widgetCalendar/WidgetCalendar.stories.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAQ,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAuF5D,iBAAS,sBAAsB,gCAa9B;AA2DD,QAAA,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAc6B,CAAC;AAExC,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,OAAO,EAAE,KAMrB,CAAC;AAEF,eAAO,MAAM,KAAK,EAAE,KAMnB,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,KAMjC,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KA4BxB,CAAC"}
1
+ {"version":3,"file":"WidgetCalendar.stories.d.ts","sourceRoot":"","sources":["../../../src/components/widgetCalendar/WidgetCalendar.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAa5D,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AA0JrD,iBAAS,sBAAsB,gCAmB9B;AAMD,QAAA,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiF6B,CAAC;AAExC,eAAe,IAAI,CAAC;AAGpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,cAAc,CAAC,CAAC;AA0G7C,eAAO,MAAM,OAAO,EAAE,KAgBrB,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,KAsCzB,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,KA6B9B,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,KA6BhC,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,KA6BjC,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,KAwB7B,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,KA0BpC,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,KAoBzB,CAAC;AAEF,eAAO,MAAM,KAAK,EAAE,KAUnB,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,KAiB9B,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,KAmBjC,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KAkDxB,CAAC"}
@@ -1,8 +1,12 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Controls, Heading as DocHeading, Primary as DocPrimary, Markdown, Stories, Subtitle, Title, } from '@storybook/addon-docs/blocks';
2
+ import { Controls, Heading as DocHeading, Markdown, Primary as DocPrimary, Stories, Subtitle, Title, } from '@storybook/addon-docs/blocks';
3
+ import { useState } from 'react';
3
4
  import { Widget } from '../widget/Widget.js';
4
5
  import { fn } from 'storybook/test';
5
6
  import { WidgetCalendar } from './WidgetCalendar.js';
7
+ // ---------------------------------------------------------------------------
8
+ // Fixture data
9
+ // ---------------------------------------------------------------------------
6
10
  const classEventTime = {
7
11
  eventSecondaryTerm: 'Time and location',
8
12
  eventDateTime: '2026-06-08T08:45/2026-06-08T09:00',
@@ -76,24 +80,181 @@ const overflowItems = [
76
80
  ...initialItems,
77
81
  ...initialItems.map((item, index) => ({ ...item, id: `${item.id}-overflow-${index}` })),
78
82
  ];
83
+ // ---------------------------------------------------------------------------
84
+ // Docs page content
85
+ // ---------------------------------------------------------------------------
79
86
  const DESCRIPTION_INTRO = [
80
- '`WidgetCalendar` is a pre-composed dashboard widget for upcoming calendar events.',
81
- 'It wraps [`Widget`](?path=/docs/components-widget--docs) with event rows, type indicators, and optional navigation.',
82
- 'Pass ISO `eventDateTime` and a formatted `eventTimeLabel` for each item — the component renders a semantic `<time>` element, matching the pattern used by `WidgetNotifications`.',
83
- ].join(' ');
87
+ '`WidgetCalendar` is a pre-composed dashboard widget for date/time-anchored calendar events.',
88
+ 'It wraps [`Widget`](?path=/docs/components-widget--docs) with an event list, primary/secondary type indicators, and',
89
+ 'optional heading navigation. Each item renders a semantic `<time>` element from an ISO `eventDateTime` and a',
90
+ 'human-readable `eventTimeLabel`.',
91
+ ].join('\n');
92
+ const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
93
+ const USAGE_GUIDANCE = [
94
+ '### When to use',
95
+ '',
96
+ '- Date/time-anchored list items — events, appointments, or timetable entries — that need a semantic `<time>` element',
97
+ '- Optional location context alongside the time (classroom, hall, room number)',
98
+ '- A dashboard widget where rows may optionally navigate somewhere on click',
99
+ '',
100
+ '---',
101
+ '',
102
+ '### When NOT to use',
103
+ '',
104
+ '| Situation | Use instead |',
105
+ '|---|---|',
106
+ '| Favourite/shortcut links with no date fields, needing a remove action | [`WidgetFavourites`](?path=/docs/components-widget-widgetfavourites--docs) (link + remove pattern) |',
107
+ '| Read/unread notification feed with a mark-all-as-read action | [`WidgetNotifications`](?path=/docs/components-widget-widgetnotifications--docs) |',
108
+ ].join('\n');
109
+ const DEVELOPER_NOTES = [
110
+ '### Critical usage patterns',
111
+ '',
112
+ '**`onItemClick` signature differs between `WidgetCalendar` and `WidgetCalendar.Item`.** The parent\'s',
113
+ '`onItemClick` is `(id: string) => void` — it receives the clicked item\'s id. `WidgetCalendar.Item`\'s own',
114
+ '`onItemClick` prop takes **no arguments** (`() => void`). When composing manually (see the Composable',
115
+ 'story), you must close over the id yourself: `onItemClick={() => handleClick(item.id)}`.',
116
+ '',
117
+ '**`buttonA11yText` fallback only fires for plain strings.** When `onItemClick` makes a row interactive,',
118
+ 'the accessible label defaults to `Open ${eventPrimaryDefinition}` — but only when `eventPrimaryDefinition`',
119
+ 'is a plain string. Pass a JSX node as the primary definition and the fallback silently degrades to the',
120
+ 'generic `\'Open event\'`. Always supply `buttonA11yText` explicitly when `eventPrimaryDefinition` is rich content.',
121
+ '',
122
+ '**`eventSecondaryDefinition` is an all-or-nothing override.** When provided, it entirely replaces the',
123
+ 'default time + location composition — `eventLocation` becomes dead data with no warning. Never pass both',
124
+ 'in the same item.',
125
+ '',
126
+ '**The type-bar colour is semantic, not decorative.** `eventType="primary"` renders the brand-green bar',
127
+ '(`--widget-calendar-type-primary-color`), `"secondary"` renders neutral grey',
128
+ '(`--widget-calendar-type-secondary-color`). Choose deliberately — don\'t treat it as a purely visual toggle.',
129
+ '',
130
+ '**No loading, disabled, or error state exists on this component.** Handle those states in the consumer',
131
+ '(e.g. render a Skeleton in place of `WidgetCalendar` while data loads).',
132
+ '',
133
+ '---',
134
+ '',
135
+ '### Accessibility',
136
+ '',
137
+ '- Each item renders a `<dl>` with `sr-only` `<dt>` terms and visible `<dd>` definitions, pairing both the',
138
+ ' primary and secondary content for screen readers.',
139
+ '- `eventDateTime` must be a valid ISO datetime string — it is passed directly to the `<time dateTime>` attribute.',
140
+ '- Interactive rows (`onItemClick` provided) get `role="button"`, `tabIndex={0}`, and respond to both',
141
+ ' Enter and Space, matching the standard button-role keyboard contract.',
142
+ '- Rows without `onItemClick` render as plain, non-interactive `<li>` elements — no button semantics are added.',
143
+ ].join('\n');
144
+ const RELATED_COMPONENTS = [
145
+ '## Related components',
146
+ '',
147
+ '[Widget](?path=/docs/components-widget--docs) · [WidgetFavourites](?path=/docs/components-widget-widgetfavourites--docs) · [WidgetNotifications](?path=/docs/components-widget-widgetnotifications--docs)',
148
+ ].join('\n');
84
149
  function WidgetCalendarDocsPage() {
85
- return (_jsxs(_Fragment, { children: [_jsx(Title, {}), _jsx(Subtitle, {}), _jsx(Markdown, { children: DESCRIPTION_INTRO }), _jsx(DocHeading, { children: "Interactive example" }), _jsx(DocPrimary, {}), _jsx(Controls, {}), _jsx(DocHeading, { children: "Examples" }), _jsx(Stories, { title: "" })] }));
150
+ return (_jsxs(_Fragment, { children: [_jsx(Title, {}), _jsx(Subtitle, {}), _jsx(Markdown, { children: DESCRIPTION_INTRO }), _jsx(DocHeading, { children: "Interactive example" }), _jsx(Markdown, { children: PROPS_INTRO }), _jsx(DocPrimary, {}), _jsx(Controls, {}), _jsx(DocHeading, { children: "Usage guidance" }), _jsx(Markdown, { children: USAGE_GUIDANCE }), _jsx(DocHeading, { children: "Developer notes" }), _jsx(Markdown, { children: DEVELOPER_NOTES }), _jsx(DocHeading, { children: "Examples" }), _jsx(Stories, { title: "" }), _jsx(Markdown, { children: RELATED_COMPONENTS })] }));
86
151
  }
152
+ // ---------------------------------------------------------------------------
153
+ // Meta
154
+ // ---------------------------------------------------------------------------
155
+ const meta = {
156
+ title: 'Components/Widget/WidgetCalendar',
157
+ component: WidgetCalendar,
158
+ parameters: {
159
+ layout: 'padded',
160
+ docs: {
161
+ page: WidgetCalendarDocsPage,
162
+ },
163
+ },
164
+ tags: ['autodocs'],
165
+ argTypes: {
166
+ heading: {
167
+ control: 'text',
168
+ description: 'Content rendered in the widget header. Usually a plain string, but accepts any `React.ReactNode`.',
169
+ table: {
170
+ type: { summary: 'React.ReactNode' },
171
+ },
172
+ },
173
+ tooltip: {
174
+ control: 'text',
175
+ description: 'Optional tooltip text shown alongside the heading, for supplementary context.',
176
+ table: {
177
+ type: { summary: 'string' },
178
+ },
179
+ },
180
+ headingHref: {
181
+ control: 'text',
182
+ description: 'When provided, renders the heading as a link to this URL (e.g. a full calendar page).',
183
+ table: {
184
+ type: { summary: 'string' },
185
+ },
186
+ },
187
+ items: {
188
+ control: false,
189
+ description: [
190
+ 'Array of `WidgetCalendar.ItemData` objects rendered as event rows. Each item requires `id`,',
191
+ '`eventPrimaryTerm`, `eventPrimaryDefinition`, `eventSecondaryTerm`, `eventDateTime`, and `eventTimeLabel`.',
192
+ '`eventType`, `eventLocation`, `eventSecondaryDefinition`, and `buttonA11yText` are optional.',
193
+ ].join(' '),
194
+ table: {
195
+ type: { summary: 'WidgetCalendar.ItemData[]' },
196
+ },
197
+ },
198
+ isFloating: {
199
+ control: 'boolean',
200
+ description: 'Applies the floating (elevated/shadowed) widget style. Defaults to `false`.',
201
+ table: {
202
+ type: { summary: 'boolean' },
203
+ defaultValue: { summary: 'false' },
204
+ },
205
+ },
206
+ onItemClick: {
207
+ control: false,
208
+ description: [
209
+ 'Called with the clicked item\'s `id` when a row is activated (click, Enter, or Space).',
210
+ 'Rows with no `onItemClick` render as plain, non-interactive list items.',
211
+ ].join(' '),
212
+ table: {
213
+ type: { summary: '(id: string) => void' },
214
+ },
215
+ },
216
+ emptyState: {
217
+ control: false,
218
+ description: [
219
+ 'Custom content rendered in place of the event list when `items` is empty.',
220
+ 'Defaults to a built-in EmptyState with a date icon, "Nothing scheduled" heading, and',
221
+ '"There are no upcoming events." sub-heading.',
222
+ ].join(' '),
223
+ table: {
224
+ type: { summary: 'React.ReactNode' },
225
+ defaultValue: { summary: 'Built-in EmptyState ("Nothing scheduled")' },
226
+ },
227
+ },
228
+ className: {
229
+ control: false,
230
+ description: 'Additional CSS class names on the root Widget element. Use sparingly.',
231
+ table: {
232
+ type: { summary: 'string' },
233
+ },
234
+ },
235
+ },
236
+ };
237
+ export default meta;
238
+ // ---------------------------------------------------------------------------
239
+ // Helper: attach a per-story description to docs
240
+ // ---------------------------------------------------------------------------
87
241
  const withDescription = (story, description) => ({
88
242
  ...story,
89
243
  parameters: {
90
244
  ...story.parameters,
91
245
  docs: {
92
246
  ...story.parameters?.docs,
93
- description: { story: description },
247
+ description: {
248
+ story: description,
249
+ },
94
250
  },
95
251
  },
96
252
  });
253
+ // ---------------------------------------------------------------------------
254
+ // Template components for composition and stateful stories.
255
+ // Named components avoid react-hooks lint issues — the react-hooks ESLint
256
+ // plugin is NOT configured in this project, so do NOT add eslint-disable.
257
+ // ---------------------------------------------------------------------------
97
258
  function FixedHeightWidgetContainer({ children, height = 280, }) {
98
259
  return (_jsx("div", { style: {
99
260
  height,
@@ -102,39 +263,291 @@ function FixedHeightWidgetContainer({ children, height = 280, }) {
102
263
  border: '1px dashed var(--color-grey-300)',
103
264
  }, children: children }));
104
265
  }
105
- const DefaultTemplate = () => (_jsx(WidgetCalendar, { heading: "Calendar", headingHref: "/calendar", isFloating: true, items: initialItems, onItemClick: fn() }));
106
- const EmptyTemplate = () => (_jsx(WidgetCalendar, { heading: "Calendar", isFloating: true, items: [] }));
107
- const FixedHeightOverflowTemplate = () => (_jsx(FixedHeightWidgetContainer, { children: _jsx(WidgetCalendar, { heading: "Calendar", isFloating: true, items: overflowItems, onItemClick: fn() }) }));
108
- const meta = {
109
- title: 'Components/Widget/WidgetCalendar',
110
- component: WidgetCalendar,
111
- parameters: {
112
- layout: 'padded',
113
- docs: { page: WidgetCalendarDocsPage },
114
- },
115
- tags: ['autodocs'],
266
+ const InteractiveTemplate = () => {
267
+ const [lastClickedId, setLastClickedId] = useState(null);
268
+ const lastClickedLabel = lastClickedId
269
+ ? initialItems.find(item => item.id === lastClickedId)?.eventPrimaryDefinition
270
+ : null;
271
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx(WidgetCalendar, { heading: "Calendar", isFloating: true, items: initialItems, onItemClick: setLastClickedId }), _jsx("span", { className: "ds-text", style: { color: 'var(--color-grey-600)' }, children: lastClickedLabel
272
+ ? (_jsxs(_Fragment, { children: ["Last clicked:", ' ', lastClickedLabel] }))
273
+ : 'Click an event to see its id captured here.' })] }));
274
+ };
275
+ const handleComposableItemClick = fn();
276
+ const ComposableTemplate = () => (_jsxs(Widget, { isFloating: true, children: [_jsx(Widget.Header, { children: "Calendar" }), _jsx(Widget.Body, { children: _jsx("ul", { className: "ds-widget-calendar__list", children: initialItems.slice(0, 2).map(item => (_jsx(WidgetCalendar.Item, { id: item.id, eventType: item.eventType, eventPrimaryTerm: item.eventPrimaryTerm, eventPrimaryDefinition: item.eventPrimaryDefinition, eventSecondaryTerm: item.eventSecondaryTerm, eventDateTime: item.eventDateTime, eventTimeLabel: item.eventTimeLabel, eventLocation: item.eventLocation, onItemClick: () => handleComposableItemClick(item.id) }, item.id))) }) })] }));
277
+ // ---------------------------------------------------------------------------
278
+ // Stories
279
+ // ---------------------------------------------------------------------------
280
+ export const Default = withDescription({
116
281
  args: {
117
282
  heading: 'Calendar',
283
+ headingHref: '/calendar',
118
284
  isFloating: true,
119
285
  items: initialItems,
120
286
  onItemClick: fn(),
121
287
  },
122
- };
123
- export default meta;
124
- export const Default = withDescription({
125
- render: DefaultTemplate,
288
+ render: args => _jsx(WidgetCalendar, { ...args }),
289
+ }, [
290
+ 'The interactive canvas — every prop is wired to the Controls panel below.',
291
+ 'A linked header, floating style, a realistic mix of primary (green) and secondary (grey) event types,',
292
+ 'and clickable rows. This is the only story where combining every prop together is the point.',
293
+ ].join(' '));
294
+ export const Interactive = withDescription({
295
+ render: InteractiveTemplate,
296
+ parameters: {
297
+ controls: { disable: true },
298
+ docs: {
299
+ source: {
300
+ language: 'tsx',
301
+ code: `
302
+ import { useState } from 'react';
303
+ import { WidgetCalendar } from '@arbor-education/design-system.components';
304
+
305
+ function InteractiveCalendarExample() {
306
+ const [lastClickedId, setLastClickedId] = useState<string | null>(null);
307
+
308
+ return (
309
+ <>
310
+ <WidgetCalendar
311
+ heading="Calendar"
312
+ isFloating
313
+ items={items}
314
+ onItemClick={setLastClickedId}
315
+ />
316
+ <span>Last clicked id: {lastClickedId ?? 'none yet'}</span>
317
+ </>
318
+ );
319
+ }
320
+ export default InteractiveCalendarExample;
321
+ `.trim(),
322
+ },
323
+ },
324
+ },
325
+ }, [
326
+ 'Demonstrates `onItemClick(id)` behaviourally: clicking a row calls back with that item\'s `id`,',
327
+ 'tracked here in a named template component\'s `useState` and displayed below the widget as',
328
+ '"Last clicked: {label}". In a real consumer, use the id to navigate or open a detail view.',
329
+ ].join(' '));
330
+ export const PrimaryEventType = withDescription({
331
+ args: {
332
+ heading: 'Calendar',
333
+ items: [
334
+ {
335
+ id: 'year-10-10g',
336
+ eventType: 'primary',
337
+ eventPrimaryTerm: 'Class',
338
+ eventPrimaryDefinition: 'Year 10: 10G',
339
+ ...classEventTime,
340
+ },
341
+ {
342
+ id: 'maths-year-10',
343
+ eventType: 'primary',
344
+ eventPrimaryTerm: 'Class',
345
+ eventPrimaryDefinition: 'Maths: Year 10: 10x/Ma3',
346
+ ...classEventTime,
347
+ },
348
+ ],
349
+ onItemClick: fn(),
350
+ },
351
+ render: args => _jsx(WidgetCalendar, { ...args }),
126
352
  parameters: { controls: { disable: true } },
127
- }, 'Linked header with primary (green) and secondary (grey) event type indicators. Rows are keyboard-focusable when `onItemClick` is provided.');
353
+ }, [
354
+ 'Isolates `eventType="primary"` — the brand-green type bar. This is also the default when',
355
+ '`eventType` is omitted entirely.',
356
+ ].join(' '));
357
+ export const SecondaryEventType = withDescription({
358
+ args: {
359
+ heading: 'Calendar',
360
+ items: [
361
+ {
362
+ id: 'class-generic',
363
+ eventType: 'secondary',
364
+ eventPrimaryTerm: 'Class',
365
+ eventPrimaryDefinition: 'Class',
366
+ ...classEventTime,
367
+ },
368
+ {
369
+ id: 'class-generic-2',
370
+ eventType: 'secondary',
371
+ eventPrimaryTerm: 'Class',
372
+ eventPrimaryDefinition: 'Class',
373
+ eventDateTime: '2026-06-08T10:15/2026-06-08T11:00',
374
+ eventTimeLabel: '10:15 – 11:00',
375
+ eventLocation: 'Hall',
376
+ eventSecondaryTerm: 'Time and location',
377
+ },
378
+ ],
379
+ onItemClick: fn(),
380
+ },
381
+ render: args => _jsx(WidgetCalendar, { ...args }),
382
+ parameters: { controls: { disable: true } },
383
+ }, 'Isolates `eventType="secondary"` — the neutral grey type bar, used for lower-priority or generic events.');
384
+ export const NonInteractiveItems = withDescription({
385
+ args: {
386
+ heading: 'Calendar',
387
+ items: [
388
+ {
389
+ id: 'year-10-10g',
390
+ eventType: 'primary',
391
+ eventPrimaryTerm: 'Class',
392
+ eventPrimaryDefinition: 'Year 10: 10G',
393
+ ...classEventTime,
394
+ },
395
+ {
396
+ id: 'maths-year-10',
397
+ eventType: 'secondary',
398
+ eventPrimaryTerm: 'Class',
399
+ eventPrimaryDefinition: 'Maths: Year 10: 10x/Ma3',
400
+ ...classEventTime,
401
+ },
402
+ ],
403
+ },
404
+ render: args => _jsx(WidgetCalendar, { ...args }),
405
+ parameters: { controls: { disable: true } },
406
+ }, [
407
+ 'No `onItemClick` is passed. Rows render as plain, non-interactive `<li>` elements —',
408
+ 'no `role="button"`, no `tabIndex`, no hover chevron-to-arrow swap. The absence of interactivity',
409
+ 'is exactly what this story demonstrates.',
410
+ ].join(' '));
411
+ export const WithoutLocation = withDescription({
412
+ args: {
413
+ heading: 'Calendar',
414
+ items: [
415
+ {
416
+ id: 'staff-briefing',
417
+ eventType: 'primary',
418
+ eventPrimaryTerm: 'Meeting',
419
+ eventPrimaryDefinition: 'Staff briefing',
420
+ eventSecondaryTerm: 'Time',
421
+ eventDateTime: '2026-06-08T08:00/2026-06-08T08:15',
422
+ eventTimeLabel: '08:00 – 08:15',
423
+ },
424
+ ],
425
+ onItemClick: fn(),
426
+ },
427
+ render: args => _jsx(WidgetCalendar, { ...args }),
428
+ parameters: { controls: { disable: true } },
429
+ }, [
430
+ '`eventLocation` is omitted and `eventSecondaryDefinition` is not set, so the secondary line',
431
+ 'renders as time-only — no trailing comma fragment where the location would otherwise appear.',
432
+ ].join(' '));
433
+ export const CustomSecondaryContent = withDescription({
434
+ args: {
435
+ heading: 'Calendar',
436
+ items: [
437
+ {
438
+ id: 'exam-window',
439
+ eventType: 'primary',
440
+ eventPrimaryTerm: 'Exam',
441
+ eventPrimaryDefinition: 'GCSE Maths: Paper 2',
442
+ eventSecondaryTerm: 'Details',
443
+ eventDateTime: '2026-06-08T09:00/2026-06-08T10:30',
444
+ eventTimeLabel: '09:00 – 10:30',
445
+ eventSecondaryDefinition: 'Silent conditions — no calculators permitted',
446
+ },
447
+ ],
448
+ onItemClick: fn(),
449
+ },
450
+ render: args => _jsx(WidgetCalendar, { ...args }),
451
+ parameters: { controls: { disable: true } },
452
+ }, [
453
+ '`eventSecondaryDefinition` replaces the default time + location line entirely with custom content.',
454
+ 'Note that `eventLocation` is deliberately not passed alongside it here — the two are an all-or-nothing',
455
+ 'pair, and passing both would silently drop the location, which would be misleading to show in a story.',
456
+ ].join(' '));
457
+ export const WithTooltip = withDescription({
458
+ args: {
459
+ heading: 'Calendar',
460
+ tooltip: 'Shows your next 5 scheduled events',
461
+ items: [
462
+ {
463
+ id: 'year-10-10g',
464
+ eventType: 'primary',
465
+ eventPrimaryTerm: 'Class',
466
+ eventPrimaryDefinition: 'Year 10: 10G',
467
+ ...classEventTime,
468
+ },
469
+ ],
470
+ onItemClick: fn(),
471
+ },
472
+ render: args => _jsx(WidgetCalendar, { ...args }),
473
+ parameters: { controls: { disable: true } },
474
+ }, 'Adding `tooltip` shows supplementary context alongside the heading. A single event keeps the focus on the tooltip.');
128
475
  export const Empty = withDescription({
129
- render: EmptyTemplate,
476
+ args: {
477
+ heading: 'Calendar',
478
+ items: [],
479
+ },
480
+ render: args => _jsx(WidgetCalendar, { ...args }),
481
+ parameters: { controls: { disable: true } },
482
+ }, 'An empty `items` array shows the default built-in EmptyState — a date icon, "Nothing scheduled", and "There are no upcoming events."');
483
+ export const CustomEmptyState = withDescription({
484
+ args: {
485
+ heading: 'Calendar',
486
+ items: [],
487
+ emptyState: (_jsx("div", { style: { padding: 'var(--spacing-large)', textAlign: 'center' }, children: _jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "No events for the rest of term. Enjoy the break!" }) })),
488
+ },
489
+ render: args => _jsx(WidgetCalendar, { ...args }),
130
490
  parameters: { controls: { disable: true } },
131
- }, 'Empty items array shows the default EmptyState inside the Widget body.');
491
+ }, 'Overrides the default EmptyState with custom content via the `emptyState` prop when the default copy does not fit the context.');
132
492
  export const FixedHeightOverflow = withDescription({
133
- render: FixedHeightOverflowTemplate,
493
+ args: {
494
+ heading: 'Calendar',
495
+ items: overflowItems,
496
+ onItemClick: fn(),
497
+ },
498
+ render: args => (_jsx(FixedHeightWidgetContainer, { children: _jsx(WidgetCalendar, { ...args }) })),
134
499
  parameters: { controls: { disable: true } },
135
- }, 'Parent container constrains height; the header stays pinned while the event list scrolls inside Widget.Body.');
500
+ }, [
501
+ 'A parent container with a fixed 280px height constrains the widget — the header stays pinned while',
502
+ 'the 14-item event list scrolls inside `Widget.Body`. The dashed border marks the constraining wrapper,',
503
+ 'not part of the widget itself.',
504
+ ].join(' '));
136
505
  export const Composable = withDescription({
137
- render: () => (_jsxs(Widget, { isFloating: true, children: [_jsx(Widget.Header, { children: "Calendar" }), _jsx(Widget.Body, { children: _jsx("ul", { className: "ds-widget-calendar__list", children: initialItems.slice(0, 2).map(item => (_jsx(WidgetCalendar.Item, { id: item.id, eventType: item.eventType, eventPrimaryTerm: item.eventPrimaryTerm, eventPrimaryDefinition: item.eventPrimaryDefinition, eventSecondaryTerm: item.eventSecondaryTerm, eventDateTime: item.eventDateTime, eventTimeLabel: item.eventTimeLabel, eventLocation: item.eventLocation, onItemClick: fn() }, item.id))) }) })] })),
138
- parameters: { controls: { disable: true } },
139
- }, 'Compose manually with `WidgetCalendar.Item` inside `Widget.Body` when you need custom list logic beyond the `items` prop.');
506
+ render: ComposableTemplate,
507
+ parameters: {
508
+ controls: { disable: true },
509
+ docs: {
510
+ source: {
511
+ language: 'tsx',
512
+ code: `
513
+ import { Widget, WidgetCalendar } from '@arbor-education/design-system.components';
514
+
515
+ function ComposableCalendarExample() {
516
+ return (
517
+ <Widget isFloating>
518
+ <Widget.Header>Calendar</Widget.Header>
519
+ <Widget.Body>
520
+ <ul className="ds-widget-calendar__list">
521
+ {items.map(item => (
522
+ <WidgetCalendar.Item
523
+ key={item.id}
524
+ id={item.id}
525
+ eventType={item.eventType}
526
+ eventPrimaryTerm={item.eventPrimaryTerm}
527
+ eventPrimaryDefinition={item.eventPrimaryDefinition}
528
+ eventSecondaryTerm={item.eventSecondaryTerm}
529
+ eventDateTime={item.eventDateTime}
530
+ eventTimeLabel={item.eventTimeLabel}
531
+ eventLocation={item.eventLocation}
532
+ // WidgetCalendar.Item's onItemClick takes NO id argument, unlike
533
+ // WidgetCalendar's own onItemClick — close over the id yourself.
534
+ onItemClick={() => handleItemClick(item.id)}
535
+ />
536
+ ))}
537
+ </ul>
538
+ </Widget.Body>
539
+ </Widget>
540
+ );
541
+ }
542
+ export default ComposableCalendarExample;
543
+ `.trim(),
544
+ },
545
+ },
546
+ },
547
+ }, [
548
+ 'Composes `Widget` and `WidgetCalendar.Item` manually — bypassing the `items` prop entirely — to prove',
549
+ '`WidgetCalendar.Item` is genuine public API for custom list logic. Note the closure: `WidgetCalendar.Item`\'s',
550
+ 'own `onItemClick` takes no id argument, so each item must close over its own `id` (`() => handleClick(item.id)`),',
551
+ 'unlike the parent `WidgetCalendar`\'s `onItemClick(id)`.',
552
+ ].join(' '));
140
553
  //# sourceMappingURL=WidgetCalendar.stories.js.map