@fpkit/acss 1.0.0 → 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 (63) hide show
  1. package/README.md +60 -0
  2. package/libs/{chunk-7XPFW7CB.js → chunk-43TK2ICH.js} +2 -2
  3. package/libs/chunk-5PJYLVFY.cjs +17 -0
  4. package/libs/chunk-5PJYLVFY.cjs.map +1 -0
  5. package/libs/chunk-E4OSROCA.cjs +17 -0
  6. package/libs/chunk-E4OSROCA.cjs.map +1 -0
  7. package/libs/chunk-KVKQLRJG.js +10 -0
  8. package/libs/chunk-KVKQLRJG.js.map +1 -0
  9. package/libs/{chunk-QVW6W76L.cjs → chunk-MGPWZRBX.cjs} +3 -3
  10. package/libs/chunk-NNTBIHSD.js +8 -0
  11. package/libs/chunk-NNTBIHSD.js.map +1 -0
  12. package/libs/{chunk-X3JCTEPD.js → chunk-QKHPHMG2.js} +2 -2
  13. package/libs/{chunk-T4T6GWYQ.cjs → chunk-R7NLLZU2.cjs} +3 -3
  14. package/libs/{chunk-X5LGFCWG.js → chunk-UJAQVHWC.js} +3 -3
  15. package/libs/{chunk-DKTHCQ5P.cjs → chunk-X5RKCLDC.cjs} +3 -3
  16. package/libs/components/breadcrumbs/breadcrumb.cjs +5 -5
  17. package/libs/components/breadcrumbs/breadcrumb.d.cts +1 -1
  18. package/libs/components/breadcrumbs/breadcrumb.d.ts +1 -1
  19. package/libs/components/breadcrumbs/breadcrumb.js +2 -2
  20. package/libs/components/button.cjs +3 -3
  21. package/libs/components/button.d.cts +1 -1
  22. package/libs/components/button.d.ts +1 -1
  23. package/libs/components/button.js +1 -1
  24. package/libs/components/dialog/dialog.cjs +4 -4
  25. package/libs/components/dialog/dialog.js +2 -2
  26. package/libs/components/link/link.cjs +11 -3
  27. package/libs/components/link/link.d.cts +131 -3
  28. package/libs/components/link/link.d.ts +131 -3
  29. package/libs/components/link/link.js +1 -1
  30. package/libs/components/modal.cjs +3 -3
  31. package/libs/components/modal.js +2 -2
  32. package/libs/hooks.cjs +3 -3
  33. package/libs/hooks.d.cts +1 -1
  34. package/libs/hooks.d.ts +1 -1
  35. package/libs/hooks.js +2 -2
  36. package/libs/index.cjs +12 -12
  37. package/libs/index.d.cts +237 -2
  38. package/libs/index.d.ts +237 -2
  39. package/libs/index.js +5 -5
  40. package/package.json +2 -2
  41. package/src/components/breadcrumbs/breadcrumb.test.tsx +1 -2
  42. package/src/components/buttons/README.mdx +19 -9
  43. package/src/components/buttons/button.tsx +19 -15
  44. package/src/components/link/link.stories.tsx +205 -8
  45. package/src/components/link/link.test.tsx +1 -1
  46. package/src/components/link/link.tsx +22 -0
  47. package/src/components/link/link.types.ts +11 -3
  48. package/src/docs/fpkit-developer.mdx +131 -53
  49. package/libs/chunk-33PNJ4LO.cjs +0 -15
  50. package/libs/chunk-33PNJ4LO.cjs.map +0 -1
  51. package/libs/chunk-GT77BX4L.cjs +0 -17
  52. package/libs/chunk-GT77BX4L.cjs.map +0 -1
  53. package/libs/chunk-OVWLQYMK.js +0 -10
  54. package/libs/chunk-OVWLQYMK.js.map +0 -1
  55. package/libs/chunk-UEPAWMDF.js +0 -8
  56. package/libs/chunk-UEPAWMDF.js.map +0 -1
  57. package/libs/link-5192f411.d.ts +0 -323
  58. /package/libs/{chunk-7XPFW7CB.js.map → chunk-43TK2ICH.js.map} +0 -0
  59. /package/libs/{chunk-QVW6W76L.cjs.map → chunk-MGPWZRBX.cjs.map} +0 -0
  60. /package/libs/{chunk-X3JCTEPD.js.map → chunk-QKHPHMG2.js.map} +0 -0
  61. /package/libs/{chunk-T4T6GWYQ.cjs.map → chunk-R7NLLZU2.cjs.map} +0 -0
  62. /package/libs/{chunk-X5LGFCWG.js.map → chunk-UJAQVHWC.js.map} +0 -0
  63. /package/libs/{chunk-DKTHCQ5P.cjs.map → chunk-X5RKCLDC.cjs.map} +0 -0
