@fpkit/acss 1.0.0-beta.1 → 2.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 (103) hide show
  1. package/README.md +92 -0
  2. package/docs/README.md +325 -0
  3. package/docs/guides/accessibility.md +764 -0
  4. package/docs/guides/architecture.md +705 -0
  5. package/docs/guides/composition.md +688 -0
  6. package/docs/guides/css-variables.md +522 -0
  7. package/docs/guides/storybook.md +828 -0
  8. package/docs/guides/testing.md +817 -0
  9. package/docs/testing/focus-indicator-testing.md +437 -0
  10. package/libs/{chunk-7XPFW7CB.js → chunk-43TK2ICH.js} +2 -2
  11. package/libs/chunk-5PJYLVFY.cjs +17 -0
  12. package/libs/chunk-5PJYLVFY.cjs.map +1 -0
  13. package/libs/chunk-E4OSROCA.cjs +17 -0
  14. package/libs/chunk-E4OSROCA.cjs.map +1 -0
  15. package/libs/chunk-KVKQLRJG.js +10 -0
  16. package/libs/chunk-KVKQLRJG.js.map +1 -0
  17. package/libs/{chunk-QVW6W76L.cjs → chunk-MGPWZRBX.cjs} +3 -3
  18. package/libs/chunk-NNTBIHSD.js +8 -0
  19. package/libs/chunk-NNTBIHSD.js.map +1 -0
  20. package/libs/{chunk-X3JCTEPD.js → chunk-QKHPHMG2.js} +2 -2
  21. package/libs/{chunk-T4T6GWYQ.cjs → chunk-R7NLLZU2.cjs} +3 -3
  22. package/libs/{chunk-X5LGFCWG.js → chunk-UJAQVHWC.js} +3 -3
  23. package/libs/{chunk-DKTHCQ5P.cjs → chunk-X5RKCLDC.cjs} +3 -3
  24. package/libs/components/breadcrumbs/breadcrumb.cjs +5 -5
  25. package/libs/components/breadcrumbs/breadcrumb.d.cts +1 -1
  26. package/libs/components/breadcrumbs/breadcrumb.d.ts +1 -1
  27. package/libs/components/breadcrumbs/breadcrumb.js +2 -2
  28. package/libs/components/button.cjs +3 -3
  29. package/libs/components/button.d.cts +1 -1
  30. package/libs/components/button.d.ts +1 -1
  31. package/libs/components/button.js +1 -1
  32. package/libs/components/buttons/button.css +1 -1
  33. package/libs/components/buttons/button.css.map +1 -1
  34. package/libs/components/buttons/button.min.css +2 -2
  35. package/libs/components/dialog/dialog.cjs +4 -4
  36. package/libs/components/dialog/dialog.js +2 -2
  37. package/libs/components/icons/icon.d.cts +32 -32
  38. package/libs/components/icons/icon.d.ts +32 -32
  39. package/libs/components/link/link.cjs +11 -3
  40. package/libs/components/link/link.d.cts +131 -3
  41. package/libs/components/link/link.d.ts +131 -3
  42. package/libs/components/link/link.js +1 -1
  43. package/libs/components/list/list.css +1 -1
  44. package/libs/components/list/list.min.css +1 -1
  45. package/libs/components/modal.cjs +3 -3
  46. package/libs/components/modal.js +2 -2
  47. package/libs/hooks.cjs +3 -3
  48. package/libs/hooks.d.cts +1 -1
  49. package/libs/hooks.d.ts +1 -1
  50. package/libs/hooks.js +2 -2
  51. package/libs/index.cjs +12 -12
  52. package/libs/index.css +1 -1
  53. package/libs/index.css.map +1 -1
  54. package/libs/index.d.cts +237 -2
  55. package/libs/index.d.ts +237 -2
  56. package/libs/index.js +5 -5
  57. package/package.json +4 -3
  58. package/src/components/README.mdx +1 -1
  59. package/src/components/breadcrumbs/breadcrumb.test.tsx +1 -2
  60. package/src/components/buttons/README.mdx +19 -9
  61. package/src/components/buttons/button.scss +5 -0
  62. package/src/components/buttons/button.stories.tsx +8 -5
  63. package/src/components/buttons/button.tsx +19 -15
  64. package/src/components/cards/card.stories.tsx +1 -1
  65. package/src/components/details/details.stories.tsx +1 -1
  66. package/src/components/form/form.stories.tsx +1 -1
  67. package/src/components/form/input.stories.tsx +1 -1
  68. package/src/components/form/select.stories.tsx +1 -1
  69. package/src/components/heading/README.mdx +292 -0
  70. package/src/components/icons/icon.stories.tsx +1 -1
  71. package/src/components/link/link.stories.tsx +205 -8
  72. package/src/components/link/link.test.tsx +1 -1
  73. package/src/components/link/link.tsx +22 -0
  74. package/src/components/link/link.types.ts +11 -3
  75. package/src/components/list/list.scss +1 -1
  76. package/src/components/nav/nav.stories.tsx +1 -1
  77. package/src/components/ui.stories.tsx +53 -19
  78. package/src/docs/accessibility.mdx +484 -0
  79. package/src/docs/composition.mdx +549 -0
  80. package/src/docs/css-variables.mdx +380 -0
  81. package/src/docs/fpkit-developer.mdx +623 -0
  82. package/src/introduction.mdx +356 -0
  83. package/src/styles/buttons/button.css +4 -0
  84. package/src/styles/buttons/button.css.map +1 -1
  85. package/src/styles/index.css +9 -3
  86. package/src/styles/index.css.map +1 -1
  87. package/src/styles/list/list.css +1 -1
  88. package/src/styles/utilities/_disabled.scss +5 -4
  89. package/libs/chunk-33PNJ4LO.cjs +0 -15
  90. package/libs/chunk-33PNJ4LO.cjs.map +0 -1
  91. package/libs/chunk-GT77BX4L.cjs +0 -17
  92. package/libs/chunk-GT77BX4L.cjs.map +0 -1
  93. package/libs/chunk-OVWLQYMK.js +0 -10
  94. package/libs/chunk-OVWLQYMK.js.map +0 -1
  95. package/libs/chunk-UEPAWMDF.js +0 -8
  96. package/libs/chunk-UEPAWMDF.js.map +0 -1
  97. package/libs/link-5192f411.d.ts +0 -323
  98. /package/libs/{chunk-7XPFW7CB.js.map → chunk-43TK2ICH.js.map} +0 -0
  99. /package/libs/{chunk-QVW6W76L.cjs.map → chunk-MGPWZRBX.cjs.map} +0 -0
  100. /package/libs/{chunk-X3JCTEPD.js.map → chunk-QKHPHMG2.js.map} +0 -0
  101. /package/libs/{chunk-T4T6GWYQ.cjs.map → chunk-R7NLLZU2.cjs.map} +0 -0
  102. /package/libs/{chunk-X5LGFCWG.js.map → chunk-UJAQVHWC.js.map} +0 -0
  103. /package/libs/{chunk-DKTHCQ5P.cjs.map → chunk-X5RKCLDC.cjs.map} +0 -0
