@fpkit/acss 0.5.13 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/libs/{chunk-PQ2K3BM6.cjs → chunk-2NRIP6RB.cjs} +3 -3
  2. package/libs/chunk-33PNJ4LO.cjs +15 -0
  3. package/libs/chunk-33PNJ4LO.cjs.map +1 -0
  4. package/libs/chunk-4BZKFPEC.cjs +17 -0
  5. package/libs/chunk-4BZKFPEC.cjs.map +1 -0
  6. package/libs/{chunk-772NRB75.js → chunk-5QD3DWFI.js} +2 -2
  7. package/libs/chunk-6SAHIYCZ.js +7 -0
  8. package/libs/chunk-6SAHIYCZ.js.map +1 -0
  9. package/libs/{chunk-3MKLDCKQ.cjs → chunk-6WTC4JXH.cjs} +3 -3
  10. package/libs/chunk-75QHTLFO.js +7 -0
  11. package/libs/chunk-75QHTLFO.js.map +1 -0
  12. package/libs/{chunk-ZANSFMTD.js → chunk-7XPFW7CB.js} +3 -3
  13. package/libs/chunk-BFK62VX5.js +5 -0
  14. package/libs/chunk-BFK62VX5.js.map +1 -0
  15. package/libs/{chunk-ROZI23GS.cjs → chunk-DKTHCQ5P.cjs} +4 -4
  16. package/libs/chunk-E2AJURUW.cjs +13 -0
  17. package/libs/chunk-E2AJURUW.cjs.map +1 -0
  18. package/libs/{chunk-L75OQKEI.cjs → chunk-ENTCUJ3A.cjs} +3 -3
  19. package/libs/chunk-ENTCUJ3A.cjs.map +1 -0
  20. package/libs/chunk-F5EYMVQM.js +10 -0
  21. package/libs/chunk-F5EYMVQM.js.map +1 -0
  22. package/libs/chunk-FVROL3V5.js +9 -0
  23. package/libs/chunk-FVROL3V5.js.map +1 -0
  24. package/libs/chunk-GT77BX4L.cjs +17 -0
  25. package/libs/chunk-GT77BX4L.cjs.map +1 -0
  26. package/libs/chunk-GUJSMQ3V.cjs +16 -0
  27. package/libs/chunk-GUJSMQ3V.cjs.map +1 -0
  28. package/libs/chunk-HHLNOC5T.js +7 -0
  29. package/libs/chunk-HHLNOC5T.js.map +1 -0
  30. package/libs/chunk-HRRHPLER.js +8 -0
  31. package/libs/chunk-HRRHPLER.js.map +1 -0
  32. package/libs/chunk-IEB64SWY.js +8 -0
  33. package/libs/chunk-IEB64SWY.js.map +1 -0
  34. package/libs/{chunk-NGTJDDFO.js → chunk-IQ76HGVP.js} +2 -2
  35. package/libs/chunk-IRLFZ3OL.js +9 -0
  36. package/libs/chunk-IRLFZ3OL.js.map +1 -0
  37. package/libs/{chunk-JJ43O4Y5.js → chunk-KK47SYZI.js} +2 -2
  38. package/libs/chunk-O3JIHC5M.cjs +15 -0
  39. package/libs/chunk-O3JIHC5M.cjs.map +1 -0
  40. package/libs/chunk-O5XAJ7BY.cjs +18 -0
  41. package/libs/chunk-O5XAJ7BY.cjs.map +1 -0
  42. package/libs/chunk-OVWLQYMK.js +10 -0
  43. package/libs/chunk-OVWLQYMK.js.map +1 -0
  44. package/libs/chunk-PNWIRCG3.cjs +7 -0
  45. package/libs/chunk-PNWIRCG3.cjs.map +1 -0
  46. package/libs/{chunk-D4YLRWAO.cjs → chunk-QVW6W76L.cjs} +6 -6
  47. package/libs/chunk-T4T6GWYQ.cjs +17 -0
  48. package/libs/chunk-T4T6GWYQ.cjs.map +1 -0
  49. package/libs/chunk-TON2YGMD.cjs +9 -0
  50. package/libs/chunk-TON2YGMD.cjs.map +1 -0
  51. package/libs/chunk-UEPAWMDF.js +8 -0
  52. package/libs/chunk-UEPAWMDF.js.map +1 -0
  53. package/libs/{chunk-LT5KZ2QW.cjs → chunk-US2I5GI7.cjs} +3 -3
  54. package/libs/{chunk-B7F5FS6D.cjs → chunk-W2UIN7EV.cjs} +3 -3
  55. package/libs/{chunk-P2DC76ZZ.cjs → chunk-W5TKWBFC.cjs} +3 -3
  56. package/libs/chunk-WXBFBWYF.cjs +16 -0
  57. package/libs/chunk-WXBFBWYF.cjs.map +1 -0
  58. package/libs/{chunk-VUH3FXGJ.js → chunk-X3JCTEPD.js} +5 -5
  59. package/libs/chunk-X5LGFCWG.js +9 -0
  60. package/libs/chunk-X5LGFCWG.js.map +1 -0
  61. package/libs/{chunk-5M57K4SW.js → chunk-Y2PFDELK.js} +2 -2
  62. package/libs/{chunk-ETFLFC2S.js → chunk-ZFJ4U45S.js} +2 -2
  63. package/libs/{component-props-a8a2f97e.d.ts → component-props-67d978a2.d.ts} +4 -4
  64. package/libs/components/alert/alert.css +1 -1
  65. package/libs/components/alert/alert.css.map +1 -1
  66. package/libs/components/alert/alert.min.css +2 -2
  67. package/libs/components/breadcrumbs/breadcrumb.cjs +6 -6
  68. package/libs/components/breadcrumbs/breadcrumb.d.cts +11 -11
  69. package/libs/components/breadcrumbs/breadcrumb.d.ts +11 -11
  70. package/libs/components/breadcrumbs/breadcrumb.js +3 -3
  71. package/libs/components/button.cjs +6 -4
  72. package/libs/components/button.d.cts +97 -4
  73. package/libs/components/button.d.ts +97 -4
  74. package/libs/components/button.js +4 -2
  75. package/libs/components/card.cjs +7 -7
  76. package/libs/components/card.d.cts +14 -14
  77. package/libs/components/card.d.ts +14 -14
  78. package/libs/components/card.js +2 -2
  79. package/libs/components/dialog/dialog.cjs +9 -7
  80. package/libs/components/dialog/dialog.d.cts +3 -3
  81. package/libs/components/dialog/dialog.d.ts +3 -3
  82. package/libs/components/dialog/dialog.js +7 -5
  83. package/libs/components/form/fields.cjs +4 -4
  84. package/libs/components/form/fields.d.cts +16 -7
  85. package/libs/components/form/fields.d.ts +16 -7
  86. package/libs/components/form/fields.js +2 -2
  87. package/libs/components/form/inputs.cjs +6 -4
  88. package/libs/components/form/inputs.d.cts +50 -2
  89. package/libs/components/form/inputs.d.ts +50 -2
  90. package/libs/components/form/inputs.js +4 -2
  91. package/libs/components/form/textarea.cjs +5 -4
  92. package/libs/components/form/textarea.d.cts +32 -23
  93. package/libs/components/form/textarea.d.ts +32 -23
  94. package/libs/components/form/textarea.js +3 -2
  95. package/libs/components/heading/heading.cjs +3 -3
  96. package/libs/components/heading/heading.d.cts +2 -2
  97. package/libs/components/heading/heading.d.ts +2 -2
  98. package/libs/components/heading/heading.js +2 -2
  99. package/libs/components/icons/icon.cjs +4 -4
  100. package/libs/components/icons/icon.d.cts +38 -38
  101. package/libs/components/icons/icon.d.ts +38 -38
  102. package/libs/components/icons/icon.js +2 -2
  103. package/libs/components/link/link.cjs +4 -4
  104. package/libs/components/link/link.css +1 -1
  105. package/libs/components/link/link.css.map +1 -1
  106. package/libs/components/link/link.d.cts +3 -19
  107. package/libs/components/link/link.d.ts +3 -19
  108. package/libs/components/link/link.js +2 -2
  109. package/libs/components/link/link.min.css +2 -2
  110. package/libs/components/list/list.cjs +5 -5
  111. package/libs/components/list/list.css +1 -0
  112. package/libs/components/list/list.css.map +1 -0
  113. package/libs/components/list/list.d.cts +120 -33
  114. package/libs/components/list/list.d.ts +120 -33
  115. package/libs/components/list/list.js +2 -2
  116. package/libs/components/list/list.min.css +3 -0
  117. package/libs/components/modal.cjs +6 -4
  118. package/libs/components/modal.d.cts +8 -8
  119. package/libs/components/modal.d.ts +8 -8
  120. package/libs/components/modal.js +5 -3
  121. package/libs/components/nav/nav.cjs +7 -7
  122. package/libs/components/nav/nav.css +1 -1
  123. package/libs/components/nav/nav.css.map +1 -1
  124. package/libs/components/nav/nav.d.cts +550 -34
  125. package/libs/components/nav/nav.d.ts +550 -34
  126. package/libs/components/nav/nav.js +3 -3
  127. package/libs/components/nav/nav.min.css +2 -2
  128. package/libs/components/popover/popover.d.cts +5 -5
  129. package/libs/components/popover/popover.d.ts +5 -5
  130. package/libs/components/tables/table.cjs +5 -5
  131. package/libs/components/tables/table.d.cts +8 -8
  132. package/libs/components/tables/table.d.ts +8 -8
  133. package/libs/components/tables/table.js +2 -2
  134. package/libs/components/tag/tag.css +1 -1
  135. package/libs/components/tag/tag.css.map +1 -1
  136. package/libs/components/tag/tag.min.css +2 -2
  137. package/libs/components/text/text.cjs +5 -5
  138. package/libs/components/text/text.d.cts +5 -5
  139. package/libs/components/text/text.d.ts +5 -5
  140. package/libs/components/text/text.js +2 -2
  141. package/libs/form.types-d25ebfac.d.ts +233 -0
  142. package/libs/{heading-3648c538.d.ts → heading-7446cb46.d.ts} +8 -8
  143. package/libs/hooks.cjs +9 -4
  144. package/libs/hooks.d.cts +137 -3
  145. package/libs/hooks.d.ts +137 -3
  146. package/libs/hooks.js +4 -3
  147. package/libs/icons.cjs +3 -3
  148. package/libs/icons.d.cts +2 -2
  149. package/libs/icons.d.ts +2 -2
  150. package/libs/icons.js +2 -2
  151. package/libs/index.cjs +53 -51
  152. package/libs/index.cjs.map +1 -1
  153. package/libs/index.css +1 -1
  154. package/libs/index.css.map +1 -1
  155. package/libs/index.d.cts +338 -49
  156. package/libs/index.d.ts +338 -49
  157. package/libs/index.js +24 -22
  158. package/libs/index.js.map +1 -1
  159. package/libs/link-5192f411.d.ts +323 -0
  160. package/libs/list.types-d26de310.d.ts +245 -0
  161. package/libs/{ui-645f95b5.d.ts → ui-d01b50d4.d.ts} +16 -12
  162. package/package.json +4 -6
  163. package/src/components/alert/alert.scss +1 -4
  164. package/src/components/breadcrumbs/breadcrumb.tsx +4 -1
  165. package/src/components/buttons/README.mdx +102 -1
  166. package/src/components/buttons/button.stories.tsx +106 -0
  167. package/src/components/buttons/button.tsx +82 -52
  168. package/src/components/dialog/dialog-a11y-review.md +653 -0
  169. package/src/components/form/README.mdx +725 -43
  170. package/src/components/form/WCAG-REVIEW.md +654 -0
  171. package/src/components/form/fields.tsx +10 -1
  172. package/src/components/form/form.stories.tsx +604 -23
  173. package/src/components/form/form.tsx +204 -63
  174. package/src/components/form/form.types.ts +378 -0
  175. package/src/components/form/input.stories.tsx +71 -3
  176. package/src/components/form/inputs.tsx +159 -67
  177. package/src/components/form/select.tsx +122 -66
  178. package/src/components/form/textarea.tsx +120 -73
  179. package/src/components/fp.tsx +86 -11
  180. package/src/components/link/README.mdx +923 -0
  181. package/src/components/link/link.scss +79 -26
  182. package/src/components/link/link.stories.tsx +383 -30
  183. package/src/components/link/link.test.tsx +677 -0
  184. package/src/components/link/link.tsx +163 -57
  185. package/src/components/link/link.types.ts +261 -0
  186. package/src/components/list/README.mdx +764 -0
  187. package/src/components/list/list.scss +285 -0
  188. package/src/components/list/list.stories.tsx +514 -27
  189. package/src/components/list/list.test.tsx +554 -0
  190. package/src/components/list/list.tsx +153 -51
  191. package/src/components/list/list.types.ts +255 -0
  192. package/src/components/nav/ACCESSIBILITY.md +649 -0
  193. package/src/components/nav/README.mdx +782 -0
  194. package/src/components/nav/nav.scss +37 -4
  195. package/src/components/nav/nav.stories.tsx +44 -6
  196. package/src/components/nav/nav.tsx +302 -51
  197. package/src/components/nav/nav.types.ts +308 -0
  198. package/src/components/tag/README.mdx +426 -0
  199. package/src/components/tag/tag.scss +101 -27
  200. package/src/components/tag/tag.stories.tsx +384 -10
  201. package/src/components/tag/tag.test.tsx +210 -0
  202. package/src/components/tag/tag.tsx +106 -9
  203. package/src/components/tag/tag.types.ts +107 -0
  204. package/src/components/ui.tsx +8 -3
  205. package/src/hooks/use-disabled-state.test.tsx +536 -0
  206. package/src/hooks/use-disabled-state.ts +246 -0
  207. package/src/hooks/useDisabledState.md +393 -0
  208. package/src/hooks.ts +6 -0
  209. package/src/index.scss +2 -0
  210. package/src/index.ts +2 -1
  211. package/src/sass/_globals.scss +2 -7
  212. package/src/styles/alert/alert.css +1 -3
  213. package/src/styles/alert/alert.css.map +1 -1
  214. package/src/styles/index.css +461 -81
  215. package/src/styles/index.css.map +1 -1
  216. package/src/styles/link/link.css +45 -28
  217. package/src/styles/link/link.css.map +1 -1
  218. package/src/styles/list/list.css +214 -0
  219. package/src/styles/list/list.css.map +1 -0
  220. package/src/styles/nav/nav.css +32 -6
  221. package/src/styles/nav/nav.css.map +1 -1
  222. package/src/styles/tag/tag.css +113 -35
  223. package/src/styles/tag/tag.css.map +1 -1
  224. package/src/styles/utilities/_disabled.scss +58 -0
  225. package/src/types/shared.ts +43 -6
  226. package/src/utils/accessibility.ts +109 -0
  227. package/libs/chunk-2LTJ7HHX.cjs +0 -18
  228. package/libs/chunk-2LTJ7HHX.cjs.map +0 -1
  229. package/libs/chunk-2Y7W75TT.js +0 -9
  230. package/libs/chunk-2Y7W75TT.js.map +0 -1
  231. package/libs/chunk-5S4ORA4C.cjs +0 -15
  232. package/libs/chunk-5S4ORA4C.cjs.map +0 -1
  233. package/libs/chunk-AHDJGCG5.cjs +0 -15
  234. package/libs/chunk-AHDJGCG5.cjs.map +0 -1
  235. package/libs/chunk-BHRQBJRY.js +0 -8
  236. package/libs/chunk-BHRQBJRY.js.map +0 -1
  237. package/libs/chunk-GZ4QFPRY.js +0 -9
  238. package/libs/chunk-GZ4QFPRY.js.map +0 -1
  239. package/libs/chunk-IYUN2EW3.cjs +0 -15
  240. package/libs/chunk-IYUN2EW3.cjs.map +0 -1
  241. package/libs/chunk-J32EZPYD.cjs +0 -15
  242. package/libs/chunk-J32EZPYD.cjs.map +0 -1
  243. package/libs/chunk-KUKIVRC2.js +0 -7
  244. package/libs/chunk-KUKIVRC2.js.map +0 -1
  245. package/libs/chunk-L75OQKEI.cjs.map +0 -1
  246. package/libs/chunk-M5RRNTVX.cjs +0 -15
  247. package/libs/chunk-M5RRNTVX.cjs.map +0 -1
  248. package/libs/chunk-OK5QEIMD.cjs +0 -17
  249. package/libs/chunk-OK5QEIMD.cjs.map +0 -1
  250. package/libs/chunk-P7TTEYCD.js +0 -7
  251. package/libs/chunk-P7TTEYCD.js.map +0 -1
  252. package/libs/chunk-QLZWHAMK.js +0 -8
  253. package/libs/chunk-QLZWHAMK.js.map +0 -1
  254. package/libs/chunk-RIVUMPOG.js +0 -8
  255. package/libs/chunk-RIVUMPOG.js.map +0 -1
  256. package/libs/chunk-S7BABR7Z.cjs +0 -13
  257. package/libs/chunk-S7BABR7Z.cjs.map +0 -1
  258. package/libs/chunk-SMYRLO3E.js +0 -8
  259. package/libs/chunk-SMYRLO3E.js.map +0 -1
  260. package/libs/chunk-TYRCEX2L.js +0 -8
  261. package/libs/chunk-TYRCEX2L.js.map +0 -1
  262. package/libs/chunk-XBA562WW.js +0 -8
  263. package/libs/chunk-XBA562WW.js.map +0 -1
  264. package/libs/chunk-XTQKWY7W.cjs +0 -32
  265. package/libs/chunk-XTQKWY7W.cjs.map +0 -1
  266. package/libs/inputs-f3a216db.d.ts +0 -45
  267. /package/libs/{chunk-PQ2K3BM6.cjs.map → chunk-2NRIP6RB.cjs.map} +0 -0
  268. /package/libs/{chunk-772NRB75.js.map → chunk-5QD3DWFI.js.map} +0 -0
  269. /package/libs/{chunk-3MKLDCKQ.cjs.map → chunk-6WTC4JXH.cjs.map} +0 -0
  270. /package/libs/{chunk-ZANSFMTD.js.map → chunk-7XPFW7CB.js.map} +0 -0
  271. /package/libs/{chunk-ROZI23GS.cjs.map → chunk-DKTHCQ5P.cjs.map} +0 -0
  272. /package/libs/{chunk-NGTJDDFO.js.map → chunk-IQ76HGVP.js.map} +0 -0
  273. /package/libs/{chunk-JJ43O4Y5.js.map → chunk-KK47SYZI.js.map} +0 -0
  274. /package/libs/{chunk-D4YLRWAO.cjs.map → chunk-QVW6W76L.cjs.map} +0 -0
  275. /package/libs/{chunk-LT5KZ2QW.cjs.map → chunk-US2I5GI7.cjs.map} +0 -0
  276. /package/libs/{chunk-B7F5FS6D.cjs.map → chunk-W2UIN7EV.cjs.map} +0 -0
  277. /package/libs/{chunk-P2DC76ZZ.cjs.map → chunk-W5TKWBFC.cjs.map} +0 -0
  278. /package/libs/{chunk-VUH3FXGJ.js.map → chunk-X3JCTEPD.js.map} +0 -0
  279. /package/libs/{chunk-5M57K4SW.js.map → chunk-Y2PFDELK.js.map} +0 -0
  280. /package/libs/{chunk-ETFLFC2S.js.map → chunk-ZFJ4U45S.js.map} +0 -0