@@ -1,8 +1,8 @@
1
- import UI from '../ui'
2
- import React from 'react'
3
- import { useDisabledState } from '../../hooks/use-disabled-state'
4
- import { resolveDisabledState } from '../../utils/accessibility'
5
- import type { DisabledStateProps } from '../../types/shared'
1
+ import UI from "../ui";
2
+ import React from "react";
3
+ import { useDisabledState } from "../../hooks/use-disabled-state";
4
+ import { resolveDisabledState } from "../../utils/accessibility";
5
+ import type { DisabledStateProps } from "../../types/shared";
6
6
 
7
7
  export type ButtonProps = Partial<React.ComponentProps<typeof UI>> &
8
8
  DisabledStateProps & {
@@ -10,8 +10,8 @@ export type ButtonProps = Partial<React.ComponentProps<typeof UI>> &
10
10
  * The button type
11
11
  * Required - 'button' | 'submit' | 'reset'
12
12
  */
13
- type: 'button' | 'submit' | 'reset'
14
- }
13
+ type: "button" | "submit" | "reset";
14
+ };
15
15
 
16
16
  /**
17
17
  * Accessible Button component with WCAG 2.1 Level AA compliant disabled state.
@@ -67,7 +67,7 @@ export type ButtonProps = Partial<React.ComponentProps<typeof UI>> &
67
67
  * @see {@link file://./../../hooks/useDisabledState.md useDisabledState Hook Documentation}
68
68
  */
