ab-nextjs-components 0.1.9 โ†’ 0.1.11

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 (49) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/ab-brand/index.tsx +30 -1
  3. package/ab-brand/styles.module.css +40 -1
  4. package/ab-button/helpers.ts +90 -0
  5. package/ab-button/index.tsx +164 -87
  6. package/ab-button/styles.module.css +79 -1
  7. package/ab-carousel/helpers.ts +146 -0
  8. package/ab-carousel/index.tsx +643 -0
  9. package/ab-carousel/styles.module.css +206 -0
  10. package/ab-chip/index.tsx +75 -0
  11. package/ab-chip/styles.module.css +87 -0
  12. package/ab-collapsible/index.tsx +30 -1
  13. package/ab-collapsible/styles.module.css +40 -1
  14. package/ab-flow-diagram/helpers.ts +77 -0
  15. package/ab-flow-diagram/index.tsx +152 -0
  16. package/ab-flow-diagram/styles.module.css +158 -0
  17. package/ab-icon/builtin.ts +61 -0
  18. package/ab-icon/helpers.ts +102 -0
  19. package/ab-icon/index.tsx +77 -16
  20. package/ab-icon/styles.module.css +53 -9
  21. package/ab-icon-button/index.tsx +110 -46
  22. package/ab-icon-button/styles.module.css +28 -10
  23. package/ab-install-snippet/helpers.ts +62 -0
  24. package/ab-install-snippet/index.tsx +170 -0
  25. package/ab-install-snippet/styles.module.css +104 -0
  26. package/ab-like-button/index.tsx +33 -1
  27. package/ab-like-button/styles.module.css +41 -6
  28. package/ab-linear-progress/index.tsx +5 -1
  29. package/ab-linear-progress/styles.module.css +8 -24
  30. package/ab-locale-switcher/index.tsx +213 -0
  31. package/ab-locale-switcher/styles.module.css +127 -0
  32. package/ab-pill/index.tsx +74 -0
  33. package/ab-progress-chips/index.tsx +30 -1
  34. package/ab-progress-chips/styles.module.css +40 -1
  35. package/ab-searchbar/index.tsx +33 -2
  36. package/ab-searchbar/styles.module.css +41 -5
  37. package/ab-splash/index.tsx +3 -1
  38. package/ab-splash/styles.module.css +2 -5
  39. package/ab-theme-toggle/index.tsx +188 -0
  40. package/ab-theme-toggle/styles.module.css +89 -0
  41. package/css/dialog.css +38 -3
  42. package/index.ts +41 -0
  43. package/package.json +33 -17
  44. package/server/ab-brand/index.tsx +29 -1
  45. package/server/ab-brand/styles.module.css +40 -1
  46. package/server/ab-button/index.tsx +96 -79
  47. package/server/ab-button/styles.module.css +79 -1
  48. package/server/ab-icon/index.tsx +68 -16
  49. package/server/ab-icon/styles.module.css +53 -9
