@component-anatomy/storybook 0.1.0 → 0.3.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.
package/README.md CHANGED
@@ -48,6 +48,8 @@ export const Anatomy: Story = {
48
48
 
49
49
  Omit `parts` (pass `{}`) and the panel lists parts auto-discovered from `data-part` attributes, with names derived from the ids.
50
50
 
51
+ `preset` and `theme` reach the panel and the `<Anatomy>` block too, not just the canvas overlays: the active row is accented with the same color the overlay paints. Where that color would be illegible on Storybook's own panel — `contrast` is black, the manager is dark by default — the table falls back to the most colorful alternative that meets WCAG AA, so `contrast` accents with its yellow on a dark panel and with black on a light one.
52
+
51
53
  Parameters follow Storybook's normal inheritance — project-wide defaults in `.storybook/preview.ts`, per-component in `meta.parameters`, per-story overrides in `story.parameters`.
52
54
 
53
55
  ## In MDX
@@ -58,34 +60,56 @@ renders MDX), which stays an optional peer dependency — the panel above works
58
60
  without it.
59
61
 
60
62
  ```mdx
61
- import { Canvas, Meta } from '@storybook/addon-docs/blocks';
63
+ import { Meta } from '@storybook/addon-docs/blocks';
62
64
  import { Anatomy } from '@component-anatomy/storybook/blocks';
63
65
 
64
66
  import * as ButtonStories from './Button.stories';
65
67
 
66
68
  <Meta of={ButtonStories} />
67
69
 
68
- <Canvas of={ButtonStories.Anatomy} />
69
70
  <Anatomy of={ButtonStories.Anatomy} />
70
71
  ```
71
72
 
72
- Hover sync works both ways, exactly as in the panel.
73
+ `<Anatomy>` renders the canvas and the table together, tightened into one
74
+ pairing, with the canvas's show-code button dropped by default (chrome that
75
+ mostly restates what the table already documents). Hover sync works both ways,
76
+ exactly as in the panel.
73
77
 
74
78
  | Prop | Type | Description |
75
79
  |---|---|---|
76
80
  | `of` | CSF export | The story to document, or a whole CSF module to read the meta's parameters. Omit on an attached docs page (one with `<Meta of={…} />`) to fall back to the page's current story. |
77
81
  | `parts` | `AnatomyPartDefinition[]` | Curated list, overriding both `parameters.anatomy.parts` and auto-discovery. |
78
82
  | `sync` | `boolean` | Two-way hover sync with the rendered story. Default `true`. `false` gives a purely static table. |
83
+ | `sourceState` | `'hidden' \| 'shown' \| 'none'` | Passed to the underlying `<Canvas>`. Default `'none'` — set it to keep the show-code button. |
84
+
85
+ For a hand-placed canvas — a custom layout, or one that keeps its source panel
86
+ some other way — use `<AnatomyTable>` next to your own `<Canvas>` instead. It
87
+ takes the same `of`, `parts` and `sync` props, minus `sourceState`, which is
88
+ `<Canvas>`'s concern:
89
+
90
+ ```mdx
91
+ import { Canvas, Meta } from '@storybook/addon-docs/blocks';
92
+ import { AnatomyTable } from '@component-anatomy/storybook/blocks';
93
+
94
+ import * as ButtonStories from './Button.stories';
95
+
96
+ <Meta of={ButtonStories} />
97
+
98
+ <Canvas of={ButtonStories.Anatomy} />
99
+ <AnatomyTable of={ButtonStories.Anatomy} />
100
+ ```
79
101
 
80
- Two things worth knowing:
102
+ Two things worth knowing, for either block:
81
103
 
82
- - **Auto-discovery needs the story on the page.** Parts are read from the story's
83
- `data-part` attributes as it renders, so a block that relies on discovery needs
84
- a `<Canvas of={…} />` (or `<Story of={…} />`) on the same page. A block with an
85
- explicit `parts` list stands on its own.
104
+ - **Auto-discovery needs the story's canvas on the page.** Parts are read from
105
+ the story's `data-part` attributes as it renders, so a block that relies on
106
+ discovery needs a canvas on the same page `<Anatomy>` brings its own,
107
+ `<AnatomyTable>` needs a `<Canvas of={…} />` (or `<Story of={…} />`) next to
108
+ it. A block with an explicit `parts` list stands on its own.
86
109
  - **A meta has no canvas.** `of={ButtonStories}` reads `meta.parameters.anatomy`,
87
110
  but there is nothing to discover parts from or to sync hover with — give it
88
- `parts` explicitly.
111
+ `parts` explicitly, and use `<AnatomyTable>` rather than `<Anatomy>` since
112
+ there is no story to draw a canvas from.
89
113
 
90
114
  Several blocks can share one page; each talks only to the story it names.
91
115
 
@@ -8,9 +8,7 @@
8
8
  * data acquisition differs between the two; the markup must not.
9
9
  */
10
10
  import React from 'react';
11
- import type { AnatomyPartDefinition } from '@component-anatomy/core';
12
- /** Fallback accent when the story sets no `anatomy.theme.accent`. */
13
- export declare const ACCENT_FALLBACK = "#4f46e5";
11
+ import type { AnatomyPartDefinition, AnatomyPresetName, AnatomyTheme } from '@component-anatomy/core';
14
12
  /**
15
13
  * An inline `<code>` styled for the surrounding message text. Exported so
16
14
  * both runtimes can compose their own empty-state copy without duplicating
@@ -28,8 +26,10 @@ export type AnatomyTableProps = {
28
26
  parts: AnatomyPartDefinition[];
29
27
  /** Id of the part currently highlighted in the canvas, if any. */
30
28
  activeId?: string | null;
31
- /** Accent color for the active state. Defaults to {@link ACCENT_FALLBACK}. */
32
- accent?: string;
29
+ /** The story's `anatomy.preset` the active row follows it. */
30
+ preset?: AnatomyPresetName;
31
+ /** The story's `anatomy.theme` — token overrides on top of the preset. */
32
+ theme?: AnatomyTheme;
33
33
  /** Called when the user hovers or focuses an entry. */
34
34
  onItemEnter?: (partId: string) => void;
35
35
  /** Called when the user leaves or blurs an entry. */