@@ -1,61 +1,167 @@
1
- import UI from "../ui";
2
1
  import React from "react";
2
+ import UI from "../ui";
3
+ import type { LinkProps } from "./link.types";
3
4
 
4
- export type LinkProps = {
5
- href?: string;
6
-
7
- target?: string;
8
-
9
- rel?: string;
10
-
11
- children: React.ReactNode;
12
-
13
- styles?: React.CSSProperties;
14
-
15
- prefetch?: boolean;
16
-
17
- btnStyle?: string;
18
-
19
- onPointerDown?: (event: React.PointerEvent<HTMLAnchorElement>) => void;
20
- } & React.ComponentProps<typeof UI> &
21
- React.ComponentProps<"a">;
22
-
23
- export const Link = ({
24
- href,
25
- target,
26
- rel,
27
- children,
28
- styles,
29
- prefetch,
30
- btnStyle,
31
- onPointerDown,
32
- ...props
33
- }: LinkProps) => {
34
- let relValue = rel;
35
-
36
- if (target === "_blank")
37
- relValue = `noopener noreferrer ${prefetch ? "prefetch" : ""}`;
38
-
39
- const handleOnpointerDown = (e: React.PointerEvent<HTMLAnchorElement>) => {
40
- if (onPointerDown) onPointerDown?.(e);
41
- };
42
-
43
- return (
44
- <UI
45
- as="a"
46
- href={href}
47
- target={target}
48
- styles={styles}
49
- rel={relValue}
50
- onPointerDown={handleOnpointerDown}
51
- data-btn={btnStyle}
52
- prefetch={prefetch}
53
- {...props}
54
- >
55
- {children}
56
- </UI>
57
- );
58
- };
5
+ /**
6
+ * Link - A semantic, accessible anchor component with enhanced security and styling.
7
+ *
8
+ * The Link component renders accessible `<a>` elements with automatic security
9
+ * attributes for external links, customizable styling variants, and full WCAG 2.1
10
+ * AA compliance. It supports traditional text links, button-styled links, and
11
+ * programmatic focus management via ref forwarding.
12
+ *
13
+ * ## Features
14
+ *
15
+ * - 🔒 **Automatic Security**: External links get `rel="noopener noreferrer"`
16
+ * - ♿ **WCAG 2.1 AA Compliant**: Accessible focus indicators and semantic HTML
17
+ * - 🎨 **Flexible Styling**: Text links, button links, and pill variants
18
+ * - ⚡ **Performance**: Optional prefetch hints for faster navigation
19
+ * - 🎯 **Ref Forwarding**: Direct DOM access for focus management and scroll
20
+ * - 🧪 **Type-Safe**: Full TypeScript support with comprehensive prop types
21
+ *
22
+ * ## Accessibility
23
+ *
24
+ * - Semantic `<a>` element for proper keyboard navigation
25
+ * - ✅ Focus indicators meet WCAG 2.4.7 (3:1 contrast ratio)
26
+ * - ✅ Screen readers announce link purpose and destination
27
+ * - ✅ External links include security attributes automatically
28
+ * - ✅ Supports `aria-label` for icon-only or ambiguous links
29
+ * - ✅ Ref forwarding enables skip-link patterns
30
+ *
31
+ * @example
32
+ * // Basic internal link
33
+ * <Link href="/about">About Us</Link>
34
+ *
35
+ * @example
36
+ * // External link with automatic security
37
+ * <Link href="https://example.com" target="_blank">
38
+ * Visit Example
39
+ * </Link>
40
+ *
41
+ * @example
42
+ * // Button-styled call-to-action link
43
+ * <Link href="/signup">
44
+ * <b>Get Started</b>
45
+ * </Link>
46
+ *
47
+ * @example
48
+ * // Icon-only link with accessible label
49
+ * <Link href="/settings" aria-label="Open settings">
50
+ * <SettingsIcon aria-hidden="true" />
51
+ * </Link>
52
+ *
53
+ * @example
54
+ * // Analytics tracking with onClick (includes keyboard users)
55
+ * <Link
56
+ * href="/products"
57
+ * onClick={(e) => trackEvent('link_click', { href: '/products' })}
58
+ * >
59
+ * Browse Products
60
+ * </Link>
61
+ *
62
+ * @example
63
+ * // Skip link with ref forwarding for focus management
64
+ * const mainRef = useRef<HTMLAnchorElement>(null);
65
+ *
66
+ * <Link ref={mainRef} href="#main-content">
67
+ * Skip to main content
68
+ * </Link>
69
+ *
70
+ * @example
71
+ * // Custom styled link with CSS variables
72
+ * <Link
73
+ * href="/products"
74
+ * styles={{
75
+ * '--link-color': '#0066cc',
76
+ * '--link-decoration': 'underline',
77
+ * }}
78
+ * >
79
+ * Browse Products
80
+ * </Link>
81
+ *
82
+ * @example
83
+ * // ✅ GOOD: Descriptive link text
84
+ * <Link href="/docs/installation">
85
+ * Read installation guide
86
+ * </Link>
87
+ *
88
+ * @example
89
+ * // ❌ BAD: Generic link text (poor for screen readers)
90
+ * <Link href="/docs/installation">
91
+ * Click here
92
+ * </Link>
93
+ *
94
+ * @see {@link LinkProps} for complete prop documentation
95
+ */
96
+ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
97
+ (
98
+ {
99
+ href,
100
+ target,
101
+ rel,
102
+ children,
103
+ styles,
104
+ prefetch = false,
105
+ btnStyle,
106
+ onClick,
107
+ onPointerDown,
108
+ ...props
109
+ },
110
+ ref
111
+ ) => {
112
+ /**
113
+ * Compute the final `rel` attribute value with security defaults.
114
+ *
115
+ * For external links (target="_blank"), we merge user-provided `rel` values
116
+ * with security defaults `noopener noreferrer` to prevent:
117
+ * - window.opener exploitation (noopener)
118
+ * - Referrer header leakage (noreferrer)
119
+ *
120
+ * If prefetch is enabled, we also add the `prefetch` hint.
121
+ */
122
+ const computedRel = React.useMemo(() => {
123
+ if (target === "_blank") {
124
+ // Start with security defaults
125
+ const securityTokens = new Set(["noopener", "noreferrer"]);
126
+
127
+ // Add prefetch if enabled
128
+ if (prefetch) {
129
+ securityTokens.add("prefetch");
130
+ }
131
+
132
+ // Merge with user-provided rel tokens (if any)
133
+ if (rel) {
134
+ rel.split(/\s+/).forEach((token) => {
135
+ if (token) securityTokens.add(token);
136
+ });
137
+ }
138
+
139
+ return Array.from(securityTokens).join(" ");
140
+ }
141
+
142
+ // For non-external links, use provided rel as-is
143
+ return rel;
144
+ }, [target, rel, prefetch]);
145
+
146
+ return (
147
+ <UI
148
+ as="a"
149
+ ref={ref}
150
+ href={href}
151
+ target={target}
152
+ rel={computedRel}
153
+ styles={styles}
154
+ data-btn={btnStyle}
155
+ onClick={onClick}
156
+ onPointerDown={onPointerDown}
157
+ {...props}
158
+ >
159
+ {children}
160
+ </UI>
161
+ );
162
+ }
163
+ );
59
164
 