package/CHANGELOG.md CHANGED
@@ -1,3 +1,39 @@
1
+ ## 0.1.11 โ€” 2026-10-15
2
+
3
+ * ๐Ÿ› **AbFlowDiagram: nodes no longer cover the header in small boxes.** At 390px wide the Live Playground's top node ("core") sat under the "Live Playground" header. With a header (title, live dot or caption) the root now gets `data-ab-header="true"`, and nodes + wires live in a stage that starts below the header band: `--ab-flow-diagram-inset-top` (default `2.75rem`, tunable). Node `x` / `y` stay 0โ€“100 but now map onto that stage, and the wires share the same box so they still meet the nodes. Without a header the stage is the full card (unchanged)
4
+ * ๐Ÿ“ **AbFlowDiagram nodes scale with the card:** `container-type: inline-size` on the root; node text, padding, radius and the hub glow use clamped `cqi` sizes (13px on a 760px card, 11px at 390px)
5
+ * ๐Ÿช **AbFlowDiagram: side nodes are never clipped** (Joseph: s04's `--ab-color-bronze` and s07's `reduced motion` were cut off at the card edge in a 390px box). The orbit now scales with the box. Each node is anchored at the same fraction of its own box as its position (`translate(-x%, -y%)` via `--ab-flow-node-x` / `-y`; `x = y = 50` is the old centring), so the outer ring pulls in by exactly the node's width, and the wire end stays inside the pill. Nodes + wires also sit inside a side margin, `--ab-flow-diagram-inset-x` (default `0.5rem`), and an over-long label ellipsises (`max-width: 100%`). Positions are clamped to 0โ€“100 like the wires
6
+ * Tests: header flag, stage insets shared by nodes + wires, 390px geometry, clamped scaling, proportional anchoring + side margin, s04 / s07 orbit geometry at 390 / 760px ร— 276 / 380px. Checked in WebKit at 390px: "core" top 9px โ†’ 53px (header ends at 33px); no node overlaps the header or is clipped at 390 / 760px. After the orbit fix: s04 + s07 have no node clipped (x or y) or over the header at 390 / 760px wide ร— 276px / 380px / default-ratio heights (before: 5 nodes clipped at 390px, 2 at 760px)
7
+
8
+ ## 0.1.10 โ€” 2026-10-14
9
+
10
+ * **Motion comes from ab-nextjs-animations (>= 0.2.4, now a required peer):** no component CSS declares its own `transition`, `animation` or `@keyframes` any more. Each component imports the package stylesheet it needs:
11
+ * AbCarousel: `slide-step` (direction-aware enter / leave via `data-ab-direction`) + `motion` (buttons, dots)
12
+ * AbFlowDiagram: `wire` (`--ab-wire-*` tokens; `data-ab-paused`; 3 loops on phones unless `loops` is set)
13
+ * AbThemeToggle, AbSearchbar, AbSplash fade: `motion` colours
14
+ * AbIconButton: `motion` (`expands` โ†’ `ab-motion-lift`, `shrinks` โ†’ `ab-motion-press`)
15
+ * AbProgressChips: `motion` size
16
+ * AbLinearProgress: `motion` transform + `progress-sweep`
17
+ * AbCollapsible, AbBrand: `motion` transform
18
+ * AbLikeButton / AbSearchbar spinners: `spin`
19
+ * Reduced motion (OS, or `reducedMotion="always"` via `data-ab-reduced-motion`) is handled by the package
20
+ * **AbCarousel:** context gains `direction` + `previousIndex`; `--ab-carousel-duration` / `-ease` / `-shift` map onto `--ab-slide-step-*`; the old positional slide offsets are gone
21
+ * **Peers:** `ab-nextjs-icons` and `ab-nextjs-animations` are no longer optional. All four Welcome peers (and devDependencies) use caret ranges: `ab-nextjs-hooks ^0.1.6`, `ab-nextjs-theme ^0.3.1`, `ab-nextjs-icons ^0.2.3`, `ab-nextjs-animations ^0.2.4`
22
+ * **Motion exceptions (named, test-guarded):** the dialog list-item transition (`css/dialog.css`) and the self-animating spinner SVGs stay local
23
+ * **Lockfile:** refreshed against the published hooks 0.1.6, theme 0.3.1, icons 0.2.3 and animations 0.2.4 (`pnpm install --frozen-lockfile` verified on a fresh `node_modules`). `pnpm-workspace.yaml` exempts exactly the five Welcome releases (fonts 0.2.5 included) from pnpm 11's 1-day `minimumReleaseAge`, and that block should be removed once they are a day old
24
+
25
+ * **AbCarousel (G31):** new subpath `ab-nextjs-components/ab-carousel` (+ server-safe `ab-carousel/helpers`). Compound parts `AbCarousel`, `AbCarouselTrack`, `AbCarouselSlide`, `AbCarouselPrev`, `AbCarouselNext`, `AbCarouselDots`, `AbCarouselCounter` + `useAbCarousel()`. APG carousel semantics (`aria-roledescription` carousel/slide, "n of total" labels, polite live region, inactive slides `inert` + `aria-hidden` after hydration), RTL-aware โ†/โ†’ + Home/End scoped to the region, swipe via `useAbSwipe`, reduced motion via `useAbReducedMotion` + CSS, controlled / uncontrolled, i18n labels on every control, `AB_CAROUSEL_NOSCRIPT_CSS` for the no-JS stack
26
+ * **AbIconButton (G6):** `aria-label` + `title` now sit on the interactive element (never the wrapper span); new `label` prop (falls back to `title`); the `href` variant is named and no longer has `role="button"`; the ligature glyph is `aria-hidden`; disabled links leave the tab order; loading sets `aria-busy`
27
+ * **AbButton (G22):** `startIcon` / `endIcon`, `badge` + `badgeTone` (e.g. "Coming soon"), `disabledReason` (visually hidden text appended to the accessible name), `focusableWhenDisabled`; with a badge / reason the button uses `aria-disabled` (stays focusable, clicks guarded) instead of native `disabled`; disabled links render as `span role="link" aria-disabled`; links no longer get `role="button"`; server twin matches
28
+ * **AbThemeToggle (G34, new, `ab-nextjs-components/ab-theme-toggle`):** light / dark / system radiogroup (roving focus, RTL arrows, icon-only variant, i18n labels), driven by `useAbThemeMode()`; `getNextAbThemeMode` helper
29
+ * **AbChip / AbPill (G36, new, server components):** `ab-chip`, `ab-pill`
30
+ * **AbInstallSnippet (G35, new):** pnpm / npm / yarn / bun tabs (APG tablist), choice shared via `useAbLocalStorage`, copy button with polite status, `data-ab-swipe-ignore`; `getAbInstallCommand` helper
31
+ * **AbLocaleSwitcher (G7, new):** menu button + `menuitemradio`, typeahead, `onSelect` or `getHref` (+ `hrefLang`)
32
+ * **AbIcon (G2, new, `ab-nextjs-components/ab-icon`):** AbIcons as inline SVG; client twin resolves `svg` prop โ†’ `registerAbIcons` registry โ†’ Material Symbols font (never imports the full map); server twin renders any icon; weights 100โ€“700 map to the icons package stroke table; 8 built-ins (arrows, theme, language, copy / check) imported per icon from `ab-nextjs-icons/ab-icons/icons/*` (needs icons >= 0.2.3)
33
+ * **AbCarousel:** prev / next arrows now render through AbIcon (weight 300); `--ab-carousel-icon-stroke` removed (unreleased)
34
+ * **AbFlowDiagram (G38, new, server component):** `figure role="img"` scenes, CSS-only draw + pulse, paused while inactive, 3 loops on mobile, reduced motion shows the poster frame; `getAbFlowEdgePath` helper
35
+ * Dev: `happy-dom` + `@testing-library/react` for DOM tests
36
+
1
37
  ## 0.1.9 โ€” 2026-10-14