69
69
  export const Button = ({
70
- type = 'button',
70
+ type = "button",
71
71
  children,
72
72
  styles,
73
73
  disabled,
@@ -81,7 +81,7 @@ export const Button = ({
81
81
  ...props
82
82
  }: ButtonProps) => {
83
83
  // Resolve disabled state from both props (disabled takes precedence)
84
- const isActuallyDisabled = resolveDisabledState(disabled, isDisabled)
84
+ const isActuallyDisabled = resolveDisabledState(disabled, isDisabled);
85
85
 
86
86
  // Use the disabled state hook with enhanced API for automatic className merging
87
87
  const { disabledProps, handlers } = useDisabledState<HTMLButtonElement>(
@@ -97,14 +97,14 @@ export const Button = ({
97
97
  // Note: onPointerOver and onPointerLeave are intentionally NOT wrapped
98
98
  // to allow hover effects on disabled buttons for visual feedback
99
99
  }
100
- )
100
+ );
101
101
 
102
102
  /* Returning a button element with accessible disabled state */
103
103
  return (
104
104
  <UI
105
105
  as="button"
106
106
  type={type}
107
- aria-disabled={disabledProps['aria-disabled']}
107
+ aria-disabled={disabledProps["aria-disabled"]}
108
108
  onPointerOver={onPointerOver}
109
109
  onPointerLeave={onPointerLeave}
110
110
  style={styles}
@@ -114,8 +114,12 @@ export const Button = ({
114
114
  >
115
115
  {children}
116
116
  </UI>
117
- )
118
- }
117
+ );
118
+ };
119
119
 
120
- export default Button
121
- Button.displayName = 'Button'
120
+ export const IconButton = ({ icon, ...props }: ButtonProps) => {
121
+ return <Button {...props}>{icon}</Button>;
122
+ };
123
+
124
+ export default Button;
125
+ Button.displayName = "Button";
@@ -1,7 +1,8 @@
1
+ import React from "react";
1
2
  import { StoryObj, Meta } from "@storybook/react-vite";
2
3
  import { within, expect, userEvent, fn } from "storybook/test";
3
4
  import { useRef, useEffect } from "react";
4
- import Link from "./link";
5
+ import Link, { IconLink, LinkButton } from "./link";
5
6
  import type { LinkProps } from "./link.types";
6
7
  import "../../styles/link/link.css";
7
8
 
@@ -41,8 +42,30 @@ const meta = {
41
42
  description: {
42
43
  component: `A semantic, accessible anchor component with enhanced security for external links and flexible styling variants.
43
44
 
45
+ ## Features
46
+
47
+ - **Automatic Security**: External links (\`target="_blank"\`) automatically get \`rel="noopener noreferrer"\`
48
+ - **WCAG 2.1 AA Compliant**: Focus indicators, semantic HTML, keyboard navigation
49
+ - **Flexible Styling**: Text links, button-styled links, and pill variants
50
+ - **Performance**: Optional prefetch hints for faster navigation
51
+ - **Ref Forwarding**: Direct DOM access for focus management
52
+ - **Type-Safe**: Full TypeScript support with comprehensive prop types
53
+
54
+ ## Exported Components
55
+
56
+ ### Link (default)
57
+ Main component for creating semantic anchor elements with enhanced features.
58
+
59
+ ### IconLink
60
+ Specialized component for icon-only or icon-with-text links.
61
+
62
+ ### LinkButton
63
+ Convenience component for button-styled links.
64
+
44
65
  ## CSS Variables
45
66
 
67
+ All units use **rem** (not px). Base: 1rem = 16px.
68
+
46
69
  ### Typography & Color
47
70
  - \`--link-color\`: Link text color (default: #085ab7)
48
71
  - \`--link-fw\`: Font weight (default: 400)
@@ -50,32 +73,40 @@ const meta = {
50
73
 
51
74
  ### Text Decoration
52
75
  - \`--link-decoration\`: Text decoration style (default: none, underline on hover/focus)
53
- - \`--link-decoration-offset\`: Underline offset (default: 0.09375rem / 1.5px)
54
- - \`--link-decoration-thickness\`: Underline thickness (default: 0.0625rem / 1px)
76
+ - \`--link-decoration-offset\`: Underline offset (default: 0.09375rem)
77
+ - \`--link-decoration-thickness\`: Underline thickness (default: 0.0625rem)
55
78
  - \`--link-skip-ink\`: Text decoration skip ink (default: auto)
56
79
 
57
80
  ### Background & Border
58
81
  - \`--link-bg\`: Background color (default: transparent)
59
82
  - \`--link-radius\`: Border radius (default: 0.25rem, 99rem for pills)
60
83
 
61
- ### Spacing (Button Variants)
84
+ ### Spacing
62
85
  - \`--link-padding-inline\`: Horizontal padding (default: 0, calculated for button variants)
63
86
  - \`--link-padding-block\`: Vertical padding (default: 0, calculated for button variants)
64
87
 
65
88
  ### Focus Indicators (WCAG 2.4.7)
66
89
  - \`--link-focus-color\`: Focus outline color (default: currentColor)
67
- - \`--link-focus-width\`: Focus outline width (default: 0.125rem / 2px)
68
- - \`--link-focus-offset\`: Focus outline offset (default: 0.125rem / 2px)
90
+ - \`--link-focus-width\`: Focus outline width (default: 0.125rem)
91
+ - \`--link-focus-offset\`: Focus outline offset (default: 0.125rem)
69
92
  - \`--link-focus-style\`: Focus outline style (default: solid)
70
93
 
71
94
  ### Transitions
72
95
  - \`--link-transition\`: Transition timing (default: all 0.75s ease-in-out)
73
96
 
74
- ### Button Variants
97
+ ### Button Variant Variables
98
+ Applied when using \`btnStyle\` prop, \`data-btn\` attribute, or \`<b>\`/\`<i>\` wrappers:
75
99
  - \`--link-button-color\`: Button link text color (default: var(--link-color))
76
- - \`--link-border-width\`: Button border width (default: 0.125rem / 2px)
100
+ - \`--link-border-width\`: Button border width (default: 0.125rem)
77
101
  - \`--link-border-color\`: Button border color (default: currentColor)
78
102
  - \`--link-border-style\`: Button border style (default: solid)
103
+
104
+ ## Usage Patterns
105
+
106
+ ### Button Styling (3 ways)
107
+ 1. **btnStyle prop**: \`<Link href="/signup" btnStyle="btn">Sign Up</Link>\`
108
+ 2. **Wrapper elements**: \`<Link href="/signup"><b>Sign Up</b></Link>\` (button) or \`<Link href="/signup"><i>Sign Up</i></Link>\` (pill)
109
+ 3. **Direct attribute**: \`<Link href="/signup" data-btn>Sign Up</Link>\`
79
110
  `,
80
111
  },
81
112
  },
@@ -458,3 +489,169 @@ export const WithBothHandlers: Story = {
458
489
  },
459
490
  },
460
491
  };
