ab-nextjs-components 0.1.10 → 0.1.12

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 CHANGED
@@ -1,3 +1,17 @@
1
+ ## 0.1.12 — 2026-10-16
2
+
3
+ * ♿ **AbInstallSnippet tabs pass AA in every state.** Each state now paints an explicit text/background pair from the live palette instead of a transparent tab and a 12% bronze tint: inactive `on-surface-variant` on `surface-container` (8.04:1 light / 9.67:1 dark), hover `on-surface` on an 8% `on-surface` state layer (12.63:1 / 10.39:1), active and active + hover `on-primary-container` on `primary-container` (13.28:1 / 7.22:1). Fallbacks are the light bronze palette, so it also passes without the theme
4
+ * 🥉 **New `AbButton type="filled"`** (client + server): solid bronze `--md-sys-color-primary` with `on-primary` label and icon. Sets `data-type="filled"`, styled with hover / focus / disabled by `ab-nextjs-theme@^0.3.3`; a `.isFilled` fallback paints the rest state on older themes. `AbButtonType` gains `'filled'`
5
+ * ⬆️ **`ab-nextjs-theme ^0.3.3`** (peer + dev, was `^0.3.1`) for the filled hover / focus / disabled states and the button icon-colour fix. The `.isFilled` fallback stays for apps still on an older theme
6
+ * Tests: contrast computed from the theme palette (light + dark, every state, plus the no-theme fallbacks); filled type on client + server twins. Checked in WebKit (ratios above; filled 6.47:1 light / 7.74:1 dark)
7
+
8
+ ## 0.1.11 — 2026-10-15
9
+
10
+ * 🐛 **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)
11
+ * 📏 **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)
12
+ * 🪐 **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
13
+ * 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)
14
+
1
15
  ## 0.1.10 — 2026-10-14
2
16
 
3
17
  * **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:
package/README.md CHANGED
@@ -66,6 +66,14 @@ Shipped assets: `assets/logos/ab-logo.svg`, `assets/spinners/3-dots-scale.svg`,
66
66
  * `<AbLogo>` defaults to the Ab logo **shipped in this package** (data URI / `ab-logo/ab-logo.svg`). Pass `src` to override.
67
67
 
68
68
 
69
+ ### Buttons & install snippet (0.1.12)
70
+
71
+ * `<AbButton type="filled">` 🥉 is a solid bronze button (`--md-sys-color-primary` background, `--md-sys-color-on-primary` label and icon). It sets `data-type="filled"`, which **`ab-nextjs-theme@^0.3.3`** styles with hover, focus and disabled states. On older themes a built-in fallback still paints the solid bronze rest state.
72
+ ```tsx
73
+ <AbButton type="filled" label="Get started" />
74
+ ```
75
+ * `<AbInstallSnippet>` tabs are **≥ 4.5:1** (WCAG AA) in light and dark, active, inactive and hover: inactive = on-surface-variant on surface-container, hover = on-surface on an 8% state layer, active = on-primary-container on primary-container.
76
+
69
77
  ## Getting Started
70
78
 
71
79
  ### Installation
@@ -37,7 +37,9 @@
37
37
  */
38
38
 
39
39
  // 🎨 visual types shared by the client + server twins
40
- export type AbButtonType = 'contained' | 'outlined' | 'text';
40
+ // 🥉 'filled' (0.1.12): solid bronze, painted by ab-nextjs-theme ≥ 0.3.3's
41
+ // `button[data-type="filled"]` rule (fallback in styles.module.css → .isFilled)
42
+ export type AbButtonType = 'contained' | 'filled' | 'outlined' | 'text';
41
43
  // 🎨 colour roles
42
44
  export type AbButtonColor = 'primary' | 'secondary' | 'tertiary' | 'white' | 'black';
43
45
  // 🏷️ badge tones (G22): neutral grey or bronze accent