@@ -0,0 +1,437 @@
1
+ # Focus Indicator Testing Guide
2
+
3
+ ## Overview
4
+
5
+ This guide provides manual testing procedures for verifying that focus indicators meet WCAG 2.4.7 (Focus Visible) Level AA requirements. Focus indicators must have at least **3:1 contrast ratio** against both the background and adjacent colors.
6
+
7
+ ## WCAG 2.4.7 Requirements
8
+
9
+ **Success Criterion:** Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible.
10
+
11
+ **Level AA Requirements:**
12
+ - Focus indicators must be visible when an element receives keyboard focus
13
+ - Minimum **3:1 contrast ratio** against:
14
+ - The background color
15
+ - Adjacent (non-focused) component colors
16
+ - Minimum **2px thick** or equivalent area coverage
17
+
18
+ **Reference:** [WCAG 2.4.7 Focus Visible](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible)
19
+
20
+ ---
21
+
22
+ ## Testing Tools
23
+
24
+ ### Required Tools
25
+
26
+ 1. **Keyboard** - For navigation testing
27
+ 2. **Chrome DevTools** or **Firefox DevTools** - For contrast measurement
28
+ 3. **axe DevTools Browser Extension** (Recommended)
29
+ - [Chrome](https://chrome.google.com/webstore/detail/axe-devtools-web-accessib/lhdoppojpmngadmnindnejefpokejbdd)
30
+ - [Firefox](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/)
31
+
32
+ ### Optional Tools
33
+
34
+ - **WAVE Browser Extension** - Additional accessibility scanning
35
+ - **Color Contrast Analyzer** - Desktop app for detailed contrast checking
36
+ - **Screen Readers:**
37
+ - NVDA (Windows) - [Download](https://www.nvaccess.org/download/)
38
+ - VoiceOver (macOS) - Built-in
39
+ - JAWS (Windows) - Commercial
40
+
41
+ ---
42
+
43
+ ## Test Procedures
44
+
45
+ ### 1. Keyboard Navigation Test
46
+
47
+ **Objective:** Verify all interactive elements show visible focus indicators when navigated with keyboard.
48
+
49
+ **Steps:**
50
+
51
+ 1. **Open the component/page** in your browser
52
+ 2. **Click in the address bar** to ensure page loses focus
53
+ 3. **Press Tab key** to start keyboard navigation
54
+ 4. **Continue pressing Tab** through all interactive elements
55
+ 5. **Press Shift+Tab** to navigate backwards
56
+
57
+ **Pass Criteria:**
58
+ - ✅ Every interactive element (buttons, inputs, links, selects) shows a visible outline when focused
59
+ - ✅ Focus indicator is clearly distinguishable from the non-focused state
60
+ - ✅ Disabled elements are focusable and show focus indicators
61
+ - ✅ Focus order follows logical reading order
62
+
63
+ **Common Issues:**
64
+ - ❌ No visible outline on focused elements
65
+ - ❌ Outline color matches background (invisible)
66
+ - ❌ Disabled elements skip in tab order (should NOT happen with aria-disabled pattern)
67
+
68
+ ---
69
+
70
+ ### 2. Contrast Verification Test
71
+
72
+ **Objective:** Verify focus indicators meet 3:1 contrast ratio requirement.
73
+
74
+ #### Method 1: Chrome DevTools (Recommended)
75
+
76
+ **Steps:**
77
+
78
+ 1. **Open DevTools** (F12 or Right-click → Inspect)
79
+ 2. **Navigate to element** using Tab key until it's focused
80
+ 3. **Click on the element** in the Elements panel
81
+ 4. **In the Styles panel**, find the `:focus-visible` styles
82
+ 5. **Click the color swatch** next to `outline-color`
83
+ 6. **Check the Contrast Ratio section** in the color picker
84
+
85
+ **Pass Criteria:**
86
+ - ✅ Contrast ratio shows **≥ 3.0:1** against background
87
+ - ✅ Both AA and AAA indicators show checkmarks (for UI components)
88
+
89
+ **Example Screenshot Interpretation:**
90
+ ```
91
+ Contrast Ratio:
92
+ AA ✓ 4.5 (Pass - Text contrast)
93
+ AAA ✓ 7.1 (Pass - Enhanced contrast)
94
+
95
+ UI Components:
96
+ AA ✓ 3.2 (Pass - Meets 3:1 minimum)
97
+ ```
98
+
99
+ #### Method 2: axe DevTools Browser Extension
100
+
101
+ **Steps:**
102
+
103
+ 1. **Install axe DevTools** browser extension
104
+ 2. **Open the page** with your components
105
+ 3. **Open DevTools** and select the **axe DevTools** tab
106
+ 4. **Click "Scan ALL of my page"**
107
+ 5. **Review Issues** section for focus indicator violations
108
+
109
+ **Pass Criteria:**
110
+ - ✅ No violations related to "Focus Visible" or "Color Contrast"
111
+ - ✅ Disabled elements appear in "Needs Review" (expected, manual verification needed)
112
+
113
+ #### Method 3: Manual Color Contrast Calculation
114
+
115
+ **Steps:**
116
+
117
+ 1. **Identify focus indicator color** (e.g., from DevTools Computed styles)
118
+ 2. **Identify background color** of the page/container
119
+ 3. **Use WebAIM Contrast Checker:** https://webaim.org/resources/contrastchecker/
120
+ 4. **Enter foreground color** (focus indicator color)
121
+ 5. **Enter background color** (page/container background)
122
+ 6. **Check "Graphical Objects and UI Components" section**
123
+
124
+ **Pass Criteria:**
125
+ - ✅ Contrast ratio shows **≥ 3:1** in the UI Components section
126
+
127
+ **Example Values:**
128
+ ```
129
+ Foreground: #005fcc (example custom --focus-color)
130
+ Background: #ffffff (white)
131
+ Result: 8.59:1 - PASS ✓
132
+
133
+ Foreground: #666666 (disabled color, currentColor fallback)
134
+ Background: #ffffff (white)
135
+ Result: 4.54:1 - PASS ✓
136
+
137
+ Foreground: #666666 (disabled color)
138
+ Background: #999999 (gray container)
139
+ Result: 1.54:1 - FAIL ✗ (This is why --focus-color override is needed!)
140
+ ```
141
+
142
+ ---
143
+
144
+ ### 3. Theme Testing
145
+
146
+ **Objective:** Verify focus indicators work correctly across different themes and backgrounds.
147
+
148
+ **Test Matrix:**
149
+
150
+ | Background | Focus Color | Expected Contrast | Pass/Fail |
151
+ |------------|-------------|-------------------|-----------|
152
+ | `#ffffff` (white) | `currentColor` (#666) | 4.54:1 | ✅ Pass |
153
+ | `#ffffff` (white) | `--focus-color` (#005fcc) | 8.59:1 | ✅ Pass |
154
+ | `#f5f5f5` (light gray) | `currentColor` (#666) | 3.84:1 | ✅ Pass |
155
+ | `#999999` (gray) | `currentColor` (#666) | 1.54:1 | ⚠️ **Needs --focus-color** |
156
+ | `#333333` (dark) | `currentColor` (#666) | 2.4:1 | ⚠️ **Needs --focus-color** |
157
+
158
+ **Steps:**
159
+
160
+ 1. **Test on white background** (default theme)
161
+ - Navigate with Tab
162
+ - Verify focus indicator is visible
163
+ - Measure contrast with DevTools
164
+
165
+ 2. **Test on gray backgrounds** (if your theme uses gray containers)
166
+ - Add test component to gray container
167
+ - Navigate with Tab
168
+ - Measure contrast - if < 3:1, add `--focus-color` override
169
+
170
+ 3. **Test in dark mode** (if your theme supports it)
171
+ - Switch to dark mode
172
+ - Verify focus indicators are visible
173
+ - Measure contrast against dark backgrounds
174
+
175
+ **Pass Criteria:**
176
+ - ✅ All backgrounds show focus indicators with ≥ 3:1 contrast
177
+ - ✅ Custom themes define `--focus-color` when needed
178
+
179
+ **Fix for Failing Themes:**
180
+
181
+ If contrast fails on certain backgrounds, add theme-specific `--focus-color`:
182
+
183
+ ```css
184
+ /* Light theme (default) - currentColor (#666) works fine */
185
+ :root {
186
+ /* No override needed, currentColor provides 4.54:1 on white */
187
+ }
188
+
189
+ /* Gray theme - needs custom focus color */
190
+ .theme-gray {
191
+ --focus-color: #005fcc; /* 8.59:1 on white, 3.2:1 on #999 */
192
+ }
193
+
194
+ /* Dark theme - needs lighter focus color */
195
+ .theme-dark {
196
+ --focus-color: #4da6ff; /* Lighter blue for dark backgrounds */
197
+ }
198
+ ```
199
+
200
+ ---
201
+
202
+ ### 4. Disabled Element Focus Test
203
+
204
+ **Objective:** Verify disabled elements remain focusable and show focus indicators (aria-disabled pattern).
205
+
206
+ **Steps:**
207
+
208
+ 1. **Create test form** with disabled elements:
209
+ ```tsx
210
+ <form>
211
+ <Input id="name" name="name" placeholder="Name" />
212
+ <Input id="email" name="email" disabled={true} placeholder="Email (disabled)" />
213
+ <Button type="button" disabled={true}>Disabled Button</Button>
214
+ <Button type="submit">Submit</Button>
215
+ </form>
216
+ ```
217
+
218
+ 2. **Press Tab** to navigate through the form
219
+ 3. **Verify disabled elements receive focus** (tab stops on them)
220
+ 4. **Verify focus indicator is visible** on disabled elements
221
+ 5. **Measure contrast** of focus indicator on disabled elements
222
+
223
+ **Pass Criteria:**
224
+ - ✅ Disabled elements receive keyboard focus (tab stops on them)
225
+ - ✅ Focus indicator is visible with ≥ 3:1 contrast
226
+ - ✅ Disabled elements have `.is-disabled` class or `aria-disabled="true"`
227
+ - ✅ Visual disabled styling (opacity, color) is applied
228
+ - ✅ Clicking or pressing Enter/Space does NOT trigger actions
229
+
230
+ **Common Issues:**
231
+ - ❌ Disabled elements not in tab order (indicates native `disabled` attribute used instead of `aria-disabled`)
232
+ - ❌ No focus indicator on disabled elements
233
+ - ❌ Focus indicator has insufficient contrast due to disabled opacity
234
+
235
+ ---
236
+
237
+ ### 5. Screen Reader Announcement Test
238
+
239
+ **Objective:** Verify screen readers properly announce focus indicators and disabled states.
240
+
241
+ #### Windows - NVDA
242
+
243
+ **Steps:**
244
+
245
+ 1. **Start NVDA** (Control+Alt+N)
246
+ 2. **Navigate to test page**
247
+ 3. **Press Tab** to focus on disabled element
248
+ 4. **Listen for announcement**
249
+
250
+ **Expected Announcement:**
251
+ ```
252
+ "Email, edit, disabled, blank"
253
+ or
254
+ "Submit, button, disabled"
255
+ ```
256
+
257
+ **Key Commands:**
258
+ - `Tab` - Navigate to next element
259
+ - `Shift+Tab` - Navigate to previous element
260
+ - `Insert+↓` - Read current element
261
+ - `Insert+F7` - List all form fields
262
+
263
+ **Pass Criteria:**
264
+ - ✅ Screen reader announces "disabled" state
265
+ - ✅ Element type is announced (button, edit, etc.)
266
+ - ✅ Label or placeholder is announced
267
+
268
+ #### macOS - VoiceOver
269
+
270
+ **Steps:**
271
+
272
+ 1. **Start VoiceOver** (Cmd+F5)
273
+ 2. **Navigate to test page**
274
+ 3. **Press Tab** to focus on disabled element
275
+ 4. **Listen for announcement**
276
+
277
+ **Expected Announcement:**
278
+ ```
279
+ "Email, dimmed, edit text"
280
+ or
281
+ "Submit, dimmed, button"
282
+ ```
283
+
284
+ **Key Commands:**
285
+ - `Tab` - Navigate to next element
286
+ - `Shift+Tab` - Navigate to previous element
287
+ - `Control+Option+A` - Read element attributes
288
+ - `Control+Option+Shift+H` - Read hint
289
+
290
+ **Pass Criteria:**
291
+ - ✅ Screen reader announces "dimmed" or "disabled" state
292
+ - ✅ Element type is announced (button, edit text, etc.)
293
+ - ✅ Label or placeholder is announced
294
+
295
+ ---
296
+
297
+ ## Testing Checklist
298
+
299
+ Use this checklist for comprehensive focus indicator testing:
300
+
301
+ ### Basic Functionality
302
+ - [ ] All interactive elements are keyboard accessible
303
+ - [ ] Tab key navigates through all elements in logical order
304
+ - [ ] Shift+Tab navigates backwards
305
+ - [ ] Disabled elements remain in tab order (aria-disabled pattern)
306
+ - [ ] Focus indicator is visible on all focused elements
307
+
308
+ ### Contrast Requirements
309
+ - [ ] Focus indicator has ≥ 3:1 contrast against white background
310
+ - [ ] Focus indicator has ≥ 3:1 contrast against light gray backgrounds
311
+ - [ ] Focus indicator has ≥ 3:1 contrast against dark backgrounds (if applicable)
312
+ - [ ] Focus indicator is visible on disabled elements
313
+ - [ ] No information conveyed by focus indicator color alone
314
+
315
+ ### Theme Compatibility
316
+ - [ ] Focus indicator works in default light theme
317
+ - [ ] Focus indicator works in dark mode (if supported)
318
+ - [ ] Custom themes define `--focus-color` when needed
319
+ - [ ] Focus indicator contrast verified with DevTools
320
+
321
+ ### Screen Reader Compatibility
322
+ - [ ] NVDA announces disabled state correctly
323
+ - [ ] VoiceOver announces disabled state correctly
324
+ - [ ] Element labels are read by screen readers
325
+ - [ ] Form structure is understandable without visual cues
326
+
327
+ ### Component-Specific Tests
328
+ - [ ] Buttons: Visible focus indicator, "disabled" announced
329
+ - [ ] Inputs: Visible focus indicator, "disabled" announced
330
+ - [ ] Selects: Visible focus indicator, "disabled" announced
331
+ - [ ] Textareas: Visible focus indicator, "disabled" announced
332
+ - [ ] Links: Visible focus indicator (if used in disabled state)
333
+
334
+ ---
335
+
336
+ ## Reporting Issues
337
+
338
+ When reporting focus indicator issues, include:
339
+
340
+ 1. **Component name and state** (e.g., "Button, disabled state")
341
+ 2. **Browser and version** (e.g., "Chrome 120")
342
+ 3. **Theme/background** (e.g., "Gray container background #999999")
343
+ 4. **Measured contrast ratio** (e.g., "1.54:1 - FAIL")
344
+ 5. **Screenshot** showing the focused element
345
+ 6. **DevTools screenshot** showing the contrast measurement
346
+
347
+ **Example Issue Report:**
348
+
349
+ ```markdown
350
+ ## Focus Indicator Contrast Failure
351
+
352
+ **Component:** Input (disabled)
353
+ **Browser:** Chrome 120
354
+ **Background:** Gray container (#999999)
355
+ **Measured Contrast:** 1.54:1 (FAIL - requires 3:1 minimum)
356
+
357
+ **Screenshot:** [attach screenshot]
358
+
359
+ **Recommendation:** Add custom --focus-color for gray theme:
360
+ .theme-gray {
361
+ --focus-color: #005fcc; /* Provides 3.2:1 on gray */
362
+ }
363
+ ```
364
+
365
+ ---
366
+
367
+ ## Automated Testing Recommendations
368
+
369
+ While this guide focuses on manual testing, consider adding these automated tests:
370
+
371
+ ### jest-axe Integration
372
+
373
+ ```typescript
374
+ import { axe, toHaveNoViolations } from 'jest-axe';
375
+
376
+ expect.extend(toHaveNoViolations);
377
+
378
+ describe('Focus Indicator Accessibility', () => {
379
+ it('should not have focus indicator violations', async () => {
380
+ const { container } = render(
381
+ <Button type="button" disabled={true}>
382
+ Disabled Button
383
+ </Button>
384
+ );
385
+ const results = await axe(container);
386
+ expect(results).toHaveNoViolations();
387
+ });
388
+ });
389
+ ```
390
+
391
+ ### Keyboard Navigation Test
392
+
393
+ ```typescript
394
+ it('should be focusable when disabled', () => {
395
+ render(<Button type="button" disabled={true}>Disabled</Button>);
396
+ const button = screen.getByRole('button');
397
+
398
+ button.focus();
399
+ expect(button).toHaveFocus();
400
+ expect(button).toHaveAttribute('aria-disabled', 'true');
401
+ });
402
+ ```
403
+
404
+ ### Visual Regression Testing
405
+
406
+ Consider using tools like:
407
+ - **Chromatic** - Visual regression testing for Storybook
408
+ - **Percy** - Visual testing platform
409
+ - **BackstopJS** - Visual regression testing
410
+
411
+ ---
412
+
413
+ ## Additional Resources
414
+
415
+ ### WCAG References
416
+ - [WCAG 2.4.7 Focus Visible](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible)
417
+ - [WCAG 2.1.1 Keyboard](https://www.w3.org/WAI/WCAG21/Understanding/keyboard)
418
+ - [WCAG 1.4.11 Non-text Contrast](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast)
419
+
420
+ ### Tools
421
+ - [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
422
+ - [Chrome DevTools](https://developer.chrome.com/docs/devtools/)
423
+ - [axe DevTools Extension](https://www.deque.com/axe/devtools/)
424
+ - [WAVE Browser Extension](https://wave.webaim.org/extension/)
425
+
426
+ ### Articles
427
+ - [Understanding Focus Indicators](https://www.sarasoueidan.com/blog/focus-indicators/)
428
+ - [Accessible Focus Indicators](https://www.deque.com/blog/give-site-focus-tips-designing-usable-focus-indicators/)
429
+ - [The :focus-visible Selector](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible)
430
+
431
+ ---
432
+
433
+ ## Changelog
434
+
435
+ | Date | Version | Changes |
436
+ |------|---------|---------|
437
+ | 2025-11-02 | 1.0.0 | Initial focus indicator testing guide |
@@ -1,4 +1,4 @@
1
- import { a as a$1 } from './chunk-OVWLQYMK.js';
1
+ import { a as a$1 } from './chunk-KVKQLRJG.js';
2
2
  import { a } from './chunk-HHLNOC5T.js';
3
3
  import e from 'react';
4
4
 
@@ -6,4 +6,4 @@ var l=({id:n,children:a$1,classes:r,modalRef:d,openOnMount:i,...t})=>e.createEle
6
6
 
7
7
  export { u as a };
8
8
  //# sourceMappingURL=out.js.map
9
- //# sourceMappingURL=chunk-7XPFW7CB.js.map
9
+ //# sourceMappingURL=chunk-43TK2ICH.js.map
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ var chunkENTCUJ3A_cjs = require('./chunk-ENTCUJ3A.cjs');
4
+ var o = require('react');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var o__default = /*#__PURE__*/_interopDefault(o);
9
+
10
+ var i=o__default.default.forwardRef(({href:e,target:n,rel:r,children:t,styles:k,prefetch:s=!1,btnStyle:m,onClick:L,onPointerDown:d,...c},l)=>{let u=o__default.default.useMemo(()=>{if(n==="_blank"){let f=new Set(["noopener","noreferrer"]);return s&&f.add("prefetch"),r&&r.split(/\s+/).forEach(p=>{p&&f.add(p);}),Array.from(f).join(" ")}return r},[n,r,s]);return o__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"a",ref:l,href:e,target:n,rel:u,styles:k,"data-btn":m,onClick:L,onPointerDown:d,...c},t)}),y=o__default.default.forwardRef(({href:e,icon:n,...r},t)=>o__default.default.createElement(i,{ref:t,href:e,...r},n)),h=o__default.default.forwardRef(({href:e,children:n,...r},t)=>o__default.default.createElement(i,{ref:t,href:e,...r},n));y.displayName="IconLink";h.displayName="LinkButton";i.displayName="Link";var x=i;
11
+
12
+ exports.a = i;
13
+ exports.b = y;
14
+ exports.c = h;
15
+ exports.d = x;
16
+ //# sourceMappingURL=out.js.map
17
+ //# sourceMappingURL=chunk-5PJYLVFY.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/link/link.tsx"],"names":["React","Link","href","target","rel","children","styles","prefetch","btnStyle","onClick","onPointerDown","props","ref","computedRel","securityTokens","token","ui_default","IconLink","icon","LinkButton","link_default"],"mappings":"oCAAA,OAAOA,MAAW,QA+FX,IAAMC,EAAOD,EAAM,WACxB,CACE,CACE,KAAAE,EACA,OAAAC,EACA,IAAAC,EACA,SAAAC,EACA,OAAAC,EACA,SAAAC,EAAW,GACX,SAAAC,EACA,QAAAC,EACA,cAAAC,EACA,GAAGC,CACL,EACAC,IACG,CAWH,IAAMC,EAAcb,EAAM,QAAQ,IAAM,CACtC,GAAIG,IAAW,SAAU,CAEvB,IAAMW,EAAiB,IAAI,IAAI,CAAC,WAAY,YAAY,CAAC,EAGzD,OAAIP,GACFO,EAAe,IAAI,UAAU,EAI3BV,GACFA,EAAI,MAAM,KAAK,EAAE,QAASW,GAAU,CAC9BA,GAAOD,EAAe,IAAIC,CAAK,CACrC,CAAC,EAGI,MAAM,KAAKD,CAAc,EAAE,KAAK,GAAG,CAC5C,CAGA,OAAOV,CACT,EAAG,CAACD,EAAQC,EAAKG,CAAQ,CAAC,EAE1B,OACEP,EAAA,cAACgB,EAAA,CACC,GAAG,IACH,IAAKJ,EACL,KAAMV,EACN,OAAQC,EACR,IAAKU,EACL,OAAQP,EACR,WAAUE,EACV,QAASC,EACT,cAAeC,EACd,GAAGC,GAEHN,CACH,CAEJ,CACF,EAEaY,EAAWjB,EAAM,WAC5B,CAAC,CAAE,KAAAE,EAAM,KAAAgB,EAAM,GAAGP,CAAM,EAAGC,IAEvBZ,EAAA,cAACC,EAAA,CAAK,IAAKW,EAAK,KAAMV,EAAO,GAAGS,GAC7BO,CACH,CAGN,EAEaC,EAAanB,EAAM,WAC9B,CAAC,CAAE,KAAAE,EAAM,SAAAG,EAAU,GAAGM,CAAM,EAAGC,IAE3BZ,EAAA,cAACC,EAAA,CAAK,IAAKW,EAAK,KAAMV,EAAO,GAAGS,GAC7BN,CACH,CAGN,EAEAY,EAAS,YAAc,WACvBE,EAAW,YAAc,aACzBlB,EAAK,YAAc,OAEnB,IAAOmB,EAAQnB","sourcesContent":["import React from \"react\";\nimport UI from \"../ui\";\nimport type { LinkProps } from \"./link.types\";\n\n/**\n * Link - A semantic, accessible anchor component with enhanced security and styling.\n *\n * The Link component renders accessible `<a>` elements with automatic security\n * attributes for external links, customizable styling variants, and full WCAG 2.1\n * AA compliance. It supports traditional text links, button-styled links, and\n * programmatic focus management via ref forwarding.\n *\n * ## Features\n *\n * - 🔒 **Automatic Security**: External links get `rel=\"noopener noreferrer\"`\n * - ♿ **WCAG 2.1 AA Compliant**: Accessible focus indicators and semantic HTML\n * - 🎨 **Flexible Styling**: Text links, button links, and pill variants\n * - ⚡ **Performance**: Optional prefetch hints for faster navigation\n * - 🎯 **Ref Forwarding**: Direct DOM access for focus management and scroll\n * - 🧪 **Type-Safe**: Full TypeScript support with comprehensive prop types\n *\n * ## Accessibility\n *\n * - ✅ Semantic `<a>` element for proper keyboard navigation\n * - ✅ Focus indicators meet WCAG 2.4.7 (3:1 contrast ratio)\n * - ✅ Screen readers announce link purpose and destination\n * - ✅ External links include security attributes automatically\n * - ✅ Supports `aria-label` for icon-only or ambiguous links\n * - ✅ Ref forwarding enables skip-link patterns\n *\n * @example\n * // Basic internal link\n * <Link href=\"/about\">About Us</Link>\n *\n * @example\n * // External link with automatic security\n * <Link href=\"https://example.com\" target=\"_blank\">\n * Visit Example\n * </Link>\n *\n * @example\n * // Button-styled call-to-action link\n * <Link href=\"/signup\">\n * <b>Get Started</b>\n * </Link>\n *\n * @example\n * // Icon-only link with accessible label\n * <Link href=\"/settings\" aria-label=\"Open settings\">\n * <SettingsIcon aria-hidden=\"true\" />\n * </Link>\n *\n * @example\n * // Analytics tracking with onClick (includes keyboard users)\n * <Link\n * href=\"/products\"\n * onClick={(e) => trackEvent('link_click', { href: '/products' })}\n * >\n * Browse Products\n * </Link>\n *\n * @example\n * // Skip link with ref forwarding for focus management\n * const mainRef = useRef<HTMLAnchorElement>(null);\n *\n * <Link ref={mainRef} href=\"#main-content\">\n * Skip to main content\n * </Link>\n *\n * @example\n * // Custom styled link with CSS variables\n * <Link\n * href=\"/products\"\n * styles={{\n * '--link-color': '#0066cc',\n * '--link-decoration': 'underline',\n * }}\n * >\n * Browse Products\n * </Link>\n *\n * @example\n * // ✅ GOOD: Descriptive link text\n * <Link href=\"/docs/installation\">\n * Read installation guide\n * </Link>\n *\n * @example\n * // ❌ BAD: Generic link text (poor for screen readers)\n * <Link href=\"/docs/installation\">\n * Click here\n * </Link>\n *\n * @see {@link LinkProps} for complete prop documentation\n */\nexport const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(\n (\n {\n href,\n target,\n rel,\n children,\n styles,\n prefetch = false,\n btnStyle,\n onClick,\n onPointerDown,\n ...props\n },\n ref\n ) => {\n /**\n * Compute the final `rel` attribute value with security defaults.\n *\n * For external links (target=\"_blank\"), we merge user-provided `rel` values\n * with security defaults `noopener noreferrer` to prevent:\n * - window.opener exploitation (noopener)\n * - Referrer header leakage (noreferrer)\n *\n * If prefetch is enabled, we also add the `prefetch` hint.\n */\n const computedRel = React.useMemo(() => {\n if (target === \"_blank\") {\n // Start with security defaults\n const securityTokens = new Set([\"noopener\", \"noreferrer\"]);\n\n // Add prefetch if enabled\n if (prefetch) {\n securityTokens.add(\"prefetch\");\n }\n\n // Merge with user-provided rel tokens (if any)\n if (rel) {\n rel.split(/\\s+/).forEach((token) => {\n if (token) securityTokens.add(token);\n });\n }\n\n return Array.from(securityTokens).join(\" \");\n }\n\n // For non-external links, use provided rel as-is\n return rel;\n }, [target, rel, prefetch]);\n\n return (\n <UI\n as=\"a\"\n ref={ref}\n href={href}\n target={target}\n rel={computedRel}\n styles={styles}\n data-btn={btnStyle}\n onClick={onClick}\n onPointerDown={onPointerDown}\n {...props}\n >\n {children}\n </UI>\n );\n }\n);\n\nexport const IconLink = React.forwardRef<HTMLAnchorElement, LinkProps>(\n ({ href, icon, ...props }, ref) => {\n return (\n <Link ref={ref} href={href} {...props}>\n {icon}\n </Link>\n );\n }\n);\n\nexport const LinkButton = React.forwardRef<HTMLAnchorElement, LinkProps>(\n ({ href, children, ...props }, ref) => {\n return (\n <Link ref={ref} href={href} {...props}>\n {children}\n </Link>\n );\n }\n);\n\nIconLink.displayName = \"IconLink\";\nLinkButton.displayName = \"LinkButton\";\nLink.displayName = \"Link\";\n\nexport default Link;\n"]}
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ var chunkPNWIRCG3_cjs = require('./chunk-PNWIRCG3.cjs');
4
+ var chunkTON2YGMD_cjs = require('./chunk-TON2YGMD.cjs');
5
+ var chunkENTCUJ3A_cjs = require('./chunk-ENTCUJ3A.cjs');
6
+ var D = require('react');
7
+
8
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
+
10
+ var D__default = /*#__PURE__*/_interopDefault(D);
11
+
12
+ var r=({type:a="button",children:n,styles:p,disabled:i,isDisabled:l,classes:m,onPointerDown:u,onPointerOver:d,onPointerLeave:b,onClick:P,onKeyDown:c,...y})=>{let B=chunkPNWIRCG3_cjs.a(i,l),{disabledProps:t,handlers:f}=chunkTON2YGMD_cjs.a(B,{handlers:{onClick:P,onPointerDown:u,onKeyDown:c},className:m});return D__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"button",type:a,"aria-disabled":t["aria-disabled"],onPointerOver:d,onPointerLeave:b,style:p,className:t.className,...f,...y},n)};var h=r;r.displayName="Button";
13
+
14
+ exports.a = r;
15
+ exports.b = h;
16
+ //# sourceMappingURL=out.js.map
17
+ //# sourceMappingURL=chunk-E4OSROCA.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/buttons/button.tsx"],"names":["React","Button","type","children","styles","disabled","isDisabled","classes","onPointerDown","onPointerOver","onPointerLeave","onClick","onKeyDown","props","isActuallyDisabled","resolveDisabledState","disabledProps","handlers","useDisabledState","ui_default","button_default"],"mappings":"2HACA,OAAOA,MAAW,QAmEX,IAAMC,EAAS,CAAC,CACrB,KAAAC,EAAO,SACP,SAAAC,EACA,OAAAC,EACA,SAAAC,EACA,WAAAC,EACA,QAAAC,EACA,cAAAC,EACA,cAAAC,EACA,eAAAC,EACA,QAAAC,EACA,UAAAC,EACA,GAAGC,CACL,IAAmB,CAEjB,IAAMC,EAAqBC,EAAqBV,EAAUC,CAAU,EAG9D,CAAE,cAAAU,EAAe,SAAAC,CAAS,EAAIC,EAClCJ,EACA,CACE,SAAU,CACR,QAAAH,EACA,cAAAH,EACA,UAAAI,CACF,EAEA,UAAWL,CAGb,CACF,EAGA,OACEP,EAAA,cAACmB,EAAA,CACC,GAAG,SACH,KAAMjB,EACN,gBAAec,EAAc,eAAe,EAC5C,cAAeP,EACf,eAAgBC,EAChB,MAAON,EACP,UAAWY,EAAc,UACxB,GAAGC,EACH,GAAGJ,GAEHV,CACH,CAEJ,EAMA,IAAOiB,EAAQnB,EACfA,EAAO,YAAc","sourcesContent":["import UI from \"../ui\";\nimport React from \"react\";\nimport { useDisabledState } from \"../../hooks/use-disabled-state\";\nimport { resolveDisabledState } from \"../../utils/accessibility\";\nimport type { DisabledStateProps } from \"../../types/shared\";\n\nexport type ButtonProps = Partial<React.ComponentProps<typeof UI>> &\n DisabledStateProps & {\n /**\n * The button type\n * Required - 'button' | 'submit' | 'reset'\n */\n type: \"button\" | \"submit\" | \"reset\";\n };\n\n/**\n * Accessible Button component with WCAG 2.1 Level AA compliant disabled state.\n *\n * **Key Accessibility Features:**\n * - Uses `aria-disabled` pattern instead of native `disabled` attribute\n * - Maintains keyboard focusability when disabled (stays in tab order)\n * - Prevents all interactions when disabled via optimized `useDisabledState` hook\n * - Automatic className merging for seamless styling\n * - Supports both modern `disabled` and legacy `isDisabled` props\n *\n * **Why aria-disabled?**\n * - Elements remain in keyboard tab order (WCAG 2.1.1 - Keyboard)\n * - Screen readers can discover and announce disabled state (WCAG 4.1.2)\n * - Enables tooltips and help text on disabled buttons\n * - Better visual styling control for WCAG AA contrast compliance\n *\n * **Performance:**\n * - Uses optimized `useDisabledState` hook with stable references\n * - Automatic className merging eliminates boilerplate\n * - ~90% reduction in unnecessary re-renders compared to previous implementation\n *\n * @example\n * // Basic usage\n * <Button type=\"button\" onClick={handleClick}>\n * Click me\n * </Button>\n *\n * @example\n * // Disabled state (prevents all interactions but stays focusable)\n * <Button type=\"button\" disabled={true} onClick={handleClick}>\n * Cannot click (but can focus for screen readers)\n * </Button>\n *\n * @example\n * // With custom classes (automatic merging with .is-disabled)\n * <Button\n * type=\"button\"\n * disabled={true}\n * classes=\"my-custom-btn\"\n * >\n * Custom disabled button\n * </Button>\n *\n * @example\n * // Legacy isDisabled prop (still supported)\n * <Button type=\"button\" isDisabled={true} onClick={handleClick}>\n * Legacy disabled\n * </Button>\n *\n * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/keyboard WCAG 2.1.1 - Keyboard}\n * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/name-role-value WCAG 4.1.2 - Name, Role, Value}\n * @see {@link file://./../../hooks/useDisabledState.md useDisabledState Hook Documentation}\n */\nexport const Button = ({\n type = \"button\",\n children,\n styles,\n disabled,\n isDisabled,\n classes,\n onPointerDown,\n onPointerOver,\n onPointerLeave,\n onClick,\n onKeyDown,\n ...props\n}: ButtonProps) => {\n // Resolve disabled state from both props (disabled takes precedence)\n const isActuallyDisabled = resolveDisabledState(disabled, isDisabled);\n\n // Use the disabled state hook with enhanced API for automatic className merging\n const { disabledProps, handlers } = useDisabledState<HTMLButtonElement>(\n isActuallyDisabled,\n {\n handlers: {\n onClick,\n onPointerDown,\n onKeyDown,\n },\n // Automatic className merging - hook combines disabled class with user classes\n className: classes,\n // Note: onPointerOver and onPointerLeave are intentionally NOT wrapped\n // to allow hover effects on disabled buttons for visual feedback\n }\n );\n\n /* Returning a button element with accessible disabled state */\n return (\n <UI\n as=\"button\"\n type={type}\n aria-disabled={disabledProps[\"aria-disabled\"]}\n onPointerOver={onPointerOver}\n onPointerLeave={onPointerLeave}\n style={styles}\n className={disabledProps.className}\n {...handlers}\n {...props}\n >\n {children}\n </UI>\n );\n};\n\nexport const IconButton = ({ icon, ...props }: ButtonProps) => {\n return <Button {...props}>{icon}</Button>;\n};\n\nexport default Button;\nButton.displayName = \"Button\";\n"]}
@@ -0,0 +1,10 @@
1
+ import { a } from './chunk-BFK62VX5.js';
2
+ import { a as a$1 } from './chunk-75QHTLFO.js';
3
+ import { a as a$2 } from './chunk-HHLNOC5T.js';
4
+ import D from 'react';
5
+
6
+ var r=({type:a$3="button",children:n,styles:p,disabled:i,isDisabled:l,classes:m,onPointerDown:u,onPointerOver:d,onPointerLeave:b,onClick:P,onKeyDown:c,...y})=>{let B=a(i,l),{disabledProps:t,handlers:f}=a$1(B,{handlers:{onClick:P,onPointerDown:u,onKeyDown:c},className:m});return D.createElement(a$2,{as:"button",type:a$3,"aria-disabled":t["aria-disabled"],onPointerOver:d,onPointerLeave:b,style:p,className:t.className,...f,...y},n)};var h=r;r.displayName="Button";
7
+
8
+ export { r as a, h as b };
9
+ //# sourceMappingURL=out.js.map
10
+ //# sourceMappingURL=chunk-KVKQLRJG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/buttons/button.tsx"],"names":["React","Button","type","children","styles","disabled","isDisabled","classes","onPointerDown","onPointerOver","onPointerLeave","onClick","onKeyDown","props","isActuallyDisabled","resolveDisabledState","disabledProps","handlers","useDisabledState","ui_default","button_default"],"mappings":"wHACA,OAAOA,MAAW,QAmEX,IAAMC,EAAS,CAAC,CACrB,KAAAC,EAAO,SACP,SAAAC,EACA,OAAAC,EACA,SAAAC,EACA,WAAAC,EACA,QAAAC,EACA,cAAAC,EACA,cAAAC,EACA,eAAAC,EACA,QAAAC,EACA,UAAAC,EACA,GAAGC,CACL,IAAmB,CAEjB,IAAMC,EAAqBC,EAAqBV,EAAUC,CAAU,EAG9D,CAAE,cAAAU,EAAe,SAAAC,CAAS,EAAIC,EAClCJ,EACA,CACE,SAAU,CACR,QAAAH,EACA,cAAAH,EACA,UAAAI,CACF,EAEA,UAAWL,CAGb,CACF,EAGA,OACEP,EAAA,cAACmB,EAAA,CACC,GAAG,SACH,KAAMjB,EACN,gBAAec,EAAc,eAAe,EAC5C,cAAeP,EACf,eAAgBC,EAChB,MAAON,EACP,UAAWY,EAAc,UACxB,GAAGC,EACH,GAAGJ,GAEHV,CACH,CAEJ,EAMA,IAAOiB,EAAQnB,EACfA,EAAO,YAAc","sourcesContent":["import UI from \"../ui\";\nimport React from \"react\";\nimport { useDisabledState } from \"../../hooks/use-disabled-state\";\nimport { resolveDisabledState } from \"../../utils/accessibility\";\nimport type { DisabledStateProps } from \"../../types/shared\";\n\nexport type ButtonProps = Partial<React.ComponentProps<typeof UI>> &\n DisabledStateProps & {\n /**\n * The button type\n * Required - 'button' | 'submit' | 'reset'\n */\n type: \"button\" | \"submit\" | \"reset\";\n };\n\n/**\n * Accessible Button component with WCAG 2.1 Level AA compliant disabled state.\n *\n * **Key Accessibility Features:**\n * - Uses `aria-disabled` pattern instead of native `disabled` attribute\n * - Maintains keyboard focusability when disabled (stays in tab order)\n * - Prevents all interactions when disabled via optimized `useDisabledState` hook\n * - Automatic className merging for seamless styling\n * - Supports both modern `disabled` and legacy `isDisabled` props\n *\n * **Why aria-disabled?**\n * - Elements remain in keyboard tab order (WCAG 2.1.1 - Keyboard)\n * - Screen readers can discover and announce disabled state (WCAG 4.1.2)\n * - Enables tooltips and help text on disabled buttons\n * - Better visual styling control for WCAG AA contrast compliance\n *\n * **Performance:**\n * - Uses optimized `useDisabledState` hook with stable references\n * - Automatic className merging eliminates boilerplate\n * - ~90% reduction in unnecessary re-renders compared to previous implementation\n *\n * @example\n * // Basic usage\n * <Button type=\"button\" onClick={handleClick}>\n * Click me\n * </Button>\n *\n * @example\n * // Disabled state (prevents all interactions but stays focusable)\n * <Button type=\"button\" disabled={true} onClick={handleClick}>\n * Cannot click (but can focus for screen readers)\n * </Button>\n *\n * @example\n * // With custom classes (automatic merging with .is-disabled)\n * <Button\n * type=\"button\"\n * disabled={true}\n * classes=\"my-custom-btn\"\n * >\n * Custom disabled button\n * </Button>\n *\n * @example\n * // Legacy isDisabled prop (still supported)\n * <Button type=\"button\" isDisabled={true} onClick={handleClick}>\n * Legacy disabled\n * </Button>\n *\n * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/keyboard WCAG 2.1.1 - Keyboard}\n * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/name-role-value WCAG 4.1.2 - Name, Role, Value}\n * @see {@link file://./../../hooks/useDisabledState.md useDisabledState Hook Documentation}\n */\nexport const Button = ({\n type = \"button\",\n children,\n styles,\n disabled,\n isDisabled,\n classes,\n onPointerDown,\n onPointerOver,\n onPointerLeave,\n onClick,\n onKeyDown,\n ...props\n}: ButtonProps) => {\n // Resolve disabled state from both props (disabled takes precedence)\n const isActuallyDisabled = resolveDisabledState(disabled, isDisabled);\n\n // Use the disabled state hook with enhanced API for automatic className merging\n const { disabledProps, handlers } = useDisabledState<HTMLButtonElement>(\n isActuallyDisabled,\n {\n handlers: {\n onClick,\n onPointerDown,\n onKeyDown,\n },\n // Automatic className merging - hook combines disabled class with user classes\n className: classes,\n // Note: onPointerOver and onPointerLeave are intentionally NOT wrapped\n // to allow hover effects on disabled buttons for visual feedback\n }\n );\n\n /* Returning a button element with accessible disabled state */\n return (\n <UI\n as=\"button\"\n type={type}\n aria-disabled={disabledProps[\"aria-disabled\"]}\n onPointerOver={onPointerOver}\n onPointerLeave={onPointerLeave}\n style={styles}\n className={disabledProps.className}\n {...handlers}\n {...props}\n >\n {children}\n </UI>\n );\n};\n\nexport const IconButton = ({ icon, ...props }: ButtonProps) => {\n return <Button {...props}>{icon}</Button>;\n};\n\nexport default Button;\nButton.displayName = \"Button\";\n"]}
@@ -2,7 +2,7 @@
2
2
 
3
3
  var chunk2NRIP6RB_cjs = require('./chunk-2NRIP6RB.cjs');
4
4
  var chunk6WTC4JXH_cjs = require('./chunk-6WTC4JXH.cjs');
5
- var chunkGT77BX4L_cjs = require('./chunk-GT77BX4L.cjs');
5
+ var chunkE4OSROCA_cjs = require('./chunk-E4OSROCA.cjs');
6
6
  var chunkENTCUJ3A_cjs = require('./chunk-ENTCUJ3A.cjs');
7
7
  var s = require('react');
8
8
 
@@ -10,9 +10,9 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
10
 
11
11
  var s__default = /*#__PURE__*/_interopDefault(s);
12
12
 
13
- var y=({dialogTitle:t,onClick:o,id:l,type:e="h3"})=>{let a=s.useCallback(()=>{o();},[o]);return s__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"div",classes:"dialog-header"},s__default.default.createElement(chunk2NRIP6RB_cjs.b,{type:e,className:"dialog-title",id:l},t||"Dialog"),s__default.default.createElement(chunkGT77BX4L_cjs.b,{type:"button",onClick:a,className:"dialog-close","aria-label":"Close dialog","data-btn":"icon"},s__default.default.createElement(chunk6WTC4JXH_cjs.b,null,s__default.default.createElement(chunk6WTC4JXH_cjs.b.Remove,{size:16}))))},D=s__default.default.memo(y);y.displayName="DialogHeader";var H=({onClose:t,onConfirm:o,confirmLabel:l,cancelLabel:e})=>{let a=s.useCallback(()=>{t();},[t]),m=s.useCallback(()=>{o&&o();},[o]);return s__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"section",className:"dialog-footer"},e&&s__default.default.createElement(chunkGT77BX4L_cjs.b,{type:"button",onClick:a,className:"dialog-button button-secondary","data-btn":"sm"},e),o&&s__default.default.createElement(chunkGT77BX4L_cjs.b,{type:"button",onClick:m,className:"dialog-button button-primary","data-btn":"sm"},l))};H.displayName="DialogFooter";var I=s__default.default.memo(H);var h=(t,o)=>s.useCallback(e=>{let a=t.current?.getBoundingClientRect();a&&(e.clientY<a.top||e.clientY>a.bottom||e.clientX<a.left||e.clientX>a.right)&&o();},[t,o]);var F=({isOpen:t,onOpenChange:o,isAlertDialog:l=!1,onClose:e,dialogTitle:a,dialogLabel:m,children:P,onConfirm:U,confirmLabel:B="Confirm",cancelLabel:E="Cancel",className:M="",hideFooter:v=!1,styles:x})=>{let f=s.useRef(null),u=s.useId();s.useEffect(()=>{let r=f.current;r&&(t?l?r.show():r.showModal():r.close());},[t,l]);let c=s.useCallback(()=>{o(!1),e&&e();},[o,e]),L=h(f,c),b=s.useId();return s__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"dialog",role:l?"alertdialog":"dialog",ref:f,onClose:c,onClick:L,"aria-modal":t&&!l?"true":void 0,"aria-labelledby":u,"aria-describedby":b,"aria-label":m,className:`dialog-modal ${M}`.trim(),style:x},s__default.default.createElement(D,{dialogTitle:a,onClick:c,id:u}),s__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"section",id:b,className:"dialog-content",onClick:r=>r.stopPropagation()},P,!v&&s__default.default.createElement(I,{onClose:c,onConfirm:U,confirmLabel:B,cancelLabel:E})))};F.displayName="Dialog";var ao=s__default.default.memo(F);
13
+ var y=({dialogTitle:t,onClick:o,id:l,type:e="h3"})=>{let a=s.useCallback(()=>{o();},[o]);return s__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"div",classes:"dialog-header"},s__default.default.createElement(chunk2NRIP6RB_cjs.b,{type:e,className:"dialog-title",id:l},t||"Dialog"),s__default.default.createElement(chunkE4OSROCA_cjs.b,{type:"button",onClick:a,className:"dialog-close","aria-label":"Close dialog","data-btn":"icon"},s__default.default.createElement(chunk6WTC4JXH_cjs.b,null,s__default.default.createElement(chunk6WTC4JXH_cjs.b.Remove,{size:16}))))},D=s__default.default.memo(y);y.displayName="DialogHeader";var H=({onClose:t,onConfirm:o,confirmLabel:l,cancelLabel:e})=>{let a=s.useCallback(()=>{t();},[t]),m=s.useCallback(()=>{o&&o();},[o]);return s__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"section",className:"dialog-footer"},e&&s__default.default.createElement(chunkE4OSROCA_cjs.b,{type:"button",onClick:a,className:"dialog-button button-secondary","data-btn":"sm"},e),o&&s__default.default.createElement(chunkE4OSROCA_cjs.b,{type:"button",onClick:m,className:"dialog-button button-primary","data-btn":"sm"},l))};H.displayName="DialogFooter";var I=s__default.default.memo(H);var h=(t,o)=>s.useCallback(e=>{let a=t.current?.getBoundingClientRect();a&&(e.clientY<a.top||e.clientY>a.bottom||e.clientX<a.left||e.clientX>a.right)&&o();},[t,o]);var F=({isOpen:t,onOpenChange:o,isAlertDialog:l=!1,onClose:e,dialogTitle:a,dialogLabel:m,children:P,onConfirm:U,confirmLabel:B="Confirm",cancelLabel:E="Cancel",className:M="",hideFooter:v=!1,styles:x})=>{let f=s.useRef(null),u=s.useId();s.useEffect(()=>{let r=f.current;r&&(t?l?r.show():r.showModal():r.close());},[t,l]);let c=s.useCallback(()=>{o(!1),e&&e();},[o,e]),L=h(f,c),b=s.useId();return s__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"dialog",role:l?"alertdialog":"dialog",ref:f,onClose:c,onClick:L,"aria-modal":t&&!l?"true":void 0,"aria-labelledby":u,"aria-describedby":b,"aria-label":m,className:`dialog-modal ${M}`.trim(),style:x},s__default.default.createElement(D,{dialogTitle:a,onClick:c,id:u}),s__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"section",id:b,className:"dialog-content",onClick:r=>r.stopPropagation()},P,!v&&s__default.default.createElement(I,{onClose:c,onConfirm:U,confirmLabel:B,cancelLabel:E})))};F.displayName="Dialog";var ao=s__default.default.memo(F);
14
14
 
