ab-nextjs-components 0.1.8 → 0.1.10

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 (59) hide show
  1. package/CHANGELOG.md +42 -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 +141 -0
  16. package/ab-flow-diagram/styles.module.css +141 -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 +129 -0
  29. package/ab-linear-progress/styles.module.css +105 -0
  30. package/ab-locale-switcher/index.tsx +213 -0
  31. package/ab-locale-switcher/styles.module.css +127 -0
  32. package/ab-logo/index.tsx +88 -30
  33. package/ab-logo/styles.module.css +79 -11
  34. package/ab-pill/index.tsx +74 -0
  35. package/ab-progress-chips/index.tsx +30 -1
  36. package/ab-progress-chips/styles.module.css +40 -1
  37. package/ab-searchbar/index.tsx +33 -2
  38. package/ab-searchbar/styles.module.css +41 -5
  39. package/ab-splash/csp.d.ts +54 -0
  40. package/ab-splash/csp.mjs +82 -0
  41. package/ab-splash/csp.ts +60 -0
  42. package/ab-splash/gate.ts +216 -0
  43. package/ab-splash/index.tsx +140 -0
  44. package/ab-splash/styles.module.css +103 -0
  45. package/ab-theme-toggle/index.tsx +188 -0
  46. package/ab-theme-toggle/styles.module.css +89 -0
  47. package/css/dialog.css +38 -3
  48. package/index.ts +47 -0
  49. package/package.json +46 -18
  50. package/server/ab-brand/index.tsx +29 -1
  51. package/server/ab-brand/styles.module.css +40 -1
  52. package/server/ab-button/index.tsx +96 -79
  53. package/server/ab-button/styles.module.css +79 -1
  54. package/server/ab-icon/index.tsx +68 -16
  55. package/server/ab-icon/styles.module.css +53 -9
  56. package/server/ab-logo/index.tsx +88 -29
  57. package/server/ab-logo/styles.module.css +79 -11
  58. package/types/css.d.ts +43 -0
  59. package/types/index.d.ts +2 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,45 @@
1
+ ## 0.1.10 — 2026-10-14
2
+
3
+ * **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:
4
+ * AbCarousel: `slide-step` (direction-aware enter / leave via `data-ab-direction`) + `motion` (buttons, dots)
5
+ * AbFlowDiagram: `wire` (`--ab-wire-*` tokens; `data-ab-paused`; 3 loops on phones unless `loops` is set)
6
+ * AbThemeToggle, AbSearchbar, AbSplash fade: `motion` colours
7
+ * AbIconButton: `motion` (`expands` → `ab-motion-lift`, `shrinks` → `ab-motion-press`)
8
+ * AbProgressChips: `motion` size
9
+ * AbLinearProgress: `motion` transform + `progress-sweep`
10
+ * AbCollapsible, AbBrand: `motion` transform
11
+ * AbLikeButton / AbSearchbar spinners: `spin`
12
+ * Reduced motion (OS, or `reducedMotion="always"` via `data-ab-reduced-motion`) is handled by the package
13
+ * **AbCarousel:** context gains `direction` + `previousIndex`; `--ab-carousel-duration` / `-ease` / `-shift` map onto `--ab-slide-step-*`; the old positional slide offsets are gone
14
+ * **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`
15
+ * **Motion exceptions (named, test-guarded):** the dialog list-item transition (`css/dialog.css`) and the self-animating spinner SVGs stay local
16
+ * **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
17
+
18
+ * **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
19
+ * **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`
20
+ * **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
21
+ * **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
22
+ * **AbChip / AbPill (G36, new, server components):** `ab-chip`, `ab-pill`
23
+ * **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
24
+ * **AbLocaleSwitcher (G7, new):** menu button + `menuitemradio`, typeahead, `onSelect` or `getHref` (+ `hrefLang`)
25
+ * **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)
26
+ * **AbCarousel:** prev / next arrows now render through AbIcon (weight 300); `--ab-carousel-icon-stroke` removed (unreleased)
27
+ * **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
28
+ * Dev: `happy-dom` + `@testing-library/react` for DOM tests
29
+
30
+ ## 0.1.9 — 2026-10-14
31
+
32
+ - **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`)
33
+ - **AbLogo fix:** `naked` with no `src` now masks with the shipped Ab mark (via `--app-logo-url` fallback) instead of a bronze square when the chrome CSS isn't loaded
34
+ - **AbSplash (new, `ab-nextjs-components/ab-splash`):** brand overlay + `abSplashGateScript` / `buildAbSplashGateScript({ locales, defaultLocale })`; root-path RegExp built from locales minus the default and JSON-injected; skips `navigator.webdriver` / bot UAs; standalone via `matchMedia` OR `navigator.standalone`; each storage get/set in its own try (failed get = seen); `data-ab-splash` set last; page behind is `inert` while shown; track `min(18rem, 28vw)`
35
+ - **CSP:** `ab-nextjs-components/ab-splash/csp` → `getAbSplashGateSha256(script)`, `buildAbSplashGate(options)`, `AB_SPLASH_GATE_SHA256`
36
+ - **AbLinearProgress (new here, `ab-nextjs-components/ab-linear-progress`):** ported from the reviewed core work — determinate / indeterminate, `color`, reduced motion, dark, RTL sweep, empty `aria-label` dropped; shipped CSS module. AbSplash uses it, so **no `ab-nextjs-core` peer is needed**
37
+ - **CSP (P1):** `ab-nextjs-components/ab-splash/csp` now ships compiled `csp.mjs` + `csp.d.ts` (Node-loadable from `next.config.*`); `build:csp` script, `check:publish` / `prepublishOnly` fail when it's stale
38
+ - **AbSplash:** unmount removes `data-ab-splash` + `inert`; scroll lock gets the same 1.2 s CSS failsafe; surfaces `#FFF8F3` / `#1A1310` (mocks)
39
+ - **Tooling:** `esbuild ^0.28.2` is now an explicit devDependency (direct in `pnpm-lock.yaml`; frozen install verified)
40
+ - **AbSplash:** StrictMode-safe teardown (`scheduleAbSplashFinish` / `cancelAbSplashFinish`); overflow failsafe verified in Chromium + WebKit
41
+ - **Tooling:** `typecheck` script, `types/css.d.ts`, vitest automatic JSX, `scripts/build-demo.mjs`
42
+
1
43
  ## 0.1.8 — 2026-10-05
2
44
 
3
45
  - **Chrome styles:** dialog, toast, logo (`span.app-logo`), spinner, and empty-state doodle CSS + SVG assets now ship here via `@import "ab-nextjs-components/styles.css"` (import after `ab-nextjs-theme/styles.css`)
@@ -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
+ }