@component-anatomy/core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/theme.ts ADDED
@@ -0,0 +1,118 @@
1
+ import type { AnatomyTheme, AnatomyPresetName } from './types.js';
2
+
3
+ /**
4
+ * Theming — resolves { preset, theme } into a set of CSS custom properties
5
+ * applied per controller instance.
6
+ *
7
+ * Resolution order (lowest → highest priority):
8
+ * 1. Stylesheet defaults (the `var(--ca-*, fallback)` chain — the "default" look)
9
+ * 2. Global CSS variables set by the user on :root or an ancestor
10
+ * 3. A named preset passed to createAnatomy()
11
+ * 4. Individual `theme` tokens passed to createAnatomy()
12
+ *
13
+ * The `accent` token is a shorthand: when provided, overlay border, overlay
14
+ * background and label background are derived from it (via color-mix) unless
15
+ * they are explicitly set too.
16
+ */
17
+
18
+ /** Maps theme token names → CSS custom property names used by the stylesheet. */
19
+ export const TOKEN_TO_VAR: Record<
20
+ keyof Omit<AnatomyTheme, 'accent' | 'overlayBorderStyle'>,
21
+ string
22
+ > = {
23
+ overlayBg: '--ca-overlay-bg',
24
+ overlayBorder: '--ca-overlay-border',
25
+ overlayBorderWidth: '--ca-overlay-border-width',
26
+ overlayRadius: '--ca-overlay-radius',
27
+ labelBg: '--ca-label-bg',
28
+ labelFg: '--ca-label-fg',
29
+ labelFont: '--ca-label-font',
30
+ labelFontSize: '--ca-label-font-size',
31
+ zIndex: '--ca-overlay-z',
32
+ transitionMs: '--ca-transition',
33
+ };
34
+
35
+ /**
36
+ * Built-in presets. Each is a plain AnatomyTheme — you can also import these,
37
+ * spread them, and tweak individual tokens.
38
+ */
39
+ export const presets: Record<AnatomyPresetName, AnatomyTheme> = {
40
+ /** The built-in indigo look. Empty on purpose: it lives in the stylesheet defaults. */
41
+ default: {},
42
+
43
+ /** Quiet: no fill, thin neutral border, subdued label. */
44
+ minimal: {
45
+ overlayBg: 'transparent',
46
+ overlayBorder: 'rgba(107, 114, 128, 0.9)',
47
+ overlayBorderWidth: '1px',
48
+ overlayRadius: '2px',
49
+ labelBg: '#374151',
50
+ labelFg: '#f9fafb',
51
+ },
52
+
53
+ /** High-visibility: strong yellow/black, thick border. WCAG-friendly. */
54
+ contrast: {
55
+ overlayBg: 'rgba(250, 204, 21, 0.25)',
56
+ overlayBorder: '#000000',
57
+ overlayBorderWidth: '3px',
58
+ overlayRadius: '0px',
59
+ labelBg: '#000000',
60
+ labelFg: '#facc15',
61
+ },
62
+
63
+ /** Technical drawing look: blue dashed outline on a light wash. */
64
+ blueprint: {
65
+ overlayBg: 'rgba(37, 99, 235, 0.08)',
66
+ overlayBorder: '#2563eb',
67
+ overlayBorderWidth: '1.5px',
68
+ overlayRadius: '0px',
69
+ overlayBorderStyle: 'dashed',
70
+ labelBg: '#1d4ed8',
71
+ labelFg: '#ffffff',
72
+ },
73
+ };
74
+
75
+ /**
76
+ * Resolve a { preset, theme } pair into a flat CSS-variable map.
77
+ * Only *customized* tokens are returned — an empty result means
78
+ * "use the stylesheet defaults", which keeps global `--ca-*` variables
79
+ * set by users fully functional (backward compatible).
80
+ */
81
+ export function resolveThemeVars(
82
+ preset?: AnatomyPresetName,
83
+ theme?: AnatomyTheme
84
+ ): Record<string, string> {
85
+ const merged: AnatomyTheme = {
86
+ ...(preset ? presets[preset] : undefined),
87
+ ...theme,
88
+ };
89
+
90
+ const vars: Record<string, string> = {};
91
+
92
+ // Accent shorthand — derive dependent tokens unless explicitly overridden.
93
+ if (merged.accent) {
94
+ vars['--ca-overlay-border'] = merged.accent;
95
+ vars['--ca-overlay-bg'] = `color-mix(in srgb, ${merged.accent} 15%, transparent)`;
96
+ vars['--ca-label-bg'] = merged.accent;
97
+ }
98
+
99
+ for (const [token, cssVar] of Object.entries(TOKEN_TO_VAR)) {
100
+ const value = merged[token as keyof AnatomyTheme];
101
+ if (value !== undefined) {
102
+ vars[cssVar] =
103
+ typeof value === 'number'
104
+ ? token === 'zIndex'
105
+ ? String(value)
106
+ : token === 'transitionMs'
107
+ ? `${value}ms`
108
+ : `${value}px`
109
+ : String(value);
110
+ }
111
+ }
112
+
113
+ if (merged.overlayBorderStyle) {
114
+ vars['--ca-overlay-border-style'] = merged.overlayBorderStyle;
115
+ }
116
+
117
+ return vars;
118
+ }
package/src/types.ts ADDED
@@ -0,0 +1,115 @@
1
+ export type AnatomyPartDefinition = {
2
+ /** Matches the value of data-part="..." on the DOM element */
3
+ id: string;
4
+ /** Display name shown in the documentation panel and overlay label */
5
+ name: string;
6
+ /** Raw Markdown string — rendered by the integration layer */
7
+ description?: string;
8
+ };
9
+
10
+ export type AnatomyEvent = 'part:enter' | 'part:leave';
11
+ export type AnatomyEventHandler = (partId: string) => void;
12
+
13
+ /* ─────────────────────────── Theming ─────────────────────────── */
14
+
15
+ /** Names of the built-in visual presets. */
16
+ export type AnatomyPresetName = 'default' | 'minimal' | 'contrast' | 'blueprint';
17
+
18
+ /**
19
+ * Theme tokens. All optional — anything you don't set falls back to the
20
+ * preset, then to global `--ca-*` CSS variables, then to the default look.
21
+ */
22
+ export type AnatomyTheme = {
23
+ /**
24
+ * Shorthand: a single brand color. Overlay border, overlay background
25
+ * (15% wash) and label background are derived from it automatically.
26
+ */
27
+ accent?: string;
28
+ /** Overlay fill. Any CSS color. */
29
+ overlayBg?: string;
30
+ /** Overlay border color. */
31
+ overlayBorder?: string;
32
+ /** Overlay border width — number (px) or CSS length. */
33
+ overlayBorderWidth?: string | number;
34
+ /** Overlay border style: 'solid' | 'dashed' | 'dotted'… */
35
+ overlayBorderStyle?: string;
36
+ /** Overlay corner radius — number (px) or CSS length. */
37
+ overlayRadius?: string | number;
38
+ /** Label chip background. */
39
+ labelBg?: string;
40
+ /** Label chip text color. */
41
+ labelFg?: string;
42
+ /** Label chip font-family. */
43
+ labelFont?: string;
44
+ /** Label chip font-size — number (px) or CSS length. */
45
+ labelFontSize?: string | number;
46
+ /** z-index of overlays and labels. */
47
+ zIndex?: number;
48
+ /** Show/hide transition duration — number (ms) or CSS time. */
49
+ transitionMs?: string | number;
50
+ };
51
+
52
+ /* ─────────────────────── Overlay customization ─────────────────────── */
53
+
54
+ export type OverlayRenderContext = {
55
+ /** The part being highlighted. */
56
+ part: AnatomyPartDefinition;
57
+ /** The DOM element this overlay box covers. */
58
+ element: HTMLElement;
59
+ /** Index of this element among all elements matching the part (0-based). */
60
+ index: number;
61
+ };
62
+
63
+ export type OverlayOptions = {
64
+ /** Show the floating name label chip. Default: true. */
65
+ label?: boolean;
66
+ /**
67
+ * Custom label content. Return a string (used as textContent) or a Node
68
+ * (appended). Return null/undefined to fall back to the part name.
69
+ */
70
+ renderLabel?: (ctx: OverlayRenderContext) => string | Node | null | undefined;
71
+ /**
72
+ * Called after each overlay box is created and positioned — mutate it,
73
+ * append children, add classes. The box is `position: fixed` and sized
74
+ * to the target element.
75
+ */
76
+ decorateOverlay?: (box: HTMLElement, ctx: OverlayRenderContext) => void;
77
+ /** Extra class name(s) added to every overlay box. */
78
+ className?: string;
79
+ /** Inflate the highlight box by N pixels on every side. Default: 0. */
80
+ padding?: number;
81
+ };
82
+
83
+ /* ─────────────────────────── Options ─────────────────────────── */
84
+
85
+ export type AnatomyOptions = {
86
+ /** The component preview container — where data-part elements live */
87
+ root: HTMLElement;
88
+ /** The documentation panel container — where data-anatomy-item elements live */
89
+ panel?: HTMLElement;
90
+ /** Part definitions. If omitted, auto-discovers from data-part values in the DOM */
91
+ parts?: AnatomyPartDefinition[];
92
+ /** Named visual preset. Default: 'default'. */
93
+ preset?: AnatomyPresetName;
94
+ /** Per-instance theme token overrides (applied on top of the preset). */
95
+ theme?: AnatomyTheme;
96
+ /** Overlay rendering options and hooks. */
97
+ overlay?: OverlayOptions;
98
+ };
99
+
100
+ export type AnatomyController = {
101
+ /** Programmatically activate a part (overlay + panel highlight) */
102
+ highlight(partId: string): void;
103
+ /** Deactivate all parts */
104
+ unhighlight(): void;
105
+ /** Re-query the DOM — call after dynamic updates */
106
+ refresh(): void;
107
+ /** Tear down all listeners and overlays */
108
+ destroy(): void;
109
+ /** Subscribe to part activation events. Returns an unsubscribe function. */
110
+ on(event: AnatomyEvent, handler: AnatomyEventHandler): () => void;
111
+ /** The resolved part definitions (explicit or auto-discovered). */
112
+ getParts(): AnatomyPartDefinition[];
113
+ /** Swap theme tokens at runtime. Pass a preset name, tokens, or both. */
114
+ setTheme(theme: AnatomyTheme, preset?: AnatomyPresetName): void;
115
+ };