@fpkit/acss 0.5.13 → 0.6.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.
- package/libs/{chunk-PQ2K3BM6.cjs → chunk-2NRIP6RB.cjs} +3 -3
- package/libs/chunk-33PNJ4LO.cjs +15 -0
- package/libs/chunk-33PNJ4LO.cjs.map +1 -0
- package/libs/chunk-4BZKFPEC.cjs +17 -0
- package/libs/chunk-4BZKFPEC.cjs.map +1 -0
- package/libs/{chunk-772NRB75.js → chunk-5QD3DWFI.js} +2 -2
- package/libs/chunk-6SAHIYCZ.js +7 -0
- package/libs/chunk-6SAHIYCZ.js.map +1 -0
- package/libs/{chunk-3MKLDCKQ.cjs → chunk-6WTC4JXH.cjs} +3 -3
- package/libs/chunk-75QHTLFO.js +7 -0
- package/libs/chunk-75QHTLFO.js.map +1 -0
- package/libs/{chunk-ZANSFMTD.js → chunk-7XPFW7CB.js} +3 -3
- package/libs/chunk-BFK62VX5.js +5 -0
- package/libs/chunk-BFK62VX5.js.map +1 -0
- package/libs/{chunk-ROZI23GS.cjs → chunk-DKTHCQ5P.cjs} +4 -4
- package/libs/chunk-E2AJURUW.cjs +13 -0
- package/libs/chunk-E2AJURUW.cjs.map +1 -0
- package/libs/{chunk-L75OQKEI.cjs → chunk-ENTCUJ3A.cjs} +3 -3
- package/libs/chunk-ENTCUJ3A.cjs.map +1 -0
- package/libs/chunk-F5EYMVQM.js +10 -0
- package/libs/chunk-F5EYMVQM.js.map +1 -0
- package/libs/chunk-FVROL3V5.js +9 -0
- package/libs/chunk-FVROL3V5.js.map +1 -0
- package/libs/chunk-GT77BX4L.cjs +17 -0
- package/libs/chunk-GT77BX4L.cjs.map +1 -0
- package/libs/chunk-GUJSMQ3V.cjs +16 -0
- package/libs/chunk-GUJSMQ3V.cjs.map +1 -0
- package/libs/chunk-HHLNOC5T.js +7 -0
- package/libs/chunk-HHLNOC5T.js.map +1 -0
- package/libs/chunk-HRRHPLER.js +8 -0
- package/libs/chunk-HRRHPLER.js.map +1 -0
- package/libs/chunk-IEB64SWY.js +8 -0
- package/libs/chunk-IEB64SWY.js.map +1 -0
- package/libs/{chunk-NGTJDDFO.js → chunk-IQ76HGVP.js} +2 -2
- package/libs/chunk-IRLFZ3OL.js +9 -0
- package/libs/chunk-IRLFZ3OL.js.map +1 -0
- package/libs/{chunk-JJ43O4Y5.js → chunk-KK47SYZI.js} +2 -2
- package/libs/chunk-O3JIHC5M.cjs +15 -0
- package/libs/chunk-O3JIHC5M.cjs.map +1 -0
- package/libs/chunk-O5XAJ7BY.cjs +18 -0
- package/libs/chunk-O5XAJ7BY.cjs.map +1 -0
- package/libs/chunk-OVWLQYMK.js +10 -0
- package/libs/chunk-OVWLQYMK.js.map +1 -0
- package/libs/chunk-PNWIRCG3.cjs +7 -0
- package/libs/chunk-PNWIRCG3.cjs.map +1 -0
- package/libs/{chunk-D4YLRWAO.cjs → chunk-QVW6W76L.cjs} +6 -6
- package/libs/chunk-T4T6GWYQ.cjs +17 -0
- package/libs/chunk-T4T6GWYQ.cjs.map +1 -0
- package/libs/chunk-TON2YGMD.cjs +9 -0
- package/libs/chunk-TON2YGMD.cjs.map +1 -0
- package/libs/chunk-UEPAWMDF.js +8 -0
- package/libs/chunk-UEPAWMDF.js.map +1 -0
- package/libs/{chunk-LT5KZ2QW.cjs → chunk-US2I5GI7.cjs} +3 -3
- package/libs/{chunk-B7F5FS6D.cjs → chunk-W2UIN7EV.cjs} +3 -3
- package/libs/{chunk-P2DC76ZZ.cjs → chunk-W5TKWBFC.cjs} +3 -3
- package/libs/chunk-WXBFBWYF.cjs +16 -0
- package/libs/chunk-WXBFBWYF.cjs.map +1 -0
- package/libs/{chunk-VUH3FXGJ.js → chunk-X3JCTEPD.js} +5 -5
- package/libs/chunk-X5LGFCWG.js +9 -0
- package/libs/chunk-X5LGFCWG.js.map +1 -0
- package/libs/{chunk-5M57K4SW.js → chunk-Y2PFDELK.js} +2 -2
- package/libs/{chunk-ETFLFC2S.js → chunk-ZFJ4U45S.js} +2 -2
- package/libs/{component-props-a8a2f97e.d.ts → component-props-67d978a2.d.ts} +4 -4
- package/libs/components/alert/alert.css +1 -1
- package/libs/components/alert/alert.css.map +1 -1
- package/libs/components/alert/alert.min.css +2 -2
- package/libs/components/breadcrumbs/breadcrumb.cjs +6 -6
- package/libs/components/breadcrumbs/breadcrumb.d.cts +11 -11
- package/libs/components/breadcrumbs/breadcrumb.d.ts +11 -11
- package/libs/components/breadcrumbs/breadcrumb.js +3 -3
- package/libs/components/button.cjs +6 -4
- package/libs/components/button.d.cts +97 -4
- package/libs/components/button.d.ts +97 -4
- package/libs/components/button.js +4 -2
- package/libs/components/card.cjs +7 -7
- package/libs/components/card.d.cts +14 -14
- package/libs/components/card.d.ts +14 -14
- package/libs/components/card.js +2 -2
- package/libs/components/dialog/dialog.cjs +9 -7
- package/libs/components/dialog/dialog.d.cts +3 -3
- package/libs/components/dialog/dialog.d.ts +3 -3
- package/libs/components/dialog/dialog.js +7 -5
- package/libs/components/form/fields.cjs +4 -4
- package/libs/components/form/fields.d.cts +16 -7
- package/libs/components/form/fields.d.ts +16 -7
- package/libs/components/form/fields.js +2 -2
- package/libs/components/form/inputs.cjs +6 -4
- package/libs/components/form/inputs.d.cts +50 -2
- package/libs/components/form/inputs.d.ts +50 -2
- package/libs/components/form/inputs.js +4 -2
- package/libs/components/form/textarea.cjs +5 -4
- package/libs/components/form/textarea.d.cts +32 -23
- package/libs/components/form/textarea.d.ts +32 -23
- package/libs/components/form/textarea.js +3 -2
- package/libs/components/heading/heading.cjs +3 -3
- package/libs/components/heading/heading.d.cts +2 -2
- package/libs/components/heading/heading.d.ts +2 -2
- package/libs/components/heading/heading.js +2 -2
- package/libs/components/icons/icon.cjs +4 -4
- package/libs/components/icons/icon.d.cts +38 -38
- package/libs/components/icons/icon.d.ts +38 -38
- package/libs/components/icons/icon.js +2 -2
- package/libs/components/link/link.cjs +4 -4
- package/libs/components/link/link.css +1 -1
- package/libs/components/link/link.css.map +1 -1
- package/libs/components/link/link.d.cts +3 -19
- package/libs/components/link/link.d.ts +3 -19
- package/libs/components/link/link.js +2 -2
- package/libs/components/link/link.min.css +2 -2
- package/libs/components/list/list.cjs +5 -5
- package/libs/components/list/list.css +1 -0
- package/libs/components/list/list.css.map +1 -0
- package/libs/components/list/list.d.cts +120 -33
- package/libs/components/list/list.d.ts +120 -33
- package/libs/components/list/list.js +2 -2
- package/libs/components/list/list.min.css +3 -0
- package/libs/components/modal.cjs +6 -4
- package/libs/components/modal.d.cts +8 -8
- package/libs/components/modal.d.ts +8 -8
- package/libs/components/modal.js +5 -3
- package/libs/components/nav/nav.cjs +7 -7
- package/libs/components/nav/nav.css +1 -1
- package/libs/components/nav/nav.css.map +1 -1
- package/libs/components/nav/nav.d.cts +550 -34
- package/libs/components/nav/nav.d.ts +550 -34
- package/libs/components/nav/nav.js +3 -3
- package/libs/components/nav/nav.min.css +2 -2
- package/libs/components/popover/popover.d.cts +5 -5
- package/libs/components/popover/popover.d.ts +5 -5
- package/libs/components/tables/table.cjs +5 -5
- package/libs/components/tables/table.d.cts +8 -8
- package/libs/components/tables/table.d.ts +8 -8
- package/libs/components/tables/table.js +2 -2
- package/libs/components/tag/tag.css +1 -1
- package/libs/components/tag/tag.css.map +1 -1
- package/libs/components/tag/tag.min.css +2 -2
- package/libs/components/text/text.cjs +5 -5
- package/libs/components/text/text.d.cts +5 -5
- package/libs/components/text/text.d.ts +5 -5
- package/libs/components/text/text.js +2 -2
- package/libs/form.types-d25ebfac.d.ts +233 -0
- package/libs/{heading-3648c538.d.ts → heading-7446cb46.d.ts} +8 -8
- package/libs/hooks.cjs +9 -4
- package/libs/hooks.d.cts +137 -3
- package/libs/hooks.d.ts +137 -3
- package/libs/hooks.js +4 -3
- package/libs/icons.cjs +3 -3
- package/libs/icons.d.cts +2 -2
- package/libs/icons.d.ts +2 -2
- package/libs/icons.js +2 -2
- package/libs/index.cjs +53 -51
- package/libs/index.cjs.map +1 -1
- package/libs/index.css +1 -1
- package/libs/index.css.map +1 -1
- package/libs/index.d.cts +338 -49
- package/libs/index.d.ts +338 -49
- package/libs/index.js +24 -22
- package/libs/index.js.map +1 -1
- package/libs/link-5192f411.d.ts +323 -0
- package/libs/list.types-d26de310.d.ts +245 -0
- package/libs/{ui-645f95b5.d.ts → ui-d01b50d4.d.ts} +16 -12
- package/package.json +4 -6
- package/src/components/alert/alert.scss +1 -4
- package/src/components/breadcrumbs/breadcrumb.tsx +4 -1
- package/src/components/buttons/README.mdx +102 -1
- package/src/components/buttons/button.stories.tsx +106 -0
- package/src/components/buttons/button.tsx +82 -52
- package/src/components/dialog/dialog-a11y-review.md +653 -0
- package/src/components/form/README.mdx +725 -43
- package/src/components/form/WCAG-REVIEW.md +654 -0
- package/src/components/form/fields.tsx +10 -1
- package/src/components/form/form.stories.tsx +604 -23
- package/src/components/form/form.tsx +204 -63
- package/src/components/form/form.types.ts +378 -0
- package/src/components/form/input.stories.tsx +71 -3
- package/src/components/form/inputs.tsx +159 -67
- package/src/components/form/select.tsx +122 -66
- package/src/components/form/textarea.tsx +120 -73
- package/src/components/fp.tsx +86 -11
- package/src/components/link/README.mdx +923 -0
- package/src/components/link/link.scss +79 -26
- package/src/components/link/link.stories.tsx +383 -30
- package/src/components/link/link.test.tsx +677 -0
- package/src/components/link/link.tsx +163 -57
- package/src/components/link/link.types.ts +261 -0
- package/src/components/list/README.mdx +764 -0
- package/src/components/list/list.scss +285 -0
- package/src/components/list/list.stories.tsx +514 -27
- package/src/components/list/list.test.tsx +554 -0
- package/src/components/list/list.tsx +153 -51
- package/src/components/list/list.types.ts +255 -0
- package/src/components/nav/ACCESSIBILITY.md +649 -0
- package/src/components/nav/README.mdx +782 -0
- package/src/components/nav/nav.scss +32 -1
- package/src/components/nav/nav.stories.tsx +44 -6
- package/src/components/nav/nav.tsx +302 -51
- package/src/components/nav/nav.types.ts +308 -0
- package/src/components/tag/README.mdx +426 -0
- package/src/components/tag/tag.scss +101 -27
- package/src/components/tag/tag.stories.tsx +384 -10
- package/src/components/tag/tag.test.tsx +210 -0
- package/src/components/tag/tag.tsx +106 -9
- package/src/components/tag/tag.types.ts +107 -0
- package/src/components/ui.tsx +8 -3
- package/src/hooks/use-disabled-state.test.tsx +536 -0
- package/src/hooks/use-disabled-state.ts +246 -0
- package/src/hooks/useDisabledState.md +393 -0
- package/src/hooks.ts +6 -0
- package/src/index.scss +2 -0
- package/src/index.ts +2 -1
- package/src/sass/_globals.scss +2 -7
- package/src/styles/alert/alert.css +1 -3
- package/src/styles/alert/alert.css.map +1 -1
- package/src/styles/index.css +450 -76
- package/src/styles/index.css.map +1 -1
- package/src/styles/link/link.css +45 -28
- package/src/styles/link/link.css.map +1 -1
- package/src/styles/list/list.css +214 -0
- package/src/styles/list/list.css.map +1 -0
- package/src/styles/nav/nav.css +21 -1
- package/src/styles/nav/nav.css.map +1 -1
- package/src/styles/tag/tag.css +113 -35
- package/src/styles/tag/tag.css.map +1 -1
- package/src/styles/utilities/_disabled.scss +58 -0
- package/src/types/shared.ts +43 -6
- package/src/utils/accessibility.ts +109 -0
- package/libs/chunk-2LTJ7HHX.cjs +0 -18
- package/libs/chunk-2LTJ7HHX.cjs.map +0 -1
- package/libs/chunk-2Y7W75TT.js +0 -9
- package/libs/chunk-2Y7W75TT.js.map +0 -1
- package/libs/chunk-5S4ORA4C.cjs +0 -15
- package/libs/chunk-5S4ORA4C.cjs.map +0 -1
- package/libs/chunk-AHDJGCG5.cjs +0 -15
- package/libs/chunk-AHDJGCG5.cjs.map +0 -1
- package/libs/chunk-BHRQBJRY.js +0 -8
- package/libs/chunk-BHRQBJRY.js.map +0 -1
- package/libs/chunk-GZ4QFPRY.js +0 -9
- package/libs/chunk-GZ4QFPRY.js.map +0 -1
- package/libs/chunk-IYUN2EW3.cjs +0 -15
- package/libs/chunk-IYUN2EW3.cjs.map +0 -1
- package/libs/chunk-J32EZPYD.cjs +0 -15
- package/libs/chunk-J32EZPYD.cjs.map +0 -1
- package/libs/chunk-KUKIVRC2.js +0 -7
- package/libs/chunk-KUKIVRC2.js.map +0 -1
- package/libs/chunk-L75OQKEI.cjs.map +0 -1
- package/libs/chunk-M5RRNTVX.cjs +0 -15
- package/libs/chunk-M5RRNTVX.cjs.map +0 -1
- package/libs/chunk-OK5QEIMD.cjs +0 -17
- package/libs/chunk-OK5QEIMD.cjs.map +0 -1
- package/libs/chunk-P7TTEYCD.js +0 -7
- package/libs/chunk-P7TTEYCD.js.map +0 -1
- package/libs/chunk-QLZWHAMK.js +0 -8
- package/libs/chunk-QLZWHAMK.js.map +0 -1
- package/libs/chunk-RIVUMPOG.js +0 -8
- package/libs/chunk-RIVUMPOG.js.map +0 -1
- package/libs/chunk-S7BABR7Z.cjs +0 -13
- package/libs/chunk-S7BABR7Z.cjs.map +0 -1
- package/libs/chunk-SMYRLO3E.js +0 -8
- package/libs/chunk-SMYRLO3E.js.map +0 -1
- package/libs/chunk-TYRCEX2L.js +0 -8
- package/libs/chunk-TYRCEX2L.js.map +0 -1
- package/libs/chunk-XBA562WW.js +0 -8
- package/libs/chunk-XBA562WW.js.map +0 -1
- package/libs/chunk-XTQKWY7W.cjs +0 -32
- package/libs/chunk-XTQKWY7W.cjs.map +0 -1
- package/libs/inputs-f3a216db.d.ts +0 -45
- /package/libs/{chunk-PQ2K3BM6.cjs.map → chunk-2NRIP6RB.cjs.map} +0 -0
- /package/libs/{chunk-772NRB75.js.map → chunk-5QD3DWFI.js.map} +0 -0
- /package/libs/{chunk-3MKLDCKQ.cjs.map → chunk-6WTC4JXH.cjs.map} +0 -0
- /package/libs/{chunk-ZANSFMTD.js.map → chunk-7XPFW7CB.js.map} +0 -0
- /package/libs/{chunk-ROZI23GS.cjs.map → chunk-DKTHCQ5P.cjs.map} +0 -0
- /package/libs/{chunk-NGTJDDFO.js.map → chunk-IQ76HGVP.js.map} +0 -0
- /package/libs/{chunk-JJ43O4Y5.js.map → chunk-KK47SYZI.js.map} +0 -0
- /package/libs/{chunk-D4YLRWAO.cjs.map → chunk-QVW6W76L.cjs.map} +0 -0
- /package/libs/{chunk-LT5KZ2QW.cjs.map → chunk-US2I5GI7.cjs.map} +0 -0
- /package/libs/{chunk-B7F5FS6D.cjs.map → chunk-W2UIN7EV.cjs.map} +0 -0
- /package/libs/{chunk-P2DC76ZZ.cjs.map → chunk-W5TKWBFC.cjs.map} +0 -0
- /package/libs/{chunk-VUH3FXGJ.js.map → chunk-X3JCTEPD.js.map} +0 -0
- /package/libs/{chunk-5M57K4SW.js.map → chunk-Y2PFDELK.js.map} +0 -0
- /package/libs/{chunk-ETFLFC2S.js.map → chunk-ZFJ4U45S.js.map} +0 -0
|
@@ -1,92 +1,145 @@
|
|
|
1
1
|
@use '../../sass/mixins';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Link Component Styles
|
|
5
|
+
*
|
|
6
|
+
* Provides accessible, customizable link styles using CSS custom properties.
|
|
7
|
+
* Supports standard text links, button-styled links, and pill variants.
|
|
8
|
+
*
|
|
9
|
+
* WCAG 2.1 AA Compliance:
|
|
10
|
+
* - Focus indicators meet 2.4.7 (3:1 contrast minimum)
|
|
11
|
+
* - Color contrast meets 1.4.3 (4.5:1 for normal text)
|
|
12
|
+
* - Uses :focus-visible for better UX (keyboard vs mouse)
|
|
13
|
+
*/
|
|
14
|
+
|
|
3
15
|
a[href] {
|
|
4
|
-
|
|
16
|
+
// Color & Typography
|
|
5
17
|
--link-color: #085ab7;
|
|
18
|
+
--link-weight: 400;
|
|
19
|
+
--link-fs: 1rem;
|
|
20
|
+
|
|
21
|
+
// Text Decoration
|
|
22
|
+
--link-decoration: none;
|
|
23
|
+
--link-decoration-offset: 0.09375rem; // 1.5px converted to rem
|
|
24
|
+
--link-decoration-thickness: 0.0625rem; // 1px converted to rem
|
|
25
|
+
--link-skip-ink: auto;
|
|
26
|
+
|
|
27
|
+
// Background & Border
|
|
6
28
|
--link-bg: transparent;
|
|
29
|
+
--link-radius: 0.25rem;
|
|
30
|
+
|
|
31
|
+
// Spacing (for button variants)
|
|
7
32
|
--link-px: 0;
|
|
8
33
|
--link-py: 0;
|
|
34
|
+
|
|
35
|
+
// Transitions
|
|
9
36
|
--link-transition: all 0.75s ease-in-out;
|
|
10
|
-
--link-fs: 1rem;
|
|
11
|
-
--link-radius: 0.25rem;
|
|
12
|
-
--link-skip-ink: auto;
|
|
13
|
-
--link-decoration-offset: 1.5px;
|
|
14
|
-
--link-decoration-thickness: 3px;
|
|
15
|
-
--link-decoration: color: var(--link-color) var(--link-decoration-offset)
|
|
16
|
-
var(--link-decoration-thickness) var(--link-skip-ink);
|
|
17
|
-
--link-decoration-thickness: 3px;
|
|
18
|
-
--link-decoration: color: var(--link-color) var(--link-decoration-offset)
|
|
19
|
-
var(--link-decoration-thickness) var(--link-skip-ink);
|
|
20
37
|
|
|
38
|
+
// Focus Indicator (WCAG 2.4.7 - 3:1 contrast minimum)
|
|
39
|
+
--link-focus-color: currentColor;
|
|
40
|
+
--link-focus-width: 0.125rem; // 2px
|
|
41
|
+
--link-focus-offset: 0.125rem; // 2px
|
|
42
|
+
--link-focus-style: solid;
|
|
43
|
+
|
|
44
|
+
// Apply base styles
|
|
21
45
|
color: var(--link-color);
|
|
22
46
|
font-size: var(--link-fs);
|
|
47
|
+
font-weight: var(--link-weight);
|
|
23
48
|
text-decoration: var(--link-decoration);
|
|
24
49
|
text-underline-offset: var(--link-decoration-offset);
|
|
50
|
+
text-decoration-thickness: var(--link-decoration-thickness);
|
|
25
51
|
text-decoration-skip-ink: var(--link-skip-ink);
|
|
26
52
|
background-color: var(--link-bg);
|
|
27
53
|
border-radius: var(--link-radius);
|
|
28
|
-
|
|
29
|
-
border-radius: var(--link-radius);
|
|
30
|
-
font-weight: var(--link-weight);
|
|
54
|
+
transition: var(--link-transition);
|
|
31
55
|
|
|
56
|
+
// Ensure child elements inherit weight/style
|
|
32
57
|
> i,
|
|
33
58
|
> b {
|
|
34
59
|
font-weight: var(--link-weight);
|
|
35
60
|
font-style: normal;
|
|
36
61
|
}
|
|
37
62
|
|
|
63
|
+
// Hover state - add underline for clarity
|
|
38
64
|
&:hover {
|
|
39
65
|
--link-decoration: underline;
|
|
40
66
|
}
|
|
41
67
|
|
|
68
|
+
// Focus state - WCAG 2.4.7 compliant focus indicator
|
|
42
69
|
&:focus {
|
|
43
|
-
outline:
|
|
70
|
+
outline: var(--link-focus-width) var(--link-focus-style)
|
|
71
|
+
var(--link-focus-color);
|
|
72
|
+
outline-offset: var(--link-focus-offset);
|
|
44
73
|
--link-decoration: underline;
|
|
45
74
|
}
|
|
46
75
|
|
|
76
|
+
// Focus-visible for better UX (only show outline on keyboard focus)
|
|
77
|
+
&:focus-visible {
|
|
78
|
+
outline: var(--link-focus-width) var(--link-focus-style)
|
|
79
|
+
var(--link-focus-color);
|
|
80
|
+
outline-offset: var(--link-focus-offset);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Visited and active states
|
|
47
84
|
&:visited,
|
|
48
85
|
&:active {
|
|
49
86
|
--link-color: currentColor;
|
|
50
87
|
}
|
|
51
88
|
|
|
89
|
+
// Button-styled links (via <b> wrapper, data-btn attribute, or <i> for pill)
|
|
52
90
|
&:has(> b),
|
|
53
|
-
&[data-
|
|
91
|
+
&[data-btn],
|
|
54
92
|
&:has(> i) {
|
|
55
93
|
--link-button-color: var(--link-color);
|
|
56
94
|
--link-bg: transparent;
|
|
57
95
|
--link-decoration: none;
|
|
58
|
-
--link-border:
|
|
96
|
+
--link-border-width: 0.125rem; // 2px
|
|
97
|
+
--link-border-color: currentColor;
|
|
98
|
+
--link-border-style: solid;
|
|
59
99
|
--link-fs: 0.9rem;
|
|
100
|
+
|
|
101
|
+
color: var(--link-button-color);
|
|
60
102
|
background-color: var(--link-bg);
|
|
61
103
|
font-style: normal;
|
|
62
104
|
font-size: var(--link-fs);
|
|
63
|
-
color: var(--link-button-color);
|
|
64
105
|
padding-inline: var(--link-fs);
|
|
65
106
|
padding-block: calc(var(--link-fs) - 0.4rem);
|
|
66
107
|
border-radius: var(--link-radius, 99rem);
|
|
67
108
|
display: inline-flex;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
109
|
+
align-items: center;
|
|
110
|
+
justify-content: center;
|
|
111
|
+
outline: var(--link-border-width) var(--link-border-color)
|
|
112
|
+
var(--link-border-style);
|
|
113
|
+
|
|
114
|
+
// Focus state for button links
|
|
115
|
+
&:focus,
|
|
116
|
+
&:focus-visible {
|
|
117
|
+
outline: var(--link-border-width) var(--link-border-color)
|
|
118
|
+
var(--link-border-style);
|
|
119
|
+
outline-offset: var(--link-focus-offset);
|
|
71
120
|
--link-decoration: none;
|
|
72
121
|
}
|
|
122
|
+
|
|
123
|
+
// Hover state for button links
|
|
73
124
|
&:hover {
|
|
74
125
|
--link-decoration: none;
|
|
75
126
|
}
|
|
127
|
+
|
|
128
|
+
// Apply scale transition from mixins
|
|
76
129
|
@include mixins.scale-transitions;
|
|
77
130
|
}
|
|
131
|
+
|
|
132
|
+
// Pill variant (rounded corners)
|
|
78
133
|
&[data-link~='pill'],
|
|
79
134
|
&:has(> i) {
|
|
80
135
|
--link-radius: 99rem;
|
|
81
136
|
--link-decoration: none;
|
|
82
137
|
font-style: normal;
|
|
138
|
+
|
|
83
139
|
&:hover,
|
|
84
|
-
&:focus
|
|
140
|
+
&:focus,
|
|
141
|
+
&:focus-visible {
|
|
85
142
|
--link-decoration: none;
|
|
86
143
|
}
|
|
87
144
|
}
|
|
88
145
|
}
|
|
89
|
-
|
|
90
|
-
header > section {
|
|
91
|
-
width: auto;
|
|
92
|
-
}
|
|
@@ -1,71 +1,424 @@
|
|
|
1
1
|
import { StoryObj, Meta } from "@storybook/react-vite";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { within, expect, userEvent, fn } from "storybook/test";
|
|
3
|
+
import { useRef, useEffect } from "react";
|
|
5
4
|
import Link from "./link";
|
|
5
|
+
import type { LinkProps } from "./link.types";
|
|
6
6
|
import "../../styles/link/link.css";
|
|
7
7
|
|
|
8
|
-
const meta
|
|
8
|
+
const meta = {
|
|
9
9
|
title: "FP.React Components/Links",
|
|
10
|
-
tags: ["version:1.0.0"],
|
|
10
|
+
tags: ["version:1.0.0", "autodocs"],
|
|
11
11
|
component: Link,
|
|
12
12
|
args: {
|
|
13
13
|
href: "/",
|
|
14
14
|
children: "Link",
|
|
15
15
|
},
|
|
16
|
-
|
|
16
|
+
argTypes: {
|
|
17
|
+
href: {
|
|
18
|
+
control: "text",
|
|
19
|
+
description: "The URL the link points to",
|
|
20
|
+
},
|
|
21
|
+
target: {
|
|
22
|
+
control: "select",
|
|
23
|
+
options: ["_self", "_blank", "_parent", "_top"],
|
|
24
|
+
description: "Where to open the linked URL",
|
|
25
|
+
},
|
|
26
|
+
rel: {
|
|
27
|
+
control: "text",
|
|
28
|
+
description: "Relationship between current and linked document",
|
|
29
|
+
},
|
|
30
|
+
prefetch: {
|
|
31
|
+
control: "boolean",
|
|
32
|
+
description: "Hint to browser to prefetch the resource",
|
|
33
|
+
},
|
|
34
|
+
btnStyle: {
|
|
35
|
+
control: "text",
|
|
36
|
+
description: "Apply button styling to the link",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
parameters: {
|
|
40
|
+
docs: {
|
|
41
|
+
description: {
|
|
42
|
+
component:
|
|
43
|
+
"A semantic, accessible anchor component with enhanced security for external links and flexible styling variants.",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
} as Meta<typeof Link>;
|
|
17
48
|
|
|
18
49
|
export default meta;
|
|
19
|
-
type Story = StoryObj<
|
|
50
|
+
type Story = StoryObj<LinkProps>;
|
|
20
51
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Basic link component with default styling.
|
|
54
|
+
* Links should have descriptive text that makes sense out of context.
|
|
55
|
+
*/
|
|
56
|
+
export const Default: Story = {
|
|
57
|
+
args: {
|
|
58
|
+
href: "/about",
|
|
59
|
+
children: "About Us",
|
|
60
|
+
},
|
|
61
|
+
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
24
62
|
const canvas = within(canvasElement);
|
|
25
63
|
const link = canvas.getByRole("link");
|
|
64
|
+
|
|
65
|
+
// Verify basic rendering
|
|
26
66
|
expect(link).toBeInTheDocument();
|
|
27
|
-
expect(link).toHaveTextContent("
|
|
67
|
+
expect(link).toHaveTextContent("About Us");
|
|
68
|
+
expect(link).toHaveAttribute("href", "/about");
|
|
69
|
+
|
|
70
|
+
// Verify accessibility
|
|
71
|
+
expect(link).toBeVisible();
|
|
72
|
+
expect(link.tagName).toBe("A");
|
|
28
73
|
},
|
|
29
74
|
};
|
|
30
75
|
|
|
76
|
+
/**
|
|
77
|
+
* External link with automatic security attributes.
|
|
78
|
+
* The component automatically adds rel="noopener noreferrer" for target="_blank".
|
|
79
|
+
*/
|
|
31
80
|
export const ExternalLink: Story = {
|
|
32
81
|
args: {
|
|
33
82
|
href: "https://www.google.com",
|
|
34
83
|
target: "_blank",
|
|
35
|
-
|
|
36
|
-
prefetch: true,
|
|
37
|
-
children: "Google",
|
|
84
|
+
children: "Visit Google",
|
|
38
85
|
},
|
|
39
|
-
play: async ({ canvasElement }) => {
|
|
86
|
+
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
40
87
|
const canvas = within(canvasElement);
|
|
41
88
|
const link = canvas.getByRole("link");
|
|
89
|
+
|
|
90
|
+
// Verify external link attributes
|
|
42
91
|
expect(link).toBeInTheDocument();
|
|
43
|
-
expect(link).toHaveTextContent("Google");
|
|
92
|
+
expect(link).toHaveTextContent("Visit Google");
|
|
44
93
|
expect(link).toHaveAttribute("href", "https://www.google.com");
|
|
45
94
|
expect(link).toHaveAttribute("target", "_blank");
|
|
46
|
-
|
|
95
|
+
|
|
96
|
+
// Verify automatic security attributes
|
|
97
|
+
const rel = link.getAttribute("rel");
|
|
98
|
+
expect(rel).toContain("noopener");
|
|
99
|
+
expect(rel).toContain("noreferrer");
|
|
47
100
|
},
|
|
48
|
-
}
|
|
101
|
+
};
|
|
49
102
|
|
|
50
|
-
|
|
103
|
+
/**
|
|
104
|
+
* External link with prefetch hint.
|
|
105
|
+
* Combines security attributes with performance optimization.
|
|
106
|
+
*/
|
|
107
|
+
export const ExternalLinkWithPrefetch: Story = {
|
|
51
108
|
args: {
|
|
52
|
-
|
|
53
|
-
|
|
109
|
+
href: "https://example.com/next-page",
|
|
110
|
+
target: "_blank",
|
|
111
|
+
prefetch: true,
|
|
112
|
+
children: "Prefetch Example",
|
|
113
|
+
},
|
|
114
|
+
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
115
|
+
const canvas = within(canvasElement);
|
|
116
|
+
const link = canvas.getByRole("link");
|
|
117
|
+
|
|
118
|
+
// Verify prefetch is included with security
|
|
119
|
+
const rel = link.getAttribute("rel");
|
|
120
|
+
expect(rel).toContain("noopener");
|
|
121
|
+
expect(rel).toContain("noreferrer");
|
|
122
|
+
expect(rel).toContain("prefetch");
|
|
54
123
|
},
|
|
55
|
-
}
|
|
124
|
+
};
|
|
56
125
|
|
|
57
|
-
|
|
126
|
+
/**
|
|
127
|
+
* External link with custom rel attributes.
|
|
128
|
+
* Custom rel values are merged with security defaults, not overwritten.
|
|
129
|
+
*/
|
|
130
|
+
export const ExternalLinkWithCustomRel: Story = {
|
|
58
131
|
args: {
|
|
59
|
-
|
|
60
|
-
|
|
132
|
+
href: "https://example.com",
|
|
133
|
+
target: "_blank",
|
|
134
|
+
rel: "nofollow author",
|
|
135
|
+
children: "External Link with Custom Rel",
|
|
61
136
|
},
|
|
62
|
-
}
|
|
137
|
+
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
138
|
+
const canvas = within(canvasElement);
|
|
139
|
+
const link = canvas.getByRole("link");
|
|
63
140
|
|
|
64
|
-
|
|
141
|
+
// Verify all rel tokens are present (merged, not replaced)
|
|
142
|
+
const rel = link.getAttribute("rel");
|
|
143
|
+
expect(rel).toContain("noopener");
|
|
144
|
+
expect(rel).toContain("noreferrer");
|
|
145
|
+
expect(rel).toContain("nofollow");
|
|
146
|
+
expect(rel).toContain("author");
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Button-styled link using <b> wrapper.
|
|
152
|
+
* Maintains semantic <a> element while applying button appearance.
|
|
153
|
+
*/
|
|
154
|
+
export const ButtonStyled: Story = {
|
|
155
|
+
args: {
|
|
156
|
+
href: "/signup",
|
|
157
|
+
children: <b>Sign Up Now</b>,
|
|
158
|
+
},
|
|
159
|
+
parameters: {
|
|
160
|
+
docs: {
|
|
161
|
+
description: {
|
|
162
|
+
story:
|
|
163
|
+
"Wrapping link text in `<b>` automatically applies button styling. This is useful for call-to-action links.",
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Pill-styled button link using <i> wrapper.
|
|
171
|
+
* Applies rounded pill styling with button appearance.
|
|
172
|
+
*/
|
|
173
|
+
export const PillStyled: Story = {
|
|
174
|
+
args: {
|
|
175
|
+
href: "/get-started",
|
|
176
|
+
children: <i>Get Started</i>,
|
|
177
|
+
},
|
|
178
|
+
parameters: {
|
|
179
|
+
docs: {
|
|
180
|
+
description: {
|
|
181
|
+
story:
|
|
182
|
+
"Wrapping link text in `<i>` applies pill (fully rounded) button styling.",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Button-styled link with custom border radius.
|
|
190
|
+
* Demonstrates CSS custom property override.
|
|
191
|
+
*/
|
|
192
|
+
export const ButtonWithCustomRadius: Story = {
|
|
65
193
|
args: {
|
|
66
|
-
|
|
194
|
+
href: "/action",
|
|
195
|
+
children: <b>Custom Radius</b>,
|
|
67
196
|
styles: {
|
|
68
|
-
"--link-radius": "
|
|
197
|
+
"--link-radius": "0.5rem",
|
|
198
|
+
} as React.CSSProperties,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Link with custom color using CSS variables.
|
|
204
|
+
* All styling is controlled via CSS custom properties.
|
|
205
|
+
*/
|
|
206
|
+
export const CustomColor: Story = {
|
|
207
|
+
args: {
|
|
208
|
+
href: "/products",
|
|
209
|
+
children: "Browse Products",
|
|
210
|
+
styles: {
|
|
211
|
+
"--link-color": "#d63384",
|
|
212
|
+
"--link-decoration": "underline",
|
|
213
|
+
} as React.CSSProperties,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Keyboard navigation test.
|
|
219
|
+
* Verifies link is keyboard accessible with proper focus indicators.
|
|
220
|
+
*/
|
|
221
|
+
export const KeyboardNavigation: Story = {
|
|
222
|
+
args: {
|
|
223
|
+
href: "/keyboard-test",
|
|
224
|
+
children: "Keyboard Accessible Link",
|
|
225
|
+
},
|
|
226
|
+
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
227
|
+
const canvas = within(canvasElement);
|
|
228
|
+
const link = canvas.getByRole("link");
|
|
229
|
+
|
|
230
|
+
// Tab to the link
|
|
231
|
+
await userEvent.tab();
|
|
232
|
+
|
|
233
|
+
// Verify focus
|
|
234
|
+
expect(link).toHaveFocus();
|
|
235
|
+
|
|
236
|
+
// Verify the link is announced by screen readers
|
|
237
|
+
expect(link).toHaveAccessibleName("Keyboard Accessible Link");
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Icon-only link with aria-label.
|
|
243
|
+
* Demonstrates accessible pattern for icon-only links.
|
|
244
|
+
*/
|
|
245
|
+
export const IconOnlyWithAriaLabel: Story = {
|
|
246
|
+
args: {
|
|
247
|
+
href: "/settings",
|
|
248
|
+
"aria-label": "Open settings",
|
|
249
|
+
children: (
|
|
250
|
+
<svg
|
|
251
|
+
aria-hidden="true"
|
|
252
|
+
width="16"
|
|
253
|
+
height="16"
|
|
254
|
+
viewBox="0 0 16 16"
|
|
255
|
+
fill="currentColor"
|
|
256
|
+
>
|
|
257
|
+
<path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z" />
|
|
258
|
+
<path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319z" />
|
|
259
|
+
</svg>
|
|
260
|
+
),
|
|
261
|
+
},
|
|
262
|
+
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
263
|
+
const canvas = within(canvasElement);
|
|
264
|
+
const link = canvas.getByRole("link");
|
|
265
|
+
|
|
266
|
+
// Verify accessible name from aria-label
|
|
267
|
+
expect(link).toHaveAccessibleName("Open settings");
|
|
268
|
+
|
|
269
|
+
// Verify SVG is hidden from screen readers
|
|
270
|
+
const svg = link.querySelector("svg");
|
|
271
|
+
expect(svg).toHaveAttribute("aria-hidden", "true");
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Skip link pattern with ref forwarding.
|
|
277
|
+
* Demonstrates programmatic focus management for accessibility.
|
|
278
|
+
*/
|
|
279
|
+
export const SkipLink: Story = {
|
|
280
|
+
render: () => {
|
|
281
|
+
const SkipLinkExample = () => {
|
|
282
|
+
const mainRef = useRef<HTMLAnchorElement>(null);
|
|
283
|
+
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
// Simulate focus on mount (for demonstration)
|
|
286
|
+
const timer = setTimeout(() => {
|
|
287
|
+
mainRef.current?.focus();
|
|
288
|
+
}, 100);
|
|
289
|
+
|
|
290
|
+
return () => clearTimeout(timer);
|
|
291
|
+
}, []);
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<div style={{ padding: "1rem" }}>
|
|
295
|
+
<Link ref={mainRef} href="#main-content">
|
|
296
|
+
Skip to main content
|
|
297
|
+
</Link>
|
|
298
|
+
<p id="main-content" style={{ marginTop: "1rem" }}>
|
|
299
|
+
Main content starts here
|
|
300
|
+
</p>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
return <SkipLinkExample />;
|
|
306
|
+
},
|
|
307
|
+
parameters: {
|
|
308
|
+
docs: {
|
|
309
|
+
description: {
|
|
310
|
+
story:
|
|
311
|
+
"Demonstrates ref forwarding for programmatic focus management. Useful for skip links and keyboard navigation patterns.",
|
|
312
|
+
},
|
|
69
313
|
},
|
|
70
314
|
},
|
|
71
|
-
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Email link (mailto:).
|
|
319
|
+
* Demonstrates non-HTTP URL schemes.
|
|
320
|
+
*/
|
|
321
|
+
export const EmailLink: Story = {
|
|
322
|
+
args: {
|
|
323
|
+
href: "mailto:hello@example.com",
|
|
324
|
+
children: "Contact us via email",
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Phone link (tel:).
|
|
330
|
+
* Demonstrates telephone URL scheme.
|
|
331
|
+
*/
|
|
332
|
+
export const PhoneLink: Story = {
|
|
333
|
+
args: {
|
|
334
|
+
href: "tel:+1234567890",
|
|
335
|
+
children: "Call us: +1 (234) 567-890",
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Link with onClick event handler (RECOMMENDED).
|
|
341
|
+
* onClick fires for all activation methods including keyboard.
|
|
342
|
+
*/
|
|
343
|
+
export const WithOnClick: Story = {
|
|
344
|
+
args: {
|
|
345
|
+
href: "/products",
|
|
346
|
+
children: "Track All Activations",
|
|
347
|
+
onClick: fn(),
|
|
348
|
+
},
|
|
349
|
+
parameters: {
|
|
350
|
+
docs: {
|
|
351
|
+
description: {
|
|
352
|
+
story:
|
|
353
|
+
"**Recommended**: Use `onClick` for analytics and tracking. It captures mouse clicks, touch events, AND keyboard activation (Enter key).",
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
358
|
+
const canvas = within(canvasElement);
|
|
359
|
+
const link = canvas.getByRole("link");
|
|
360
|
+
|
|
361
|
+
// Click with mouse
|
|
362
|
+
await userEvent.click(link);
|
|
363
|
+
|
|
364
|
+
// Also works with keyboard
|
|
365
|
+
link.focus();
|
|
366
|
+
await userEvent.keyboard("{Enter}");
|
|
367
|
+
|
|
368
|
+
// Check console for both events
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Link with onPointerDown event handler.
|
|
374
|
+
* Only fires for pointer events (mouse, touch, pen) - NOT keyboard.
|
|
375
|
+
*/
|
|
376
|
+
export const WithOnPointerDown: Story = {
|
|
377
|
+
args: {
|
|
378
|
+
href: "/products",
|
|
379
|
+
children: "Track Pointer Only",
|
|
380
|
+
onPointerDown: fn(),
|
|
381
|
+
},
|
|
382
|
+
parameters: {
|
|
383
|
+
docs: {
|
|
384
|
+
description: {
|
|
385
|
+
story:
|
|
386
|
+
"⚠️ **Accessibility Note**: `onPointerDown` does NOT fire for keyboard activation (Enter key). Use `onClick` if you need to track keyboard users.",
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
391
|
+
const canvas = within(canvasElement);
|
|
392
|
+
const link = canvas.getByRole("link");
|
|
393
|
+
|
|
394
|
+
// Click with mouse - handler fires
|
|
395
|
+
await userEvent.click(link);
|
|
396
|
+
|
|
397
|
+
// Keyboard activation - handler does NOT fire
|
|
398
|
+
link.focus();
|
|
399
|
+
await userEvent.keyboard("{Enter}");
|
|
400
|
+
|
|
401
|
+
// Check console - only one event logged (mouse click)
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Link with both onClick and onPointerDown.
|
|
407
|
+
* Demonstrates using both handlers together for comprehensive tracking.
|
|
408
|
+
*/
|
|
409
|
+
export const WithBothHandlers: Story = {
|
|
410
|
+
args: {
|
|
411
|
+
href: "/products",
|
|
412
|
+
children: "Track Both Ways",
|
|
413
|
+
onClick: fn(),
|
|
414
|
+
onPointerDown: fn(),
|
|
415
|
+
},
|
|
416
|
+
parameters: {
|
|
417
|
+
docs: {
|
|
418
|
+
description: {
|
|
419
|
+
story:
|
|
420
|
+
"Use both handlers when you need comprehensive tracking: `onClick` captures all activations, while `onPointerDown` provides pointer-specific data.",
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
};
|