sheleg-design-skill 1.59.2 → 1.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/bin/cli.js +89 -10
  3. package/kits/almanac/src/Button.tsx +17 -16
  4. package/kits/almanac/src/Heading.tsx +18 -5
  5. package/kits/atrium/src/Button.tsx +17 -17
  6. package/kits/atrium/src/Heading.tsx +18 -5
  7. package/kits/awning/src/Button.tsx +17 -16
  8. package/kits/awning/src/Heading.tsx +20 -5
  9. package/kits/babylove/src/Button.tsx +22 -7
  10. package/kits/babylove/src/Heading.tsx +25 -6
  11. package/kits/blueprint/src/Button.tsx +17 -16
  12. package/kits/blueprint/src/Heading.tsx +18 -5
  13. package/kits/briefing-room/src/Button.tsx +17 -16
  14. package/kits/briefing-room/src/Heading.tsx +20 -5
  15. package/kits/bulletin/src/Button.tsx +17 -16
  16. package/kits/bulletin/src/Heading.tsx +18 -5
  17. package/kits/chorus/src/Button.tsx +17 -16
  18. package/kits/chorus/src/Heading.tsx +21 -6
  19. package/kits/cyclorama/src/Button.tsx +17 -25
  20. package/kits/cyclorama/src/Heading.tsx +18 -13
  21. package/kits/datasheet/src/Button.tsx +17 -16
  22. package/kits/datasheet/src/Heading.tsx +18 -5
  23. package/kits/daylight/src/Button.tsx +17 -16
  24. package/kits/daylight/src/Heading.tsx +18 -5
  25. package/kits/deskmate/src/Button.tsx +17 -16
  26. package/kits/deskmate/src/Heading.tsx +18 -5
  27. package/kits/editorial-luxury/src/Button.tsx +17 -16
  28. package/kits/editorial-luxury/src/Heading.tsx +18 -5
  29. package/kits/field-notes/src/Button.tsx +17 -22
  30. package/kits/field-notes/src/Heading.tsx +18 -10
  31. package/kits/instrument-console/src/Button.tsx +17 -16
  32. package/kits/instrument-console/src/Heading.tsx +18 -5
  33. package/kits/ledger/src/Button.tsx +17 -16
  34. package/kits/ledger/src/Heading.tsx +18 -5
  35. package/kits/manpage/src/Button.tsx +17 -16
  36. package/kits/manpage/src/Heading.tsx +18 -5
  37. package/kits/maquette/src/Button.tsx +17 -16
  38. package/kits/maquette/src/Heading.tsx +18 -5
  39. package/kits/nameplate/src/Button.tsx +17 -16
  40. package/kits/nameplate/src/Heading.tsx +18 -5
  41. package/kits/notation/src/Button.tsx +17 -16
  42. package/kits/notation/src/Heading.tsx +18 -5
  43. package/kits/onionskin/src/Button.tsx +17 -16
  44. package/kits/onionskin/src/Heading.tsx +18 -5
  45. package/kits/ora/src/Button.tsx +17 -16
  46. package/kits/ora/src/Heading.tsx +18 -5
  47. package/kits/orchard/src/Button.tsx +17 -16
  48. package/kits/orchard/src/Heading.tsx +16 -5
  49. package/kits/orchard/src/styles.css +16 -0
  50. package/kits/outrank/src/Button.tsx +20 -10
  51. package/kits/outrank/src/Heading.tsx +25 -6
  52. package/kits/paperclip/src/Button.tsx +17 -16
  53. package/kits/paperclip/src/Heading.tsx +18 -5
  54. package/kits/patchbay/src/Button.tsx +20 -19
  55. package/kits/patchbay/src/Heading.tsx +25 -6
  56. package/kits/pigeonhole/src/Button.tsx +17 -16
  57. package/kits/pigeonhole/src/Heading.tsx +18 -5
  58. package/kits/prism/src/Button.tsx +17 -16
  59. package/kits/prism/src/Heading.tsx +18 -5
  60. package/kits/proscenium/src/Button.tsx +17 -16
  61. package/kits/proscenium/src/Heading.tsx +18 -5
  62. package/kits/rimlight/src/Button.tsx +17 -16
  63. package/kits/rimlight/src/Heading.tsx +18 -5
  64. package/kits/roster/src/Button.tsx +17 -16
  65. package/kits/roster/src/Heading.tsx +18 -5
  66. package/kits/router/src/Button.tsx +17 -16
  67. package/kits/router/src/Heading.tsx +18 -5
  68. package/kits/scoreboard/src/Button.tsx +17 -16
  69. package/kits/scoreboard/src/Heading.tsx +18 -5
  70. package/kits/showroom/src/Button.tsx +17 -16
  71. package/kits/showroom/src/Heading.tsx +18 -5
  72. package/kits/surveyor/src/Button.tsx +17 -16
  73. package/kits/surveyor/src/Heading.tsx +18 -5
  74. package/kits/tenor/src/Button.tsx +17 -16
  75. package/kits/tenor/src/Heading.tsx +18 -5
  76. package/kits/test-drive/src/Button.tsx +17 -16
  77. package/kits/test-drive/src/Heading.tsx +18 -5
  78. package/kits/vitrine/src/Button.tsx +17 -16
  79. package/kits/vitrine/src/Heading.tsx +18 -5
  80. package/kits/workbench/src/Button.tsx +17 -16
  81. package/kits/workbench/src/Heading.tsx +18 -5
  82. package/kits/workbench/src/styles.css +18 -0
  83. package/package.json +5 -4
  84. package/plugins/sheleg-design/.claude-plugin/plugin.json +1 -1
  85. package/plugins/sheleg-design/skills/sheleg-design/ACCESSIBILITY_EVIDENCE.md +49 -0
  86. package/plugins/sheleg-design/skills/sheleg-design/AI_PRODUCT_PATTERNS.md +7 -1
  87. package/plugins/sheleg-design/skills/sheleg-design/APPLYING.md +37 -0
  88. package/plugins/sheleg-design/skills/sheleg-design/CALIBRATION.md +41 -0
  89. package/plugins/sheleg-design/skills/sheleg-design/COMPONENT_LAYER.md +43 -0
  90. package/plugins/sheleg-design/skills/sheleg-design/CREATIVE_DIRECTOR.md +168 -13
  91. package/plugins/sheleg-design/skills/sheleg-design/DESIGN_SYNC_BRIDGE.md +34 -1
  92. package/plugins/sheleg-design/skills/sheleg-design/KNOWLEDGE_PROVENANCE.md +60 -0
  93. package/plugins/sheleg-design/skills/sheleg-design/LAYOUT_CRAFT.md +81 -0
  94. package/plugins/sheleg-design/skills/sheleg-design/MOBILE_SURFACES.md +54 -0
  95. package/plugins/sheleg-design/skills/sheleg-design/MOTION_DOCTRINE.md +47 -23
  96. package/plugins/sheleg-design/skills/sheleg-design/SHELEG_DESIGN.md +26 -0
  97. package/plugins/sheleg-design/skills/sheleg-design/SKILL.md +135 -97
  98. package/plugins/sheleg-design/skills/sheleg-design/TYPE_CRAFT.md +72 -0
  99. package/plugins/sheleg-design/skills/sheleg-design/VISUAL_EXPLORATION.md +43 -0
  100. package/plugins/sheleg-design/skills/sheleg-design/VISUAL_REVIEW.md +74 -0
  101. package/plugins/sheleg-design/skills/sheleg-design/styles/tokens/orchard.css +16 -0
  102. package/plugins/sheleg-design/skills/sheleg-design/styles/tokens/workbench.css +18 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,80 @@