2
38
 
3
39
  - **AbLogo:** `wordmark` prop (`true` โ†’ "Elements", or any string) renders a mark + Quicksand Bold lockup (`font-quicksand-bold` from ab-nextjs-fonts); `color` prop (`--ab-logo-color`); light / dark (`html.dark`)
@@ -5,11 +5,38 @@
5
5
  * ~~~~~~~~~~~~
6
6
  * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
7
  *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the 'Software'), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
8
26
  * @project: ab-nextjs-components
9
27
  * @name: AbBrand - Client (port of lyd-brand)
10
28
  * @file: ab-brand/index.tsx
29
+ * @type: TypeScript (React)
30
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
11
31
  */
12
32
 
33
+ /*
34
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
35
+ * MOTTO: We'll always do more ๐Ÿ˜œ!!!
36
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
37
+ */
38
+
39
+
13
40
  'use client';
14
41
 
15
42
  import type { CSSProperties, ReactElement } from 'react';
@@ -17,6 +44,8 @@ import Link from 'next/link';
17
44
  import Image from 'next/image';
18
45
  import clsx from 'clsx';
19
46
  import styles from './styles.module.css';
47
+ import { AB_MOTION } from 'ab-nextjs-animations/motion'; // ๐ŸŽฌ transition class names (ab-nextjs-animations >= 0.2.4)
48
+ import 'ab-nextjs-animations/motion/styles.css'; // ๐ŸŽจ .ab-motion-*
20
49
 
