@object-ui/components 0.5.0 → 3.0.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 (119) hide show
  1. package/.turbo/turbo-build.log +12 -25
  2. package/CHANGELOG.md +32 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.js +23987 -22576
  5. package/dist/index.umd.cjs +30 -30
  6. package/dist/src/custom/action-param-dialog.d.ts +21 -0
  7. package/dist/src/custom/index.d.ts +4 -0
  8. package/dist/src/custom/navigation-overlay.d.ts +50 -0
  9. package/dist/src/custom/view-skeleton.d.ts +37 -0
  10. package/dist/src/custom/view-states.d.ts +33 -0
  11. package/dist/src/index.d.ts +1 -0
  12. package/dist/src/renderers/action/action-button.d.ts +11 -0
  13. package/dist/src/renderers/action/action-group.d.ts +25 -0
  14. package/dist/src/renderers/action/action-icon.d.ts +10 -0
  15. package/dist/src/renderers/action/action-menu.d.ts +19 -0
  16. package/dist/src/renderers/action/index.d.ts +0 -0
  17. package/dist/src/renderers/action/resolve-icon.d.ts +6 -0
  18. package/package.json +20 -19
  19. package/src/__tests__/PageRendererRegions.test.tsx +664 -55
  20. package/src/__tests__/__snapshots__/snapshot-critical.test.tsx.snap +811 -0
  21. package/src/__tests__/__snapshots__/snapshot.test.tsx.snap +327 -0
  22. package/src/__tests__/accessibility.test.tsx +137 -0
  23. package/src/__tests__/api-consistency.test.tsx +596 -0
  24. package/src/__tests__/color-contrast.test.tsx +212 -0
  25. package/src/__tests__/compliance.test.tsx +72 -0
  26. package/src/__tests__/edge-cases.test.tsx +285 -0
  27. package/src/__tests__/navigation-overlay.test.tsx +273 -0
  28. package/src/__tests__/snapshot-critical.test.tsx +317 -0
  29. package/src/__tests__/snapshot.test.tsx +205 -0
  30. package/src/__tests__/view-compliance.test.tsx +153 -0
  31. package/src/__tests__/wcag-audit.test.tsx +493 -0
  32. package/src/custom/action-param-dialog.tsx +264 -0
  33. package/src/custom/index.ts +4 -0
  34. package/src/custom/navigation-overlay.tsx +296 -0
  35. package/src/custom/view-skeleton.tsx +243 -0
  36. package/src/custom/view-states.tsx +153 -0
  37. package/src/index.ts +1 -0
  38. package/src/renderers/action/action-button.tsx +147 -0
  39. package/src/renderers/action/action-group.tsx +270 -0
  40. package/src/renderers/action/action-icon.tsx +150 -0
  41. package/src/renderers/action/action-menu.tsx +203 -0
  42. package/src/renderers/action/index.ts +18 -0
  43. package/src/renderers/action/resolve-icon.ts +35 -0
  44. package/src/renderers/complex/__tests__/data-table-batch-editing.test.tsx +275 -0
  45. package/src/renderers/complex/__tests__/data-table-cell-renderer.test.tsx +120 -0
  46. package/src/renderers/complex/__tests__/data-table-editing.test.tsx +221 -0
  47. package/src/renderers/complex/data-table.tsx +269 -33
  48. package/src/renderers/complex/resizable.tsx +20 -17
  49. package/src/renderers/data-display/list.tsx +1 -1
  50. package/src/renderers/data-display/table.tsx +1 -1
  51. package/src/renderers/data-display/tree-view.tsx +2 -1
  52. package/src/renderers/form/form.tsx +33 -10
  53. package/src/renderers/index.ts +1 -0
  54. package/src/renderers/layout/aspect-ratio.tsx +1 -1
  55. package/src/renderers/layout/page.tsx +416 -52
  56. package/src/renderers/navigation/sidebar.tsx +6 -0
  57. package/src/renderers/placeholders.tsx +2 -2
  58. package/src/stories/MockedData.stories.tsx +87 -37
  59. package/src/stories-json/Accessibility.mdx +297 -0
  60. package/src/stories-json/EdgeCases.stories.tsx +160 -0
  61. package/src/stories-json/GettingStarted.mdx +89 -0
  62. package/src/stories-json/Introduction.mdx +127 -0
  63. package/src/stories-json/accordion.stories.tsx +1 -1
  64. package/src/stories-json/aggrid.stories.tsx +1 -1
  65. package/src/stories-json/alert.stories.tsx +1 -1
  66. package/src/stories-json/aspect-ratio.stories.tsx +1 -1
  67. package/src/stories-json/avatar.stories.tsx +1 -1
  68. package/src/stories-json/badge.stories.tsx +1 -1
  69. package/src/stories-json/breadcrumb.stories.tsx +1 -1
  70. package/src/stories-json/button-group.stories.tsx +1 -1
  71. package/src/stories-json/button.stories.tsx +1 -1
  72. package/src/stories-json/calendar.stories.tsx +1 -1
  73. package/src/stories-json/card.stories.tsx +1 -1
  74. package/src/stories-json/carousel.stories.tsx +1 -1
  75. package/src/stories-json/charts.stories.tsx +1 -1
  76. package/src/stories-json/chatbot.stories.tsx +1 -1
  77. package/src/stories-json/code-editor.stories.tsx +1 -1
  78. package/src/stories-json/collapsible.stories.tsx +1 -1
  79. package/src/stories-json/controls.stories.tsx +1 -1
  80. package/src/stories-json/crm-live-data.stories.tsx +154 -0
  81. package/src/stories-json/data-table.stories.tsx +80 -4
  82. package/src/stories-json/data_display_extras.stories.tsx +1 -1
  83. package/src/stories-json/date-picker.stories.tsx +1 -1
  84. package/src/stories-json/detail-view.stories.tsx +1 -1
  85. package/src/stories-json/dialog.stories.tsx +1 -1
  86. package/src/stories-json/feedback_extras.stories.tsx +1 -1
  87. package/src/stories-json/feedback_others.stories.tsx +1 -1
  88. package/src/stories-json/form-variants.stories.tsx +210 -0
  89. package/src/stories-json/form_advanced.stories.tsx +1 -1
  90. package/src/stories-json/form_extras.stories.tsx +1 -1
  91. package/src/stories-json/grid.stories.tsx +1 -1
  92. package/src/stories-json/icon.stories.tsx +1 -1
  93. package/src/stories-json/input.stories.tsx +1 -1
  94. package/src/stories-json/kanban.stories.tsx +1 -1
  95. package/src/stories-json/layout_extended.stories.tsx +1 -1
  96. package/src/stories-json/layout_flex.stories.tsx +1 -1
  97. package/src/stories-json/list-view.stories.tsx +1 -1
  98. package/src/stories-json/markdown.stories.tsx +1 -1
  99. package/src/stories-json/menus.stories.tsx +1 -1
  100. package/src/stories-json/metric-card.stories.tsx +1 -1
  101. package/src/stories-json/navigation-menu.stories.tsx +1 -1
  102. package/src/stories-json/object-aggrid-advanced.stories.tsx +389 -0
  103. package/src/stories-json/object-aggrid.stories.tsx +1 -1
  104. package/src/stories-json/object-form.stories.tsx +1 -1
  105. package/src/stories-json/object-gantt.stories.tsx +1 -1
  106. package/src/stories-json/object-grid.stories.tsx +159 -1
  107. package/src/stories-json/object-map.stories.tsx +1 -1
  108. package/src/stories-json/object-view.stories.tsx +1 -1
  109. package/src/stories-json/overlay_extras.stories.tsx +1 -1
  110. package/src/stories-json/overlay_others.stories.tsx +1 -1
  111. package/src/stories-json/resizable.stories.tsx +1 -1
  112. package/src/stories-json/select.stories.tsx +1 -1
  113. package/src/stories-json/separator.stories.tsx +1 -1
  114. package/src/stories-json/statistic.stories.tsx +1 -1
  115. package/src/stories-json/tabs.stories.tsx +1 -1
  116. package/src/stories-json/timeline.stories.tsx +1 -1
  117. package/src/stories-json/typography.stories.tsx +1 -1
  118. package/src/ui/slider.tsx +6 -2
  119. package/src/stories/Introduction.mdx +0 -34
