sheleg-design-skill 1.59.4 → 1.60.1

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 +34 -2
  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 +129 -95
  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,12 +1,44 @@
1
+ ## 1.60.1 — the ledger catches up to the version it ships
2
+
3
+ A member's ledger is fixed in that member's own release. The umbrella's ratchet
4
+ counts members whose ledger describes a version older than they ship, and the
5
+ release wave of 2026-09-10 put this one on that list — its gate did not demand a
6
+ section, the umbrella does.
7
+
8
+ The v1.60.0 section is written now, naming what shipped and what
9
+ confirmed it.
10
+
11
+ ## 1.60.0 — the SKILL body splits back under budget, and the guards read the bundle
12
+
13
+ Sherlock external-v3 (34 findings) plus the CREATIVE_DIRECTOR handoff (PR #28).
14
+
15
+ - **The body breached the 5000-token budget** — 4275 → 5703 from the audit's own
16
+ doctrine — and the house rule's answer to a breach is a SPLIT. Three documents
17
+ now carry what a run does not need in the same breath: `CALIBRATION.md` (what
18
+ to do when two brief rows disagree), `COMPONENT_LAYER.md` (how a kit composes
19
+ with a token layer), `APPLYING.md` (the order of work and the repeat-failure
20
+ list). `PACK_COMPARISON.md` was tried and REVERTED: every sentence in it is
21
+ asserted by a guard, so the comparison protocol and the ten-role adapter
22
+ contract belong in the SKILL.
23
+ - **Two guards now read the resolved BUNDLE rather than SKILL.md alone.** They
24
+ assert that a sentence REACHES the agent, and the agent loads the SKILL plus
25
+ what it links to — so reading one file made the house rule's own remedy delete
26
+ the doctrine those guards protect. Links are resolved, not listed: a sentence
27
+ parked where nothing points is still gone, and the negative assertion got
28
+ stricter, not looser.
29
+ - The CLI smoke test compares the bundle rather than the installer's own manifest,
30
+ and asserts that manifest was written.
31
+
1
32
  ## [Unreleased]
2
33
 
34
+
35
+ ## [1.59.4] - 2026-09-06
36
+
3
37
  > **v1.59.3 is a burned tag.** It was pushed against `origin/main` before the release
4
38
  > PR had actually merged, so it names the v1.59.2 tree; the version-sync gate refuses
5
39
  > it and the remote forbids tag deletion. Nothing shipped under it — this version is
6
40
  > the same change, tagged on the verified merge commit.
7
41
 
8
- ## [1.59.4] - 2026-09-06
9
-
10
42
  ### Fixed
11
43
 
12
44
  - **The front matter declared the optional siblings and said nothing about the
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
+ });