21
50
  export interface AbBrandProps {
22
51
  className?: string;
@@ -71,7 +100,7 @@ const AbBrand = ({
71
100
  style={style}
72
101
  >
73
102
  <div className={styles.abBrandImageWrap}>
74
- <Image className={styles.abBrandImage} src={image} alt={imageAlt ?? name} width={640} height={360} unoptimized />
103
+ <Image className={clsx(styles.abBrandImage, AB_MOTION.transform)} src={image} alt={imageAlt ?? name} width={640} height={360} unoptimized />
75
104
  </div>
76
105
  <div className={styles.abBrandMeta}>
77
106
  <Image className={styles.abBrandLogo} src={logo} alt={logoAlt ?? shortName} width={40} height={40} unoptimized />
@@ -1,3 +1,42 @@
1
+ /*
2
+ * @license MIT
3
+ * ~~~~~~~~~~~~
4
+ * ab-nextjs-components
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the 'Software'), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
26
+ * @project: ab-nextjs-components
27
+ * @name: ab-brand - styles
28
+ * @file: ab-brand/styles.module.css
29
+ * @type: CSS Module
30
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
31
+ */
32
+
33
+ /*
34
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
35
+ * MOTTO: We'll always do more ๐Ÿ˜œ!!!
36
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
37
+ */
38
+
39
+
1
40
  .abBrand {
2
41
  position: relative;
3
42
  display: flex;
@@ -28,7 +67,7 @@
28
67
  object-fit: cover;
29
68
  width: 100%;
30
69
  height: 100%;
31
- transition: transform 0.35s ease;
70
+ --ab-motion-duration: 350ms; /* ๐Ÿ” zoom tween = .ab-motion-transform (ab-nextjs-animations) */
32
71
  }
33
72
  .abBrandLogo {
34
73
  width: 2.5rem;
@@ -0,0 +1,90 @@
1
+ /*
2
+ * @license MIT
3
+ * ~~~~~~~~~~~~
4
+ * ab-nextjs-components
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the 'Software'), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
26
+ * @project: ab-nextjs-components
27
+ * @name: AbButton - Shared helpers (G22)
28
+ * @file: ab-button/helpers.ts
29
+ * @type: TypeScript (pure, server-safe)
30
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
31
+ */
32
+
33
+ /*
34
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
35
+ * MOTTO: We'll always do more ๐Ÿ˜œ!!!
36
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
37
+ */
38
+
39
+ // ๐ŸŽจ visual types shared by the client + server twins
40
+ export type AbButtonType = 'contained' | 'outlined' | 'text';
41
+ // ๐ŸŽจ colour roles
42
+ export type AbButtonColor = 'primary' | 'secondary' | 'tertiary' | 'white' | 'black';
43
+ // ๐Ÿท๏ธ badge tones (G22): neutral grey or bronze accent
44
+ export type AbButtonBadgeTone = 'neutral' | 'accent';
45
+
46
+ /**
47
+ * ๐Ÿšฆ How a button is disabled:
48
+ * - 'none' โ†’ interactive
49
+ * - 'native' โ†’ `disabled` attribute (pre-G22 behaviour, skipped by Tab)
50
+ * - 'aria' โ†’ `aria-disabled="true"`: stays focusable so screen-reader users can
51
+ * discover it and hear *why* (badge / reason); activation is suppressed
52
+ */
53
+ export type AbButtonDisabledMode = 'none' | 'native' | 'aria';
54
+
55
+ // โš™๏ธ inputs that decide the mode
56
+ export interface AbButtonDisabledInput {
57
+ disabled?: boolean; // ๐Ÿšซ
58
+ isLoading?: boolean; // โณ loading also blocks activation
59
+ badge?: unknown; // ๐Ÿท๏ธ a visible "Coming soon"-style badge
60
+ disabledReason?: string; // ๐Ÿ’ฌ SR-only reason
61
+ focusableWhenDisabled?: boolean; // โŒจ๏ธ explicit override
62
+ }
63
+
64
+ /**
65
+ * @name getAbButtonDisabledMode
66
+ * @description ๐Ÿšฆ Pick the disabled mode. Explaining a disabled button (badge or
67
+ * reason) only helps if it can be focused, so those switch to 'aria' by default.
68
+ */
69
+ export function getAbButtonDisabledMode(input: AbButtonDisabledInput): AbButtonDisabledMode {
70
+ const blocked = Boolean(input.disabled || input.isLoading); // ๐Ÿšซ anything blocking?
71
+ if (!blocked) return 'none'; // โœ… interactive
72
+ // ๐Ÿงญ explicit choice wins; otherwise explainable buttons stay focusable
73
+ const focusable = input.focusableWhenDisabled ?? Boolean(input.disabledReason || (input.disabled && hasBadge(input.badge)));
74
+ return focusable ? 'aria' : 'native'; // ๐ŸŽฏ
75
+ }
76
+
77
+ // ๐Ÿ” does the badge render anything?
78
+ function hasBadge(badge: unknown): boolean {
79
+ return badge !== undefined && badge !== null && badge !== false && badge !== ''; // ๐Ÿงฎ
80
+ }
81
+
82
+ /**
83
+ * @name getAbButtonSrReason
84
+ * @description ๐Ÿ’ฌ Text appended (visually hidden) to the accessible name when blocked:
85
+ * explicit `disabledReason`, else nothing (a visible badge is already in the name).
86
+ */
87
+ export function getAbButtonSrReason(input: AbButtonDisabledInput): string | undefined {
88
+ if (!(input.disabled || input.isLoading)) return undefined; // โœ… nothing to explain
89
+ return input.disabledReason?.trim() || undefined; // ๐Ÿ’ฌ
90
+ }
@@ -22,137 +22,213 @@
22
22
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
23
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
24
  * SOFTWARE.
25
-
26
25
  *
27
26
  * @project: ab-nextjs-components
28
27
  * @name: AbButton - Client (port of lyd-button)
29
28
  * @file: ab-button/index.tsx
29
+ * @type: Component (client) โ€” G22 disabled + badge + start/end icons
30
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
31
+ *
32
+ * Example usage:
33
+ * 1+|> <AbButton type="outlined" label="Continue with GitHub" startIcon={<AbIcon name="github" />} disabled badge="Coming soon" />
34
+ */
35
+
36
+ /*
37
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
38
+ * MOTTO: We'll always do more ๐Ÿ˜œ!!!
39
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
30
40
  */
31
41
 
32
42
  'use client';
33
43
 
34
- import type { CSSProperties, MouseEvent, ReactNode } from 'react';
44
+ // โš›๏ธ React types + forwardRef
45
+ import type { CSSProperties, KeyboardEvent, MouseEvent, ReactNode, Ref } from 'react';
35
46
  import { forwardRef } from 'react';
47
+ // ๐Ÿงต class joiner
36
48
  import clsx from 'clsx';
49
+ // ๐ŸŽจ styles
37
50
  import styles from './styles.module.css';
51
+ // ๐Ÿงฎ shared (server-safe) rules
52
+ import {
53
+ getAbButtonDisabledMode,
54
+ getAbButtonSrReason,
55
+ type AbButtonBadgeTone,
56
+ type AbButtonColor,
57
+ type AbButtonType,
58
+ } from './helpers';
38
59
 
39
- export type AbButtonType = 'contained' | 'outlined' | 'text';
40
- export type AbButtonColor = 'primary' | 'secondary' | 'tertiary' | 'white' | 'black';
60
+ // ๐Ÿ“ฆ re-export the shared types (pre-G22 import paths keep working)
61
+ export type { AbButtonBadgeTone, AbButtonColor, AbButtonType, AbButtonDisabledMode } from './helpers';
41
62
 
63
+ // โš™๏ธ props
42
64
  export interface AbButtonProps {
43
- id?: string;
44
- type?: AbButtonType;
45
- htmlType?: 'button' | 'submit' | 'reset';
46
- className?: string;
47
- spinnerClassName?: string;
48
- hidden?: boolean;
49
- style?: CSSProperties;
50
- iconStyle?: CSSProperties;
51
- rounded?: boolean;
52
- raised?: boolean;
53
- shrinks?: boolean;
54
- expands?: boolean;
55
- withIcon?: boolean;
56
- disabled?: boolean;
57
- isLoading?: boolean;
58
- iconPosition?: 'left' | 'right';
59
- label: string;
60
- icon?: string;
61
- color?: AbButtonColor;
62
- href?: string;
63
- children?: ReactNode;
64
- onClick?: (event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
65
+ id?: string; // ๐Ÿ†”
66
+ type?: AbButtonType; // ๐ŸŽจ contained / outlined / text
67
+ htmlType?: 'button' | 'submit' | 'reset'; // ๐Ÿงพ
68
+ className?: string; // ๐Ÿท๏ธ
69
+ spinnerClassName?: string; // โณ spinner class
70
+ hidden?: boolean; // ๐Ÿ™ˆ
71
+ style?: CSSProperties; // ๐ŸŽจ
72
+ iconStyle?: CSSProperties; // ๐ŸŽจ Material icon style
73
+ rounded?: boolean; // ๐Ÿ’Š
74
+ raised?: boolean; // ๐Ÿ”๏ธ
75
+ shrinks?: boolean; // ๐Ÿค
76
+ expands?: boolean; // ๐Ÿ”
77
+ withIcon?: boolean; // ๐Ÿ”ฃ legacy Material icon on/off
78
+ disabled?: boolean; // ๐Ÿšซ
79
+ isLoading?: boolean; // โณ
80
+ iconPosition?: 'left' | 'right'; // โ†”๏ธ legacy icon side
81
+ label: string; // ๐Ÿท๏ธ text (also the accessible name)
82
+ icon?: string; // ๐Ÿ”ฃ legacy Material ligature
83
+ color?: AbButtonColor; // ๐ŸŽจ
84
+ href?: string; // ๐Ÿ”— link mode
85
+ children?: ReactNode; // ๐Ÿงฉ replaces label visually
86
+ onClick?: (event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void; // ๐Ÿ–ฑ๏ธ
87
+ // ===== G22 =====
88
+ startIcon?: ReactNode; // ๐Ÿ–ผ๏ธ NEW: any node before the label (e.g. GitHub mark via AbIcon)
89
+ endIcon?: ReactNode; // ๐Ÿ–ผ๏ธ NEW: any node after the label (e.g. arrow_forward)
90
+ badge?: ReactNode; // ๐Ÿท๏ธ NEW: visible badge, e.g. "Coming soon"; part of the accessible name
91
+ badgeTone?: AbButtonBadgeTone; // ๐ŸŽจ NEW: 'neutral' (default) | 'accent'
92
+ disabledReason?: string; // ๐Ÿ’ฌ NEW: SR-only reason, appended to the name while blocked
93
+ focusableWhenDisabled?: boolean; // โŒจ๏ธ NEW: force aria-disabled (true) or native disabled (false)
65
94
  }
66
95
 
67
96
  const AbButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, AbButtonProps>(function AbButton(props, ref) {
97
+ // ๐Ÿงฉ props + defaults
68
98
  const {
69
- id,
70
- type = 'contained',
71
- htmlType = 'button',
72
- className,
73
- spinnerClassName,
74
- hidden = false,
75
- style,
76
- iconStyle,
77
- rounded = true,
78
- raised = false,
79
- shrinks = false,
80
- expands = false,
81
- withIcon = false,
82
- disabled = false,
83
- isLoading = false,
84
- iconPosition = 'left',
85
- label,
86
- icon,
87
- color = 'primary',
88
- href,
89
- children,
90
- onClick,
99
+ id, // ๐Ÿ†”
100
+ type = 'contained', // ๐ŸŽจ
101
+ htmlType = 'button', // ๐Ÿงพ
102
+ className, // ๐Ÿท๏ธ
103
+ spinnerClassName, // โณ
104
+ hidden = false, // ๐Ÿ™ˆ
105
+ style, // ๐ŸŽจ
106
+ iconStyle, // ๐ŸŽจ
107
+ rounded = true, // ๐Ÿ’Š
108
+ raised = false, // ๐Ÿ”๏ธ
109
+ shrinks = false, // ๐Ÿค
110
+ expands = false, // ๐Ÿ”
111
+ withIcon = false, // ๐Ÿ”ฃ
112
+ disabled = false, // ๐Ÿšซ
113
+ isLoading = false, // โณ
114
+ iconPosition = 'left', // โ†”๏ธ
115
+ label, // ๐Ÿท๏ธ
116
+ icon, // ๐Ÿ”ฃ
117
+ color = 'primary', // ๐ŸŽจ
118
+ href, // ๐Ÿ”—
119
+ children, // ๐Ÿงฉ
120
+ onClick, // ๐Ÿ–ฑ๏ธ
121
+ startIcon, // ๐Ÿ–ผ๏ธ
122
+ endIcon, // ๐Ÿ–ผ๏ธ
123
+ badge, // ๐Ÿท๏ธ
124
+ badgeTone = 'neutral', // ๐ŸŽจ
125
+ disabledReason, // ๐Ÿ’ฌ
126
+ focusableWhenDisabled, // โŒจ๏ธ
91
127
  } = props;
92
128
 
129
+ // ๐Ÿšฆ how are we disabled? (shared with the server twin)
130
+ const mode = getAbButtonDisabledMode({ disabled, isLoading, badge, disabledReason, focusableWhenDisabled });
131
+ // ๐Ÿ’ฌ hidden reason appended to the name
132
+ const srReason = getAbButtonSrReason({ disabled, isLoading, disabledReason });
133
+ // ๐Ÿšซ any kind of blocked?
134
+ const blocked = mode !== 'none';
135
+
136
+ // ๐Ÿท๏ธ classes: pointer-events:none only for native mode (aria mode must stay focusable/hoverable)
93
137
  const classNames = clsx(
94
- 'AbButton',
95
- styles.abButton,
96
- className,
97
- rounded && styles.abButtonRounded,
98
- type === 'contained' && styles.isContained,
99
- (disabled || isLoading) && styles.isDisabled,
138
+ 'AbButton', // ๐Ÿท๏ธ public hook
139
+ styles.abButton, // ๐ŸŽจ
140
+ className, // ๐Ÿท๏ธ
141
+ rounded && styles.abButtonRounded, // ๐Ÿ’Š
142
+ type === 'contained' && styles.isContained, // ๐ŸŽจ
143
+ mode === 'native' && styles.isDisabled, // ๐Ÿšซ
100
144
  );
101
145
 
146
+ // ๐Ÿ—‚๏ธ data attributes for styling/tests
102
147
  const dataAttrs = {
103
- 'data-type': type,
104
- 'data-raised': raised ? 'true' : 'false',
105
- 'data-shrinks': shrinks ? 'true' : 'false',
106
- 'data-expands': expands ? 'true' : 'false',
107
- 'data-rounded': rounded ? 'true' : 'false',
108
- 'data-iconpos': iconPosition,
109
- 'data-color': color,
148
+ 'data-type': type, // ๐ŸŽจ
149
+ 'data-raised': raised ? 'true' : 'false', // ๐Ÿ”๏ธ
150
+ 'data-shrinks': shrinks ? 'true' : 'false', // ๐Ÿค
151
+ 'data-expands': expands ? 'true' : 'false', // ๐Ÿ”
152
+ 'data-rounded': rounded ? 'true' : 'false', // ๐Ÿ’Š
153
+ 'data-iconpos': iconPosition, // โ†”๏ธ
154
+ 'data-color': color, // ๐ŸŽจ
155
+ 'data-disabled-mode': mode, // ๐Ÿšฆ
110
156
  } as const;
111
157
 
158
+ // ๐Ÿ”ฃ legacy Material icon (decorative)
159
+ const legacyIcon = withIcon && icon
160
+ ? <span className={clsx('material-symbols-rounded', 'icon', styles.abButtonIcon)} style={iconStyle} aria-hidden="true">{icon}</span>
161
+ : null;
162
+
163
+ // ๐Ÿงฑ inner content
112
164
  const content = (
113
165
  <>
114
- {withIcon && iconPosition === 'left' && icon ? (
115
- <span className={clsx('material-symbols-rounded', 'icon', styles.abButtonIcon)} style={iconStyle}>{icon}</span>
116
- ) : null}
117
- <span className={clsx(styles.abButtonSpinnerSlot, !isLoading && styles.hidden)}>
166
+ {iconPosition === 'left' ? legacyIcon : null /* ๐Ÿ”ฃ legacy left */}
167
+ {startIcon ? <span className={styles.abButtonSlot} aria-hidden="true">{startIcon}</span> : null /* ๐Ÿ–ผ๏ธ decorative */}
168
+ <span className={clsx(styles.abButtonSpinnerSlot, !isLoading && styles.hidden)} aria-hidden="true">
118
169
  <span className={clsx('spinner', 'dot-3', styles.abButtonSpinner, spinnerClassName)} />
119
170
  </span>
120
171
  <span className={clsx(isLoading && styles.invisible)}>{children ?? label}</span>
121
- {withIcon && iconPosition === 'right' && icon ? (
122
- <span className={clsx('material-symbols-rounded', 'icon', styles.abButtonIcon)} style={iconStyle}>{icon}</span>
172
+ {badge ? (
173
+ <>
174
+ <span className={styles.srOnly}>, </span>{/* ๐Ÿ—ฃ๏ธ pause: "Continue with GitHub, Coming soon" */}
175
+ <span className={styles.abButtonBadge} data-tone={badgeTone} data-ab-part="badge" aria-hidden={srReason ? true : undefined}>{badge}</span>
176
+ </>
123
177
  ) : null}
178
+ {srReason ? <span className={styles.srOnly}>, {srReason}</span> : null /* ๐Ÿ’ฌ hidden reason */}
179
+ {endIcon ? <span className={styles.abButtonSlot} aria-hidden="true">{endIcon}</span> : null /* ๐Ÿ–ผ๏ธ */}
180
+ {iconPosition === 'right' ? legacyIcon : null /* ๐Ÿ”ฃ legacy right */}
124
181
  </>
125
182
  );
126
183
 
184
+ // ๐Ÿ›‘ aria mode: swallow activation (mouse, Enter/Space, form submit)
185
+ const guardClick = (event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
186
+ if (blocked) { event.preventDefault(); return; } // ๐Ÿšซ no submit / navigation / handler
187
+ onClick?.(event); // โœ…
188
+ };
189
+
190
+ // ๐Ÿ”— link mode
127
191
  if (href) {
192
+ // ๐Ÿšซ a blocked link has no target: render a non-navigating span with role=link
193
+ if (blocked) {
194
+ return (
195
+ <span
196
+ ref={ref as Ref<HTMLAnchorElement> as unknown as Ref<HTMLSpanElement>} // ๐Ÿ”—
197
+ id={id} // ๐Ÿ†”
198
+ role="link" // โ™ฟ still announced as a link
199
+ aria-disabled="true" // ๐Ÿšฆ
200
+ tabIndex={mode === 'aria' ? 0 : -1} // โŒจ๏ธ focusable only in aria mode
201
+ className={classNames} // ๐Ÿท๏ธ
202
+ style={style} // ๐ŸŽจ
203
+ hidden={hidden} // ๐Ÿ™ˆ
204
+ onKeyDown={(e: KeyboardEvent<HTMLSpanElement>) => { if (e.key === 'Enter') e.preventDefault(); }} // ๐Ÿšซ
205
+ {...dataAttrs}
206
+ >
207
+ {content}
208
+ </span>
209
+ );
210
+ }
211
+ // โœ… live link (no role=button: it navigates, so it is a link)
128
212
  return (
129
- <a
130
- ref={ref as React.Ref<HTMLAnchorElement>}
131
- id={id}
132
- href={disabled ? undefined : href}
133
- role="button"
134
- className={classNames}
135
- style={style}
136
- hidden={hidden}
137
- aria-disabled={disabled || isLoading}
138
- onClick={onClick}
139
- {...dataAttrs}
140
- >
213
+ <a ref={ref as Ref<HTMLAnchorElement>} id={id} href={href} className={classNames} style={style} hidden={hidden} onClick={guardClick} {...dataAttrs}>
141
214
  {content}
142
215
  </a>
143
216
  );
144
217
  }
145
218
 
219
+ // ๐Ÿ”˜ button mode
146
220
  return (
147
221
  <button
148
- ref={ref as React.Ref<HTMLButtonElement>}
149
- id={id}
150
- type={htmlType}
151
- className={classNames}
152
- style={style}
153
- hidden={hidden}
154
- disabled={disabled || isLoading}
155
- onClick={onClick}
222
+ ref={ref as Ref<HTMLButtonElement>} // ๐Ÿ”—
223
+ id={id} // ๐Ÿ†”
224
+ type={mode === 'aria' ? 'button' : htmlType} // ๐Ÿงพ aria-disabled must never submit
225
+ className={classNames} // ๐Ÿท๏ธ
226
+ style={style} // ๐ŸŽจ
227
+ hidden={hidden} // ๐Ÿ™ˆ
228
+ disabled={mode === 'native'} // ๐Ÿšซ native mode
229
+ aria-disabled={mode === 'aria' ? true : undefined} // ๐Ÿšฆ aria mode
230
+ aria-busy={isLoading || undefined} // โณ
231
+ onClick={guardClick} // ๐Ÿ›‘ guarded
156
232
  {...dataAttrs}
157
233
  >
158
234
  {content}
@@ -160,5 +236,6 @@ const AbButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, AbButtonProps
160
236
  );
161
237
  });
162
238
 
239
+ // ๐Ÿ“ฆ exports
163
240
  export default AbButton;
164
241
  export { AbButton };
@@ -2,7 +2,32 @@
2
2
  * @license MIT
3
3
  * ~~~~~~~~~~~~
4
4
  * ab-nextjs-components
5
- * Plain CSS modules โ€” no Tailwind Tailwind apply (Turbopack-safe).
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the 'Software'), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
26
+ * @project: ab-nextjs-components
27
+ * @name: styles.module.css
28
+ * @file: ab-button/styles.module.css
29
+ * @type: CSS
30
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
6
31
  */
7
32
 
8
33
  /*
@@ -11,6 +36,7 @@
11
36
  * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12
37
  */
13
38
 
39
+
14
40
  .abButton {
15
41
  position: relative;
16
42
  display: inline-flex;
@@ -61,3 +87,55 @@
61
87
 
62
88
  .hidden { display: none !important; }
63
89
  .invisible { visibility: hidden; }
90
+
91
+ /* ===== G22: start/end icons, badge, aria-disabled ===== */
92
+
93
+ /* ๐Ÿ–ผ๏ธ icon slots (any ReactNode, e.g. <AbIcon name="github" />) */
94
+ .abButtonSlot {
95
+ display: inline-flex; /* ๐Ÿ“ align with label */
96
+ flex: none; /* ๐Ÿšซ never squash */
97
+ width: 1.25em; /* ๐Ÿ“ icon box */
98
+ height: 1.25em; /* ๐Ÿ“ */
99
+ align-items: center; /* โ†•๏ธ */
100
+ justify-content: center; /* โ†”๏ธ */
101
+ }
102
+
103
+ /* ๐Ÿท๏ธ "Coming soon" style badge inside the button */
104
+ .abButtonBadge {
105
+ display: inline-flex; /* ๐Ÿ“ */
106
+ align-items: center; /* โ†•๏ธ */
107
+ padding: 0.125rem 0.5rem; /* ๐Ÿงฑ */
108
+ border-radius: 9999px; /* ๐Ÿ’Š */
109
+ font-size: 0.75rem; /* ๐Ÿ”ก small */
110
+ font-weight: 600; /* ๐Ÿ’ช */
111
+ line-height: 1.2; /* ๐Ÿ“ */
112
+ white-space: nowrap; /* ๐Ÿšซ no wrap */
113
+ /* โšช neutral tone (default) */
114
+ color: var(--ab-color-on-surface-variant, currentColor);
115
+ background: var(--ab-color-surface-variant, rgb(0 0 0 / 0.06));
116
+ }
117
+
118
+ /* ๐Ÿฅ‰ accent tone: small text โ†’ the AA bronze text token (theme 0.3.1) */
119
+ .abButtonBadge[data-tone="accent"] {
120
+ color: var(--ab-color-bronze-text, #8A6440); /* โ™ฟ 5.02:1 on #fff8f3 */
121
+ background: color-mix(in srgb, var(--ab-color-bronze, #A67C52) 14%, transparent); /* ๐ŸŽจ soft wash */
122
+ }
123
+
124
+ /* ๐Ÿšฆ aria-disabled: looks disabled, stays focusable (no pointer-events:none so tooltips/focus still work) */
125
+ .abButton[aria-disabled="true"] {
126
+ cursor: not-allowed; /* ๐Ÿšซ */
127
+ opacity: 0.6; /* ๐ŸŒซ๏ธ */
128
+ }
129
+
130
+ /* ๐Ÿ™ˆ visually hidden, still announced */
131
+ .srOnly {
132
+ position: absolute; /* ๐Ÿ“Œ */
133
+ width: 1px; /* ๐Ÿ“ */
134
+ height: 1px; /* ๐Ÿ“ */
135
+ padding: 0; /* ๐Ÿงน */
136
+ margin: -1px; /* ๐Ÿงน */
137
+ overflow: hidden; /* โœ‚๏ธ */
138
+ clip: rect(0 0 0 0); /* โœ‚๏ธ */
139
+ white-space: nowrap; /* ๐Ÿšซ */
140
+ border: 0; /* ๐Ÿงน */
141
+ }