ab-nextjs-components 0.1.10 β†’ 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 CHANGED
@@ -1,3 +1,10 @@
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
+
1
8
  ## 0.1.10 β€” 2026-10-14
2
9
 
3
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:
@@ -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; } /* πŸ–ΌοΈ */
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.11",
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",