@@ -1 +1 @@
1
- {"version":3,"file":"AnatomyTable.d.ts","sourceRoot":"","sources":["../src/AnatomyTable.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAErE,qEAAqE;AACrE,eAAO,MAAM,eAAe,YAAY,CAAC;AA4GzC;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,CAG/D,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,CAOlE,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,wDAAwD;IACxD,KAAK,EAAE,qBAAqB,EAAE,CAAC;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAsDpD,CAAC"}
1
+ {"version":3,"file":"AnatomyTable.d.ts","sourceRoot":"","sources":["../src/AnatomyTable.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EACV,qBAAqB,EACrB,iBAAiB,EACjB,YAAY,EACb,MAAM,yBAAyB,CAAC;AAkIjC;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,CAG/D,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,CAOlE,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,wDAAwD;IACxD,KAAK,EAAE,qBAAqB,EAAE,CAAC;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gEAAgE;IAChE,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,0EAA0E;IAC1E,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,uDAAuD;IACvD,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAwDpD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"Panel.d.ts","sourceRoot":"","sources":["../src/Panel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAUnD,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,EAkEzB,CAAC"}
1
+ {"version":3,"file":"Panel.d.ts","sourceRoot":"","sources":["../src/Panel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAUnD,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,EA8EzB,CAAC"}
package/dist/blocks.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Docs blocks entry — the `<Anatomy>` block for MDX pages.
2
+ * Docs blocks entry — the `<Anatomy>` and `<AnatomyTable>` blocks for MDX
3
+ * pages.
3
4
  *
4
5
  * Unlike the addon panel, this runs in the **preview iframe**, where
5
6
  * `storybook/manager-api` does not exist. It reaches the story's controller
@@ -8,21 +9,37 @@
8
9
  * mounted the story talk to each other directly, in-frame, with no extra
9
10
  * plumbing.
10
11
  *
12
+ * `<Anatomy>` is the batteries-included block — canvas and table together,
13
+ * laid out for that pairing specifically ([#13](https://github.com/julien-deramond/component-anatomy/issues/13#issuecomment-5475936860)):
14
+ *
11
15
  * ```mdx
12
- * import { Meta, Canvas } from '@storybook/addon-docs/blocks';
16
+ * import { Meta } from '@storybook/addon-docs/blocks';
13
17
  * import { Anatomy } from '@component-anatomy/storybook/blocks';
14
18
  * import * as ButtonStories from './Button.stories';
15
19
  *
16
20
  * <Meta of={ButtonStories} />
17
21
  *
18
- * <Canvas of={ButtonStories.Anatomy} />
19
22
  * <Anatomy of={ButtonStories.Anatomy} />
20
23
  * ```
24
+ *
25
+ * `<AnatomyTable>` is the table alone, for a hand-placed `<Canvas>` or a
26
+ * custom layout:
27
+ *
28
+ * ```mdx
29
+ * import { Canvas, Meta } from '@storybook/addon-docs/blocks';
30
+ * import { AnatomyTable } from '@component-anatomy/storybook/blocks';
31
+ * import * as ButtonStories from './Button.stories';
32
+ *
33
+ * <Meta of={ButtonStories} />
34
+ *
35
+ * <Canvas of={ButtonStories.Anatomy} />
36
+ * <AnatomyTable of={ButtonStories.Anatomy} />
37
+ * ```
21
38
  */
22
39
  import React from 'react';
23
40
  import type { Of } from '@storybook/addon-docs/blocks';
24
41
  import type { AnatomyPartDefinition } from '@component-anatomy/core';
25
- export type AnatomyBlockProps = {
42
+ export type AnatomyTableProps = {
26
43
  /**
27
44
  * The CSF export to document — a story export, or the whole module export
28
45
  * of a CSF file to read the meta's parameters.
@@ -44,5 +61,22 @@ export type AnatomyBlockProps = {
44
61
  */
45
62
  sync?: boolean;
46
63
  };
47
- export declare const Anatomy: React.FC<AnatomyBlockProps>;
64
+ export declare const AnatomyTable: React.FC<AnatomyTableProps>;
65
+ export type AnatomyProps = AnatomyTableProps & {
66
+ /**
67
+ * Passed straight to the underlying `<Canvas>`. `'none'` (the default)
68
+ * drops the show-code button along with the panel it opens — chrome that
69
+ * mostly restates what the anatomy table already documents. Set explicitly
70
+ * to keep it, e.g. for a consumer relying on that source elsewhere.
71
+ */
72
+ sourceState?: 'hidden' | 'shown' | 'none';
73
+ };
74
+ /**
75
+ * The canvas and its anatomy table, paired and tightened for that specific
76
+ * combination ([#13](https://github.com/julien-deramond/component-anatomy/issues/13#issuecomment-5475936860)).
77
+ * For a hand-placed `<Canvas>` — a custom layout, or a source panel this
78
+ * block would otherwise hide — use `<AnatomyTable>` next to your own
79
+ * `<Canvas>` instead.
80
+ */
81
+ export declare const Anatomy: React.FC<AnatomyProps>;
48
82
  //# sourceMappingURL=blocks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"blocks.d.ts","sourceRoot":"","sources":["../src/blocks.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAGnD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,8BAA8B,CAAC;AACvD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAQrE,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;;OAOG;IACH,EAAE,CAAC,EAAE,EAAE,CAAC;IACR;;;OAGG;IACH,KAAK,CAAC,EAAE,qBAAqB,EAAE,CAAC;IAChC;;;;OAIG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAgBF,eAAO,MAAM,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAoH/C,CAAC"}
1
+ {"version":3,"file":"blocks.d.ts","sourceRoot":"","sources":["../src/blocks.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAGnD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,8BAA8B,CAAC;AACvD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAQrE,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;;OAOG;IACH,EAAE,CAAC,EAAE,EAAE,CAAC;IACR;;;OAGG;IACH,KAAK,CAAC,EAAE,qBAAqB,EAAE,CAAC;IAChC;;;;OAIG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAgBF,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA6HpD,CAAC;AAwBF,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG;IAC7C;;;;;OAKG;IACH,WAAW,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;CAC3C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,YAAY,CAa1C,CAAC"}
package/dist/blocks.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/blocks.tsx
2
2
  import React2, { useEffect, useState } from "react";
3
3
  import { addons } from "storybook/preview-api";
4
- import { Unstyled, useOf } from "@storybook/addon-docs/blocks";
4
+ import { Canvas, Unstyled, useOf } from "@storybook/addon-docs/blocks";
5
5
 
6
6
  // src/constants.ts
7
7
  var ADDON_ID = "component-anatomy";
@@ -28,7 +28,7 @@ var matchesStory = (eventStoryId, storyId) => !eventStoryId || !storyId || event
28
28
  // src/AnatomyTable.tsx
29
29
  import React from "react";
30
30
  import { useTheme } from "storybook/theming";
31
- var ACCENT_FALLBACK = "#4f46e5";
31
+ import { resolvePanelAccent } from "@component-anatomy/core";
32
32
  var FONT_BASE_FALLBACK = '"Nunito Sans", -apple-system, ".SFNSText-Regular", "San Francisco", BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif';
33
33
  var FONT_MONO_FALLBACK = 'ui-monospace, "Cascadia Code", "Fira Mono", monospace';
34
34
  function usePalette() {
@@ -38,10 +38,18 @@ function usePalette() {
38
38
  muted: theme.fgColor?.muted ?? "#6b7280",
39
39
  mutedBg: theme.bgColor?.muted ?? "rgba(0,0,0,0.06)",
40
40
  border: theme.borderColor?.default ?? "rgba(0,0,0,0.08)",
41
+ surfaceBg: theme.bgColor?.default ?? "#ffffff",
42
+ surfaceFg: theme.fgColor?.default ?? "#2e3438",
41
43
  fontBase: theme.typography?.fonts?.base ?? FONT_BASE_FALLBACK,
42
44
  fontMono: theme.typography?.fonts?.mono ?? FONT_MONO_FALLBACK
43
45
  };
44
46
  }
47
+ function useAccent(palette, preset, theme) {
48
+ return resolvePanelAccent(preset, theme, {
49
+ background: palette.surfaceBg,
50
+ foreground: palette.surfaceFg
51
+ });
52
+ }
45
53
  var makeStyles = (p) => ({
46
54
  container: {
47
55
  padding: "12px 16px",
@@ -121,12 +129,14 @@ var AnatomyMessage = ({ children }) => {
121
129
  var AnatomyTable = ({
122
130
  parts,
123
131
  activeId = null,
124
- accent = ACCENT_FALLBACK,
132
+ preset,
133
+ theme,
125
134
  onItemEnter,
126
135
  onItemLeave
127
136
  }) => {
128
137
  const palette = usePalette();
129
138
  const styles = makeStyles(palette);
139
+ const accent = useAccent(palette, preset, theme);
130
140
  return /* @__PURE__ */ React.createElement("div", { style: styles.container }, /* @__PURE__ */ React.createElement("ul", { style: styles.list, role: "list", "aria-label": "Anatomy parts" }, parts.map((part) => {
131
141
  const active = activeId === part.id;
132
142
  return /* @__PURE__ */ React.createElement(
@@ -170,7 +180,7 @@ function getChannelSafely() {
170
180
  return null;
171
181
  }
172
182
  }
173
- var Anatomy = ({ of, parts: partsProp, sync = true }) => {
183
+ var AnatomyTable2 = ({ of, parts: partsProp, sync = true }) => {
174
184
  const resolved = useOf(of ?? "story", ["story", "meta"]);
175
185
  const params = resolved.type === "meta" ? resolved.preparedMeta.parameters?.[PARAM_KEY] : resolved.story.parameters?.[PARAM_KEY];
176
186
  const storyId = resolved.type === "story" ? resolved.story.id : void 0;
@@ -207,7 +217,6 @@ var Anatomy = ({ of, parts: partsProp, sync = true }) => {
207
217
  };
208
218
  }, [wired, storyId]);
209
219
  const parts = staticParts ?? discovered;
210
- const accent = params?.theme?.accent ?? ACCENT_FALLBACK;
211
220
  const emitHover = (partId) => {
212
221
  if (!wired) return;
213
222
  getChannelSafely()?.emit(EVENTS.HOVER_ITEM, { storyId, partId });
@@ -223,20 +232,42 @@ var Anatomy = ({ of, parts: partsProp, sync = true }) => {
223
232
  return /* @__PURE__ */ React2.createElement(Unstyled, null, /* @__PURE__ */ React2.createElement(AnatomyMessage, null, "Anatomy is disabled for this story (", /* @__PURE__ */ React2.createElement(AnatomyCode, null, "anatomy.disable"), ")."));
224
233
  }
225
234
  if (parts.length === 0) {
226
- return /* @__PURE__ */ React2.createElement(Unstyled, null, /* @__PURE__ */ React2.createElement(AnatomyMessage, null, resolved.type === "meta" ? "No parts found. A meta has no canvas to discover parts from \u2014 pass a story to `of`, or list `parts` explicitly." : "No parts found. Auto-discovery reads the rendered story, so make sure it is on this page (e.g. with a `<Canvas of={\u2026} />` block above), or list `parts` explicitly."));
235
+ return /* @__PURE__ */ React2.createElement(Unstyled, null, /* @__PURE__ */ React2.createElement(AnatomyMessage, null, resolved.type === "meta" ? /* @__PURE__ */ React2.createElement(React2.Fragment, null, "No parts found. A meta has no canvas to discover parts from \u2014 pass a story to", " ", /* @__PURE__ */ React2.createElement(AnatomyCode, null, "of"), ", or list ", /* @__PURE__ */ React2.createElement(AnatomyCode, null, "parts"), " explicitly.") : /* @__PURE__ */ React2.createElement(React2.Fragment, null, "No parts found. Auto-discovery reads the rendered story, so make sure it is on this page (e.g. with a ", /* @__PURE__ */ React2.createElement(AnatomyCode, null, "<Canvas of={\u2026} />"), " block above), or list ", /* @__PURE__ */ React2.createElement(AnatomyCode, null, "parts"), " explicitly.")));
227
236
  }
228
237
  return /* @__PURE__ */ React2.createElement(Unstyled, null, /* @__PURE__ */ React2.createElement(
229
238
  AnatomyTable,
230
239
  {
231
240
  parts,
232
241
  activeId,
233
- accent,
242
+ preset: params?.preset,
243
+ theme: params?.theme,
234
244
  onItemEnter: emitHover,
235
245
  onItemLeave: emitLeave
236
246
  }
237
247
  ));
238
248
  };
249
+ var canvasGapStyleInjected = false;
250
+ function useCanvasGapStyle() {
251
+ useEffect(() => {
252
+ if (canvasGapStyleInjected || typeof document === "undefined") return;
253
+ canvasGapStyleInjected = true;
254
+ const style = document.createElement("style");
255
+ style.dataset.componentAnatomy = "anatomy-block";
256
+ style.textContent = ".ca-anatomy .sbdocs-preview { margin-bottom: 8px; }";
257
+ document.head.appendChild(style);
258
+ }, []);
259
+ }
260
+ var Anatomy = ({
261
+ of,
262
+ parts,
263
+ sync,
264
+ sourceState = "none"
265
+ }) => {
266
+ useCanvasGapStyle();
267
+ return /* @__PURE__ */ React2.createElement("div", { className: "ca-anatomy" }, /* @__PURE__ */ React2.createElement(Canvas, { of, sourceState }), /* @__PURE__ */ React2.createElement(AnatomyTable2, { of, parts, sync }));
268
+ };
239
269
  export {
240
- Anatomy
270
+ Anatomy,
271
+ AnatomyTable2 as AnatomyTable
241
272
  };
242
273
  //# sourceMappingURL=blocks.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/blocks.tsx", "../src/constants.ts", "../src/channel.ts", "../src/AnatomyTable.tsx"],
4
- "sourcesContent": ["/**\n * Docs blocks entry \u2014 the `<Anatomy>` block for MDX pages.\n *\n * Unlike the addon panel, this runs in the **preview iframe**, where\n * `storybook/manager-api` does not exist. It reaches the story's controller\n * over the addon channel instead: `Channel.emit` dispatches to local\n * listeners as well as across transports, so a block and the decorator that\n * mounted the story talk to each other directly, in-frame, with no extra\n * plumbing.\n *\n * ```mdx\n * import { Meta, Canvas } from '@storybook/addon-docs/blocks';\n * import { Anatomy } from '@component-anatomy/storybook/blocks';\n * import * as ButtonStories from './Button.stories';\n *\n * <Meta of={ButtonStories} />\n *\n * <Canvas of={ButtonStories.Anatomy} />\n * <Anatomy of={ButtonStories.Anatomy} />\n * ```\n */\nimport React, { useEffect, useState } from 'react';\nimport { addons } from 'storybook/preview-api';\nimport { Unstyled, useOf } from '@storybook/addon-docs/blocks';\nimport type { Of } from '@storybook/addon-docs/blocks';\nimport type { AnatomyPartDefinition } from '@component-anatomy/core';\n\nimport { EVENTS, PARAM_KEY } from './constants.js';\nimport { matchesStory } from './channel.js';\nimport type { PartEnterEvent, PartsEvent, StoryScopedEvent } from './channel.js';\nimport { ACCENT_FALLBACK, AnatomyCode, AnatomyMessage, AnatomyTable } from './AnatomyTable.js';\nimport type { AnatomyParameters } from './types.js';\n\nexport type AnatomyBlockProps = {\n /**\n * The CSF export to document \u2014 a story export, or the whole module export\n * of a CSF file to read the meta's parameters.\n *\n * Omit it on an attached docs page (one with `<Meta of={...} />`, or an\n * autodocs page) to fall back to the page's current story, mirroring how\n * the other docs blocks resolve `of`.\n */\n of?: Of;\n /**\n * Part list override. Skips both `parameters.anatomy.parts` and\n * auto-discovery \u2014 useful for a hand-curated subset in prose.\n */\n parts?: AnatomyPartDefinition[];\n /**\n * Two-way hover sync with the rendered story. Default: `true`. Set to\n * `false` for a purely static table (also skips auto-discovery, since that\n * arrives over the channel).\n */\n sync?: boolean;\n};\n\n/**\n * `addons.getChannel()` throws when no channel is installed. That should not\n * happen inside a rendered docs page, but an MDX page is user-authored\n * content and a throw here would blank the whole page \u2014 degrade to a static\n * table instead.\n */\nfunction getChannelSafely() {\n try {\n return addons.getChannel();\n } catch {\n return null;\n }\n}\n\nexport const Anatomy: React.FC<AnatomyBlockProps> = ({ of, parts: partsProp, sync = true }) => {\n const resolved = useOf(of ?? 'story', ['story', 'meta']);\n\n const params = (\n resolved.type === 'meta'\n ? resolved.preparedMeta.parameters?.[PARAM_KEY]\n : resolved.story.parameters?.[PARAM_KEY]\n ) as AnatomyParameters | undefined;\n\n // Only a story has a canvas to sync with. `of={SomeStories}` (a meta)\n // documents the component as a whole and can render static parts only.\n const storyId = resolved.type === 'story' ? resolved.story.id : undefined;\n\n const [discovered, setDiscovered] = useState<AnatomyPartDefinition[]>([]);\n const [activeId, setActiveId] = useState<string | null>(null);\n\n const staticParts = partsProp ?? params?.parts;\n // Hover sync is wired even when the parts are static \u2014 an explicit list\n // still wants the canvas to light up. Only discovery depends on the channel.\n const wired = sync && !!storyId;\n\n useEffect(() => {\n setDiscovered([]);\n setActiveId(null);\n if (!wired) return;\n\n const channel = getChannelSafely();\n if (!channel) return;\n\n const onParts = (event: PartsEvent) => {\n if (!matchesStory(event?.storyId, storyId)) return;\n setDiscovered(event.parts);\n };\n const onEnter = (event: PartEnterEvent) => {\n if (!matchesStory(event?.storyId, storyId)) return;\n setActiveId(event.partId);\n };\n const onLeave = (event: StoryScopedEvent = {}) => {\n if (!matchesStory(event?.storyId, storyId)) return;\n setActiveId(null);\n };\n\n channel.on(EVENTS.PARTS, onParts);\n channel.on(EVENTS.PART_ENTER, onEnter);\n channel.on(EVENTS.PART_LEAVE, onLeave);\n\n // The block usually mounts before the story below it finishes rendering;\n // the request covers the other order.\n channel.emit(EVENTS.PARTS_REQUEST, { storyId } satisfies StoryScopedEvent);\n\n return () => {\n channel.off(EVENTS.PARTS, onParts);\n channel.off(EVENTS.PART_ENTER, onEnter);\n channel.off(EVENTS.PART_LEAVE, onLeave);\n };\n }, [wired, storyId]);\n\n const parts = staticParts ?? discovered;\n const accent = params?.theme?.accent ?? ACCENT_FALLBACK;\n\n const emitHover = (partId: string) => {\n if (!wired) return;\n getChannelSafely()?.emit(EVENTS.HOVER_ITEM, { storyId, partId });\n };\n const emitLeave = () => {\n if (!wired) return;\n getChannelSafely()?.emit(EVENTS.LEAVE_ITEM, { storyId } satisfies StoryScopedEvent);\n };\n\n // `Unstyled` keeps the docs page's prose CSS (`.sbdocs` restyles ul/li/p/\n // code) from reaching the table.\n if (!params && !partsProp) {\n return (\n <Unstyled>\n <AnatomyMessage>\n No anatomy configured. Add <AnatomyCode>parameters.anatomy</AnatomyCode> to the story you\n pass to <AnatomyCode>of</AnatomyCode>, or pass a{' '}\n <AnatomyCode>parts</AnatomyCode> list to this block.\n </AnatomyMessage>\n </Unstyled>\n );\n }\n\n if (params?.disable && !partsProp) {\n return (\n <Unstyled>\n <AnatomyMessage>\n Anatomy is disabled for this story (<AnatomyCode>anatomy.disable</AnatomyCode>).\n </AnatomyMessage>\n </Unstyled>\n );\n }\n\n if (parts.length === 0) {\n return (\n <Unstyled>\n <AnatomyMessage>\n {resolved.type === 'meta'\n ? 'No parts found. A meta has no canvas to discover parts from \u2014 pass a story to `of`, or list `parts` explicitly.'\n : 'No parts found. Auto-discovery reads the rendered story, so make sure it is on this page (e.g. with a `<Canvas of={\u2026} />` block above), or list `parts` explicitly.'}\n </AnatomyMessage>\n </Unstyled>\n );\n }\n\n return (\n <Unstyled>\n <AnatomyTable\n parts={parts}\n activeId={activeId}\n accent={accent}\n onItemEnter={emitHover}\n onItemLeave={emitLeave}\n />\n </Unstyled>\n );\n};\n", "export const ADDON_ID = 'component-anatomy';\nexport const PANEL_ID = `${ADDON_ID}/panel`;\n\n/** Story parameter key: `parameters.anatomy = { ... }` */\nexport const PARAM_KEY = 'anatomy';\n\n/**\n * Channel events used to sync the manager panel \u2014 and the `<Anatomy>` MDX doc\n * block, which runs in the preview iframe \u2014 with the story canvas.\n *\n * Every payload carries the `storyId` it concerns; see `channel.ts` for the\n * payload types and the `matchesStory` filter each listener applies.\n */\nexport const EVENTS = {\n /** preview \u2192 consumers: a part became active in the canvas (hover/programmatic). */\n PART_ENTER: `${ADDON_ID}/part-enter`,\n /** preview \u2192 consumers: no part is active anymore. */\n PART_LEAVE: `${ADDON_ID}/part-leave`,\n /** preview \u2192 consumers: resolved part list for a story. */\n PARTS: `${ADDON_ID}/parts`,\n /** consumers \u2192 preview: the user hovers/focuses a panel entry. */\n HOVER_ITEM: `${ADDON_ID}/hover-item`,\n /** consumers \u2192 preview: the user left a panel entry. */\n LEAVE_ITEM: `${ADDON_ID}/leave-item`,\n /** consumers \u2192 preview: a panel/block mounted and wants the current part list. */\n PARTS_REQUEST: `${ADDON_ID}/parts-request`,\n} as const;\n", "/**\n * Shared channel payload contract between the preview decorator, the manager\n * panel, and the MDX doc block.\n *\n * Every payload carries the `storyId` it refers to. In story view this is\n * redundant \u2014 only one story is mounted \u2014 but a docs page mounts *many*\n * stories at once, each with its own controller, and each `<Anatomy>` block\n * must talk to exactly one of them. Without addressing, hovering a part in\n * one block highlights the matching part in every other story on the page.\n */\nimport type { AnatomyPartDefinition } from '@component-anatomy/core';\n\n/** preview \u2192 consumers: the resolved part list for one story. */\nexport type PartsEvent = { storyId?: string; parts: AnatomyPartDefinition[] };\n\n/** preview \u2192 consumers: a part became active in that story's canvas. */\nexport type PartEnterEvent = { storyId?: string; partId: string };\n\n/** consumer \u2192 preview: highlight this part in that story's canvas. */\nexport type HoverItemEvent = { storyId?: string; partId: string };\n\n/** Payload for the events that only need to name a story. */\nexport type StoryScopedEvent = { storyId?: string };\n\n/**\n * Whether an event addressed to `eventStoryId` concerns `storyId`.\n *\n * A missing id on *either* side matches everything. That keeps the protocol\n * backward compatible: a manager panel from a newer build still understands\n * an older preview bundle that emits unaddressed events, and vice versa.\n */\nexport const matchesStory = (\n eventStoryId: string | undefined,\n storyId: string | undefined\n): boolean => !eventStoryId || !storyId || eventStoryId === storyId;\n", "/**\n * The anatomy part table \u2014 pure presentation, no Storybook imports.\n *\n * This module is deliberately free of `storybook/manager-api` and\n * `storybook/preview-api`: it is bundled into *both* the manager entry (the\n * addon panel) and the blocks entry (the `<Anatomy>` MDX doc block), which\n * run in different runtimes and cannot share either of those APIs. Only the\n * data acquisition differs between the two; the markup must not.\n */\nimport React from 'react';\nimport { useTheme } from 'storybook/theming';\nimport type { AnatomyPartDefinition } from '@component-anatomy/core';\n\n/** Fallback accent when the story sets no `anatomy.theme.accent`. */\nexport const ACCENT_FALLBACK = '#4f46e5';\n\n/**\n * The subset of Storybook's theme this table reads. Typed loosely on purpose:\n * `useTheme()` resolves to an empty `Theme` interface (Emotion augmentation),\n * and outside a ThemeProvider it returns `{}` \u2014 so every read is optional and\n * every value has a literal fallback below.\n */\ntype PartialStorybookTheme = {\n fgColor?: { default?: string; muted?: string };\n bgColor?: { muted?: string };\n borderColor?: { default?: string };\n typography?: { fonts?: { base?: string; mono?: string } };\n};\n\nconst FONT_BASE_FALLBACK =\n '\"Nunito Sans\", -apple-system, \".SFNSText-Regular\", \"San Francisco\", BlinkMacSystemFont, \"Segoe UI\", \"Helvetica Neue\", Helvetica, Arial, sans-serif';\nconst FONT_MONO_FALLBACK = 'ui-monospace, \"Cascadia Code\", \"Fira Mono\", monospace';\n\n/**\n * Resolves the table's chrome colors from Storybook's theme so the same\n * markup reads correctly in the manager panel *and* in a docs page under a\n * dark theme. The accent is intentionally not theme-derived \u2014 it is the\n * addon's identity color and stays stable unless a story overrides it.\n */\nfunction usePalette() {\n const theme = useTheme() as PartialStorybookTheme;\n return {\n text: theme.fgColor?.default,\n muted: theme.fgColor?.muted ?? '#6b7280',\n mutedBg: theme.bgColor?.muted ?? 'rgba(0,0,0,0.06)',\n border: theme.borderColor?.default ?? 'rgba(0,0,0,0.08)',\n fontBase: theme.typography?.fonts?.base ?? FONT_BASE_FALLBACK,\n fontMono: theme.typography?.fonts?.mono ?? FONT_MONO_FALLBACK,\n };\n}\n\ntype Palette = ReturnType<typeof usePalette>;\n\nconst makeStyles = (p: Palette): Record<string, React.CSSProperties> => ({\n container: {\n padding: '12px 16px',\n fontFamily: p.fontBase,\n fontSize: 13,\n lineHeight: 1.5,\n color: p.text,\n },\n empty: {\n color: p.muted,\n margin: 0,\n },\n code: {\n fontFamily: p.fontMono,\n fontSize: '0.85em',\n background: p.mutedBg,\n borderRadius: 3,\n padding: '1px 5px',\n },\n list: {\n display: 'flex',\n flexDirection: 'column',\n gap: 2,\n margin: 0,\n padding: 0,\n listStyle: 'none',\n },\n entry: {\n padding: '8px 10px',\n borderRadius: 6,\n border: '1px solid transparent',\n cursor: 'default',\n outline: 'none',\n transition: 'background 120ms ease, border-color 120ms ease',\n },\n header: {\n display: 'flex',\n alignItems: 'center',\n gap: 8,\n },\n indicator: {\n width: 8,\n height: 8,\n borderRadius: '50%',\n border: `2px solid ${p.border}`,\n flexShrink: 0,\n transition: 'background 120ms ease, border-color 120ms ease',\n },\n name: {\n fontWeight: 700,\n flex: 1,\n },\n id: {\n fontFamily: p.fontMono,\n fontSize: 10,\n padding: '1px 6px',\n borderRadius: 4,\n background: p.mutedBg,\n border: `1px solid ${p.border}`,\n color: p.muted,\n flexShrink: 0,\n },\n description: {\n margin: '4px 0 0',\n paddingLeft: 16,\n color: p.muted,\n },\n});\n\n/**\n * An inline `<code>` styled for the surrounding message text. Exported so\n * both runtimes can compose their own empty-state copy without duplicating\n * the style object.\n */\nexport const AnatomyCode: React.FC<{ children: React.ReactNode }> = ({ children }) => {\n const styles = makeStyles(usePalette());\n return <code style={styles.code}>{children}</code>;\n};\n\n/** A padded, muted paragraph \u2014 used for every \"nothing to show\" state. */\nexport const AnatomyMessage: React.FC<{ children: React.ReactNode }> = ({ children }) => {\n const styles = makeStyles(usePalette());\n return (\n <div style={styles.container}>\n <p style={styles.empty}>{children}</p>\n </div>\n );\n};\n\nexport type AnatomyTableProps = {\n /** Parts to list, in the order they should be shown. */\n parts: AnatomyPartDefinition[];\n /** Id of the part currently highlighted in the canvas, if any. */\n activeId?: string | null;\n /** Accent color for the active state. Defaults to {@link ACCENT_FALLBACK}. */\n accent?: string;\n /** Called when the user hovers or focuses an entry. */\n onItemEnter?: (partId: string) => void;\n /** Called when the user leaves or blurs an entry. */\n onItemLeave?: () => void;\n};\n\nexport const AnatomyTable: React.FC<AnatomyTableProps> = ({\n parts,\n activeId = null,\n accent = ACCENT_FALLBACK,\n onItemEnter,\n onItemLeave,\n}) => {\n const palette = usePalette();\n const styles = makeStyles(palette);\n\n return (\n <div style={styles.container}>\n <ul style={styles.list} role=\"list\" aria-label=\"Anatomy parts\">\n {parts.map((part) => {\n const active = activeId === part.id;\n return (\n <li\n key={part.id}\n role=\"listitem\"\n tabIndex={0}\n aria-label={part.name}\n style={{\n ...styles.entry,\n background: active ? `color-mix(in srgb, ${accent} 7%, transparent)` : undefined,\n borderColor: active\n ? `color-mix(in srgb, ${accent} 20%, transparent)`\n : 'transparent',\n }}\n onMouseEnter={() => onItemEnter?.(part.id)}\n onMouseLeave={() => onItemLeave?.()}\n onFocus={() => onItemEnter?.(part.id)}\n onBlur={() => onItemLeave?.()}\n >\n <div style={styles.header}>\n <span\n aria-hidden=\"true\"\n style={{\n ...styles.indicator,\n background: active ? accent : undefined,\n borderColor: active ? accent : palette.border,\n }}\n />\n <span style={{ ...styles.name, color: active ? accent : undefined }}>\n {part.name}\n </span>\n <code style={styles.id}>{part.id}</code>\n </div>\n {part.description && <p style={styles.description}>{part.description}</p>}\n </li>\n );\n })}\n </ul>\n </div>\n );\n};\n"],
5
- "mappings": "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",
6
- "names": ["React", "React"]
4
+ "sourcesContent": ["/**\n * Docs blocks entry \u2014 the `<Anatomy>` and `<AnatomyTable>` blocks for MDX\n * pages.\n *\n * Unlike the addon panel, this runs in the **preview iframe**, where\n * `storybook/manager-api` does not exist. It reaches the story's controller\n * over the addon channel instead: `Channel.emit` dispatches to local\n * listeners as well as across transports, so a block and the decorator that\n * mounted the story talk to each other directly, in-frame, with no extra\n * plumbing.\n *\n * `<Anatomy>` is the batteries-included block \u2014 canvas and table together,\n * laid out for that pairing specifically ([#13](https://github.com/julien-deramond/component-anatomy/issues/13#issuecomment-5475936860)):\n *\n * ```mdx\n * import { Meta } from '@storybook/addon-docs/blocks';\n * import { Anatomy } from '@component-anatomy/storybook/blocks';\n * import * as ButtonStories from './Button.stories';\n *\n * <Meta of={ButtonStories} />\n *\n * <Anatomy of={ButtonStories.Anatomy} />\n * ```\n *\n * `<AnatomyTable>` is the table alone, for a hand-placed `<Canvas>` or a\n * custom layout:\n *\n * ```mdx\n * import { Canvas, Meta } from '@storybook/addon-docs/blocks';\n * import { AnatomyTable } from '@component-anatomy/storybook/blocks';\n * import * as ButtonStories from './Button.stories';\n *\n * <Meta of={ButtonStories} />\n *\n * <Canvas of={ButtonStories.Anatomy} />\n * <AnatomyTable of={ButtonStories.Anatomy} />\n * ```\n */\nimport React, { useEffect, useState } from 'react';\nimport { addons } from 'storybook/preview-api';\nimport { Canvas, Unstyled, useOf } from '@storybook/addon-docs/blocks';\nimport type { Of } from '@storybook/addon-docs/blocks';\nimport type { AnatomyPartDefinition } from '@component-anatomy/core';\n\nimport { EVENTS, PARAM_KEY } from './constants.js';\nimport { matchesStory } from './channel.js';\nimport type { PartEnterEvent, PartsEvent, StoryScopedEvent } from './channel.js';\nimport { AnatomyCode, AnatomyMessage, AnatomyTable as AnatomyTablePrimitive } from './AnatomyTable.js';\nimport type { AnatomyParameters } from './types.js';\n\nexport type AnatomyTableProps = {\n /**\n * The CSF export to document \u2014 a story export, or the whole module export\n * of a CSF file to read the meta's parameters.\n *\n * Omit it on an attached docs page (one with `<Meta of={...} />`, or an\n * autodocs page) to fall back to the page's current story, mirroring how\n * the other docs blocks resolve `of`.\n */\n of?: Of;\n /**\n * Part list override. Skips both `parameters.anatomy.parts` and\n * auto-discovery \u2014 useful for a hand-curated subset in prose.\n */\n parts?: AnatomyPartDefinition[];\n /**\n * Two-way hover sync with the rendered story. Default: `true`. Set to\n * `false` for a purely static table (also skips auto-discovery, since that\n * arrives over the channel).\n */\n sync?: boolean;\n};\n\n/**\n * `addons.getChannel()` throws when no channel is installed. That should not\n * happen inside a rendered docs page, but an MDX page is user-authored\n * content and a throw here would blank the whole page \u2014 degrade to a static\n * table instead.\n */\nfunction getChannelSafely() {\n try {\n return addons.getChannel();\n } catch {\n return null;\n }\n}\n\nexport const AnatomyTable: React.FC<AnatomyTableProps> = ({ of, parts: partsProp, sync = true }) => {\n const resolved = useOf(of ?? 'story', ['story', 'meta']);\n\n const params = (\n resolved.type === 'meta'\n ? resolved.preparedMeta.parameters?.[PARAM_KEY]\n : resolved.story.parameters?.[PARAM_KEY]\n ) as AnatomyParameters | undefined;\n\n // Only a story has a canvas to sync with. `of={SomeStories}` (a meta)\n // documents the component as a whole and can render static parts only.\n const storyId = resolved.type === 'story' ? resolved.story.id : undefined;\n\n const [discovered, setDiscovered] = useState<AnatomyPartDefinition[]>([]);\n const [activeId, setActiveId] = useState<string | null>(null);\n\n const staticParts = partsProp ?? params?.parts;\n // Hover sync is wired even when the parts are static \u2014 an explicit list\n // still wants the canvas to light up. Only discovery depends on the channel.\n const wired = sync && !!storyId;\n\n useEffect(() => {\n setDiscovered([]);\n setActiveId(null);\n if (!wired) return;\n\n const channel = getChannelSafely();\n if (!channel) return;\n\n const onParts = (event: PartsEvent) => {\n if (!matchesStory(event?.storyId, storyId)) return;\n setDiscovered(event.parts);\n };\n const onEnter = (event: PartEnterEvent) => {\n if (!matchesStory(event?.storyId, storyId)) return;\n setActiveId(event.partId);\n };\n const onLeave = (event: StoryScopedEvent = {}) => {\n if (!matchesStory(event?.storyId, storyId)) return;\n setActiveId(null);\n };\n\n channel.on(EVENTS.PARTS, onParts);\n channel.on(EVENTS.PART_ENTER, onEnter);\n channel.on(EVENTS.PART_LEAVE, onLeave);\n\n // The block usually mounts before the story below it finishes rendering;\n // the request covers the other order.\n channel.emit(EVENTS.PARTS_REQUEST, { storyId } satisfies StoryScopedEvent);\n\n return () => {\n channel.off(EVENTS.PARTS, onParts);\n channel.off(EVENTS.PART_ENTER, onEnter);\n channel.off(EVENTS.PART_LEAVE, onLeave);\n };\n }, [wired, storyId]);\n\n const parts = staticParts ?? discovered;\n\n const emitHover = (partId: string) => {\n if (!wired) return;\n getChannelSafely()?.emit(EVENTS.HOVER_ITEM, { storyId, partId });\n };\n const emitLeave = () => {\n if (!wired) return;\n getChannelSafely()?.emit(EVENTS.LEAVE_ITEM, { storyId } satisfies StoryScopedEvent);\n };\n\n // `Unstyled` keeps the docs page's prose CSS (`.sbdocs` restyles ul/li/p/\n // code) from reaching the table.\n if (!params && !partsProp) {\n return (\n <Unstyled>\n <AnatomyMessage>\n No anatomy configured. Add <AnatomyCode>parameters.anatomy</AnatomyCode> to the story you\n pass to <AnatomyCode>of</AnatomyCode>, or pass a{' '}\n <AnatomyCode>parts</AnatomyCode> list to this block.\n </AnatomyMessage>\n </Unstyled>\n );\n }\n\n if (params?.disable && !partsProp) {\n return (\n <Unstyled>\n <AnatomyMessage>\n Anatomy is disabled for this story (<AnatomyCode>anatomy.disable</AnatomyCode>).\n </AnatomyMessage>\n </Unstyled>\n );\n }\n\n if (parts.length === 0) {\n return (\n <Unstyled>\n <AnatomyMessage>\n {resolved.type === 'meta' ? (\n <>\n No parts found. A meta has no canvas to discover parts from \u2014 pass a story to{' '}\n <AnatomyCode>of</AnatomyCode>, or list <AnatomyCode>parts</AnatomyCode> explicitly.\n </>\n ) : (\n <>\n No parts found. Auto-discovery reads the rendered story, so make sure it is on this\n page (e.g. with a <AnatomyCode>{'<Canvas of={\u2026} />'}</AnatomyCode> block above), or\n list <AnatomyCode>parts</AnatomyCode> explicitly.\n </>\n )}\n </AnatomyMessage>\n </Unstyled>\n );\n }\n\n return (\n <Unstyled>\n <AnatomyTablePrimitive\n parts={parts}\n activeId={activeId}\n preset={params?.preset}\n theme={params?.theme}\n onItemEnter={emitHover}\n onItemLeave={emitLeave}\n />\n </Unstyled>\n );\n};\n\n/**\n * Injects the rule that tightens `<Anatomy>`'s canvas-to-table gap, once per\n * page \u2014 a module-level flag rather than `useState`, so mounting a second\n * `<Anatomy>` block doesn't append the rule again. Scoped under\n * `.ca-anatomy` so it never touches a bare `<Canvas>` placed by the consumer.\n *\n * Self-injected rather than asked of the consumer's `preview-head.html`: the\n * block owns both elements it is bridging, so the gap between them is its\n * concern, not config the consumer should have to wire up.\n */\nlet canvasGapStyleInjected = false;\nfunction useCanvasGapStyle() {\n useEffect(() => {\n if (canvasGapStyleInjected || typeof document === 'undefined') return;\n canvasGapStyleInjected = true;\n const style = document.createElement('style');\n style.dataset.componentAnatomy = 'anatomy-block';\n style.textContent = '.ca-anatomy .sbdocs-preview { margin-bottom: 8px; }';\n document.head.appendChild(style);\n }, []);\n}\n\nexport type AnatomyProps = AnatomyTableProps & {\n /**\n * Passed straight to the underlying `<Canvas>`. `'none'` (the default)\n * drops the show-code button along with the panel it opens \u2014 chrome that\n * mostly restates what the anatomy table already documents. Set explicitly\n * to keep it, e.g. for a consumer relying on that source elsewhere.\n */\n sourceState?: 'hidden' | 'shown' | 'none';\n};\n\n/**\n * The canvas and its anatomy table, paired and tightened for that specific\n * combination ([#13](https://github.com/julien-deramond/component-anatomy/issues/13#issuecomment-5475936860)).\n * For a hand-placed `<Canvas>` \u2014 a custom layout, or a source panel this\n * block would otherwise hide \u2014 use `<AnatomyTable>` next to your own\n * `<Canvas>` instead.\n */\nexport const Anatomy: React.FC<AnatomyProps> = ({\n of,\n parts,\n sync,\n sourceState = 'none',\n}) => {\n useCanvasGapStyle();\n return (\n <div className=\"ca-anatomy\">\n <Canvas of={of} sourceState={sourceState} />\n <AnatomyTable of={of} parts={parts} sync={sync} />\n </div>\n );\n};\n", "export const ADDON_ID = 'component-anatomy';\nexport const PANEL_ID = `${ADDON_ID}/panel`;\n\n/** Story parameter key: `parameters.anatomy = { ... }` */\nexport const PARAM_KEY = 'anatomy';\n\n/**\n * Channel events used to sync the manager panel \u2014 and the `<Anatomy>` MDX doc\n * block, which runs in the preview iframe \u2014 with the story canvas.\n *\n * Every payload carries the `storyId` it concerns; see `channel.ts` for the\n * payload types and the `matchesStory` filter each listener applies.\n */\nexport const EVENTS = {\n /** preview \u2192 consumers: a part became active in the canvas (hover/programmatic). */\n PART_ENTER: `${ADDON_ID}/part-enter`,\n /** preview \u2192 consumers: no part is active anymore. */\n PART_LEAVE: `${ADDON_ID}/part-leave`,\n /** preview \u2192 consumers: resolved part list for a story. */\n PARTS: `${ADDON_ID}/parts`,\n /** consumers \u2192 preview: the user hovers/focuses a panel entry. */\n HOVER_ITEM: `${ADDON_ID}/hover-item`,\n /** consumers \u2192 preview: the user left a panel entry. */\n LEAVE_ITEM: `${ADDON_ID}/leave-item`,\n /** consumers \u2192 preview: a panel/block mounted and wants the current part list. */\n PARTS_REQUEST: `${ADDON_ID}/parts-request`,\n} as const;\n", "/**\n * Shared channel payload contract between the preview decorator, the manager\n * panel, and the MDX doc block.\n *\n * Every payload carries the `storyId` it refers to. In story view this is\n * redundant \u2014 only one story is mounted \u2014 but a docs page mounts *many*\n * stories at once, each with its own controller, and each `<Anatomy>` block\n * must talk to exactly one of them. Without addressing, hovering a part in\n * one block highlights the matching part in every other story on the page.\n */\nimport type { AnatomyPartDefinition } from '@component-anatomy/core';\n\n/** preview \u2192 consumers: the resolved part list for one story. */\nexport type PartsEvent = { storyId?: string; parts: AnatomyPartDefinition[] };\n\n/** preview \u2192 consumers: a part became active in that story's canvas. */\nexport type PartEnterEvent = { storyId?: string; partId: string };\n\n/** consumer \u2192 preview: highlight this part in that story's canvas. */\nexport type HoverItemEvent = { storyId?: string; partId: string };\n\n/** Payload for the events that only need to name a story. */\nexport type StoryScopedEvent = { storyId?: string };\n\n/**\n * Whether an event addressed to `eventStoryId` concerns `storyId`.\n *\n * A missing id on *either* side matches everything. That keeps the protocol\n * backward compatible: a manager panel from a newer build still understands\n * an older preview bundle that emits unaddressed events, and vice versa.\n */\nexport const matchesStory = (\n eventStoryId: string | undefined,\n storyId: string | undefined\n): boolean => !eventStoryId || !storyId || eventStoryId === storyId;\n", "/**\n * The anatomy part table \u2014 pure presentation, no Storybook imports.\n *\n * This module is deliberately free of `storybook/manager-api` and\n * `storybook/preview-api`: it is bundled into *both* the manager entry (the\n * addon panel) and the blocks entry (the `<Anatomy>` MDX doc block), which\n * run in different runtimes and cannot share either of those APIs. Only the\n * data acquisition differs between the two; the markup must not.\n */\nimport React from 'react';\nimport { useTheme } from 'storybook/theming';\nimport { resolvePanelAccent } from '@component-anatomy/core';\nimport type {\n AnatomyPartDefinition,\n AnatomyPresetName,\n AnatomyTheme,\n} from '@component-anatomy/core';\n\n/**\n * The subset of Storybook's theme this table reads. Typed loosely on purpose:\n * `useTheme()` resolves to an empty `Theme` interface (Emotion augmentation),\n * and outside a ThemeProvider it returns `{}` \u2014 so every read is optional and\n * every value has a literal fallback below.\n */\ntype PartialStorybookTheme = {\n fgColor?: { default?: string; muted?: string };\n bgColor?: { default?: string; muted?: string };\n borderColor?: { default?: string };\n typography?: { fonts?: { base?: string; mono?: string } };\n};\n\nconst FONT_BASE_FALLBACK =\n '\"Nunito Sans\", -apple-system, \".SFNSText-Regular\", \"San Francisco\", BlinkMacSystemFont, \"Segoe UI\", \"Helvetica Neue\", Helvetica, Arial, sans-serif';\nconst FONT_MONO_FALLBACK = 'ui-monospace, \"Cascadia Code\", \"Fira Mono\", monospace';\n\n/**\n * Resolves the table's chrome colors from Storybook's theme so the same\n * markup reads correctly in the manager panel *and* in a docs page under a\n * dark theme.\n *\n * `surfaceBg`/`surfaceFg` are not painted anywhere \u2014 they are what the accent\n * is measured against in {@link useAccent}. They carry Storybook's own light\n * defaults as fallbacks so the measurement stays meaningful outside a\n * ThemeProvider, where `useTheme()` returns `{}`.\n */\nfunction usePalette() {\n const theme = useTheme() as PartialStorybookTheme;\n return {\n text: theme.fgColor?.default,\n muted: theme.fgColor?.muted ?? '#6b7280',\n mutedBg: theme.bgColor?.muted ?? 'rgba(0,0,0,0.06)',\n border: theme.borderColor?.default ?? 'rgba(0,0,0,0.08)',\n surfaceBg: theme.bgColor?.default ?? '#ffffff',\n surfaceFg: theme.fgColor?.default ?? '#2e3438',\n fontBase: theme.typography?.fonts?.base ?? FONT_BASE_FALLBACK,\n fontMono: theme.typography?.fonts?.mono ?? FONT_MONO_FALLBACK,\n };\n}\n\ntype Palette = ReturnType<typeof usePalette>;\n\n/**\n * The accent for the active row \u2014 the story's own `{ preset, theme }`, so the\n * table agrees with the overlays it describes, lifted to WCAG AA against the\n * surface it is painted on when the two disagree.\n *\n * The check is not optional polish: `preset: 'contrast'` is black on yellow\n * and the Storybook manager is dark by default, so the preset that exists for\n * users who need contrast is exactly the one that would land at 1.3:1 here.\n */\nfunction useAccent(palette: Palette, preset?: AnatomyPresetName, theme?: AnatomyTheme): string {\n return resolvePanelAccent(preset, theme, {\n background: palette.surfaceBg,\n foreground: palette.surfaceFg,\n });\n}\n\nconst makeStyles = (p: Palette): Record<string, React.CSSProperties> => ({\n container: {\n padding: '12px 16px',\n fontFamily: p.fontBase,\n fontSize: 13,\n lineHeight: 1.5,\n color: p.text,\n },\n empty: {\n color: p.muted,\n margin: 0,\n },\n code: {\n fontFamily: p.fontMono,\n fontSize: '0.85em',\n background: p.mutedBg,\n borderRadius: 3,\n padding: '1px 5px',\n },\n list: {\n display: 'flex',\n flexDirection: 'column',\n gap: 2,\n margin: 0,\n padding: 0,\n listStyle: 'none',\n },\n entry: {\n padding: '8px 10px',\n borderRadius: 6,\n border: '1px solid transparent',\n cursor: 'default',\n outline: 'none',\n transition: 'background 120ms ease, border-color 120ms ease',\n },\n header: {\n display: 'flex',\n alignItems: 'center',\n gap: 8,\n },\n indicator: {\n width: 8,\n height: 8,\n borderRadius: '50%',\n border: `2px solid ${p.border}`,\n flexShrink: 0,\n transition: 'background 120ms ease, border-color 120ms ease',\n },\n name: {\n fontWeight: 700,\n flex: 1,\n },\n id: {\n fontFamily: p.fontMono,\n fontSize: 10,\n padding: '1px 6px',\n borderRadius: 4,\n background: p.mutedBg,\n border: `1px solid ${p.border}`,\n color: p.muted,\n flexShrink: 0,\n },\n description: {\n margin: '4px 0 0',\n paddingLeft: 16,\n color: p.muted,\n },\n});\n\n/**\n * An inline `<code>` styled for the surrounding message text. Exported so\n * both runtimes can compose their own empty-state copy without duplicating\n * the style object.\n */\nexport const AnatomyCode: React.FC<{ children: React.ReactNode }> = ({ children }) => {\n const styles = makeStyles(usePalette());\n return <code style={styles.code}>{children}</code>;\n};\n\n/** A padded, muted paragraph \u2014 used for every \"nothing to show\" state. */\nexport const AnatomyMessage: React.FC<{ children: React.ReactNode }> = ({ children }) => {\n const styles = makeStyles(usePalette());\n return (\n <div style={styles.container}>\n <p style={styles.empty}>{children}</p>\n </div>\n );\n};\n\nexport type AnatomyTableProps = {\n /** Parts to list, in the order they should be shown. */\n parts: AnatomyPartDefinition[];\n /** Id of the part currently highlighted in the canvas, if any. */\n activeId?: string | null;\n /** The story's `anatomy.preset` \u2014 the active row follows it. */\n preset?: AnatomyPresetName;\n /** The story's `anatomy.theme` \u2014 token overrides on top of the preset. */\n theme?: AnatomyTheme;\n /** Called when the user hovers or focuses an entry. */\n onItemEnter?: (partId: string) => void;\n /** Called when the user leaves or blurs an entry. */\n onItemLeave?: () => void;\n};\n\nexport const AnatomyTable: React.FC<AnatomyTableProps> = ({\n parts,\n activeId = null,\n preset,\n theme,\n onItemEnter,\n onItemLeave,\n}) => {\n const palette = usePalette();\n const styles = makeStyles(palette);\n const accent = useAccent(palette, preset, theme);\n\n return (\n <div style={styles.container}>\n <ul style={styles.list} role=\"list\" aria-label=\"Anatomy parts\">\n {parts.map((part) => {\n const active = activeId === part.id;\n return (\n <li\n key={part.id}\n role=\"listitem\"\n tabIndex={0}\n aria-label={part.name}\n style={{\n ...styles.entry,\n background: active ? `color-mix(in srgb, ${accent} 7%, transparent)` : undefined,\n borderColor: active\n ? `color-mix(in srgb, ${accent} 20%, transparent)`\n : 'transparent',\n }}\n onMouseEnter={() => onItemEnter?.(part.id)}\n onMouseLeave={() => onItemLeave?.()}\n onFocus={() => onItemEnter?.(part.id)}\n onBlur={() => onItemLeave?.()}\n >\n <div style={styles.header}>\n <span\n aria-hidden=\"true\"\n style={{\n ...styles.indicator,\n background: active ? accent : undefined,\n borderColor: active ? accent : palette.border,\n }}\n />\n <span style={{ ...styles.name, color: active ? accent : undefined }}>\n {part.name}\n </span>\n <code style={styles.id}>{part.id}</code>\n </div>\n {part.description && <p style={styles.description}>{part.description}</p>}\n </li>\n );\n })}\n </ul>\n </div>\n );\n};\n"],
5
+ "mappings": "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",
6
+ "names": ["React", "AnatomyTable", "React"]
7
7
  }
package/dist/manager.js CHANGED
@@ -32,7 +32,7 @@ var matchesStory = (eventStoryId, storyId) => !eventStoryId || !storyId || event
32
32
  // src/AnatomyTable.tsx
33
33
  import React from "react";
34
34
  import { useTheme } from "storybook/theming";
35
- var ACCENT_FALLBACK = "#4f46e5";
35
+ import { resolvePanelAccent } from "@component-anatomy/core";
36
36
  var FONT_BASE_FALLBACK = '"Nunito Sans", -apple-system, ".SFNSText-Regular", "San Francisco", BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif';
37
37
  var FONT_MONO_FALLBACK = 'ui-monospace, "Cascadia Code", "Fira Mono", monospace';
38
38
  function usePalette() {
@@ -42,10 +42,18 @@ function usePalette() {
42
42
  muted: theme.fgColor?.muted ?? "#6b7280",
43
43
  mutedBg: theme.bgColor?.muted ?? "rgba(0,0,0,0.06)",
44
44
  border: theme.borderColor?.default ?? "rgba(0,0,0,0.08)",
45
+ surfaceBg: theme.bgColor?.default ?? "#ffffff",
46
+ surfaceFg: theme.fgColor?.default ?? "#2e3438",
45
47
  fontBase: theme.typography?.fonts?.base ?? FONT_BASE_FALLBACK,
46
48
  fontMono: theme.typography?.fonts?.mono ?? FONT_MONO_FALLBACK
47
49
  };
48
50
  }
51
+ function useAccent(palette, preset, theme) {
52
+ return resolvePanelAccent(preset, theme, {
53
+ background: palette.surfaceBg,
54
+ foreground: palette.surfaceFg
55
+ });
56
+ }
49
57
  var makeStyles = (p) => ({
50
58
  container: {
51
59
  padding: "12px 16px",
@@ -125,12 +133,14 @@ var AnatomyMessage = ({ children }) => {
125
133
  var AnatomyTable = ({
126
134
  parts,
127
135
  activeId = null,
128
- accent = ACCENT_FALLBACK,
136
+ preset,
137
+ theme,
129
138
  onItemEnter,
130
139
  onItemLeave
131
140
  }) => {
132
141
  const palette = usePalette();
133
142
  const styles = makeStyles(palette);
143
+ const accent = useAccent(palette, preset, theme);
134
144
  return /* @__PURE__ */ React.createElement("div", { style: styles.container }, /* @__PURE__ */ React.createElement("ul", { style: styles.list, role: "list", "aria-label": "Anatomy parts" }, parts.map((part) => {
135
145
  const active = activeId === part.id;
136
146
  return /* @__PURE__ */ React.createElement(
@@ -196,10 +206,12 @@ var Panel = () => {
196
206
  emit(EVENTS.PARTS_REQUEST, { storyId });
197
207
  }, [storyId]);
198
208
  const parts = params?.parts ?? discovered;
199
- const accent = params?.theme?.accent ?? ACCENT_FALLBACK;
200
- if (!params || params.disable) {
209
+ if (!params) {
201
210
  return /* @__PURE__ */ React2.createElement(AnatomyMessage, null, "No anatomy configured for this story. Add ", /* @__PURE__ */ React2.createElement(AnatomyCode, null, "parameters.anatomy"), " and annotate elements with ", /* @__PURE__ */ React2.createElement(AnatomyCode, null, 'data-part="name"'), ".");
202
211
  }
212
+ if (params.disable) {
213
+ return /* @__PURE__ */ React2.createElement(AnatomyMessage, null, "Anatomy is disabled for this story (", /* @__PURE__ */ React2.createElement(AnatomyCode, null, "anatomy.disable"), ").");
214
+ }
203
215
  if (parts.length === 0) {
204
216
  return /* @__PURE__ */ React2.createElement(AnatomyMessage, null, "No parts found. Annotate elements in your story with", " ", /* @__PURE__ */ React2.createElement(AnatomyCode, null, 'data-part="name"'), " or pass", " ", /* @__PURE__ */ React2.createElement(AnatomyCode, null, "parameters.anatomy.parts"), ".");
205
217
  }
@@ -208,7 +220,8 @@ var Panel = () => {
208
220
  {
209
221
  parts,
210
222
  activeId,
211
- accent,
223
+ preset: params.preset,
224
+ theme: params.theme,
212
225
  onItemEnter: (partId) => emit(EVENTS.HOVER_ITEM, { storyId, partId }),
213
226
  onItemLeave: () => emit(EVENTS.LEAVE_ITEM, { storyId })
214
227
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/manager.tsx", "../src/constants.ts", "../src/Panel.tsx", "../src/channel.ts", "../src/AnatomyTable.tsx"],
4
- "sourcesContent": ["import React from 'react';\nimport { addons, types } from 'storybook/manager-api';\nimport { AddonPanel } from 'storybook/internal/components';\n\nimport { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants.js';\nimport { Panel } from './Panel.js';\n\naddons.register(ADDON_ID, () => {\n addons.add(PANEL_ID, {\n type: types.PANEL,\n title: 'Anatomy',\n paramKey: PARAM_KEY,\n match: ({ viewMode }) => viewMode === 'story',\n render: ({ active }) => (\n <AddonPanel active={!!active}>\n <Panel />\n </AddonPanel>\n ),\n });\n});\n", "export const ADDON_ID = 'component-anatomy';\nexport const PANEL_ID = `${ADDON_ID}/panel`;\n\n/** Story parameter key: `parameters.anatomy = { ... }` */\nexport const PARAM_KEY = 'anatomy';\n\n/**\n * Channel events used to sync the manager panel \u2014 and the `<Anatomy>` MDX doc\n * block, which runs in the preview iframe \u2014 with the story canvas.\n *\n * Every payload carries the `storyId` it concerns; see `channel.ts` for the\n * payload types and the `matchesStory` filter each listener applies.\n */\nexport const EVENTS = {\n /** preview \u2192 consumers: a part became active in the canvas (hover/programmatic). */\n PART_ENTER: `${ADDON_ID}/part-enter`,\n /** preview \u2192 consumers: no part is active anymore. */\n PART_LEAVE: `${ADDON_ID}/part-leave`,\n /** preview \u2192 consumers: resolved part list for a story. */\n PARTS: `${ADDON_ID}/parts`,\n /** consumers \u2192 preview: the user hovers/focuses a panel entry. */\n HOVER_ITEM: `${ADDON_ID}/hover-item`,\n /** consumers \u2192 preview: the user left a panel entry. */\n LEAVE_ITEM: `${ADDON_ID}/leave-item`,\n /** consumers \u2192 preview: a panel/block mounted and wants the current part list. */\n PARTS_REQUEST: `${ADDON_ID}/parts-request`,\n} as const;\n", "import React, { useEffect, useState } from 'react';\nimport { useChannel, useParameter, useStorybookApi } from 'storybook/manager-api';\nimport type { AnatomyPartDefinition } from '@component-anatomy/core';\n\nimport { EVENTS, PARAM_KEY } from './constants.js';\nimport { matchesStory } from './channel.js';\nimport type { PartEnterEvent, PartsEvent, StoryScopedEvent } from './channel.js';\nimport { ACCENT_FALLBACK, AnatomyCode, AnatomyMessage, AnatomyTable } from './AnatomyTable.js';\nimport type { AnatomyParameters } from './types.js';\n\nexport const Panel: React.FC = () => {\n const params = useParameter<AnatomyParameters | undefined>(PARAM_KEY, undefined);\n const api = useStorybookApi();\n const storyId = api.getUrlState().storyId;\n\n const [discovered, setDiscovered] = useState<AnatomyPartDefinition[]>([]);\n const [activeId, setActiveId] = useState<string | null>(null);\n\n // `useChannel` captures its handlers on the given deps \u2014 `storyId` has to be\n // listed or the filters below would close over a stale story.\n const emit = useChannel(\n {\n [EVENTS.PARTS]: (event: PartsEvent) => {\n if (!matchesStory(event?.storyId, storyId)) return;\n setDiscovered(event.parts);\n },\n [EVENTS.PART_ENTER]: (event: PartEnterEvent) => {\n if (!matchesStory(event?.storyId, storyId)) return;\n setActiveId(event.partId);\n },\n [EVENTS.PART_LEAVE]: (event: StoryScopedEvent = {}) => {\n if (!matchesStory(event?.storyId, storyId)) return;\n setActiveId(null);\n },\n },\n [storyId]\n );\n\n // Ask the preview for the current part list on mount / story change.\n useEffect(() => {\n setDiscovered([]);\n setActiveId(null);\n emit(EVENTS.PARTS_REQUEST, { storyId } satisfies StoryScopedEvent);\n }, [storyId]);\n\n const parts = params?.parts ?? discovered;\n const accent = params?.theme?.accent ?? ACCENT_FALLBACK;\n\n if (!params || params.disable) {\n return (\n <AnatomyMessage>\n No anatomy configured for this story. Add <AnatomyCode>parameters.anatomy</AnatomyCode> and\n annotate elements with <AnatomyCode>data-part=\"name\"</AnatomyCode>.\n </AnatomyMessage>\n );\n }\n\n if (parts.length === 0) {\n return (\n <AnatomyMessage>\n No parts found. Annotate elements in your story with{' '}\n <AnatomyCode>data-part=\"name\"</AnatomyCode> or pass{' '}\n <AnatomyCode>parameters.anatomy.parts</AnatomyCode>.\n </AnatomyMessage>\n );\n }\n\n return (\n <AnatomyTable\n parts={parts}\n activeId={activeId}\n accent={accent}\n onItemEnter={(partId) => emit(EVENTS.HOVER_ITEM, { storyId, partId })}\n onItemLeave={() => emit(EVENTS.LEAVE_ITEM, { storyId })}\n />\n );\n};\n", "/**\n * Shared channel payload contract between the preview decorator, the manager\n * panel, and the MDX doc block.\n *\n * Every payload carries the `storyId` it refers to. In story view this is\n * redundant \u2014 only one story is mounted \u2014 but a docs page mounts *many*\n * stories at once, each with its own controller, and each `<Anatomy>` block\n * must talk to exactly one of them. Without addressing, hovering a part in\n * one block highlights the matching part in every other story on the page.\n */\nimport type { AnatomyPartDefinition } from '@component-anatomy/core';\n\n/** preview \u2192 consumers: the resolved part list for one story. */\nexport type PartsEvent = { storyId?: string; parts: AnatomyPartDefinition[] };\n\n/** preview \u2192 consumers: a part became active in that story's canvas. */\nexport type PartEnterEvent = { storyId?: string; partId: string };\n\n/** consumer \u2192 preview: highlight this part in that story's canvas. */\nexport type HoverItemEvent = { storyId?: string; partId: string };\n\n/** Payload for the events that only need to name a story. */\nexport type StoryScopedEvent = { storyId?: string };\n\n/**\n * Whether an event addressed to `eventStoryId` concerns `storyId`.\n *\n * A missing id on *either* side matches everything. That keeps the protocol\n * backward compatible: a manager panel from a newer build still understands\n * an older preview bundle that emits unaddressed events, and vice versa.\n */\nexport const matchesStory = (\n eventStoryId: string | undefined,\n storyId: string | undefined\n): boolean => !eventStoryId || !storyId || eventStoryId === storyId;\n", "/**\n * The anatomy part table \u2014 pure presentation, no Storybook imports.\n *\n * This module is deliberately free of `storybook/manager-api` and\n * `storybook/preview-api`: it is bundled into *both* the manager entry (the\n * addon panel) and the blocks entry (the `<Anatomy>` MDX doc block), which\n * run in different runtimes and cannot share either of those APIs. Only the\n * data acquisition differs between the two; the markup must not.\n */\nimport React from 'react';\nimport { useTheme } from 'storybook/theming';\nimport type { AnatomyPartDefinition } from '@component-anatomy/core';\n\n/** Fallback accent when the story sets no `anatomy.theme.accent`. */\nexport const ACCENT_FALLBACK = '#4f46e5';\n\n/**\n * The subset of Storybook's theme this table reads. Typed loosely on purpose:\n * `useTheme()` resolves to an empty `Theme` interface (Emotion augmentation),\n * and outside a ThemeProvider it returns `{}` \u2014 so every read is optional and\n * every value has a literal fallback below.\n */\ntype PartialStorybookTheme = {\n fgColor?: { default?: string; muted?: string };\n bgColor?: { muted?: string };\n borderColor?: { default?: string };\n typography?: { fonts?: { base?: string; mono?: string } };\n};\n\nconst FONT_BASE_FALLBACK =\n '\"Nunito Sans\", -apple-system, \".SFNSText-Regular\", \"San Francisco\", BlinkMacSystemFont, \"Segoe UI\", \"Helvetica Neue\", Helvetica, Arial, sans-serif';\nconst FONT_MONO_FALLBACK = 'ui-monospace, \"Cascadia Code\", \"Fira Mono\", monospace';\n\n/**\n * Resolves the table's chrome colors from Storybook's theme so the same\n * markup reads correctly in the manager panel *and* in a docs page under a\n * dark theme. The accent is intentionally not theme-derived \u2014 it is the\n * addon's identity color and stays stable unless a story overrides it.\n */\nfunction usePalette() {\n const theme = useTheme() as PartialStorybookTheme;\n return {\n text: theme.fgColor?.default,\n muted: theme.fgColor?.muted ?? '#6b7280',\n mutedBg: theme.bgColor?.muted ?? 'rgba(0,0,0,0.06)',\n border: theme.borderColor?.default ?? 'rgba(0,0,0,0.08)',\n fontBase: theme.typography?.fonts?.base ?? FONT_BASE_FALLBACK,\n fontMono: theme.typography?.fonts?.mono ?? FONT_MONO_FALLBACK,\n };\n}\n\ntype Palette = ReturnType<typeof usePalette>;\n\nconst makeStyles = (p: Palette): Record<string, React.CSSProperties> => ({\n container: {\n padding: '12px 16px',\n fontFamily: p.fontBase,\n fontSize: 13,\n lineHeight: 1.5,\n color: p.text,\n },\n empty: {\n color: p.muted,\n margin: 0,\n },\n code: {\n fontFamily: p.fontMono,\n fontSize: '0.85em',\n background: p.mutedBg,\n borderRadius: 3,\n padding: '1px 5px',\n },\n list: {\n display: 'flex',\n flexDirection: 'column',\n gap: 2,\n margin: 0,\n padding: 0,\n listStyle: 'none',\n },\n entry: {\n padding: '8px 10px',\n borderRadius: 6,\n border: '1px solid transparent',\n cursor: 'default',\n outline: 'none',\n transition: 'background 120ms ease, border-color 120ms ease',\n },\n header: {\n display: 'flex',\n alignItems: 'center',\n gap: 8,\n },\n indicator: {\n width: 8,\n height: 8,\n borderRadius: '50%',\n border: `2px solid ${p.border}`,\n flexShrink: 0,\n transition: 'background 120ms ease, border-color 120ms ease',\n },\n name: {\n fontWeight: 700,\n flex: 1,\n },\n id: {\n fontFamily: p.fontMono,\n fontSize: 10,\n padding: '1px 6px',\n borderRadius: 4,\n background: p.mutedBg,\n border: `1px solid ${p.border}`,\n color: p.muted,\n flexShrink: 0,\n },\n description: {\n margin: '4px 0 0',\n paddingLeft: 16,\n color: p.muted,\n },\n});\n\n/**\n * An inline `<code>` styled for the surrounding message text. Exported so\n * both runtimes can compose their own empty-state copy without duplicating\n * the style object.\n */\nexport const AnatomyCode: React.FC<{ children: React.ReactNode }> = ({ children }) => {\n const styles = makeStyles(usePalette());\n return <code style={styles.code}>{children}</code>;\n};\n\n/** A padded, muted paragraph \u2014 used for every \"nothing to show\" state. */\nexport const AnatomyMessage: React.FC<{ children: React.ReactNode }> = ({ children }) => {\n const styles = makeStyles(usePalette());\n return (\n <div style={styles.container}>\n <p style={styles.empty}>{children}</p>\n </div>\n );\n};\n\nexport type AnatomyTableProps = {\n /** Parts to list, in the order they should be shown. */\n parts: AnatomyPartDefinition[];\n /** Id of the part currently highlighted in the canvas, if any. */\n activeId?: string | null;\n /** Accent color for the active state. Defaults to {@link ACCENT_FALLBACK}. */\n accent?: string;\n /** Called when the user hovers or focuses an entry. */\n onItemEnter?: (partId: string) => void;\n /** Called when the user leaves or blurs an entry. */\n onItemLeave?: () => void;\n};\n\nexport const AnatomyTable: React.FC<AnatomyTableProps> = ({\n parts,\n activeId = null,\n accent = ACCENT_FALLBACK,\n onItemEnter,\n onItemLeave,\n}) => {\n const palette = usePalette();\n const styles = makeStyles(palette);\n\n return (\n <div style={styles.container}>\n <ul style={styles.list} role=\"list\" aria-label=\"Anatomy parts\">\n {parts.map((part) => {\n const active = activeId === part.id;\n return (\n <li\n key={part.id}\n role=\"listitem\"\n tabIndex={0}\n aria-label={part.name}\n style={{\n ...styles.entry,\n background: active ? `color-mix(in srgb, ${accent} 7%, transparent)` : undefined,\n borderColor: active\n ? `color-mix(in srgb, ${accent} 20%, transparent)`\n : 'transparent',\n }}\n onMouseEnter={() => onItemEnter?.(part.id)}\n onMouseLeave={() => onItemLeave?.()}\n onFocus={() => onItemEnter?.(part.id)}\n onBlur={() => onItemLeave?.()}\n >\n <div style={styles.header}>\n <span\n aria-hidden=\"true\"\n style={{\n ...styles.indicator,\n background: active ? accent : undefined,\n borderColor: active ? accent : palette.border,\n }}\n />\n <span style={{ ...styles.name, color: active ? accent : undefined }}>\n {part.name}\n </span>\n <code style={styles.id}>{part.id}</code>\n </div>\n {part.description && <p style={styles.description}>{part.description}</p>}\n </li>\n );\n })}\n </ul>\n </div>\n );\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import React from 'react';\nimport { addons, types } from 'storybook/manager-api';\nimport { AddonPanel } from 'storybook/internal/components';\n\nimport { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants.js';\nimport { Panel } from './Panel.js';\n\naddons.register(ADDON_ID, () => {\n addons.add(PANEL_ID, {\n type: types.PANEL,\n title: 'Anatomy',\n paramKey: PARAM_KEY,\n match: ({ viewMode }) => viewMode === 'story',\n render: ({ active }) => (\n <AddonPanel active={!!active}>\n <Panel />\n </AddonPanel>\n ),\n });\n});\n", "export const ADDON_ID = 'component-anatomy';\nexport const PANEL_ID = `${ADDON_ID}/panel`;\n\n/** Story parameter key: `parameters.anatomy = { ... }` */\nexport const PARAM_KEY = 'anatomy';\n\n/**\n * Channel events used to sync the manager panel \u2014 and the `<Anatomy>` MDX doc\n * block, which runs in the preview iframe \u2014 with the story canvas.\n *\n * Every payload carries the `storyId` it concerns; see `channel.ts` for the\n * payload types and the `matchesStory` filter each listener applies.\n */\nexport const EVENTS = {\n /** preview \u2192 consumers: a part became active in the canvas (hover/programmatic). */\n PART_ENTER: `${ADDON_ID}/part-enter`,\n /** preview \u2192 consumers: no part is active anymore. */\n PART_LEAVE: `${ADDON_ID}/part-leave`,\n /** preview \u2192 consumers: resolved part list for a story. */\n PARTS: `${ADDON_ID}/parts`,\n /** consumers \u2192 preview: the user hovers/focuses a panel entry. */\n HOVER_ITEM: `${ADDON_ID}/hover-item`,\n /** consumers \u2192 preview: the user left a panel entry. */\n LEAVE_ITEM: `${ADDON_ID}/leave-item`,\n /** consumers \u2192 preview: a panel/block mounted and wants the current part list. */\n PARTS_REQUEST: `${ADDON_ID}/parts-request`,\n} as const;\n", "import React, { useEffect, useState } from 'react';\nimport { useChannel, useParameter, useStorybookApi } from 'storybook/manager-api';\nimport type { AnatomyPartDefinition } from '@component-anatomy/core';\n\nimport { EVENTS, PARAM_KEY } from './constants.js';\nimport { matchesStory } from './channel.js';\nimport type { PartEnterEvent, PartsEvent, StoryScopedEvent } from './channel.js';\nimport { AnatomyCode, AnatomyMessage, AnatomyTable } from './AnatomyTable.js';\nimport type { AnatomyParameters } from './types.js';\n\nexport const Panel: React.FC = () => {\n const params = useParameter<AnatomyParameters | undefined>(PARAM_KEY, undefined);\n const api = useStorybookApi();\n const storyId = api.getUrlState().storyId;\n\n const [discovered, setDiscovered] = useState<AnatomyPartDefinition[]>([]);\n const [activeId, setActiveId] = useState<string | null>(null);\n\n // `useChannel` captures its handlers on the given deps \u2014 `storyId` has to be\n // listed or the filters below would close over a stale story.\n const emit = useChannel(\n {\n [EVENTS.PARTS]: (event: PartsEvent) => {\n if (!matchesStory(event?.storyId, storyId)) return;\n setDiscovered(event.parts);\n },\n [EVENTS.PART_ENTER]: (event: PartEnterEvent) => {\n if (!matchesStory(event?.storyId, storyId)) return;\n setActiveId(event.partId);\n },\n [EVENTS.PART_LEAVE]: (event: StoryScopedEvent = {}) => {\n if (!matchesStory(event?.storyId, storyId)) return;\n setActiveId(null);\n },\n },\n [storyId]\n );\n\n // Ask the preview for the current part list on mount / story change.\n useEffect(() => {\n setDiscovered([]);\n setActiveId(null);\n emit(EVENTS.PARTS_REQUEST, { storyId } satisfies StoryScopedEvent);\n }, [storyId]);\n\n const parts = params?.parts ?? discovered;\n\n if (!params) {\n return (\n <AnatomyMessage>\n No anatomy configured for this story. Add <AnatomyCode>parameters.anatomy</AnatomyCode> and\n annotate elements with <AnatomyCode>data-part=\"name\"</AnatomyCode>.\n </AnatomyMessage>\n );\n }\n\n // Defence in depth: Storybook removes a panel from the tab bar entirely when\n // `parameters[paramKey].disable` is set, so this normally never renders. If\n // it ever does, say the right thing \u2014 a disabled story *has* an anatomy, and\n // telling its author to add one sends them the wrong way.\n if (params.disable) {\n return (\n <AnatomyMessage>\n Anatomy is disabled for this story (<AnatomyCode>anatomy.disable</AnatomyCode>).\n </AnatomyMessage>\n );\n }\n\n if (parts.length === 0) {\n return (\n <AnatomyMessage>\n No parts found. Annotate elements in your story with{' '}\n <AnatomyCode>data-part=\"name\"</AnatomyCode> or pass{' '}\n <AnatomyCode>parameters.anatomy.parts</AnatomyCode>.\n </AnatomyMessage>\n );\n }\n\n return (\n <AnatomyTable\n parts={parts}\n activeId={activeId}\n preset={params.preset}\n theme={params.theme}\n onItemEnter={(partId) => emit(EVENTS.HOVER_ITEM, { storyId, partId })}\n onItemLeave={() => emit(EVENTS.LEAVE_ITEM, { storyId })}\n />\n );\n};\n", "/**\n * Shared channel payload contract between the preview decorator, the manager\n * panel, and the MDX doc block.\n *\n * Every payload carries the `storyId` it refers to. In story view this is\n * redundant \u2014 only one story is mounted \u2014 but a docs page mounts *many*\n * stories at once, each with its own controller, and each `<Anatomy>` block\n * must talk to exactly one of them. Without addressing, hovering a part in\n * one block highlights the matching part in every other story on the page.\n */\nimport type { AnatomyPartDefinition } from '@component-anatomy/core';\n\n/** preview \u2192 consumers: the resolved part list for one story. */\nexport type PartsEvent = { storyId?: string; parts: AnatomyPartDefinition[] };\n\n/** preview \u2192 consumers: a part became active in that story's canvas. */\nexport type PartEnterEvent = { storyId?: string; partId: string };\n\n/** consumer \u2192 preview: highlight this part in that story's canvas. */\nexport type HoverItemEvent = { storyId?: string; partId: string };\n\n/** Payload for the events that only need to name a story. */\nexport type StoryScopedEvent = { storyId?: string };\n\n/**\n * Whether an event addressed to `eventStoryId` concerns `storyId`.\n *\n * A missing id on *either* side matches everything. That keeps the protocol\n * backward compatible: a manager panel from a newer build still understands\n * an older preview bundle that emits unaddressed events, and vice versa.\n */\nexport const matchesStory = (\n eventStoryId: string | undefined,\n storyId: string | undefined\n): boolean => !eventStoryId || !storyId || eventStoryId === storyId;\n", "/**\n * The anatomy part table \u2014 pure presentation, no Storybook imports.\n *\n * This module is deliberately free of `storybook/manager-api` and\n * `storybook/preview-api`: it is bundled into *both* the manager entry (the\n * addon panel) and the blocks entry (the `<Anatomy>` MDX doc block), which\n * run in different runtimes and cannot share either of those APIs. Only the\n * data acquisition differs between the two; the markup must not.\n */\nimport React from 'react';\nimport { useTheme } from 'storybook/theming';\nimport { resolvePanelAccent } from '@component-anatomy/core';\nimport type {\n AnatomyPartDefinition,\n AnatomyPresetName,\n AnatomyTheme,\n} from '@component-anatomy/core';\n\n/**\n * The subset of Storybook's theme this table reads. Typed loosely on purpose:\n * `useTheme()` resolves to an empty `Theme` interface (Emotion augmentation),\n * and outside a ThemeProvider it returns `{}` \u2014 so every read is optional and\n * every value has a literal fallback below.\n */\ntype PartialStorybookTheme = {\n fgColor?: { default?: string; muted?: string };\n bgColor?: { default?: string; muted?: string };\n borderColor?: { default?: string };\n typography?: { fonts?: { base?: string; mono?: string } };\n};\n\nconst FONT_BASE_FALLBACK =\n '\"Nunito Sans\", -apple-system, \".SFNSText-Regular\", \"San Francisco\", BlinkMacSystemFont, \"Segoe UI\", \"Helvetica Neue\", Helvetica, Arial, sans-serif';\nconst FONT_MONO_FALLBACK = 'ui-monospace, \"Cascadia Code\", \"Fira Mono\", monospace';\n\n/**\n * Resolves the table's chrome colors from Storybook's theme so the same\n * markup reads correctly in the manager panel *and* in a docs page under a\n * dark theme.\n *\n * `surfaceBg`/`surfaceFg` are not painted anywhere \u2014 they are what the accent\n * is measured against in {@link useAccent}. They carry Storybook's own light\n * defaults as fallbacks so the measurement stays meaningful outside a\n * ThemeProvider, where `useTheme()` returns `{}`.\n */\nfunction usePalette() {\n const theme = useTheme() as PartialStorybookTheme;\n return {\n text: theme.fgColor?.default,\n muted: theme.fgColor?.muted ?? '#6b7280',\n mutedBg: theme.bgColor?.muted ?? 'rgba(0,0,0,0.06)',\n border: theme.borderColor?.default ?? 'rgba(0,0,0,0.08)',\n surfaceBg: theme.bgColor?.default ?? '#ffffff',\n surfaceFg: theme.fgColor?.default ?? '#2e3438',\n fontBase: theme.typography?.fonts?.base ?? FONT_BASE_FALLBACK,\n fontMono: theme.typography?.fonts?.mono ?? FONT_MONO_FALLBACK,\n };\n}\n\ntype Palette = ReturnType<typeof usePalette>;\n\n/**\n * The accent for the active row \u2014 the story's own `{ preset, theme }`, so the\n * table agrees with the overlays it describes, lifted to WCAG AA against the\n * surface it is painted on when the two disagree.\n *\n * The check is not optional polish: `preset: 'contrast'` is black on yellow\n * and the Storybook manager is dark by default, so the preset that exists for\n * users who need contrast is exactly the one that would land at 1.3:1 here.\n */\nfunction useAccent(palette: Palette, preset?: AnatomyPresetName, theme?: AnatomyTheme): string {\n return resolvePanelAccent(preset, theme, {\n background: palette.surfaceBg,\n foreground: palette.surfaceFg,\n });\n}\n\nconst makeStyles = (p: Palette): Record<string, React.CSSProperties> => ({\n container: {\n padding: '12px 16px',\n fontFamily: p.fontBase,\n fontSize: 13,\n lineHeight: 1.5,\n color: p.text,\n },\n empty: {\n color: p.muted,\n margin: 0,\n },\n code: {\n fontFamily: p.fontMono,\n fontSize: '0.85em',\n background: p.mutedBg,\n borderRadius: 3,\n padding: '1px 5px',\n },\n list: {\n display: 'flex',\n flexDirection: 'column',\n gap: 2,\n margin: 0,\n padding: 0,\n listStyle: 'none',\n },\n entry: {\n padding: '8px 10px',\n borderRadius: 6,\n border: '1px solid transparent',\n cursor: 'default',\n outline: 'none',\n transition: 'background 120ms ease, border-color 120ms ease',\n },\n header: {\n display: 'flex',\n alignItems: 'center',\n gap: 8,\n },\n indicator: {\n width: 8,\n height: 8,\n borderRadius: '50%',\n border: `2px solid ${p.border}`,\n flexShrink: 0,\n transition: 'background 120ms ease, border-color 120ms ease',\n },\n name: {\n fontWeight: 700,\n flex: 1,\n },\n id: {\n fontFamily: p.fontMono,\n fontSize: 10,\n padding: '1px 6px',\n borderRadius: 4,\n background: p.mutedBg,\n border: `1px solid ${p.border}`,\n color: p.muted,\n flexShrink: 0,\n },\n description: {\n margin: '4px 0 0',\n paddingLeft: 16,\n color: p.muted,\n },\n});\n\n/**\n * An inline `<code>` styled for the surrounding message text. Exported so\n * both runtimes can compose their own empty-state copy without duplicating\n * the style object.\n */\nexport const AnatomyCode: React.FC<{ children: React.ReactNode }> = ({ children }) => {\n const styles = makeStyles(usePalette());\n return <code style={styles.code}>{children}</code>;\n};\n\n/** A padded, muted paragraph \u2014 used for every \"nothing to show\" state. */\nexport const AnatomyMessage: React.FC<{ children: React.ReactNode }> = ({ children }) => {\n const styles = makeStyles(usePalette());\n return (\n <div style={styles.container}>\n <p style={styles.empty}>{children}</p>\n </div>\n );\n};\n\nexport type AnatomyTableProps = {\n /** Parts to list, in the order they should be shown. */\n parts: AnatomyPartDefinition[];\n /** Id of the part currently highlighted in the canvas, if any. */\n activeId?: string | null;\n /** The story's `anatomy.preset` \u2014 the active row follows it. */\n preset?: AnatomyPresetName;\n /** The story's `anatomy.theme` \u2014 token overrides on top of the preset. */\n theme?: AnatomyTheme;\n /** Called when the user hovers or focuses an entry. */\n onItemEnter?: (partId: string) => void;\n /** Called when the user leaves or blurs an entry. */\n onItemLeave?: () => void;\n};\n\nexport const AnatomyTable: React.FC<AnatomyTableProps> = ({\n parts,\n activeId = null,\n preset,\n theme,\n onItemEnter,\n onItemLeave,\n}) => {\n const palette = usePalette();\n const styles = makeStyles(palette);\n const accent = useAccent(palette, preset, theme);\n\n return (\n <div style={styles.container}>\n <ul style={styles.list} role=\"list\" aria-label=\"Anatomy parts\">\n {parts.map((part) => {\n const active = activeId === part.id;\n return (\n <li\n key={part.id}\n role=\"listitem\"\n tabIndex={0}\n aria-label={part.name}\n style={{\n ...styles.entry,\n background: active ? `color-mix(in srgb, ${accent} 7%, transparent)` : undefined,\n borderColor: active\n ? `color-mix(in srgb, ${accent} 20%, transparent)`\n : 'transparent',\n }}\n onMouseEnter={() => onItemEnter?.(part.id)}\n onMouseLeave={() => onItemLeave?.()}\n onFocus={() => onItemEnter?.(part.id)}\n onBlur={() => onItemLeave?.()}\n >\n <div style={styles.header}>\n <span\n aria-hidden=\"true\"\n style={{\n ...styles.indicator,\n background: active ? accent : undefined,\n borderColor: active ? accent : palette.border,\n }}\n />\n <span style={{ ...styles.name, color: active ? accent : undefined }}>\n {part.name}\n </span>\n <code style={styles.id}>{part.id}</code>\n </div>\n {part.description && <p style={styles.description}>{part.description}</p>}\n </li>\n );\n })}\n </ul>\n </div>\n );\n};\n"],
5
+ "mappings": "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",
6
6
  "names": ["React", "React", "React", "React"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@component-anatomy/storybook",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Storybook addon — interactive component anatomy panel synced with the story canvas",
5
5
  "license": "MIT",
6
6
  "author": "Julien Déramond",
@@ -60,12 +60,12 @@
60
60
  "access": "public"
61
61
  },
62
62
  "dependencies": {
63
- "@component-anatomy/core": "^0.0.1"
63
+ "@component-anatomy/core": "^0.1.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@storybook/addon-docs": "^10.5.2",
67
67
  "@types/react": "^19.2.18",
68
- "esbuild": "^0.28.1",
68
+ "esbuild": "^0.28.2",
69
69
  "react": "^19.2.8",
70
70
  "react-dom": "^19.2.8",
71
71
  "storybook": "^10.5.10",
@@ -9,10 +9,12 @@
9
9
  */
10
10
  import React from 'react';
11
11
  import { useTheme } from 'storybook/theming';
12
- import type { AnatomyPartDefinition } from '@component-anatomy/core';
13
-
14
- /** Fallback accent when the story sets no `anatomy.theme.accent`. */
15
- export const ACCENT_FALLBACK = '#4f46e5';
12
+ import { resolvePanelAccent } from '@component-anatomy/core';
13
+ import type {
14
+ AnatomyPartDefinition,
15
+ AnatomyPresetName,
16
+ AnatomyTheme,
17
+ } from '@component-anatomy/core';
16
18
 
17
19
  /**
18
20
  * The subset of Storybook's theme this table reads. Typed loosely on purpose:
@@ -22,7 +24,7 @@ export const ACCENT_FALLBACK = '#4f46e5';
22
24
  */
23
25
  type PartialStorybookTheme = {
24
26
  fgColor?: { default?: string; muted?: string };
25
- bgColor?: { muted?: string };
27
+ bgColor?: { default?: string; muted?: string };
26
28
  borderColor?: { default?: string };
27
29
  typography?: { fonts?: { base?: string; mono?: string } };
28
30
  };
@@ -34,8 +36,12 @@ const FONT_MONO_FALLBACK = 'ui-monospace, "Cascadia Code", "Fira Mono", monospac
34
36
  /**
35
37
  * Resolves the table's chrome colors from Storybook's theme so the same
36
38
  * markup reads correctly in the manager panel *and* in a docs page under a
37
- * dark theme. The accent is intentionally not theme-derived — it is the
38
- * addon's identity color and stays stable unless a story overrides it.
39
+ * dark theme.
40
+ *
41
+ * `surfaceBg`/`surfaceFg` are not painted anywhere — they are what the accent
42
+ * is measured against in {@link useAccent}. They carry Storybook's own light
43
+ * defaults as fallbacks so the measurement stays meaningful outside a
44
+ * ThemeProvider, where `useTheme()` returns `{}`.
39
45
  */
40
46
  function usePalette() {
41
47
  const theme = useTheme() as PartialStorybookTheme;
@@ -44,6 +50,8 @@ function usePalette() {
44
50
  muted: theme.fgColor?.muted ?? '#6b7280',
45
51
  mutedBg: theme.bgColor?.muted ?? 'rgba(0,0,0,0.06)',
46
52
  border: theme.borderColor?.default ?? 'rgba(0,0,0,0.08)',
53
+ surfaceBg: theme.bgColor?.default ?? '#ffffff',
54
+ surfaceFg: theme.fgColor?.default ?? '#2e3438',
47
55
  fontBase: theme.typography?.fonts?.base ?? FONT_BASE_FALLBACK,
48
56
  fontMono: theme.typography?.fonts?.mono ?? FONT_MONO_FALLBACK,
49
57
  };
@@ -51,6 +59,22 @@ function usePalette() {
51
59
 
52
60
  type Palette = ReturnType<typeof usePalette>;
53
61
 
62
+ /**
63
+ * The accent for the active row — the story's own `{ preset, theme }`, so the
64
+ * table agrees with the overlays it describes, lifted to WCAG AA against the
65
+ * surface it is painted on when the two disagree.
66
+ *
67
+ * The check is not optional polish: `preset: 'contrast'` is black on yellow
68
+ * and the Storybook manager is dark by default, so the preset that exists for
69
+ * users who need contrast is exactly the one that would land at 1.3:1 here.
70
+ */
71
+ function useAccent(palette: Palette, preset?: AnatomyPresetName, theme?: AnatomyTheme): string {
72
+ return resolvePanelAccent(preset, theme, {
73
+ background: palette.surfaceBg,
74
+ foreground: palette.surfaceFg,
75
+ });
76
+ }
77
+
54
78
  const makeStyles = (p: Palette): Record<string, React.CSSProperties> => ({
55
79
  container: {
56
80
  padding: '12px 16px',
@@ -145,8 +169,10 @@ export type AnatomyTableProps = {
145
169
  parts: AnatomyPartDefinition[];
146
170
  /** Id of the part currently highlighted in the canvas, if any. */
147
171
  activeId?: string | null;
148
- /** Accent color for the active state. Defaults to {@link ACCENT_FALLBACK}. */
149
- accent?: string;
172
+ /** The story's `anatomy.preset` the active row follows it. */
173
+ preset?: AnatomyPresetName;
174
+ /** The story's `anatomy.theme` — token overrides on top of the preset. */
175
+ theme?: AnatomyTheme;
150
176
  /** Called when the user hovers or focuses an entry. */
151
177
  onItemEnter?: (partId: string) => void;
152
178
  /** Called when the user leaves or blurs an entry. */
@@ -156,12 +182,14 @@ export type AnatomyTableProps = {
156
182
  export const AnatomyTable: React.FC<AnatomyTableProps> = ({
157
183
  parts,
158
184
  activeId = null,
159
- accent = ACCENT_FALLBACK,
185
+ preset,
186
+ theme,
160
187
  onItemEnter,
161
188
  onItemLeave,
162
189
  }) => {
163
190
  const palette = usePalette();
164
191
  const styles = makeStyles(palette);
192
+ const accent = useAccent(palette, preset, theme);
165
193
 
166
194
  return (
167
195
  <div style={styles.container}>
package/src/Panel.tsx CHANGED
@@ -5,7 +5,7 @@ import type { AnatomyPartDefinition } from '@component-anatomy/core';
5
5
  import { EVENTS, PARAM_KEY } from './constants.js';
6
6
  import { matchesStory } from './channel.js';
7
7
  import type { PartEnterEvent, PartsEvent, StoryScopedEvent } from './channel.js';
8
- import { ACCENT_FALLBACK, AnatomyCode, AnatomyMessage, AnatomyTable } from './AnatomyTable.js';
8
+ import { AnatomyCode, AnatomyMessage, AnatomyTable } from './AnatomyTable.js';
9
9
  import type { AnatomyParameters } from './types.js';
10
10
 
11
11
  export const Panel: React.FC = () => {
@@ -44,9 +44,8 @@ export const Panel: React.FC = () => {
44
44
  }, [storyId]);
45
45
 
46
46
  const parts = params?.parts ?? discovered;
47
- const accent = params?.theme?.accent ?? ACCENT_FALLBACK;
48
47
 
49
- if (!params || params.disable) {
48
+ if (!params) {
50
49
  return (
51
50
  <AnatomyMessage>
52
51
  No anatomy configured for this story. Add <AnatomyCode>parameters.anatomy</AnatomyCode> and
@@ -55,6 +54,18 @@ export const Panel: React.FC = () => {
55
54
  );
56
55
  }
57
56
 
57
+ // Defence in depth: Storybook removes a panel from the tab bar entirely when
58
+ // `parameters[paramKey].disable` is set, so this normally never renders. If
59
+ // it ever does, say the right thing — a disabled story *has* an anatomy, and
60
+ // telling its author to add one sends them the wrong way.
61
+ if (params.disable) {
62
+ return (
63
+ <AnatomyMessage>
64
+ Anatomy is disabled for this story (<AnatomyCode>anatomy.disable</AnatomyCode>).
65
+ </AnatomyMessage>
66
+ );
67
+ }
68
+
58
69
  if (parts.length === 0) {
59
70
  return (
60
71
  <AnatomyMessage>
@@ -69,7 +80,8 @@ export const Panel: React.FC = () => {
69
80
  <AnatomyTable
70
81
  parts={parts}
71
82
  activeId={activeId}
72
- accent={accent}
83
+ preset={params.preset}
84
+ theme={params.theme}
73
85
  onItemEnter={(partId) => emit(EVENTS.HOVER_ITEM, { storyId, partId })}
74
86
  onItemLeave={() => emit(EVENTS.LEAVE_ITEM, { storyId })}
75
87
  />
package/src/blocks.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Docs blocks entry — the `<Anatomy>` block for MDX pages.
2
+ * Docs blocks entry — the `<Anatomy>` and `<AnatomyTable>` blocks for MDX
3
+ * pages.
3
4
  *
4
5
  * Unlike the addon panel, this runs in the **preview iframe**, where
5
6
  * `storybook/manager-api` does not exist. It reaches the story's controller
@@ -8,30 +9,46 @@
8
9
  * mounted the story talk to each other directly, in-frame, with no extra
9
10
  * plumbing.
10
11
  *
12
+ * `<Anatomy>` is the batteries-included block — canvas and table together,
13
+ * laid out for that pairing specifically ([#13](https://github.com/julien-deramond/component-anatomy/issues/13#issuecomment-5475936860)):
14
+ *
11
15
  * ```mdx
12
- * import { Meta, Canvas } from '@storybook/addon-docs/blocks';
16
+ * import { Meta } from '@storybook/addon-docs/blocks';
13
17
  * import { Anatomy } from '@component-anatomy/storybook/blocks';
14
18
  * import * as ButtonStories from './Button.stories';
15
19
  *
16
20
  * <Meta of={ButtonStories} />
17
21
  *
18
- * <Canvas of={ButtonStories.Anatomy} />
19
22
  * <Anatomy of={ButtonStories.Anatomy} />
20
23
  * ```
24
+ *
25
+ * `<AnatomyTable>` is the table alone, for a hand-placed `<Canvas>` or a
26
+ * custom layout:
27
+ *
28
+ * ```mdx
29
+ * import { Canvas, Meta } from '@storybook/addon-docs/blocks';
30
+ * import { AnatomyTable } from '@component-anatomy/storybook/blocks';
31
+ * import * as ButtonStories from './Button.stories';
32
+ *
33
+ * <Meta of={ButtonStories} />
34
+ *
35
+ * <Canvas of={ButtonStories.Anatomy} />
36
+ * <AnatomyTable of={ButtonStories.Anatomy} />
37
+ * ```
21
38
  */
22
39
  import React, { useEffect, useState } from 'react';
23
40
  import { addons } from 'storybook/preview-api';
24
- import { Unstyled, useOf } from '@storybook/addon-docs/blocks';
41
+ import { Canvas, Unstyled, useOf } from '@storybook/addon-docs/blocks';
25
42
  import type { Of } from '@storybook/addon-docs/blocks';
26
43
  import type { AnatomyPartDefinition } from '@component-anatomy/core';
27
44
 
28
45
  import { EVENTS, PARAM_KEY } from './constants.js';
29
46
  import { matchesStory } from './channel.js';
30
47
  import type { PartEnterEvent, PartsEvent, StoryScopedEvent } from './channel.js';
31
- import { ACCENT_FALLBACK, AnatomyCode, AnatomyMessage, AnatomyTable } from './AnatomyTable.js';
48
+ import { AnatomyCode, AnatomyMessage, AnatomyTable as AnatomyTablePrimitive } from './AnatomyTable.js';
32
49
  import type { AnatomyParameters } from './types.js';
33
50
 
34
- export type AnatomyBlockProps = {
51
+ export type AnatomyTableProps = {
35
52
  /**
36
53
  * The CSF export to document — a story export, or the whole module export
37
54
  * of a CSF file to read the meta's parameters.
@@ -68,7 +85,7 @@ function getChannelSafely() {
68
85
  }
69
86
  }
70
87
 
71
- export const Anatomy: React.FC<AnatomyBlockProps> = ({ of, parts: partsProp, sync = true }) => {
88
+ export const AnatomyTable: React.FC<AnatomyTableProps> = ({ of, parts: partsProp, sync = true }) => {
72
89
  const resolved = useOf(of ?? 'story', ['story', 'meta']);
73
90
 
74
91
  const params = (
@@ -126,7 +143,6 @@ export const Anatomy: React.FC<AnatomyBlockProps> = ({ of, parts: partsProp, syn
126
143
  }, [wired, storyId]);
127
144
 
128
145
  const parts = staticParts ?? discovered;
129
- const accent = params?.theme?.accent ?? ACCENT_FALLBACK;
130
146
 
131
147
  const emitHover = (partId: string) => {
132
148
  if (!wired) return;
@@ -165,9 +181,18 @@ export const Anatomy: React.FC<AnatomyBlockProps> = ({ of, parts: partsProp, syn
165
181
  return (
166
182
  <Unstyled>
167
183
  <AnatomyMessage>
168
- {resolved.type === 'meta'
169
- ? 'No parts found. A meta has no canvas to discover parts from — pass a story to `of`, or list `parts` explicitly.'
170
- : 'No parts found. Auto-discovery reads the rendered story, so make sure it is on this page (e.g. with a `<Canvas of={…} />` block above), or list `parts` explicitly.'}
184
+ {resolved.type === 'meta' ? (
185
+ <>
186
+ No parts found. A meta has no canvas to discover parts from pass a story to{' '}
187
+ <AnatomyCode>of</AnatomyCode>, or list <AnatomyCode>parts</AnatomyCode> explicitly.
188
+ </>
189
+ ) : (
190
+ <>
191
+ No parts found. Auto-discovery reads the rendered story, so make sure it is on this
192
+ page (e.g. with a <AnatomyCode>{'<Canvas of={…} />'}</AnatomyCode> block above), or
193
+ list <AnatomyCode>parts</AnatomyCode> explicitly.
194
+ </>
195
+ )}
171
196
  </AnatomyMessage>
172
197
  </Unstyled>
173
198
  );
@@ -175,13 +200,68 @@ export const Anatomy: React.FC<AnatomyBlockProps> = ({ of, parts: partsProp, syn
175
200
 
176
201
  return (
177
202
  <Unstyled>
178
- <AnatomyTable
203
+ <AnatomyTablePrimitive
179
204
  parts={parts}
180
205
  activeId={activeId}
181
- accent={accent}
206
+ preset={params?.preset}
207
+ theme={params?.theme}
182
208
  onItemEnter={emitHover}
183
209
  onItemLeave={emitLeave}
184
210
  />
185
211
  </Unstyled>
186
212
  );
187
213
  };
214
+
215
+ /**
216
+ * Injects the rule that tightens `<Anatomy>`'s canvas-to-table gap, once per
217
+ * page — a module-level flag rather than `useState`, so mounting a second
218
+ * `<Anatomy>` block doesn't append the rule again. Scoped under
219
+ * `.ca-anatomy` so it never touches a bare `<Canvas>` placed by the consumer.
220
+ *
221
+ * Self-injected rather than asked of the consumer's `preview-head.html`: the
222
+ * block owns both elements it is bridging, so the gap between them is its
223
+ * concern, not config the consumer should have to wire up.
224
+ */
225
+ let canvasGapStyleInjected = false;
226
+ function useCanvasGapStyle() {
227
+ useEffect(() => {
228
+ if (canvasGapStyleInjected || typeof document === 'undefined') return;
229
+ canvasGapStyleInjected = true;
230
+ const style = document.createElement('style');
231
+ style.dataset.componentAnatomy = 'anatomy-block';
232
+ style.textContent = '.ca-anatomy .sbdocs-preview { margin-bottom: 8px; }';
233
+ document.head.appendChild(style);
234
+ }, []);
235
+ }
236
+
237
+ export type AnatomyProps = AnatomyTableProps & {
238
+ /**
239
+ * Passed straight to the underlying `<Canvas>`. `'none'` (the default)
240
+ * drops the show-code button along with the panel it opens — chrome that
241
+ * mostly restates what the anatomy table already documents. Set explicitly
242
+ * to keep it, e.g. for a consumer relying on that source elsewhere.
243
+ */
244
+ sourceState?: 'hidden' | 'shown' | 'none';
245
+ };
246
+
247
+ /**
248
+ * The canvas and its anatomy table, paired and tightened for that specific
249
+ * combination ([#13](https://github.com/julien-deramond/component-anatomy/issues/13#issuecomment-5475936860)).
250
+ * For a hand-placed `<Canvas>` — a custom layout, or a source panel this
251
+ * block would otherwise hide — use `<AnatomyTable>` next to your own
252
+ * `<Canvas>` instead.
253
+ */
254
+ export const Anatomy: React.FC<AnatomyProps> = ({
255
+ of,
256
+ parts,
257
+ sync,
258
+ sourceState = 'none',
259
+ }) => {
260
+ useCanvasGapStyle();
261
+ return (
262
+ <div className="ca-anatomy">
263
+ <Canvas of={of} sourceState={sourceState} />
264
+ <AnatomyTable of={of} parts={parts} sync={sync} />
265
+ </div>
266
+ );
267
+ };