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.
- package/CHANGELOG.md +42 -0
- package/ab-brand/index.tsx +30 -1
- package/ab-brand/styles.module.css +40 -1
- package/ab-button/helpers.ts +90 -0
- package/ab-button/index.tsx +164 -87
- package/ab-button/styles.module.css +79 -1
- package/ab-carousel/helpers.ts +146 -0
- package/ab-carousel/index.tsx +643 -0
- package/ab-carousel/styles.module.css +206 -0
- package/ab-chip/index.tsx +75 -0
- package/ab-chip/styles.module.css +87 -0
- package/ab-collapsible/index.tsx +30 -1
- package/ab-collapsible/styles.module.css +40 -1
- package/ab-flow-diagram/helpers.ts +77 -0
- package/ab-flow-diagram/index.tsx +141 -0
- package/ab-flow-diagram/styles.module.css +141 -0
- package/ab-icon/builtin.ts +61 -0
- package/ab-icon/helpers.ts +102 -0
- package/ab-icon/index.tsx +77 -16
- package/ab-icon/styles.module.css +53 -9
- package/ab-icon-button/index.tsx +110 -46
- package/ab-icon-button/styles.module.css +28 -10
- package/ab-install-snippet/helpers.ts +62 -0
- package/ab-install-snippet/index.tsx +170 -0
- package/ab-install-snippet/styles.module.css +104 -0
- package/ab-like-button/index.tsx +33 -1
- package/ab-like-button/styles.module.css +41 -6
- package/ab-linear-progress/index.tsx +129 -0
- package/ab-linear-progress/styles.module.css +105 -0
- package/ab-locale-switcher/index.tsx +213 -0
- package/ab-locale-switcher/styles.module.css +127 -0
- package/ab-logo/index.tsx +88 -30
- package/ab-logo/styles.module.css +79 -11
- package/ab-pill/index.tsx +74 -0
- package/ab-progress-chips/index.tsx +30 -1
- package/ab-progress-chips/styles.module.css +40 -1
- package/ab-searchbar/index.tsx +33 -2
- package/ab-searchbar/styles.module.css +41 -5
- package/ab-splash/csp.d.ts +54 -0
- package/ab-splash/csp.mjs +82 -0
- package/ab-splash/csp.ts +60 -0
- package/ab-splash/gate.ts +216 -0
- package/ab-splash/index.tsx +140 -0
- package/ab-splash/styles.module.css +103 -0
- package/ab-theme-toggle/index.tsx +188 -0
- package/ab-theme-toggle/styles.module.css +89 -0
- package/css/dialog.css +38 -3
- package/index.ts +47 -0
- package/package.json +46 -18
- package/server/ab-brand/index.tsx +29 -1
- package/server/ab-brand/styles.module.css +40 -1
- package/server/ab-button/index.tsx +96 -79
- package/server/ab-button/styles.module.css +79 -1
- package/server/ab-icon/index.tsx +68 -16
- package/server/ab-icon/styles.module.css +53 -9
- package/server/ab-logo/index.tsx +88 -29
- package/server/ab-logo/styles.module.css +79 -11
- package/types/css.d.ts +43 -0
- 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`)
|
package/ab-brand/index.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/ab-button/index.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
40
|
-
export type
|
|
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
|
-
|
|
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
|
-
{
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
{
|
|
122
|
-
|
|
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
|
|
149
|
-
id={id}
|
|
150
|
-
type={htmlType}
|
|
151
|
-
className={classNames}
|
|
152
|
-
style={style}
|
|
153
|
-
hidden={hidden}
|
|
154
|
-
disabled={
|
|
155
|
-
|
|
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
|
-
*
|
|
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
|
+
}
|