15
15
  exports.a = F;
16
16
  exports.b = ao;
17
17
  //# sourceMappingURL=out.js.map
18
- //# sourceMappingURL=chunk-QVW6W76L.cjs.map
18
+ //# sourceMappingURL=chunk-MGPWZRBX.cjs.map
@@ -0,0 +1,8 @@
1
+ import { a } from './chunk-HHLNOC5T.js';
2
+ import o from 'react';
3
+
4
+ var i=o.forwardRef(({href:e,target:n,rel:r,children:t,styles:k,prefetch:s=!1,btnStyle:m,onClick:L,onPointerDown:d,...c},l)=>{let u=o.useMemo(()=>{if(n==="_blank"){let f=new Set(["noopener","noreferrer"]);return s&&f.add("prefetch"),r&&r.split(/\s+/).forEach(p=>{p&&f.add(p);}),Array.from(f).join(" ")}return r},[n,r,s]);return o.createElement(a,{as:"a",ref:l,href:e,target:n,rel:u,styles:k,"data-btn":m,onClick:L,onPointerDown:d,...c},t)}),y=o.forwardRef(({href:e,icon:n,...r},t)=>o.createElement(i,{ref:t,href:e,...r},n)),h=o.forwardRef(({href:e,children:n,...r},t)=>o.createElement(i,{ref:t,href:e,...r},n));y.displayName="IconLink";h.displayName="LinkButton";i.displayName="Link";var x=i;
5
+
6
+ export { i as a, y as b, h as c, x as d };
7
+ //# sourceMappingURL=out.js.map
8
+ //# sourceMappingURL=chunk-NNTBIHSD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/link/link.tsx"],"names":["React","Link","href","target","rel","children","styles","prefetch","btnStyle","onClick","onPointerDown","props","ref","computedRel","securityTokens","token","ui_default","IconLink","icon","LinkButton","link_default"],"mappings":"mCAAA,OAAOA,MAAW,QA+FX,IAAMC,EAAOD,EAAM,WACxB,CACE,CACE,KAAAE,EACA,OAAAC,EACA,IAAAC,EACA,SAAAC,EACA,OAAAC,EACA,SAAAC,EAAW,GACX,SAAAC,EACA,QAAAC,EACA,cAAAC,EACA,GAAGC,CACL,EACAC,IACG,CAWH,IAAMC,EAAcb,EAAM,QAAQ,IAAM,CACtC,GAAIG,IAAW,SAAU,CAEvB,IAAMW,EAAiB,IAAI,IAAI,CAAC,WAAY,YAAY,CAAC,EAGzD,OAAIP,GACFO,EAAe,IAAI,UAAU,EAI3BV,GACFA,EAAI,MAAM,KAAK,EAAE,QAASW,GAAU,CAC9BA,GAAOD,EAAe,IAAIC,CAAK,CACrC,CAAC,EAGI,MAAM,KAAKD,CAAc,EAAE,KAAK,GAAG,CAC5C,CAGA,OAAOV,CACT,EAAG,CAACD,EAAQC,EAAKG,CAAQ,CAAC,EAE1B,OACEP,EAAA,cAACgB,EAAA,CACC,GAAG,IACH,IAAKJ,EACL,KAAMV,EACN,OAAQC,EACR,IAAKU,EACL,OAAQP,EACR,WAAUE,EACV,QAASC,EACT,cAAeC,EACd,GAAGC,GAEHN,CACH,CAEJ,CACF,EAEaY,EAAWjB,EAAM,WAC5B,CAAC,CAAE,KAAAE,EAAM,KAAAgB,EAAM,GAAGP,CAAM,EAAGC,IAEvBZ,EAAA,cAACC,EAAA,CAAK,IAAKW,EAAK,KAAMV,EAAO,GAAGS,GAC7BO,CACH,CAGN,EAEaC,EAAanB,EAAM,WAC9B,CAAC,CAAE,KAAAE,EAAM,SAAAG,EAAU,GAAGM,CAAM,EAAGC,IAE3BZ,EAAA,cAACC,EAAA,CAAK,IAAKW,EAAK,KAAMV,EAAO,GAAGS,GAC7BN,CACH,CAGN,EAEAY,EAAS,YAAc,WACvBE,EAAW,YAAc,aACzBlB,EAAK,YAAc,OAEnB,IAAOmB,EAAQnB","sourcesContent":["import React from \"react\";\nimport UI from \"../ui\";\nimport type { LinkProps } from \"./link.types\";\n\n/**\n * Link - A semantic, accessible anchor component with enhanced security and styling.\n *\n * The Link component renders accessible `<a>` elements with automatic security\n * attributes for external links, customizable styling variants, and full WCAG 2.1\n * AA compliance. It supports traditional text links, button-styled links, and\n * programmatic focus management via ref forwarding.\n *\n * ## Features\n *\n * - 🔒 **Automatic Security**: External links get `rel=\"noopener noreferrer\"`\n * - ♿ **WCAG 2.1 AA Compliant**: Accessible focus indicators and semantic HTML\n * - 🎨 **Flexible Styling**: Text links, button links, and pill variants\n * - ⚡ **Performance**: Optional prefetch hints for faster navigation\n * - 🎯 **Ref Forwarding**: Direct DOM access for focus management and scroll\n * - 🧪 **Type-Safe**: Full TypeScript support with comprehensive prop types\n *\n * ## Accessibility\n *\n * - ✅ Semantic `<a>` element for proper keyboard navigation\n * - ✅ Focus indicators meet WCAG 2.4.7 (3:1 contrast ratio)\n * - ✅ Screen readers announce link purpose and destination\n * - ✅ External links include security attributes automatically\n * - ✅ Supports `aria-label` for icon-only or ambiguous links\n * - ✅ Ref forwarding enables skip-link patterns\n *\n * @example\n * // Basic internal link\n * <Link href=\"/about\">About Us</Link>\n *\n * @example\n * // External link with automatic security\n * <Link href=\"https://example.com\" target=\"_blank\">\n * Visit Example\n * </Link>\n *\n * @example\n * // Button-styled call-to-action link\n * <Link href=\"/signup\">\n * <b>Get Started</b>\n * </Link>\n *\n * @example\n * // Icon-only link with accessible label\n * <Link href=\"/settings\" aria-label=\"Open settings\">\n * <SettingsIcon aria-hidden=\"true\" />\n * </Link>\n *\n * @example\n * // Analytics tracking with onClick (includes keyboard users)\n * <Link\n * href=\"/products\"\n * onClick={(e) => trackEvent('link_click', { href: '/products' })}\n * >\n * Browse Products\n * </Link>\n *\n * @example\n * // Skip link with ref forwarding for focus management\n * const mainRef = useRef<HTMLAnchorElement>(null);\n *\n * <Link ref={mainRef} href=\"#main-content\">\n * Skip to main content\n * </Link>\n *\n * @example\n * // Custom styled link with CSS variables\n * <Link\n * href=\"/products\"\n * styles={{\n * '--link-color': '#0066cc',\n * '--link-decoration': 'underline',\n * }}\n * >\n * Browse Products\n * </Link>\n *\n * @example\n * // ✅ GOOD: Descriptive link text\n * <Link href=\"/docs/installation\">\n * Read installation guide\n * </Link>\n *\n * @example\n * // ❌ BAD: Generic link text (poor for screen readers)\n * <Link href=\"/docs/installation\">\n * Click here\n * </Link>\n *\n * @see {@link LinkProps} for complete prop documentation\n */\nexport const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(\n (\n {\n href,\n target,\n rel,\n children,\n styles,\n prefetch = false,\n btnStyle,\n onClick,\n onPointerDown,\n ...props\n },\n ref\n ) => {\n /**\n * Compute the final `rel` attribute value with security defaults.\n *\n * For external links (target=\"_blank\"), we merge user-provided `rel` values\n * with security defaults `noopener noreferrer` to prevent:\n * - window.opener exploitation (noopener)\n * - Referrer header leakage (noreferrer)\n *\n * If prefetch is enabled, we also add the `prefetch` hint.\n */\n const computedRel = React.useMemo(() => {\n if (target === \"_blank\") {\n // Start with security defaults\n const securityTokens = new Set([\"noopener\", \"noreferrer\"]);\n\n // Add prefetch if enabled\n if (prefetch) {\n securityTokens.add(\"prefetch\");\n }\n\n // Merge with user-provided rel tokens (if any)\n if (rel) {\n rel.split(/\\s+/).forEach((token) => {\n if (token) securityTokens.add(token);\n });\n }\n\n return Array.from(securityTokens).join(\" \");\n }\n\n // For non-external links, use provided rel as-is\n return rel;\n }, [target, rel, prefetch]);\n\n return (\n <UI\n as=\"a\"\n ref={ref}\n href={href}\n target={target}\n rel={computedRel}\n styles={styles}\n data-btn={btnStyle}\n onClick={onClick}\n onPointerDown={onPointerDown}\n {...props}\n >\n {children}\n </UI>\n );\n }\n);\n\nexport const IconLink = React.forwardRef<HTMLAnchorElement, LinkProps>(\n ({ href, icon, ...props }, ref) => {\n return (\n <Link ref={ref} href={href} {...props}>\n {icon}\n </Link>\n );\n }\n);\n\nexport const LinkButton = React.forwardRef<HTMLAnchorElement, LinkProps>(\n ({ href, children, ...props }, ref) => {\n return (\n <Link ref={ref} href={href} {...props}>\n {children}\n </Link>\n );\n }\n);\n\nIconLink.displayName = \"IconLink\";\nLinkButton.displayName = \"LinkButton\";\nLink.displayName = \"Link\";\n\nexport default Link;\n"]}
@@ -1,6 +1,6 @@
1
1
  import { b } from './chunk-ZFJ4U45S.js';