@@ -63,7 +63,7 @@ export type { AbButtonBadgeTone, AbButtonColor, AbButtonType, AbButtonDisabledMo
63
63
  // ⚙️ props
64
64
  export interface AbButtonProps {
65
65
  id?: string; // 🆔
66
- type?: AbButtonType; // 🎨 contained / outlined / text
66
+ type?: AbButtonType; // 🎨 contained / filled 🥉 / outlined / text
67
67
  htmlType?: 'button' | 'submit' | 'reset'; // 🧾
68
68
  className?: string; // 🏷️
69
69
  spinnerClassName?: string; // ⏳ spinner class
@@ -140,6 +140,7 @@ const AbButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, AbButtonProps
140
140
  className, // 🏷️
141
141
  rounded && styles.abButtonRounded, // 💊
142
142
  type === 'contained' && styles.isContained, // 🎨
143
+ type === 'filled' && styles.isFilled, // 🥉 solid bronze (theme ≥ 0.3.3 paints it, this is the fallback)
143
144
  mode === 'native' && styles.isDisabled, // 🚫
144
145
  );
145
146
 
@@ -62,6 +62,17 @@
62
62
 
63
63
  .isContained {}
64
64
 
65
+ /* 🥉 Filled (0.1.12): solid bronze fallback for apps on ab-nextjs-theme < 0.3.3.
66
+ * Theme ≥ 0.3.3 ships `button[data-type="filled"]` (higher specificity), which
67
+ * adds the hover / focus / disabled state layers on top of this 🎨 */
68
+ .isFilled {
69
+ --material-icon-color: var(--ab-button-text-color, var(--md-sys-color-on-primary, #fff)); /* 🎨 icon = label */
70
+
71
+ background: var(--ab-button-background-color, var(--md-sys-color-primary, rgb(133 83 24)));
72
+ color: var(--ab-button-text-color, var(--md-sys-color-on-primary, #fff));
73
+ font-weight: bold;
74
+ }
75
+
65
76
  .isDisabled {
66
77
  pointer-events: none;
67
78
  opacity: 0.6;
@@ -73,6 +73,13 @@ export interface AbFlowDiagramProps {
73
73
  children?: ReactNode; // 🧩 optional extra overlay (decorative)
74
74
  }
75
75
 
76
+ // ✂️ 0–100 (same clamp as the wires, so a node and its wire end always agree)
77
+ const pct = (v: number): number => Math.min(100, Math.max(0, v));
78
+
79
+ /** 📍 node position + proportional anchor (CSS keeps every node inside the stage, see styles.module.css) */
80
+ const nodeStyle = (n: AbFlowNode): CSSProperties =>
81
+ ({ left: `${pct(n.x)}%`, top: `${pct(n.y)}%`, '--ab-flow-node-x': String(pct(n.x)), '--ab-flow-node-y': String(pct(n.y)) }) as CSSProperties;
82
+
76
83
  /** 🎨 Live Playground scene: SVG wires + HTML nodes, CSS-only motion, one accessible image. */
77
84
  export function AbFlowDiagram(props: AbFlowDiagramProps): ReactElement {
78
85
  // 🧩 props
@@ -88,6 +95,9 @@ export function AbFlowDiagram(props: AbFlowDiagramProps): ReactElement {
88
95
  '--ab-wire-poster': String(100 - Math.min(1, Math.max(0, posterAt)) * 100), // 🖼️ dashoffset at the poster frame
89
96
  } as CSSProperties;
90
97
 
98
+ // 🏷️ is there a header row? (drives the header-safe stage inset in CSS)
99
+ const hasHeader = Boolean(title || live || caption);
100
+
91
101
  return (
92
102
  <figure
93
103
  role="img" // ♿ the whole scene is ONE image
@@ -98,9 +108,10 @@ export function AbFlowDiagram(props: AbFlowDiagramProps): ReactElement {
98
108
  data-active={String(active)} // ▶️ / ⏸️
99
109
  data-ab-paused={String(!active)} // ⏸️ ab-nextjs-animations pauses wires on inactive slides
100
110
  data-variant={variant} // 📐
111
+ data-ab-header={String(hasHeader)} // 🏷️ header-safe stage: nodes + wires start below the header
101
112
  >
102
113
  {/* 🏷️ card header (decorative: alt already describes it) */}
103
- {title || live || caption ? (
114
+ {hasHeader ? (
104
115
  <div className={styles.header} aria-hidden="true">
105
116
  {title ? <span className={styles.title}>{title}</span> : null}
106
117
  {live ? <span className={styles.live}><span className={styles.liveDot} />{typeof live === 'string' ? live : null}</span> : null}
@@ -126,7 +137,7 @@ export function AbFlowDiagram(props: AbFlowDiagramProps): ReactElement {
126
137
  {/* 🔵 nodes layer (HTML: crisp text, real fonts) */}
127
138
  <div className={styles.nodes} aria-hidden="true">
128
139
  {nodes.map((n) => (
129
- <span key={n.id} className={styles.node} data-tone={n.tone ?? 'node'} style={{ left: `${n.x}%`, top: `${n.y}%` }}>
140
+ <span key={n.id} className={styles.node} data-tone={n.tone ?? 'node'} style={nodeStyle(n)}>
130
141
  {n.icon ? <span className={styles.nodeIcon}>{n.icon}</span> : null}
131
142
  <span>{n.label}</span>
132
143
  </span>
@@ -47,6 +47,15 @@
47
47
  background: var(--ab-color-surface-container, transparent); /* 🎨 */
48
48
  overflow: hidden; /* ✂️ */
49
49
  color: var(--ab-color-on-surface, currentColor); /* 🎨 */
50
+ container-type: inline-size; /* 📦 nodes scale with the card width (cqi units below) */
51
+ /* 🎛️ stage insets (tunable): where nodes + wires may sit. Top is 0 unless a header renders (below) */
52
+ --ab-flow-diagram-stage-top: 0px;
53
+ --ab-flow-diagram-stage-x: var(--ab-flow-diagram-inset-x, 0.5rem); /* ↔️ side margin: nodes never touch the card edge */
54
+ }
55
+ /* 🏷️ header-safe stage: with a title / live dot / caption, nodes + wires start BELOW the header band, so a
56
+ * node near the top (e.g. "core" at 390px) can't cover the header. Override with --ab-flow-diagram-inset-top */
57
+ .root[data-ab-header='true'] {
58
+ --ab-flow-diagram-stage-top: var(--ab-flow-diagram-inset-top, 2.75rem); /* 📏 0.875rem header top + ~1.25rem row + gap */
50
59
  }
51
60
  .root[data-variant='compact'] {
52
61
  aspect-ratio: auto; /* 📐 */
@@ -78,9 +87,9 @@
78
87
  /* 〰️ wires layer */
79
88
  .wires {
80
89
  position: absolute; /* 📌 */
81
- inset: 0; /* 📐 full card */
82
- width: 100%; /* ↔️ */
83
- height: 100%; /* ↕️ */
90
+ inset: var(--ab-flow-diagram-stage-top) var(--ab-flow-diagram-stage-x) 0; /* 📐 the stage (below the header, inside the side margins) */
91
+ width: calc(100% - 2 * var(--ab-flow-diagram-stage-x)); /* ↔️ same box as .nodes */
92
+ height: calc(100% - var(--ab-flow-diagram-stage-top)); /* ↕️ same box as .nodes, so wires meet the nodes */
84
93
  overflow: visible; /* 🚫 clip */
85
94
  }
86
95
  .wire,
@@ -101,25 +110,33 @@
101
110
  }
102
111
 
103
112
  /* 🔵 nodes layer */
104
- .nodes { position: absolute; inset: 0; } /* 📐 */
113
+ .nodes { position: absolute; inset: var(--ab-flow-diagram-stage-top) var(--ab-flow-diagram-stage-x) 0; } /* 📐 same stage as .wires */
105
114
  .node {
106
115
  position: absolute; /* 📌 x/y */
107
- transform: translate(-50%, -50%); /* 🎯 centre on the point */
116
+ /* 🎯 orbit that scales with the box: each node is anchored at the SAME fraction of its own box as its
117
+ * position in the stage (x = 8 → 8% of the pill sits left of the point, x = 92 → 92%). So the outer edge
118
+ * of the orbit pulls in by exactly the node's width: a side node can never cross the card edge, at any
119
+ * box size, and the wire end (the point) is always inside the pill. x = y = 50 is the old centring. */
120
+ transform: translate(calc(var(--ab-flow-node-x, 50) * -1%), calc(var(--ab-flow-node-y, 50) * -1%));
121
+ max-width: 100%; /* 📏 an over-long label can't be wider than the stage… */
122
+ overflow: hidden; /* ✂️ …it ellipsises instead */
123
+ text-overflow: ellipsis; /* … */
108
124
  display: inline-flex; /* 📐 */
109
125
  align-items: center; /* ↕️ */
110
126
  gap: 0.375rem; /* 🧱 */
111
- padding: 0.375rem 0.625rem; /* 🧱 */
112
- border-radius: 0.625rem; /* 🔲 */
127
+ /* 📏 scales with the card (cqi = 1% of its width): 760px card → 13px text, 390px → 11px; clamped both ways */
128
+ padding: clamp(0.25rem, 0.8cqi, 0.375rem) clamp(0.4375rem, 1.4cqi, 0.625rem); /* 🧱 */
129
+ border-radius: clamp(0.5rem, 1.4cqi, 0.625rem); /* 🔲 */
113
130
  border: 1px solid var(--ab-color-outline-variant, rgb(0 0 0 / 0.12)); /* 🖊️ */
114
131
  background: var(--ab-color-surface, #fff); /* 🎨 */
115
- font-size: 0.8125rem; /* 🔡 */
132
+ font-size: clamp(0.6875rem, 1.75cqi, 0.8125rem); /* 🔡 */
116
133
  white-space: nowrap; /* 🚫 wrap */
117
134
  }
118
135
  .node[data-tone='hub'] {
119
- padding: 0.625rem 0.875rem; /* 🧱 bigger */
136
+ padding: clamp(0.4375rem, 1.4cqi, 0.625rem) clamp(0.625rem, 1.9cqi, 0.875rem); /* 🧱 bigger (scales too) */
120
137
  border-color: var(--ab-color-bronze, #A67C52); /* 🥉 */
121
138
  font-weight: 700; /* 💪 */
122
- box-shadow: 0 0 0 6px color-mix(in srgb, var(--ab-color-bronze, #A67C52) 12%, transparent); /* ✨ glow */
139
+ box-shadow: 0 0 0 clamp(3px, 0.8cqi, 6px) color-mix(in srgb, var(--ab-color-bronze, #A67C52) 12%, transparent); /* ✨ glow */
123
140
  }
124
141
  .node[data-tone='accent'] { color: var(--ab-color-bronze-text, #8A6440); } /* ♿ */
125
142
  .nodeIcon { display: inline-flex; width: 1.125rem; height: 1.125rem; } /* 🖼️ */
@@ -53,21 +53,31 @@
53
53
  gap: 0.25rem; /* 🧱 */
54
54
  }
55
55
 
56
- /* 🔘 tab */
56
+ /* 🔘 tab
57
+ * ♿ 0.1.12: every state is an explicit text/background pair from the live
58
+ * Material palette, so it is ≥ 4.5:1 in light AND dark (tested in
59
+ * __tests__/ab-install-snippet-contrast.test.ts). Fallbacks are the light
60
+ * bronze palette, for apps that don't load the theme 🎨 */
57
61
  .tab {
58
62
  min-height: 28px; /* 📏 ≥ 24px target */
59
63
  padding: 0.25rem 0.625rem; /* 🧱 */
60
64
  border: 0; /* 🧹 */
61
65
  border-radius: 9999px; /* 💊 */
62
- background: transparent; /* 🫥 */
63
- color: var(--ab-color-on-surface-variant, currentColor); /* 🎨 */
66
+ background: var(--ab-color-surface-container, var(--md-sys-color-surface-container, rgb(252 235 226))); /* 🫥 inactive = card surface */
67
+ color: var(--ab-color-on-surface-variant, var(--md-sys-color-on-surface-variant, rgb(80 69 58))); /* 🎨 inactive label */
64
68
  font: inherit; /* 🔤 */
65
69
  font-size: 0.8125rem; /* 🔡 */
66
70
  cursor: pointer; /* 👆 */
67
71
  }
68
- .tab[aria-selected='true'] {
69
- color: var(--ab-color-bronze-text, #8A6440); /* ♿ AA small text */
70
- background: color-mix(in srgb, var(--ab-color-bronze, #A67C52) 12%, transparent); /* 🎨 */
72
+ /* 🖱️ hover (inactive): 8% on-surface state layer + full on-surface label */
73
+ .tab:hover {
74
+ background: color-mix(in srgb, var(--ab-color-on-surface, var(--md-sys-color-on-surface, rgb(34 26 21))) 8%, var(--ab-color-surface-container, var(--md-sys-color-surface-container, rgb(252 235 226))));
75
+ color: var(--ab-color-on-surface, var(--md-sys-color-on-surface, rgb(34 26 21)));
76
+ }
77
+ /* ✅ active (and active + hover): solid bronze container + its on-colour */
78
+ .tab[aria-selected='true'], .tab[aria-selected='true']:hover {
79
+ background: var(--md-sys-color-primary-container, rgb(255 220 189)); /* 🥉 */
80
+ color: var(--md-sys-color-on-primary-container, rgb(44 22 0)); /* ♿ */
71
81
  font-weight: 600; /* 💪 */
72
82
  }
73
83
  .tab:focus-visible {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ab-nextjs-components",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "🚀 A lightweight collection of server & client React components created by Abraham Ukachi, and optimized for Next.js applications 😎.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -43,7 +43,7 @@
43
43
  "ab-nextjs-animations": "^0.2.4",
44
44
  "ab-nextjs-hooks": "^0.1.6",
45
45
  "ab-nextjs-icons": "^0.2.3",
46
- "ab-nextjs-theme": "^0.3.1",
46
+ "ab-nextjs-theme": "^0.3.3",
47
47
  "clsx": "^2",
48
48
  "next": "^16.3.4",
49
49
  "react": "^19",
@@ -56,7 +56,7 @@
56
56
  "ab-nextjs-animations": "^0.2.4",
57
57
  "ab-nextjs-hooks": "^0.1.6",
58
58
  "ab-nextjs-icons": "^0.2.3",
59
- "ab-nextjs-theme": "^0.3.1",
59
+ "ab-nextjs-theme": "^0.3.3",
60
60
  "clsx": "^2.1.1",
61
61
  "esbuild": "^0.28.2",
62
62
  "eslint": "^9.39.0",
@@ -94,7 +94,7 @@ const AbButton = ({
94
94
  const srReason = getAbButtonSrReason({ disabled, disabledReason });
95
95
 
96
96
  // 🏷️ classes
97
- const classNames = clsx('AbButton', styles.abButton, className, rounded && styles.abButtonRounded, mode === 'native' && styles.isDisabled);
97
+ const classNames = clsx('AbButton', styles.abButton, className, rounded && styles.abButtonRounded, type === 'filled' && styles.isFilled /* 🥉 */, mode === 'native' && styles.isDisabled);
98
98
 
99
99
  // 🗂️ data attributes
100
100
  const dataAttrs = {
@@ -62,6 +62,17 @@
62
62
 
63
63
  .isContained {}
64
64
 
65
+ /* 🥉 Filled (0.1.12): solid bronze fallback for apps on ab-nextjs-theme < 0.3.3.
66
+ * Theme ≥ 0.3.3 ships `button[data-type="filled"]` (higher specificity), which
67
+ * adds the hover / focus / disabled state layers on top of this 🎨 */
68
+ .isFilled {
69
+ --material-icon-color: var(--ab-button-text-color, var(--md-sys-color-on-primary, #fff)); /* 🎨 icon = label */
70
+
71
+ background: var(--ab-button-background-color, var(--md-sys-color-primary, rgb(133 83 24)));
72
+ color: var(--ab-button-text-color, var(--md-sys-color-on-primary, #fff));
73
+ font-weight: bold;
74
+ }
75
+
65
76
  .isDisabled {
66
77
  pointer-events: none;
67
78
  opacity: 0.6;