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.
- package/CHANGELOG.md +36 -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 +152 -0
- package/ab-flow-diagram/styles.module.css +158 -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 +5 -1
- package/ab-linear-progress/styles.module.css +8 -24
- package/ab-locale-switcher/index.tsx +213 -0
- package/ab-locale-switcher/styles.module.css +127 -0
- 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/index.tsx +3 -1
- package/ab-splash/styles.module.css +2 -5
- 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 +41 -0
- package/package.json +33 -17
- 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/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`)
|
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
|
+
}
|