60
- export default Link;
61
165
  Link.displayName = "Link";
166
+
167
+ export default Link;
@@ -0,0 +1,261 @@
1
+ import React from "react";
2
+ import UI from "#components/ui";
3
+
4
+ /**
5
+ * Props for the Link component.
6
+ *
7
+ * The Link component renders accessible anchor elements with enhanced security,
8
+ * styling variants, and WCAG 2.1 AA compliance. It supports both traditional
9
+ * text links and button-styled links for call-to-action scenarios.
10
+ *
11
+ * ## Accessibility Considerations
12
+ *
13
+ * - External links automatically include `rel="noopener noreferrer"` for security
14
+ * - Links should have descriptive text or `aria-label` for screen readers
15
+ * - Focus indicators must meet WCAG 2.4.7 contrast requirements (3:1 minimum)
16
+ * - Button-styled links maintain semantic `<a>` element for proper navigation
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * // Basic link
21
+ * <Link href="/about">About Us</Link>
22
+ *
23
+ * // External link with prefetch
24
+ * <Link href="https://example.com" target="_blank" prefetch>
25
+ * Visit Example
26
+ * </Link>
27
+ *
28
+ * // Button-styled link
29
+ * <Link href="/signup" btnStyle="primary">
30
+ * <b>Sign Up Now</b>
31
+ * </Link>
32
+ * ```
33
+ */
34
+ export type LinkProps = {
35
+ /**
36
+ * The URL that the hyperlink points to.
37
+ * Can be relative or absolute, internal or external.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * href="/products"
42
+ * href="https://example.com"
43
+ * href="mailto:hello@example.com"
44
+ * href="tel:+1234567890"
45
+ * ```
46
+ */
47
+ href?: string;
48
+
49
+ /**
50
+ * Where to display the linked URL.
51
+ *
52
+ * - `_self` (default): Current browsing context
53
+ * - `_blank`: New tab/window (automatically adds security attributes)
54
+ * - `_parent`: Parent browsing context
55
+ * - `_top`: Top-level browsing context
56
+ *
57
+ * Note: When `target="_blank"`, `rel="noopener noreferrer"` is automatically
58
+ * added for security unless explicitly overridden.
59
+ *
60
+ * @example
61
+ * ```tsx
62
+ * target="_blank" // Opens in new tab with security
63
+ * ```
64
+ */
65
+ target?: string;
66
+
67
+ /**
68
+ * Relationship between current document and linked URL.
69
+ *
70
+ * Common values:
71
+ * - `noopener`: Prevents window.opener access (security)
72
+ * - `noreferrer`: Prevents referrer header (privacy)
73
+ * - `nofollow`: Hints search engines not to follow (SEO)
74
+ * - `prefetch`: Hints to prefetch the resource (performance)
75
+ *
76
+ * Note: For `target="_blank"`, this component automatically merges
77
+ * `noopener noreferrer` with any user-provided values for security.
78
+ *
79
+ * @example
80
+ * ```tsx
81
+ * rel="nofollow noopener"
82
+ * rel="author"
83
+ * ```
84
+ */
85
+ rel?: string;
86
+
87
+ /**
88
+ * Content to display inside the link.
89
+ *
90
+ * For accessibility, ensure link text is descriptive and meaningful.
91
+ * Avoid generic text like "click here" or "read more" without context.
92
+ *
93
+ * @example
94
+ * ```tsx
95
+ * // ✅ Good: Descriptive link text
96
+ * <Link href="/products">View all products</Link>
97
+ *
98
+ * // ❌ Bad: Generic link text without context
99
+ * <Link href="/products">Click here</Link>
100
+ *
101
+ * // ✅ Good: Icon with accessible label
102
+ * <Link href="/home" aria-label="Return to homepage">
103
+ * <HomeIcon aria-hidden="true" />
104
+ * </Link>
105
+ * ```
106
+ */
107
+ children: React.ReactNode;
108
+
109
+ /**
110
+ * Inline CSS styles to apply to the link element.
111
+ * Can be used to override CSS custom properties.
112
+ *
113
+ * @example
114
+ * ```tsx
115
+ * styles={{
116
+ * '--link-color': '#ff0000',
117
+ * '--link-decoration': 'underline',
118
+ * }}
119
+ * ```
120
+ */
121
+ styles?: React.CSSProperties;
122
+
123
+ /**
124
+ * Hints to the browser to prefetch the linked resource.
125
+ *
126
+ * When `true` and `target="_blank"`, adds `rel="prefetch"` along with
127
+ * security attributes. This can improve perceived performance but should
128
+ * be used judiciously as it consumes bandwidth.
129
+ *
130
+ * Note: Browser support varies. Modern browsers may ignore this hint.
131
+ *
132
+ * @default false
133
+ * @example
134
+ * ```tsx
135
+ * <Link href="/next-page" prefetch>Next Page</Link>
136
+ * ```
137
+ */
138
+ prefetch?: boolean;
139
+
140
+ /**
141
+ * Applies button-like styling to the link.
142
+ *
143
+ * When set, the link renders with button styling including padding,
144
+ * borders, and hover effects while maintaining semantic anchor behavior.
145
+ *
146
+ * Common values:
147
+ * - `"btn"`: Standard button styling
148
+ * - `"pill"`: Rounded pill button styling
149
+ *
150
+ * Alternative: Wrap children in `<b>` or `<i>` tags for automatic styling:
151
+ * - `<b>`: Applies button styling
152
+ * - `<i>`: Applies pill styling
153
+ *
154
+ * @example
155
+ * ```tsx
156
+ * // Using btnStyle prop
157
+ * <Link href="/signup" btnStyle="btn">Sign Up</Link>
158
+ *
159
+ * // Using child wrapper (automatic detection)
160
+ * <Link href="/signup"><b>Sign Up</b></Link>
161
+ * <Link href="/signup"><i>Pill Button</i></Link>
162
+ * ```
163
+ */
164
+ btnStyle?: string;
165
+
166
+ /**
167
+ * Event handler called when the link is clicked or activated.
168
+ *
169
+ * **Recommended for most use cases**, especially analytics and tracking.
170
+ * This event fires for:
171
+ * - Mouse clicks
172
+ * - Touch/tap interactions
173
+ * - Keyboard activation (Enter key)
174
+ * - Assistive technology activation
175
+ *
176
+ * Use `onClick` when you need to track ALL user activations, including
177
+ * keyboard users. This ensures full accessibility coverage.
178
+ *
179
+ * @param event - The mouse event
180
+ * @example
181
+ * ```tsx
182
+ * // ✅ RECOMMENDED: onClick tracks all activation methods
183
+ * <Link
184
+ * href="/products"
185
+ * onClick={(e) => {
186
+ * trackEvent('link_click', { href: '/products' });
187
+ * }}
188
+ * >
189
+ * Products
190
+ * </Link>
191
+ * ```
192
+ */
193
+ onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
194
+
195
+ /**
196
+ * Event handler called when a pointer device button is pressed on the link.
197
+ *
198
+ * Use this for specific pointer interactions like:
199
+ * - Drag-and-drop detection
200
+ * - Touch gesture recognition
201
+ * - Distinguishing input types (mouse vs touch vs pen)
202
+ * - Providing early visual feedback before click completes
203
+ *
204
+ * **⚠️ Accessibility Note**: Unlike `onClick`, this does NOT fire for
205
+ * keyboard activation (Enter key). If you need to track all user interactions
206
+ * including keyboard users, use `onClick` instead.
207
+ *
208
+ * @param event - The pointer event
209
+ * @example
210
+ * ```tsx
211
+ * // Use onPointerDown for pointer-specific interactions
212
+ * <Link
213
+ * href="/products"
214
+ * onPointerDown={(e) => {
215
+ * // Distinguish between mouse (2), touch (5), and pen (3)
216
+ * console.log('Pointer type:', e.pointerType);
217
+ * }}
218
+ * >
219
+ * Products
220
+ * </Link>
221
+ * ```
222
+ *
223
+ * @example
224
+ * ```tsx
225
+ * // ✅ Use both handlers together for comprehensive tracking
226
+ * <Link
227
+ * href="/products"
228
+ * onClick={(e) => trackAllActivations(e)}
229
+ * onPointerDown={(e) => provideFeedback(e)}
230
+ * >
231
+ * Products
232
+ * </Link>
233
+ * ```
234
+ */
235
+ onPointerDown?: (event: React.PointerEvent<HTMLAnchorElement>) => void;
236
+ } & React.ComponentProps<typeof UI> &
237
+ React.ComponentPropsWithoutRef<"a">;
238
+
239
+ /**
240
+ * Props for the Link component with ref support.
241
+ *
242
+ * Extends LinkProps to include forwarded ref for direct DOM access.
243
+ * Useful for focus management, scroll positioning, and programmatic navigation.
244
+ *
245
+ * @example
246
+ * ```tsx
247
+ * const linkRef = useRef<HTMLAnchorElement>(null);
248
+ *
249
+ * useEffect(() => {
250
+ * // Focus link programmatically (e.g., for skip links)
251
+ * linkRef.current?.focus();
252
+ * }, []);
253
+ *
254
+ * <Link ref={linkRef} href="#main-content">
255
+ * Skip to main content
256
+ * </Link>
257
+ * ```
258
+ */
259
+ export type LinkPropsWithRef = LinkProps & {
260
+ ref?: React.Ref<HTMLAnchorElement>;
261
+ };