1
+ ## 1.60.0 — the SKILL body splits back under budget, and the guards read the bundle
2
+
3
+ Sherlock external-v3 (34 findings) plus the CREATIVE_DIRECTOR handoff (PR #28).
4
+
5
+ - **The body breached the 5000-token budget** — 4275 → 5703 from the audit's own
6
+ doctrine — and the house rule's answer to a breach is a SPLIT. Three documents
7
+ now carry what a run does not need in the same breath: `CALIBRATION.md` (what
8
+ to do when two brief rows disagree), `COMPONENT_LAYER.md` (how a kit composes
9
+ with a token layer), `APPLYING.md` (the order of work and the repeat-failure
10
+ list). `PACK_COMPARISON.md` was tried and REVERTED: every sentence in it is
11
+ asserted by a guard, so the comparison protocol and the ten-role adapter
12
+ contract belong in the SKILL.
13
+ - **Two guards now read the resolved BUNDLE rather than SKILL.md alone.** They
14
+ assert that a sentence REACHES the agent, and the agent loads the SKILL plus
15
+ what it links to — so reading one file made the house rule's own remedy delete
16
+ the doctrine those guards protect. Links are resolved, not listed: a sentence
17
+ parked where nothing points is still gone, and the negative assertion got
18
+ stricter, not looser.
19
+ - The CLI smoke test compares the bundle rather than the installer's own manifest,
20
+ and asserts that manifest was written.
21
+
1
22
  ## [Unreleased]
2
23
 
24
+
25
+ ## [1.59.4] - 2026-09-06
26
+
27
+ > **v1.59.3 is a burned tag.** It was pushed against `origin/main` before the release
28
+ > PR had actually merged, so it names the v1.59.2 tree; the version-sync gate refuses
29
+ > it and the remote forbids tag deletion. Nothing shipped under it — this version is
30
+ > the same change, tagged on the verified merge commit.
31
+
32
+ ### Fixed
33
+
34
+ - **The front matter declared the optional siblings and said nothing about the
35
+ hard requirement.** `compatibility:` named `dataviz`, `shadcn` and
36
+ `migrate-radix-to-base` — three skills the body can work without — while the
37
+ body REQUIRES node with npx and network twice (`npx sheleg-design-skill --kit
38
+ <pack>`, `npx sshlg-skills pack design --lane`) and named no path for a
39
+ machine where npx is unreachable. Found by the family audit (2026-09-06, wave
40
+ AUDIT-WAVE-0906). Both halves fixed: the front matter now opens with
41
+ *"Kit/lane commands need node>=16, npx, network"*, and the kit-authoritative
42
+ paragraph states the degraded path in the same voice v1.59.1 gave the
43
+ director's Act 2 — nothing is blocked, the pack and its token layer in
44
+ `styles/tokens/<pack>.css` still carry the rules by reading, and the component
45
+ states only a kit materializes are then **unverified**, said out loud rather
46
+ than presented as kit-checked.
47
+
48
+ **The honest first draft was refused by this repository's own gate, and the
49
+ refusal is the interesting part.** The full declaration measured 342 chars —
50
+ comfortably inside the Agent Skills spec's 500-char `compatibility` cap — and
51
+ `check_description_canon` refused it at **432 of a 256 overhead budget**,
52
+ because every non-description key is read every session and the budget exists
53
+ so front matter never becomes a place to write prose. Two limits, and the
54
+ binding one was the house budget, not the spec cap — the same lesson v1.59.2
55
+ paid for on the description. So the front matter carries the compressed form
56
+ (**253 of 256**) and the body carries the detail, which is where a reader who
57
+ hit the failure would look anyway.
58
+
59
+ - **An empty recursive directory chain sat in checkouts, and the mirror
60
+ validator could not see the class it belongs to.**
61
+ `.cursor/skills/sheleg-design/styles/tokens/.cursor/skills/sheleg-design/` —
62
+ zero files, invisible to `git status` (git tracks no directory), to the npm
63
+ tarball (npm packs files), and to both directions of the mirror-drift check,
64
+ which walk `rglob("*")` filtered by `is_file()`. Measured on this machine
65
+ 2026-09-06: the umbrella's submodule checkout at `skills/sheleg-design`
66
+ carried the chain at commit `0902ef6`; this repository's own checkout was
67
+ clean. The artefact is removed, and the CLASS is closed: the mirror block now
68
+ reads **directories** over both trees — a nested `.cursor` anywhere inside
69
+ either bundle is refused as a copy artefact whatever it contains, and an
70
+ empty directory is refused wholesale. Watched failing against a real planted
71
+ chain before being believed — both checks answered with their own messages,
72
+ not a neighbour's — and planted permanently in the self-test as two
73
+ **directory fixtures**, a shape `PLANTS` cannot express because its mutations
74
+ are text edits to a file, and each fixture asserts the plant landed before
75
+ trusting any red. Floor raised 5621 → 5641, measured by running the gate on
76
+ this tree.
77
+
3
78
  ## [1.59.2] - 2026-09-03
4
79
 
5
80
  ### Fixed
package/bin/cli.js CHANGED
@@ -83,6 +83,69 @@ const COLORS = {
83
83
  const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
84
84
  const c = (color, s) => (useColor ? COLORS[color] + s + COLORS.reset : s);
85
85
 
86
+ /**
87
+ * A TRANSACTIONAL file install (FIX-UP-05.03): the writer contract from UP-05
88
+ * applied to the design CLI. The old path copied each managed file straight
89
+ * into the target, so a crash mid-install left a partial write. Now the managed
90
+ * set is staged into a same-filesystem sibling and VERIFIED, the previous
91
+ * generation is snapshotted (recoverable under --force), the staged files are
92
+ * renamed into place, and OBSOLETE managed files (managed, gone from the source)
93
+ * are removed — while unknown user files are never touched. No runtime
94
+ * dependency on a sibling checkout: everything reads from `srcDir`.
95
+ */
96
+ function installFilesTransactionally(srcDir, targetDir, files) {
97
+ const staging = path.join(targetDir, `.sheleg-staging-${process.pid}`);
98
+ const prevGen = path.join(targetDir, `.sheleg-prev-${process.pid}`);
99
+ const manifestPath = path.join(targetDir, ".sheleg-manifest.json");
100
+ // The previously-installed MANAGED set, so obsolete-managed-only removal can
101
+ // tell a file this installer wrote from an unknown user file it must not touch.
102
+ let prevManaged = [];
103
+ try { prevManaged = JSON.parse(fs.readFileSync(manifestPath, "utf8")).files || []; }
104
+ catch (e) { prevManaged = []; }
105
+ fs.rmSync(staging, { recursive: true, force: true });
106
+ try {
107
+ // 1. STAGE + VERIFY.
108
+ for (const f of files) {
109
+ const s = path.join(srcDir, f);
110
+ const d = path.join(staging, f);
111
+ fs.mkdirSync(path.dirname(d), { recursive: true });
112
+ fs.copyFileSync(s, d);
113
+ if (!fs.readFileSync(s).equals(fs.readFileSync(d))) {
114
+ throw new Error(`staged ${f} does not match its source`);
115
+ }
116
+ }
117
+ // 2. Snapshot the previous generation of the managed files (recoverable).
118
+ fs.rmSync(prevGen, { recursive: true, force: true });
119
+ for (const f of files) {
120
+ const cur = path.join(targetDir, f);
121
+ if (fs.existsSync(cur)) {
122
+ const save = path.join(prevGen, f);
123
+ fs.mkdirSync(path.dirname(save), { recursive: true });
124
+ fs.copyFileSync(cur, save);
125
+ }
126
+ }
127
+ // 3. SWITCH: rename each staged file into place.
128
+ for (const f of files) {
129
+ const d = path.join(targetDir, f);
130
+ fs.mkdirSync(path.dirname(d), { recursive: true });
131
+ fs.renameSync(path.join(staging, f), d);
132
+ }
133
+ // 4. Remove OBSOLETE managed files — managed before, gone from the source
134
+ // now — and never an unknown user file.
135
+ const wanted = new Set(files);
136
+ for (const f of (prevManaged || [])) {
137
+ if (!wanted.has(f)) fs.rmSync(path.join(targetDir, f), { force: true });
138
+ }
139
+ fs.writeFileSync(manifestPath, JSON.stringify({ files }));
140
+ fs.rmSync(staging, { recursive: true, force: true });
141
+ fs.rmSync(prevGen, { recursive: true, force: true });
142
+ return { installed: files, removed: prevManaged.filter((f) => !new Set(files).has(f)) };
143
+ } catch (err) {
144
+ fs.rmSync(staging, { recursive: true, force: true });
145
+ throw new Error(`install aborted, previous install intact: ${err.message}`);
146
+ }
147
+ }
148
+
86
149
  function parseArgs(argv) {
87
150
  const opts = {
88
151
  target: null, // explicit --dir
@@ -375,10 +438,25 @@ function claudeHomeOf(targetDir) {
375
438
  * as "no plugin": the fresh HOME is the common case, and an installer that
376
439
  * crashes on a parse error refuses the machines that need it most.
377
440
  */
441
+ // The bundled HostContext resolver (FIX-UP-08.02) — one contract, a local copy
442
+ // per member (npx installers share no lib): a host's config root is an explicit
443
+ // root > the documented host env var > `~/<dir>`, verbatim (spaces preserved).
444
+ // Only the USER-scoped Claude config (the plugin registry) uses this; a
445
+ // PROJECT-level `.claude/` under cwd is not a host config root and is left as
446
+ // is. Host existence stays a separate probe on the returned path.
447
+ const HOST_ENV = { claude: "CLAUDE_CONFIG_DIR", codex: "CODEX_HOME", gemini: "GEMINI_CONFIG_DIR" };
448
+ const HOST_DIR = { claude: ".claude", codex: ".codex", gemini: ".gemini" };
449
+ function hostRoot(agent, home, env, explicit) {
450
+ if (explicit) return explicit;
451
+ const e = (env || process.env)[HOST_ENV[agent]];
452
+ if (e) return e;
453
+ return path.join(home, HOST_DIR[agent]);
454
+ }
455
+
378
456
  function installedPluginSpec(home) {
379
457
  try {
380
458
  const raw = fs.readFileSync(
381
- path.join(home, ".claude", "plugins", "installed_plugins.json"),
459
+ path.join(hostRoot("claude", home, process.env), "plugins", "installed_plugins.json"),
382
460
  "utf8",
383
461
  );
384
462
  const parsed = JSON.parse(raw);
@@ -499,11 +577,7 @@ function main() {
499
577
  }
500
578
 
501
579
  fs.mkdirSync(targetDir, { recursive: true });
502
- for (const f of files) {
503
- const dest = path.join(targetDir, f);
504
- fs.mkdirSync(path.dirname(dest), { recursive: true });
505
- fs.copyFileSync(path.join(SKILL_DIR, f), dest);
506
- }
580
+ installFilesTransactionally(SKILL_DIR, targetDir, files);
507
581
 
508
582
  const rel = path.relative(cwd, targetDir) || ".";
509
583
  console.log(
@@ -512,9 +586,10 @@ function main() {
512
586
  ` ${c("dim", "SHELEG_DESIGN.md")} the full reference\n` +
513
587
  ` ${c("dim", "MOTION_DOCTRINE.md")} whether to animate at all — read before any animation\n` +
514
588
  ` ${c("dim", "styles/")} style packs + token CSS (instrument-console / editorial-luxury / workbench / briefing-room / atrium / orchard / field-notes / cyclorama / showroom / blueprint / prism / maquette / scoreboard / datasheet / manpage / pigeonhole / roster)\n\n` +
515
- `Your Cursor / Claude agent can now discover the skill and build\n` +
516
- `cinematic, scroll-driven pages or style product UI (dashboards, admin,\n` +
517
- `internal tools) from a standalone pack on its principles.\n\n` +
589
+ `Files are on disk; a running agent has NOT loaded them yet skills are\n` +
590
+ `read at session start, so reload (a new session) before expecting the\n` +
591
+ `skill, then build cinematic scroll-driven pages or style product UI from\n` +
592
+ `a standalone pack. Writing files is not the same as the host loading them.\n\n` +
518
593
  `${c("dim", "Docs: " + pkg.homepage)}\n`,
519
594
  );
520
595
 
@@ -569,4 +644,8 @@ function offerRouters() {
569
644
  }
570
645
  }
571
646
 
572
- main();
647
+ if (require.main === module) {
648
+ main();
649
+ }
650
+
651
+ module.exports = { installFilesTransactionally, copyKitTree };
@@ -1,33 +1,34 @@
1
- import type { ReactNode } from 'react';
1
+ import { forwardRef } from 'react';
2
+ import type { ButtonHTMLAttributes, ReactNode } from 'react';
2
3
 
3
- export interface ButtonProps {
4
+ export interface ButtonProps
5
+ extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
4
6
  /** `primary` is the accent fill — the pack allows one per view. */
5
7
  variant?: 'primary' | 'secondary' | 'ghost';
6
8
  size?: 'sm' | 'md' | 'lg';
7
- disabled?: boolean;
8
- onClick?: () => void;
9
9
  children: ReactNode;
10
10
  className?: string;
11
11
  }
12
12
 
13
- export function Button({
14
- variant = 'primary',
15
- size = 'md',
16
- disabled = false,
17
- onClick,
18
- children,
19
- className,
20
- }: ButtonProps) {
13
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
14
+ { variant = 'primary', size = 'md', className, type, children, ...rest },
15
+ ref,
16
+ ) {
21
17
  return (
22
18
  <button
23
- type="button"
19
+ ref={ref}
20
+ // default is `button`, but an explicit caller `type` (submit, reset) wins —
21
+ // a design default must never silently erase a prop the caller set.
22
+ type={type ?? 'button'}
24
23
  className={['al-btn', `al-btn--${variant}`, `al-btn--${size}`, className]
25
24
  .filter(Boolean)
26
25
  .join(' ')}
27
- disabled={disabled}
28
- onClick={onClick}
26
+ // native + ARIA + state pass through: aria-label for icon-only buttons,
27
+ // aria-controls / aria-expanded for trigger controls, disabled, and onClick
28
+ // with its real event. The spine forwards, it does not swallow.
29
+ {...rest}
29
30
  >
30
31
  {children}
31
32
  </button>
32
33
  );
33
- }
34
+ });
@@ -1,19 +1,32 @@
1
- import type { ReactNode } from 'react';
1
+ import { forwardRef } from 'react';
2
+ import type { HTMLAttributes, ReactNode } from 'react';
2
3
 
3
- export interface HeadingProps {
4
+ export interface HeadingProps
5
+ extends Omit<HTMLAttributes<HTMLHeadingElement>, 'className'> {
4
6
  /** 1 = page title (30px), 2 = section (24px), 3 = card title (16px). */
5
7
  level?: 1 | 2 | 3;
8
+ /** Visual size — defaults to the level, so existing call sites keep their look. */
9
+ size?: 1 | 2 | 3;
6
10
  children: ReactNode;
7
11
  className?: string;
8
12
  }
9
13
 
10
- export function Heading({ level = 2, children, className }: HeadingProps) {
14
+ export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(function Heading(
15
+ { level = 2, size, className, children, ...rest },
16
+ ref,
17
+ ) {
11
18
  const Tag = `h${level}` as 'h1' | 'h2' | 'h3';
12
19
  return (
13
20
  <Tag
14
- className={['al-heading', `al-heading--${level}`, className].filter(Boolean).join(' ')}
21
+ ref={ref}
22
+ // The TAG follows the semantic level; the CLASS follows the visual size —
23
+ // an h2 may wear the display size without touching the document outline.
24
+ className={['al-heading', `al-heading--${size ?? level}`, className]
25
+ .filter(Boolean)
26
+ .join(' ')}
27
+ {...rest}
15
28
  >
16
29
  {children}
17
30
  </Tag>
18
31
  );
19
- }
32
+ });
@@ -1,38 +1,38 @@
1
- import type { ReactNode } from 'react';
1
+ import { forwardRef } from 'react';
2
+ import type { ButtonHTMLAttributes, ReactNode } from 'react';
2
3
 
3
- export interface ButtonProps {
4
+ export interface ButtonProps
5
+ extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
4
6
  /**
5
7
  * The pill triad, and there is no fourth: `primary` is the solid accent,
6
8
  * `secondary` the accent-outline on the field, `ghost` the beige pill that
7
9
  * sits on photography.
8
10
  */
9
11
  variant?: 'primary' | 'secondary' | 'ghost';
10
- /** `md` is the pack's pill (12px 25px). `sm`/`lg` rescale it, nothing else. */
11
12
  size?: 'sm' | 'md' | 'lg';
12
- disabled?: boolean;
13
- onClick?: () => void;
14
13
  children: ReactNode;
15
14
  className?: string;
16
15
  }
17
16
 
18
- export function Button({
19
- variant = 'primary',
20
- size = 'md',
21
- disabled = false,
22
- onClick,
23
- children,
24
- className,
25
- }: ButtonProps) {
17
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
18
+ { variant = 'primary', size = 'md', className, type, children, ...rest },
19
+ ref,
20
+ ) {
26
21
  return (
27
22
  <button
28
- type="button"
23
+ ref={ref}
24
+ // default is `button`, but an explicit caller `type` (submit, reset) wins —
25
+ // a design default must never silently erase a prop the caller set.
26
+ type={type ?? 'button'}
29
27
  className={['at-btn', `at-btn--${variant}`, `at-btn--${size}`, className]
30
28
  .filter(Boolean)
31
29
  .join(' ')}
32
- disabled={disabled}
33
- onClick={onClick}
30
+ // native + ARIA + state pass through: aria-label for icon-only buttons,
31
+ // aria-controls / aria-expanded for trigger controls, disabled, and onClick
32
+ // with its real event. The spine forwards, it does not swallow.
33
+ {...rest}
34
34
  >
35
35
  {children}
36
36
  </button>
37
37
  );
38
- }
38
+ });
@@ -1,19 +1,32 @@
1
- import type { ReactNode } from 'react';
1
+ import { forwardRef } from 'react';
2
+ import type { HTMLAttributes, ReactNode } from 'react';
2
3
 
3
- export interface HeadingProps {
4
+ export interface HeadingProps
5
+ extends Omit<HTMLAttributes<HTMLHeadingElement>, 'className'> {
4
6
  /** 1 = hero claim (57→80px), 2 = section (48→64px), 3 = category (34→45px). */
5
7
  level?: 1 | 2 | 3;
8
+ /** Visual size — defaults to the level, so existing call sites keep their look. */
9
+ size?: 1 | 2 | 3;
6
10
  children: ReactNode;
7
11
  className?: string;
8
12
  }
9
13
 
10
- export function Heading({ level = 2, children, className }: HeadingProps) {
14
+ export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(function Heading(
15
+ { level = 2, size, className, children, ...rest },
16
+ ref,
17
+ ) {
11
18
  const Tag = `h${level}` as 'h1' | 'h2' | 'h3';
12
19
  return (
13
20
  <Tag
14
- className={['at-heading', `at-heading--${level}`, className].filter(Boolean).join(' ')}
21
+ ref={ref}
22
+ // The TAG follows the semantic level; the CLASS follows the visual size —
23
+ // an h2 may wear the display size without touching the document outline.
24
+ className={['at-heading', `at-heading--${size ?? level}`, className]
25
+ .filter(Boolean)
26
+ .join(' ')}
27
+ {...rest}
15
28
  >
16
29
  {children}
17
30
  </Tag>
18
31
  );
19
- }
32
+ });
@@ -1,33 +1,34 @@
1
- import type { ReactNode } from 'react';
1
+ import { forwardRef } from 'react';
2
+ import type { ButtonHTMLAttributes, ReactNode } from 'react';
2
3
 
3
- export interface ButtonProps {
4
+ export interface ButtonProps
5
+ extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
4
6
  /** `primary` is the BLACK pill — the accent, and there is no hue in the control set. */
5
7
  variant?: 'primary' | 'secondary' | 'ghost';
6
8
  size?: 'sm' | 'md' | 'lg';
7
- disabled?: boolean;
8
- onClick?: () => void;
9
9
  children: ReactNode;
10
10
  className?: string;
11
11
  }
12
12
 
13
- export function Button({
14
- variant = 'primary',
15
- size = 'md',
16
- disabled = false,
17
- onClick,
18
- children,
19
- className,
20
- }: ButtonProps) {
13
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
14
+ { variant = 'primary', size = 'md', className, type, children, ...rest },
15
+ ref,
16
+ ) {
21
17
  return (
22
18
  <button
23
- type="button"
19
+ ref={ref}
20
+ // default is `button`, but an explicit caller `type` (submit, reset) wins —
21
+ // a design default must never silently erase a prop the caller set.
22
+ type={type ?? 'button'}
24
23
  className={['aw-btn', `aw-btn--${variant}`, `aw-btn--${size}`, className]
25
24
  .filter(Boolean)
26
25
  .join(' ')}
27
- disabled={disabled}
28
- onClick={onClick}
26
+ // native + ARIA + state pass through: aria-label for icon-only buttons,
27
+ // aria-controls / aria-expanded for trigger controls, disabled, and onClick
28
+ // with its real event. The spine forwards, it does not swallow.
29
+ {...rest}
29
30
  >
30
31
  {children}
31
32
  </button>
32
33
  );
33
- }
34
+ });
@@ -1,17 +1,32 @@
1
- import type { ReactNode } from 'react';
1
+ import { forwardRef } from 'react';
2
+ import type { HTMLAttributes, ReactNode } from 'react';
2
3
 
3
- export interface HeadingProps {
4
+ export interface HeadingProps
5
+ extends Omit<HTMLAttributes<HTMLHeadingElement>, 'className'> {
4
6
  /** 1 = page title (`dsp`), 2 = section (`t1`), 3 = card title (`t3`). */
5
7
  level?: 1 | 2 | 3;
8
+ /** Visual size — defaults to the level, so existing call sites keep their look. */
9
+ size?: 1 | 2 | 3;
6
10
  children: ReactNode;
7
11
  className?: string;
8
12
  }
9
13
 
10
- export function Heading({ level = 2, children, className }: HeadingProps) {
14
+ export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(function Heading(
15
+ { level = 2, size, className, children, ...rest },
16
+ ref,
17
+ ) {
11
18
  const Tag = `h${level}` as 'h1' | 'h2' | 'h3';
12
19
  return (
13
- <Tag className={['aw-h', `aw-h--${level}`, className].filter(Boolean).join(' ')}>
20
+ <Tag
21
+ ref={ref}
22
+ // The TAG follows the semantic level; the CLASS follows the visual size —
23
+ // an h2 may wear the display size without touching the document outline.
24
+ className={['aw-h', `aw-h--${size ?? level}`, className]
25
+ .filter(Boolean)
26
+ .join(' ')}
27
+ {...rest}
28
+ >
14
29
  {children}
15
30
  </Tag>
16
31
  );
17
- }
32
+ });
@@ -1,19 +1,34 @@
1
- import type { ReactNode } from 'react';
1
+ import { forwardRef } from 'react';
2
+ import type { ButtonHTMLAttributes, ReactNode } from 'react';
2
3
 
3
- export interface ButtonProps {
4
+ export interface ButtonProps
5
+ extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
4
6
  /** `primary` is the accent fill — at most one per view. */
5
7
  variant?: 'primary' | 'secondary' | 'ghost';
6
8
  size?: 'sm' | 'md' | 'lg';
7
- disabled?: boolean;
8
- onClick?: () => void;
9
9
  children: ReactNode;
10
10
  className?: string;
11
11
  }
12
12
 
13
- export function Button({ variant = 'primary', size = 'md', disabled = false, onClick, children, className }: ButtonProps) {
13
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
14
+ { variant = 'primary', size = 'md', className, type, children, ...rest },
15
+ ref,
16
+ ) {
14
17
  return (
15
- <button type="button" className={['bl-btn', `bl-btn--${variant}`, `bl-btn--${size}`, className].filter(Boolean).join(' ')} disabled={disabled} onClick={onClick}>
18
+ <button
19
+ ref={ref}
20
+ // default is `button`, but an explicit caller `type` (submit, reset) wins —
21
+ // a design default must never silently erase a prop the caller set.
22
+ type={type ?? 'button'}
23
+ className={['bl-btn', `bl-btn--${variant}`, `bl-btn--${size}`, className]
24
+ .filter(Boolean)
25
+ .join(' ')}
26
+ // native + ARIA + state pass through: aria-label for icon-only buttons,
27
+ // aria-controls / aria-expanded for trigger controls, disabled, and onClick
28
+ // with its real event. The spine forwards, it does not swallow.
29
+ {...rest}
30
+ >
16
31
  {children}
17
32
  </button>
18
33
  );
19
- }
34
+ });
@@ -1,13 +1,32 @@
1
- import type { ReactNode } from 'react';
1
+ import { forwardRef } from 'react';
2
+ import type { HTMLAttributes, ReactNode } from 'react';
2
3
 
3
- export interface HeadingProps {
4
+ export interface HeadingProps
5
+ extends Omit<HTMLAttributes<HTMLHeadingElement>, 'className'> {
4
6
  /** 1 = page title (28px), 2 = section (20px), 3 = card title (15px). */
5
7
  level?: 1 | 2 | 3;
8
+ /** Visual size — defaults to the level, so existing call sites keep their look. */
9
+ size?: 1 | 2 | 3;
6
10
  children: ReactNode;
7
11
  className?: string;
8
12
  }
9
13
 
10
- export function Heading({ level = 2, children, className }: HeadingProps) {
11
- const Tag = (`h${level}`) as 'h1' | 'h2' | 'h3';
12
- return <Tag className={[`bl-heading--${level}`, className].filter(Boolean).join(' ')}>{children}</Tag>;
13
- }
14
+ export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(function Heading(
15
+ { level = 2, size, className, children, ...rest },
16
+ ref,
17
+ ) {
18
+ const Tag = `h${level}` as 'h1' | 'h2' | 'h3';
19
+ return (
20
+ <Tag
21
+ ref={ref}
22
+ // The TAG follows the semantic level; the CLASS follows the visual size —
23
+ // an h2 may wear the display size without touching the document outline.
24
+ className={[`bl-heading--${size ?? level}`, className]
25
+ .filter(Boolean)
26
+ .join(' ')}
27
+ {...rest}
28
+ >
29
+ {children}
30
+ </Tag>
31
+ );
32
+ });
@@ -1,33 +1,34 @@
1
- import type { ReactNode } from 'react';
1
+ import { forwardRef } from 'react';
2
+ import type { ButtonHTMLAttributes, ReactNode } from 'react';
2
3
 
3
- export interface ButtonProps {
4
+ export interface ButtonProps
5
+ extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
4
6
  /** `primary` is the pack's default fill — at most one accent action per view. */
5
7
  variant?: 'primary' | 'secondary' | 'ghost';
6
8
  size?: 'sm' | 'md' | 'lg';
7
- disabled?: boolean;
8
- onClick?: () => void;
9
9
  children: ReactNode;
10
10
  className?: string;
11
11
  }
12
12
 
13
- export function Button({
14
- variant = 'primary',
15
- size = 'md',
16
- disabled = false,
17
- onClick,
18
- children,
19
- className,
20
- }: ButtonProps) {
13
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
14
+ { variant = 'primary', size = 'md', className, type, children, ...rest },
15
+ ref,
16
+ ) {
21
17
  return (
22
18
  <button
23
- type="button"
19
+ ref={ref}
20
+ // default is `button`, but an explicit caller `type` (submit, reset) wins —
21
+ // a design default must never silently erase a prop the caller set.
22
+ type={type ?? 'button'}
24
23
  className={['bp-btn', `bp-btn--${variant}`, `bp-btn--${size}`, className]
25
24
  .filter(Boolean)
26
25
  .join(' ')}
27
- disabled={disabled}
28
- onClick={onClick}
26
+ // native + ARIA + state pass through: aria-label for icon-only buttons,
27
+ // aria-controls / aria-expanded for trigger controls, disabled, and onClick
28
+ // with its real event. The spine forwards, it does not swallow.
29
+ {...rest}
29
30
  >
30
31
  {children}
31
32
  </button>
32
33
  );
33
- }
34
+ });
@@ -1,19 +1,32 @@
1
- import type { ReactNode } from 'react';
1
+ import { forwardRef } from 'react';
2
+ import type { HTMLAttributes, ReactNode } from 'react';
2
3
 
3
- export interface HeadingProps {
4
+ export interface HeadingProps
5
+ extends Omit<HTMLAttributes<HTMLHeadingElement>, 'className'> {
4
6
  /** 1 = page title, 2 = section, 3 = sub-head. */
5
7
  level?: 1 | 2 | 3;
8
+ /** Visual size — defaults to the level, so existing call sites keep their look. */
9
+ size?: 1 | 2 | 3;
6
10
  children: ReactNode;
7
11
  className?: string;
8
12
  }
9
13
 
10
- export function Heading({ level = 2, children, className }: HeadingProps) {
14
+ export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(function Heading(
15
+ { level = 2, size, className, children, ...rest },
16
+ ref,
17
+ ) {
11
18
  const Tag = `h${level}` as 'h1' | 'h2' | 'h3';
12
19
  return (
13
20
  <Tag
14
- className={['bp-heading', `bp-heading--${level}`, className].filter(Boolean).join(' ')}
21
+ ref={ref}
22
+ // The TAG follows the semantic level; the CLASS follows the visual size —
23
+ // an h2 may wear the display size without touching the document outline.
24
+ className={['bp-heading', `bp-heading--${size ?? level}`, className]
25
+ .filter(Boolean)
26
+ .join(' ')}
27
+ {...rest}
15
28
  >
16
29
  {children}
17
30
  </Tag>
18
31
  );
19
- }
32
+ });