@@ -1,71 +1,121 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
- import React, { useEffect, useState } from 'react';
2
+ import React, { useEffect, useState, useCallback } from 'react';
3
3
  import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
4
- import { PageSchema } from '@object-ui/types';
4
+ import type { BaseSchema } from '@object-ui/types';
5
+ import { createStorybookDataSource } from '@storybook-config/datasource';
5
6
 
6
- // Define a schema that binds to fetched data
7
- const contactDetailSchema: PageSchema = {
7
+ // ==========================================
8
+ // Story 1: Static Data Binding
9
+ // ==========================================
10
+ // Demonstrates how JSON schemas can reference data via expressions like ${data.field}
11
+
12
+ const staticDataSchema: BaseSchema = {
8
13
  type: "page",
9
- props: { title: "Contact Details (Mocked)" },
14
+ props: { title: "Static Data Binding" },
10
15
  children: [
11
16
  {
12
17
  type: "page:header",
13
18
  props: {
14
- title: "${data.name}",
15
- description: "${data.title} at ${data.company}"
19
+ title: "Sarah Connors",
20
+ description: "Lead Engineer at Cyberdyne Systems"
16
21
  }
17
22
  },
18
23
  {
19
- type: "view:simple",
20
- props: { columns: 2, className: "mt-4 border p-4 rounded" },
24
+ type: "grid",
25
+ props: { cols: 2, gap: 4, className: "mt-4" },
21
26
  children: [
22
- { type: "field:text", bind: "name", props: { label: "Full Name", readonly: true } },
23
- { type: "field:email", bind: "email", props: { label: "Email", readonly: true } },
24
- { type: "field:text", bind: "status", props: { label: "Status", readonly: true } }
27
+ { type: "card", title: "Full Name", children: [{ type: "text", content: "Sarah Connors" }] },
28
+ { type: "card", title: "Email", children: [{ type: "text", content: "sarah@cyberdyne.com" }] },
29
+ { type: "card", title: "Status", children: [{ type: "badge", props: { variant: "default", children: "Active" } }] },
30
+ { type: "card", title: "Department", children: [{ type: "text", content: "Engineering" }] },
25
31
  ]
26
32
  }
27
33
  ]
28
34
  };
29
35
 
30
- // A wrapper component that fetches data from the MSW mock
31
- const DataFetcher = () => {
32
- const [data, setData] = useState<any>(null);
36
+ // ==========================================
37
+ // Story 2: MSW-Backed Data (Live API)
38
+ // ==========================================
39
+ // The ObjectStack kernel seeds CRM data via MSW. This story fetches contacts from /api/v1.
33
40
 
34
- useEffect(() => {
35
- // Simulate a fetch call locally since MSW might not be configured in this environment
36
- const mockData = {
37
- name: "Sarah Connors",
38
- title: "Lead Engineer",
39
- company: "Cyberdyne Systems",
40
- email: "sarah@cyberdyne.com",
41
- status: "Active"
42
- };
41
+ const dataSource = createStorybookDataSource();
43
42
 
44
- const timer = setTimeout(() => {
45
- setData(mockData);
46
- }, 800);
43
+ const ContactListFromAPI = () => {
44
+ const [contacts, setContacts] = useState<any[]>([]);
45
+ const [loading, setLoading] = useState(true);
46
+ const [error, setError] = useState<string | null>(null);
47
47
 
48
- return () => clearTimeout(timer);
48
+ const fetchContacts = useCallback(async () => {
49
+ setLoading(true);
50
+ setError(null);
51
+ try {
52
+ const result = await dataSource.find('contact', { $top: 10 });
53
+ setContacts(result.data as any[]);
54
+ } catch (err: any) {
55
+ console.error('[Data Binding] Failed to fetch contacts:', err);
56
+ setError(err?.message || 'Failed to fetch data from MSW API');
57
+ } finally {
58
+ setLoading(false);
59
+ }
49
60
  }, []);
50
61
 
51
- if (!data) return <div className="p-4">Loading data from MSW...</div>;
62
+ useEffect(() => {
63
+ fetchContacts();
64
+ }, [fetchContacts]);
65
+
66
+ if (loading) return <div className="p-8 text-muted-foreground">Loading contacts from MSW API...</div>;
67
+ if (error) return (
68
+ <div className="p-8 space-y-2">
69
+ <div className="text-destructive font-medium">API Error: {error}</div>
70
+ <div className="text-sm text-muted-foreground">
71
+ This story requires the ObjectStack MSW runtime to be running.
72
+ Check the browser console for [Storybook MSW] logs.
73
+ </div>
74
+ </div>
75
+ );
76
+
77
+ const gridSchema: BaseSchema = {
78
+ type: 'object-grid',
79
+ objectName: 'contact',
80
+ columns: [
81
+ { field: 'name', header: 'Name', sortable: true },
82
+ { field: 'email', header: 'Email', sortable: true },
83
+ { field: 'title', header: 'Title' },
84
+ { field: 'status', header: 'Status' },
85
+ ],
86
+ data: contacts,
87
+ className: 'w-full'
88
+ } as any;
52
89
 
53
90
  return (
54
- <SchemaRendererProvider dataSource={data}>
55
- <SchemaRenderer schema={contactDetailSchema} />
91
+ <SchemaRendererProvider dataSource={dataSource}>
92
+ <div className="space-y-4">
93
+ <div className="flex items-center justify-between">
94
+ <h2 className="text-lg font-semibold">Contacts from MSW ({contacts.length} records)</h2>
95
+ </div>
96
+ <SchemaRenderer schema={gridSchema} />
97
+ </div>
56
98
  </SchemaRendererProvider>
57
99
  );
58
100
  };
59
101
 
102
+ // ==========================================
103
+ // Meta
104
+ // ==========================================
105
+
60
106
  const meta: Meta = {
61
- title: 'Guide/Mocked Data',
62
- component: DataFetcher,
107
+ title: 'Getting Started/Data Binding',
63
108
  parameters: {
64
- // We can also override handlers per story here if needed
65
- // msw: { handlers: [...] }
66
- }
109
+ layout: 'padded',
110
+ },
67
111
  };
68
112
 
69
113
  export default meta;
70
114
 
71
- export const Default: StoryObj = {};
115
+ export const StaticData: StoryObj = {
116
+ render: () => <SchemaRenderer schema={staticDataSchema} />,
117
+ };
118
+
119
+ export const LiveAPI: StoryObj = {
120
+ render: () => <ContactListFromAPI />,
121
+ };
@@ -0,0 +1,297 @@
1
+ import { Meta } from '@storybook/blocks';
2
+
3
+ <Meta title="Getting Started/Accessibility" />
4
+
5
+ # Accessibility Reference
6
+
7
+ ObjectUI targets **WCAG 2.1 Level AA** conformance. Every component inherits accessible
8
+ primitives from [Radix UI](https://www.radix-ui.com/) and is styled with Tailwind CSS
9
+ utility classes that respect user preferences for reduced motion and color contrast.
10
+
11
+ ---
12
+
13
+ ## Compliance Baseline
14
+
15
+ | Standard | Level | Status |
16
+ |---|---|---|
17
+ | WCAG 2.1 | AA | ✅ Target conformance for all components |
18
+ | WAI-ARIA 1.2 | — | ✅ Roles, states, and properties via Radix |
19
+ | Section 508 | — | ✅ Covered by WCAG AA compliance |
20
+
21
+ ObjectUI relies on **Radix UI** for its accessible primitive layer. Radix components
22
+ ship with correct ARIA roles, keyboard interactions, and focus management built in.
23
+ Shadcn UI wraps these primitives with Tailwind styling while preserving all accessibility
24
+ behaviour.
25
+
26
+ ---
27
+
28
+ ## Keyboard Navigation Patterns
29
+
30
+ All interactive components support keyboard navigation following WAI-ARIA authoring
31
+ practices. Below is a reference of the patterns used across component types.
32
+
33
+ ### General Controls
34
+
35
+ | Component | Key | Action |
36
+ |---|---|---|
37
+ | **Button** | `Enter` / `Space` | Activate the button |
38
+ | **Link** | `Enter` | Follow the link |
39
+ | **Toggle** | `Enter` / `Space` | Toggle the state |
40
+ | All focusable | `Tab` | Move focus to next element |
41
+ | All focusable | `Shift + Tab` | Move focus to previous element |
42
+
43
+ ### Composite Widgets
44
+
45
+ | Component | Key | Action |
46
+ |---|---|---|
47
+ | **Tabs** | `Arrow Left` / `Arrow Right` | Move between tab triggers |
48
+ | **Tabs** | `Enter` / `Space` | Activate focused tab |
49
+ | **Tabs** | `Home` / `End` | Jump to first / last tab |
50
+ | **Accordion** | `Arrow Up` / `Arrow Down` | Move between headers |
51
+ | **Accordion** | `Enter` / `Space` | Expand / collapse section |
52
+ | **Accordion** | `Home` / `End` | Jump to first / last header |
53
+ | **Menu / DropdownMenu** | `Arrow Up` / `Arrow Down` | Navigate menu items |
54
+ | **Menu / DropdownMenu** | `Enter` / `Space` | Activate item |
55
+ | **Menu / DropdownMenu** | `Escape` | Close menu and return focus |
56
+ | **NavigationMenu** | `Arrow Left` / `Arrow Right` | Move between top-level items |
57
+ | **NavigationMenu** | `Arrow Down` | Open submenu |
58
+ | **NavigationMenu** | `Escape` | Close submenu |
59
+
60
+ ### Form Fields
61
+
62
+ | Component | Key | Action |
63
+ |---|---|---|
64
+ | **Input / Textarea** | Standard typing | Enter text |
65
+ | **Select** | `Arrow Up` / `Arrow Down` | Navigate options |
66
+ | **Select** | `Enter` | Select option |
67
+ | **Select** | `Escape` | Close listbox |
68
+ | **Checkbox** | `Space` | Toggle checked state |
69
+ | **Radio Group** | `Arrow Up` / `Arrow Down` | Move selection |
70
+ | **Switch** | `Space` | Toggle on/off |
71
+ | **Slider** | `Arrow Left` / `Arrow Right` | Decrease / increase value |
72
+ | **Slider** | `Home` / `End` | Set to minimum / maximum |
73
+ | **DatePicker** | `Arrow Keys` | Navigate calendar grid |
74
+ | **DatePicker** | `Enter` | Select date |
75
+ | **DatePicker** | `Escape` | Close calendar popover |
76
+
77
+ ### Overlay Components
78
+
79
+ | Component | Key | Action |
80
+ |---|---|---|
81
+ | **Dialog** | `Escape` | Close the dialog |
82
+ | **Dialog** | `Tab` | Cycle focus within dialog (focus trap) |
83
+ | **AlertDialog** | `Escape` | Close (if not critical) |
84
+ | **AlertDialog** | `Tab` | Cycle focus within alert |
85
+ | **Popover** | `Escape` | Close popover |
86
+ | **Tooltip** | `Escape` | Dismiss tooltip |
87
+ | **Sheet** | `Escape` | Close sheet overlay |
88
+ | **Sheet** | `Tab` | Cycle focus within sheet |
89
+
90
+ ### Data Views (Plugins)
91
+
92
+ | Component | Key | Action |
93
+ |---|---|---|
94
+ | **DataTable / Grid** | `Arrow Keys` | Navigate cells |
95
+ | **DataTable / Grid** | `Enter` | Activate cell / begin editing |
96
+ | **DataTable / Grid** | `Escape` | Cancel editing |
97
+ | **DataTable / Grid** | `Tab` | Move to next focusable cell |
98
+ | **Kanban** | `Tab` | Move between cards |
99
+ | **Kanban** | `Enter` / `Space` | Open card detail |
100
+
101
+ ---
102
+
103
+ ## ARIA Roles and Attributes
104
+
105
+ ObjectUI components emit the correct ARIA roles and attributes automatically through
106
+ Radix primitives. Below is a reference of the key mappings.
107
+
108
+ ### Primitive Components
109
+
110
+ | Component | ARIA Role | Key Attributes |
111
+ |---|---|---|
112
+ | **Button** | `button` | `aria-disabled`, `aria-pressed` (toggle) |
113
+ | **Badge** | `status` | `aria-label` (when used as status indicator) |
114
+ | **Card** | `region` | `aria-labelledby` (card title) |
115
+ | **Separator** | `separator` | `aria-orientation` |
116
+ | **Avatar** | `img` | `alt` text on image, fallback initials |
117
+ | **Progress** | `progressbar` | `aria-valuenow`, `aria-valuemin`, `aria-valuemax` |
118
+ | **Skeleton** | `status` | `aria-busy="true"`, `aria-label="Loading"` |
119
+
120
+ ### Composite Components
121
+
122
+ | Component | ARIA Role | Key Attributes |
123
+ |---|---|---|
124
+ | **Tabs** | `tablist`, `tab`, `tabpanel` | `aria-selected`, `aria-controls`, `aria-labelledby` |
125
+ | **Accordion** | `region` | `aria-expanded`, `aria-controls` on triggers |
126
+ | **Breadcrumb** | `navigation` | `aria-label="Breadcrumb"`, `aria-current="page"` on active |
127
+ | **NavigationMenu** | `navigation` | `aria-label`, `aria-expanded` on submenus |
128
+ | **Carousel** | `region` | `aria-roledescription="carousel"`, `aria-label` on slides |
129
+
130
+ ### Form Components
131
+
132
+ | Component | ARIA Role | Key Attributes |
133
+ |---|---|---|
134
+ | **Input** | `textbox` | `aria-required`, `aria-invalid`, `aria-describedby` (error/hint) |
135
+ | **Textarea** | `textbox` | `aria-required`, `aria-invalid`, `aria-describedby` |
136
+ | **Select** | `combobox` / `listbox` | `aria-expanded`, `aria-activedescendant`, `aria-required` |
137
+ | **Checkbox** | `checkbox` | `aria-checked`, `aria-required` |
138
+ | **Radio Group** | `radiogroup`, `radio` | `aria-checked`, `aria-required` |
139
+ | **Switch** | `switch` | `aria-checked`, `aria-label` |
140
+ | **Slider** | `slider` | `aria-valuenow`, `aria-valuemin`, `aria-valuemax`, `aria-label` |
141
+ | **Label** | — | `for` / `htmlFor` association with input |
142
+
143
+ ### Overlay Components
144
+
145
+ | Component | ARIA Role | Key Attributes |
146
+ |---|---|---|
147
+ | **Dialog** | `dialog` | `aria-modal="true"`, `aria-labelledby`, `aria-describedby` |
148
+ | **AlertDialog** | `alertdialog` | `aria-modal="true"`, `aria-labelledby`, `aria-describedby` |
149
+ | **Popover** | `dialog` | `aria-expanded` on trigger, auto `aria-labelledby` |
150
+ | **Tooltip** | `tooltip` | `aria-describedby` on trigger element |
151
+ | **Sheet** | `dialog` | `aria-modal="true"`, `aria-labelledby` |
152
+ | **DropdownMenu** | `menu`, `menuitem` | `aria-expanded`, `aria-haspopup` |
153
+ | **ContextMenu** | `menu`, `menuitem` | `aria-expanded`, `aria-haspopup` |
154
+
155
+ ### Data View Components
156
+
157
+ | Component | ARIA Role | Key Attributes |
158
+ |---|---|---|
159
+ | **DataTable** | `table`, `row`, `cell` | `aria-sort` on sortable columns, `aria-rowcount` |
160
+ | **Grid (AG Grid)** | `grid`, `row`, `gridcell` | `aria-colcount`, `aria-rowindex`, `aria-selected` |
161
+ | **Kanban** | `list`, `listitem` | `aria-label` per column, `aria-grabbed` on draggable cards |
162
+
163
+ ---
164
+
165
+ ## Screen Reader Behaviour
166
+
167
+ ObjectUI components are designed to work seamlessly with screen readers
168
+ (NVDA, JAWS, VoiceOver, TalkBack). Key behaviours include:
169
+
170
+ ### Focus Management
171
+ - **Dialogs and Sheets** trap focus within the overlay while open. On close, focus
172
+ returns to the trigger element.
173
+ - **Menus and Popovers** return focus to their trigger on dismiss.
174
+ - **Toast notifications** use `role="status"` with `aria-live="polite"` so screen
175
+ readers announce them without interrupting the user.
176
+
177
+ ### Live Regions
178
+ - **Form validation errors** are linked via `aria-describedby`, so errors are announced
179
+ when the field receives focus.
180
+ - **Loading states** use `aria-busy="true"` on the loading container and `aria-live`
181
+ regions to announce completion.
182
+ - **Toast / Alert** components use `aria-live="polite"` or `aria-live="assertive"`
183
+ depending on urgency.
184
+
185
+ ### Landmark Regions
186
+ - **AppShell** emits `<header>`, `<nav>`, `<main>`, and `<aside>` landmarks so screen
187
+ reader users can jump between sections.
188
+ - **Breadcrumb** uses `<nav aria-label="Breadcrumb">` for quick landmark navigation.
189
+ - **Sidebar** uses `<aside>` with a descriptive `aria-label`.
190
+
191
+ ### Announcements
192
+ - **Sortable table columns** announce sort direction changes via `aria-sort`.
193
+ - **Accordion** sections announce expanded/collapsed state.
194
+ - **Tabs** announce the active tab label on selection.
195
+ - **Carousel** announces slide transitions with `aria-roledescription`.
196
+
197
+ ---
198
+
199
+ ## Color Contrast Requirements
200
+
201
+ ObjectUI uses Tailwind CSS design tokens (CSS custom properties) for all color
202
+ values. The default theme meets WCAG AA contrast ratios:
203
+
204
+ | Element | Minimum Ratio | Standard |
205
+ |---|---|---|
206
+ | Body text (`foreground` on `background`) | **4.5 : 1** | WCAG AA — Normal text |
207
+ | Large text (≥ 18pt or ≥ 14pt bold) | **3 : 1** | WCAG AA — Large text |
208
+ | UI components and graphical objects | **3 : 1** | WCAG 2.1 SC 1.4.11 |
209
+ | Focus indicators | **3 : 1** | WCAG 2.1 SC 1.4.11 |
210
+
211
+ ### Theme Tokens
212
+
213
+ All colors are defined as CSS variables in HSL format:
214
+
215
+ ```css
216
+ :root {
217
+ --background: 0 0% 100%;
218
+ --foreground: 222 84% 5%;
219
+ --primary: 222 47% 11%;
220
+ --primary-foreground: 210 40% 98%;
221
+ --destructive: 0 84% 60%;
222
+ --destructive-foreground: 0 0% 98%;
223
+ --muted: 210 40% 96%;
224
+ --muted-foreground: 215 16% 47%;
225
+ /* ...additional tokens */
226
+ }
227
+ ```
228
+
229
+ When customising tokens, verify contrast ratios with a tool such as the
230
+ [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) or the
231
+ built-in Storybook accessibility panel.
232
+
233
+ ### Dark Mode
234
+
235
+ ObjectUI supports a `.dark` class on the root element. The dark theme ships with
236
+ its own set of tokens that maintain the same contrast ratios:
237
+
238
+ ```css
239
+ .dark {
240
+ --background: 222 84% 5%;
241
+ --foreground: 210 40% 98%;
242
+ /* ...dark tokens */
243
+ }
244
+ ```
245
+
246
+ ### Focus Indicators
247
+
248
+ All interactive elements display a visible focus ring when navigated via keyboard.
249
+ The default ring uses the `--ring` token and applies a `2px` outline with a `2px`
250
+ offset, ensuring it meets the **3 : 1** non-text contrast requirement.
251
+
252
+ ```css
253
+ :root {
254
+ --ring: 222 47% 11%;
255
+ }
256
+ ```
257
+
258
+ > **Tip:** Never remove focus outlines (`outline: none`) without providing an
259
+ > equivalent visible indicator. ObjectUI's `focus-visible` styles handle this
260
+ > automatically.
261
+
262
+ ---
263
+
264
+ ## Testing Accessibility
265
+
266
+ ### Storybook A11y Addon
267
+
268
+ Every story in this Storybook can be audited via the **Accessibility** panel in the
269
+ addons bar. It runs [axe-core](https://github.com/dequelabs/axe-core) checks and
270
+ reports violations, passes, and incomplete audits.
271
+
272
+ ### Automated Testing
273
+
274
+ Use `vitest` with `@testing-library/jest-dom` matchers for assertions like
275
+ `toBeVisible()`, `toHaveAccessibleName()`, and `toHaveAttribute('aria-expanded')`.
276
+
277
+ ```tsx
278
+ import { render, screen } from '@testing-library/react';
279
+ import { axe, toHaveNoViolations } from 'jest-axe';
280
+
281
+ expect.extend(toHaveNoViolations);
282
+
283
+ it('Button has no accessibility violations', async () => {
284
+ const { container } = render(<Button>Click me</Button>);
285
+ const results = await axe(container);
286
+ expect(results).toHaveNoViolations();
287
+ });
288
+ ```
289
+
290
+ ### Manual Testing Checklist
291
+
292
+ - [ ] Navigate all interactive elements using only the keyboard (`Tab`, `Enter`, `Space`, `Escape`, `Arrow` keys).
293
+ - [ ] Verify visible focus indicators on every focusable element.
294
+ - [ ] Test with a screen reader (VoiceOver on macOS, NVDA on Windows).
295
+ - [ ] Check colour contrast using the browser DevTools or WebAIM checker.
296
+ - [ ] Confirm the page is usable at 200% browser zoom.
297
+ - [ ] Verify `prefers-reduced-motion` is respected (animations are disabled or reduced).
@@ -0,0 +1,160 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { SchemaRenderer } from '../SchemaRenderer';
3
+ import type { BaseSchema } from '@object-ui/types';
4
+
5
+ const meta = {
6
+ title: 'Components / Edge Cases',
7
+ component: SchemaRenderer,
8
+ parameters: { layout: 'padded' },
9
+ tags: ['autodocs'],
10
+ argTypes: {
11
+ schema: { table: { disable: true } },
12
+ },
13
+ } satisfies Meta<any>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ const renderStory = (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />;
19
+
20
+ // ── Empty States ──────────────────────────────────────────────
21
+
22
+ export const EmptyStateDefault: Story = {
23
+ name: 'Empty State – Default',
24
+ render: renderStory,
25
+ args: {
26
+ type: 'empty',
27
+ description: 'No items to display',
28
+ } as any,
29
+ };
30
+
31
+ export const EmptyStateWithAction: Story = {
32
+ name: 'Empty State – With Action',
33
+ render: renderStory,
34
+ args: {
35
+ type: 'empty',
36
+ description: 'No results found. Try a different search or create a new item.',
37
+ children: [
38
+ { type: 'button', content: 'Create New', variant: 'default', size: 'sm' },
39
+ ],
40
+ } as any,
41
+ };
42
+
43
+ // ── Loading / Spinner ─────────────────────────────────────────
44
+
45
+ export const SpinnerSmall: Story = {
46
+ name: 'Spinner – Small',
47
+ render: renderStory,
48
+ args: {
49
+ type: 'spinner',
50
+ size: 'sm',
51
+ className: 'text-primary',
52
+ } as any,
53
+ };
54
+
55
+ export const SpinnerLarge: Story = {
56
+ name: 'Spinner – Large',
57
+ render: renderStory,
58
+ args: {
59
+ type: 'spinner',
60
+ size: 'lg',
61
+ className: 'text-primary',
62
+ } as any,
63
+ };
64
+
65
+ export const LoadingWithText: Story = {
66
+ name: 'Loading – With Text',
67
+ render: renderStory,
68
+ args: {
69
+ type: 'loading',
70
+ text: 'Please wait while we fetch your data…',
71
+ className: 'h-[200px]',
72
+ } as any,
73
+ };
74
+
75
+ // ── Overflow / Long Text ──────────────────────────────────────
76
+
77
+ const LONG_TEXT =
78
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit.';
79
+
80
+ export const CardWithLongText: Story = {
81
+ name: 'Card – Very Long Text',
82
+ render: renderStory,
83
+ args: {
84
+ type: 'card',
85
+ className: 'w-[350px]',
86
+ title: 'This is an extremely long card title that should test how the component handles overflow and text wrapping gracefully',
87
+ description: LONG_TEXT,
88
+ children: [
89
+ {
90
+ type: 'text',
91
+ content: LONG_TEXT + ' ' + LONG_TEXT,
92
+ },
93
+ ],
94
+ } as any,
95
+ };
96
+
97
+ export const BadgeWithLongText: Story = {
98
+ name: 'Badge – Very Long Text',
99
+ render: renderStory,
100
+ args: {
101
+ type: 'badge',
102
+ label: 'This is an unusually long badge label that tests truncation and overflow behaviour in tight layouts',
103
+ } as any,
104
+ };
105
+
106
+ export const ButtonWithIconAndLongText: Story = {
107
+ name: 'Button – Icon + Long Text',
108
+ render: (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />,
109
+ args: {
110
+ type: 'button',
111
+ props: { variant: 'default' },
112
+ children: [
113
+ { type: 'icon', name: 'download', className: 'mr-2 h-4 w-4' },
114
+ {
115
+ type: 'text',
116
+ content: 'Download All Reports For The Current Financial Year Including Amendments',
117
+ },
118
+ ],
119
+ } as any,
120
+ };
121
+
122
+ // ── RTL Layout ────────────────────────────────────────────────
123
+
124
+ export const RTLCard: Story = {
125
+ name: 'RTL – Card Layout',
126
+ render: (args: any) => (
127
+ <div dir="rtl">
128
+ <SchemaRenderer schema={args as unknown as BaseSchema} />
129
+ </div>
130
+ ),
131
+ args: {
132
+ type: 'card',
133
+ className: 'w-[350px]',
134
+ title: 'عنوان البطاقة',
135
+ description: 'هذا النص باللغة العربية لاختبار تخطيط الاتجاه من اليمين إلى اليسار.',
136
+ children: [
137
+ { type: 'text', content: 'محتوى البطاقة الرئيسي يظهر هنا باللغة العربية.' },
138
+ ],
139
+ footer: [
140
+ { type: 'button', props: { variant: 'outline' }, children: [{ type: 'text', content: 'إلغاء' }] },
141
+ { type: 'button', children: [{ type: 'text', content: 'حفظ' }] },
142
+ ],
143
+ } as any,
144
+ };
145
+
146
+ export const RTLAlert: Story = {
147
+ name: 'RTL – Alert',
148
+ render: (args: any) => (
149
+ <div dir="rtl">
150
+ <SchemaRenderer schema={args as unknown as BaseSchema} />
151
+ </div>
152
+ ),
153
+ args: {
154
+ type: 'alert',
155
+ variant: 'destructive',
156
+ title: 'خطأ',
157
+ description: 'انتهت صلاحية الجلسة. الرجاء تسجيل الدخول مرة أخرى.',
158
+ className: 'w-[400px]',
159
+ } as any,
160
+ };