492
+
493
+ /**
494
+ * Button-styled link using btnStyle prop.
495
+ * Demonstrates direct usage of the btnStyle prop instead of wrapper elements.
496
+ */
497
+ export const WithBtnStyleProp: Story = {
498
+ args: {
499
+ href: "/dashboard",
500
+ btnStyle: "btn",
501
+ children: "Go to Dashboard",
502
+ },
503
+ parameters: {
504
+ docs: {
505
+ description: {
506
+ story:
507
+ "Use the `btnStyle` prop to apply button styling directly. This sets the `data-btn` attribute on the link element.",
508
+ },
509
+ },
510
+ },
511
+ play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
512
+ const canvas = within(canvasElement);
513
+ const link = canvas.getByRole("link");
514
+
515
+ // Verify btnStyle creates data-btn attribute
516
+ expect(link).toHaveAttribute("data-btn", "btn");
517
+ expect(link).toHaveTextContent("Go to Dashboard");
518
+ },
519
+ };
520
+
521
+ /**
522
+ * IconLink component for icon-based navigation.
523
+ * Specialized component for links with icons.
524
+ */
525
+ export const IconLinkComponent: Story = {
526
+ render: () => (
527
+ <IconLink
528
+ href="/home"
529
+ aria-label="Return to homepage"
530
+ icon={
531
+ <svg
532
+ aria-hidden="true"
533
+ width="20"
534
+ height="20"
535
+ viewBox="0 0 16 16"
536
+ fill="currentColor"
537
+ >
538
+ <path d="M8.707 1.5a1 1 0 0 0-1.414 0L.646 8.146a.5.5 0 0 0 .708.708L2 8.207V13.5A1.5 1.5 0 0 0 3.5 15h9a1.5 1.5 0 0 0 1.5-1.5V8.207l.646.647a.5.5 0 0 0 .708-.708L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.707 1.5ZM13 7.207V13.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V7.207l5-5 5 5Z" />
539
+ </svg>
540
+ }
541
+ >
542
+ {null}
543
+ </IconLink>
544
+ ),
545
+ parameters: {
546
+ docs: {
547
+ description: {
548
+ story:
549
+ "The `IconLink` component is a convenience wrapper for icon-based links. Always include an `aria-label` for accessibility and set `aria-hidden='true'` on the icon.",
550
+ },
551
+ },
552
+ },
553
+ play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
554
+ const canvas = within(canvasElement);
555
+ const link = canvas.getByRole("link");
556
+
557
+ // Verify accessible name
558
+ expect(link).toHaveAccessibleName("Return to homepage");
559
+
560
+ // Verify icon is hidden from screen readers
561
+ const svg = link.querySelector("svg");
562
+ expect(svg).toHaveAttribute("aria-hidden", "true");
563
+ },
564
+ };
565
+
566
+ /**
567
+ * IconLink with text label.
568
+ * Icon link with visible text alongside the icon.
569
+ */
570
+ export const IconLinkWithText: Story = {
571
+ render: () => (
572
+ <IconLink
573
+ href="/downloads"
574
+ icon={
575
+ <svg
576
+ aria-hidden="true"
577
+ width="16"
578
+ height="16"
579
+ viewBox="0 0 16 16"
580
+ fill="currentColor"
581
+ style={{ marginRight: "0.5rem" }}
582
+ >
583
+ <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z" />
584
+ <path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z" />
585
+ </svg>
586
+ }
587
+ >
588
+ Download Files
589
+ </IconLink>
590
+ ),
591
+ parameters: {
592
+ docs: {
593
+ description: {
594
+ story:
595
+ "IconLink can contain both an icon and text. The icon is passed via the `icon` prop, and text is provided as children.",
596
+ },
597
+ },
598
+ },
599
+ };
600
+
601
+ /**
602
+ * LinkButton component for call-to-action links.
603
+ * Convenience component that applies button styling automatically.
604
+ */
605
+ export const LinkButtonComponent: Story = {
606
+ render: () => (
607
+ <LinkButton href="/get-started">
608
+ <b>Get Started Free</b>
609
+ </LinkButton>
610
+ ),
611
+ parameters: {
612
+ docs: {
613
+ description: {
614
+ story:
615
+ "The `LinkButton` component is a convenience wrapper for button-styled links. It maintains semantic `<a>` element while providing button appearance.",
616
+ },
617
+ },
618
+ },
619
+ play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
620
+ const canvas = within(canvasElement);
621
+ const link = canvas.getByRole("link");
622
+
623
+ // Verify it's still a semantic anchor
624
+ expect(link.tagName).toBe("A");
625
+ expect(link).toHaveAttribute("href", "/get-started");
626
+ expect(link).toHaveTextContent("Get Started Free");
627
+ },
628
+ };
629
+
630
+ /**
631
+ * LinkButton with custom styling.
632
+ * Demonstrates CSS variable overrides on LinkButton.
633
+ */
634
+ export const LinkButtonCustom: Story = {
635
+ render: () => {
636
+ const customStyles: React.CSSProperties = {
637
+ "--link-button-color": "#ffffff",
638
+ "--link-bg": "#0066cc",
639
+ "--link-border-color": "#0066cc",
640
+ "--link-radius": "0.5rem",
641
+ } as React.CSSProperties;
642
+
643
+ return (
644
+ <LinkButton href="/signup" styles={customStyles}>
645
+ <b>Sign Up Now</b>
646
+ </LinkButton>
647
+ );
648
+ },
649
+ parameters: {
650
+ docs: {
651
+ description: {
652
+ story:
653
+ "LinkButton styling can be customized using CSS custom properties for colors, borders, and border radius.",
654
+ },
655
+ },
656
+ },
657
+ };
@@ -24,7 +24,7 @@ describe("Link Component", () => {
24
24
 
25
25
  it("should render with custom classes via UI component", () => {
26
26
  render(
27
- <Link href="/" classes="custom-link-class">
27
+ <Link href="/" className="custom-link-class">
28
28
  Test
29
29
  </Link>
30
30
  );
@@ -162,6 +162,28 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
162
162
  }
163
163
  );
164
164
 
165
+ export const IconLink = React.forwardRef<HTMLAnchorElement, LinkProps>(
166
+ ({ href, icon, ...props }, ref) => {
167
+ return (
168
+ <Link ref={ref} href={href} {...props}>
169
+ {icon}
170
+ </Link>
171
+ );
172
+ }
173
+ );
174
+
175
+ export const LinkButton = React.forwardRef<HTMLAnchorElement, LinkProps>(
176
+ ({ href, children, ...props }, ref) => {
177
+ return (
178
+ <Link ref={ref} href={href} {...props}>
179
+ {children}
180
+ </Link>
181
+ );
182
+ }
183
+ );
184
+
185
+ IconLink.displayName = "IconLink";
186
+ LinkButton.displayName = "LinkButton";
165
187
  Link.displayName = "Link";
166
188
 
167
189
  export default Link;
@@ -1,5 +1,4 @@
1
1
  import React from "react";
2
- import UI from "#components/ui";
3
2
 
4
3
  /**
5
4
  * Props for the Link component.
@@ -233,8 +232,17 @@ export type LinkProps = {
233
232
  * ```
234
233
  */
235
234
  onPointerDown?: (event: React.PointerEvent<HTMLAnchorElement>) => void;
236
- } & React.ComponentProps<typeof UI> &
237
- React.ComponentPropsWithoutRef<"a">;
235
+
236
+ /**
237
+ * Icon element to display in the link (used by IconLink).
238
+ *
239
+ * @example
240
+ * ```tsx
241
+ * <IconLink href="/home" icon={<HomeIcon />} />
242
+ * ```
243
+ */
244
+ icon?: React.ReactNode;
245
+ } & Omit<React.ComponentPropsWithoutRef<"a">, 'style'>;
238
246
 
239
247
  /**
240
248
  * Props for the Link component with ref support.