2
2
  import { b as b$2 } from './chunk-5QD3DWFI.js';
3
- import { b as b$1 } from './chunk-OVWLQYMK.js';
3
+ import { b as b$1 } from './chunk-KVKQLRJG.js';
4
4
  import { a } from './chunk-HHLNOC5T.js';
5
5
  import s, { useCallback, useRef, useId, useEffect } from 'react';
6
6
 
@@ -8,4 +8,4 @@ var y=({dialogTitle:t,onClick:o,id:l,type:e="h3"})=>{let a$1=useCallback(()=>{o(
8
8
 
9
9
  export { F as a, ao as b };
10
10
  //# sourceMappingURL=out.js.map
11
- //# sourceMappingURL=chunk-X3JCTEPD.js.map
11
+ //# sourceMappingURL=chunk-QKHPHMG2.js.map
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunk33PNJ4LO_cjs = require('./chunk-33PNJ4LO.cjs');
3
+ var chunk5PJYLVFY_cjs = require('./chunk-5PJYLVFY.cjs');
4
4
  var chunkENTCUJ3A_cjs = require('./chunk-ENTCUJ3A.cjs');
5
5
  var e = require('react');
6
6
 
@@ -8,10 +8,10 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
8
 
9
9
  var e__default = /*#__PURE__*/_interopDefault(e);
10
10
 
11
- var U=(r,t=15)=>r.length>t?`${r.slice(0,t)}...`:r;var i=e__default.default.memo(({children:r,id:t,styles:s,classes:a,...m})=>{let{renderStyles:n,defaultStyles:o,as:d,ref:I,...p}=m;return e__default.default.createElement("li",{id:t,style:s,className:a,"data-list":"unstyled inline",...p},r)});i.displayName="BreadcrumbItem";var y=e__default.default.memo(({children:r,...t})=>e__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"ol","data-list":"unstyled inline",...t},r));y.displayName="BreadcrumbList";var B=e__default.default.memo(({styles:r,id:t,classes:s,children:a,...m})=>e__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"nav",id:t,styles:r,className:s,...m},e__default.default.createElement(y,null,a)));B.displayName="BreadcrumbNav";function w(r,t){let s=e__default.default.useMemo(()=>r?r.split("/").filter(n=>n):[],[r]),a=e__default.default.useCallback(n=>{let o=t?.find(d=>d.path===n);return {path:o?.path||n,name:o?.name||n,url:o?.url||n}},[t]),m=e__default.default.useMemo(()=>s.map((n,o)=>({...a(n),isLast:o===s.length-1,index:o})),[s,a]);return {segments:m,hasSegments:m.length>0}}var l=({startRoute:r="Home",startRouteUrl:t="/",currentRoute:s,spacer:a=e__default.default.createElement(e__default.default.Fragment,null,"/"),routes:m,styles:n,id:o,classes:d,ariaLabel:I="Breadcrumb",truncateLength:p=15,linkProps:N,...k})=>{let{segments:L,hasSegments:v}=w(s,m),b=e__default.default.useId();return !s?.length||!v?null:e__default.default.createElement(B,{id:o,styles:n,className:d,"aria-label":I,...k},e__default.default.createElement(i,{key:`start-${b}`},e__default.default.createElement(chunk33PNJ4LO_cjs.b,{href:t,...N},r)),L.map(({name:$,url:x,path:u,isLast:S,index:h})=>{let c=decodeURIComponent($),C=U(c,p),P=c.length>p;if(S){let M=h>0?L[h-1].path:null;return !u||u.length<=3||u===M?null:e__default.default.createElement(i,{key:`${u}-${b}`},e__default.default.createElement("span",{"aria-hidden":"true"},a),e__default.default.createElement("span",{"aria-current":"page","aria-label":P?c:void 0},C))}return e__default.default.createElement(i,{key:`${u}-${b}`},e__default.default.createElement("span",{"aria-hidden":"true"},a),e__default.default.createElement(chunk33PNJ4LO_cjs.b,{href:x,"aria-label":P?c:void 0,...N},C))}))},H=l;l.displayName="Breadcrumb";l.Nav=B;l.List=y;l.Item=i;
11
+ var U=(r,t=15)=>r.length>t?`${r.slice(0,t)}...`:r;var i=e__default.default.memo(({children:r,id:t,styles:s,classes:a,...m})=>{let{renderStyles:n,defaultStyles:o,as:d,ref:I,...p}=m;return e__default.default.createElement("li",{id:t,style:s,className:a,"data-list":"unstyled inline",...p},r)});i.displayName="BreadcrumbItem";var y=e__default.default.memo(({children:r,...t})=>e__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"ol","data-list":"unstyled inline",...t},r));y.displayName="BreadcrumbList";var B=e__default.default.memo(({styles:r,id:t,classes:s,children:a,...m})=>e__default.default.createElement(chunkENTCUJ3A_cjs.a,{as:"nav",id:t,styles:r,className:s,...m},e__default.default.createElement(y,null,a)));B.displayName="BreadcrumbNav";function w(r,t){let s=e__default.default.useMemo(()=>r?r.split("/").filter(n=>n):[],[r]),a=e__default.default.useCallback(n=>{let o=t?.find(d=>d.path===n);return {path:o?.path||n,name:o?.name||n,url:o?.url||n}},[t]),m=e__default.default.useMemo(()=>s.map((n,o)=>({...a(n),isLast:o===s.length-1,index:o})),[s,a]);return {segments:m,hasSegments:m.length>0}}var l=({startRoute:r="Home",startRouteUrl:t="/",currentRoute:s,spacer:a=e__default.default.createElement(e__default.default.Fragment,null,"/"),routes:m,styles:n,id:o,classes:d,ariaLabel:I="Breadcrumb",truncateLength:p=15,linkProps:N,...k})=>{let{segments:L,hasSegments:v}=w(s,m),b=e__default.default.useId();return !s?.length||!v?null:e__default.default.createElement(B,{id:o,styles:n,className:d,"aria-label":I,...k},e__default.default.createElement(i,{key:`start-${b}`},e__default.default.createElement(chunk5PJYLVFY_cjs.d,{href:t,...N},r)),L.map(({name:$,url:x,path:u,isLast:S,index:h})=>{let c=decodeURIComponent($),C=U(c,p),P=c.length>p;if(S){let M=h>0?L[h-1].path:null;return !u||u.length<=3||u===M?null:e__default.default.createElement(i,{key:`${u}-${b}`},e__default.default.createElement("span",{"aria-hidden":"true"},a),e__default.default.createElement("span",{"aria-current":"page","aria-label":P?c:void 0},C))}return e__default.default.createElement(i,{key:`${u}-${b}`},e__default.default.createElement("span",{"aria-hidden":"true"},a),e__default.default.createElement(chunk5PJYLVFY_cjs.d,{href:x,"aria-label":P?c:void 0,...N},C))}))},H=l;l.displayName="Breadcrumb";l.Nav=B;l.List=y;l.Item=i;
12
12
 
13
13
  exports.a = w;
14
14
  exports.b = l;
15
15
  exports.c = H;
16
16
  //# sourceMappingURL=out.js.map
17
- //# sourceMappingURL=chunk-T4T6GWYQ.cjs.map
17
+ //# sourceMappingURL=chunk-R7NLLZU2.cjs.map