@component-anatomy/astro 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/LICENSE +21 -0
- package/README.md +90 -0
- package/package.json +57 -0
- package/src/ComponentAnatomy.astro +411 -0
- package/src/index.ts +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Julien Déramond
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @component-anatomy/astro
|
|
2
|
+
|
|
3
|
+
> Astro integration for component-anatomy — SSR-rendered Markdown descriptions, live hover sync, zero client-side Markdown bundle.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @component-anatomy/astro
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Import the `<ComponentAnatomy>` component and wrap any HTML you want to document:
|
|
14
|
+
|
|
15
|
+
```astro
|
|
16
|
+
---
|
|
17
|
+
import ComponentAnatomy from '@component-anatomy/astro/ComponentAnatomy.astro';
|
|
18
|
+
|
|
19
|
+
const parts = [
|
|
20
|
+
{
|
|
21
|
+
id: 'track',
|
|
22
|
+
name: 'Track',
|
|
23
|
+
description: 'The **rail** where the thumb slides. Takes full width of the container.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'thumb',
|
|
27
|
+
name: 'Thumb',
|
|
28
|
+
description: 'The draggable handle. Constrained to the `Track`.',
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<ComponentAnatomy parts={parts}>
|
|
34
|
+
<div class="slider">
|
|
35
|
+
<div class="track" data-part="track"></div>
|
|
36
|
+
<div class="thumb" data-part="thumb"></div>
|
|
37
|
+
</div>
|
|
38
|
+
</ComponentAnatomy>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Props
|
|
42
|
+
|
|
43
|
+
| Prop | Type | Required | Description |
|
|
44
|
+
|------|------|----------|-------------|
|
|
45
|
+
| `parts` | `AnatomyPartDefinition[]` | No | Part definitions. If omitted, parts are auto-discovered from `data-part` attributes. |
|
|
46
|
+
| `preset` | `'default' \| 'minimal' \| 'contrast' \| 'blueprint'` | No | Visual preset for overlays and panel accent. |
|
|
47
|
+
| `theme` | `AnatomyTheme` | No | Theme token overrides, e.g. `{ accent: '#0d9488', overlayRadius: 8 }`. |
|
|
48
|
+
| `overlayLabel` | `boolean` | No | Show the floating name chip. Default `true`. |
|
|
49
|
+
| `label` | `string` | No | Accessible label of the block. Default "Component anatomy". |
|
|
50
|
+
| `class` | `string` | No | Extra class on the wrapper. |
|
|
51
|
+
|
|
52
|
+
## Theming
|
|
53
|
+
|
|
54
|
+
```astro
|
|
55
|
+
<ComponentAnatomy parts={parts} preset="blueprint">
|
|
56
|
+
<Slider />
|
|
57
|
+
</ComponentAnatomy>
|
|
58
|
+
|
|
59
|
+
<ComponentAnatomy parts={parts} theme={{ accent: '#0d9488' }}>
|
|
60
|
+
<Slider />
|
|
61
|
+
<p slot="header">Hover a part below, or the component itself.</p>
|
|
62
|
+
</ComponentAnatomy>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The theme applies to both the canvas overlays and the panel accents. Layout variables (`--ca-gap`, `--ca-preview-bg`, `--ca-preview-padding`, `--ca-panel-max-height`) can be set on the wrapper via `class`. Full guide: [customization docs](https://julien-deramond.github.io/component-anatomy/docs/customization).
|
|
66
|
+
|
|
67
|
+
`AnatomyPartDefinition`:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
{
|
|
71
|
+
id: string; // Matches the data-part value on the DOM element
|
|
72
|
+
name: string; // Display name shown in the panel
|
|
73
|
+
description?: string; // Raw Markdown — rendered server-side by marked
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## How it works
|
|
78
|
+
|
|
79
|
+
- The server renders the two-column layout (preview + panel) with Markdown descriptions converted to HTML by [`marked`](https://marked.js.org/) — no Markdown library is shipped to the browser
|
|
80
|
+
- The client script calls `createAnatomy()` from `@component-anatomy/core` to wire up hover sync and overlays
|
|
81
|
+
- Multiple `<ComponentAnatomy>` instances on the same page are isolated via a random instance ID
|
|
82
|
+
|
|
83
|
+
## Requirements
|
|
84
|
+
|
|
85
|
+
- Astro ≥ 4.0
|
|
86
|
+
- Node ≥ 18
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@component-anatomy/astro",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Astro integration for component-anatomy — SSR-rendered anatomy panels with live hover sync",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Julien Déramond",
|
|
7
|
+
"homepage": "https://github.com/julien-deramond/component-anatomy#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/julien-deramond/component-anatomy",
|
|
11
|
+
"directory": "packages/astro"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/julien-deramond/component-anatomy/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"component-anatomy",
|
|
18
|
+
"astro",
|
|
19
|
+
"astro-component",
|
|
20
|
+
"design-system",
|
|
21
|
+
"documentation",
|
|
22
|
+
"anatomy",
|
|
23
|
+
"interactive"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": "./src/index.ts",
|
|
29
|
+
"types": "./src/index.ts"
|
|
30
|
+
},
|
|
31
|
+
"./ComponentAnatomy.astro": "./src/ComponentAnatomy.astro"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"src"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"marked": "^12.0.0",
|
|
41
|
+
"@component-anatomy/core": "^0.0.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"astro": "^4.11.0",
|
|
45
|
+
"typescript": "^5.4.5"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"astro": ">=4.0.0"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18.0.0"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "echo 'No build step — Astro resolves .astro files directly'",
|
|
55
|
+
"typecheck": "tsc --noEmit"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { marked } from 'marked';
|
|
3
|
+
import { resolveThemeVars } from '@component-anatomy/core';
|
|
4
|
+
import type {
|
|
5
|
+
AnatomyPartDefinition,
|
|
6
|
+
AnatomyPresetName,
|
|
7
|
+
AnatomyTheme,
|
|
8
|
+
} from '@component-anatomy/core';
|
|
9
|
+
|
|
10
|
+
export interface Props {
|
|
11
|
+
parts?: AnatomyPartDefinition[];
|
|
12
|
+
class?: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
/** Named visual preset for overlays + panel accent. Default: 'default'. */
|
|
15
|
+
preset?: AnatomyPresetName;
|
|
16
|
+
/** Theme token overrides (applied on top of the preset). */
|
|
17
|
+
theme?: AnatomyTheme;
|
|
18
|
+
/** Show the floating name label chip over highlighted elements. Default: true. */
|
|
19
|
+
overlayLabel?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
parts,
|
|
24
|
+
class: className,
|
|
25
|
+
label = 'Component anatomy',
|
|
26
|
+
preset,
|
|
27
|
+
theme,
|
|
28
|
+
overlayLabel = true,
|
|
29
|
+
} = Astro.props;
|
|
30
|
+
|
|
31
|
+
const instanceId = `ca-${Math.random().toString(36).slice(2, 9)}`;
|
|
32
|
+
|
|
33
|
+
// Resolved theme vars are applied twice on purpose:
|
|
34
|
+
// 1. Inline on .ca-root — so panel accents (active entry, pill) match the theme.
|
|
35
|
+
// 2. Serialized to the client — overlays live in document.body, outside this
|
|
36
|
+
// element's cascade, so createAnatomy() needs the tokens directly.
|
|
37
|
+
const themeVars = resolveThemeVars(preset, theme);
|
|
38
|
+
const themeStyle = Object.entries(themeVars)
|
|
39
|
+
.map(([k, v]) => `${k}: ${v};`)
|
|
40
|
+
.join(' ');
|
|
41
|
+
const clientConfig = JSON.stringify({ preset, theme, overlayLabel });
|
|
42
|
+
|
|
43
|
+
const renderedParts = await Promise.all(
|
|
44
|
+
(parts ?? []).map(async (part) => ({
|
|
45
|
+
...part,
|
|
46
|
+
html: part.description ? await marked.parse(part.description) : null,
|
|
47
|
+
}))
|
|
48
|
+
);
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
<div
|
|
52
|
+
class:list={['ca-root', className]}
|
|
53
|
+
data-anatomy-root={instanceId}
|
|
54
|
+
data-anatomy-config={clientConfig}
|
|
55
|
+
aria-label={label}
|
|
56
|
+
style={themeStyle || undefined}
|
|
57
|
+
>
|
|
58
|
+
<!-- Left: live component preview with dot-grid background -->
|
|
59
|
+
<div class="ca-preview" data-anatomy-preview={instanceId}>
|
|
60
|
+
<div class="ca-preview-inner">
|
|
61
|
+
<slot />
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<!-- Right: documentation panel -->
|
|
66
|
+
<div class="ca-panel-wrapper">
|
|
67
|
+
{Astro.slots.has('header') && (
|
|
68
|
+
<div class="ca-panel-header">
|
|
69
|
+
<slot name="header" />
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
<!-- Sticky "active part" indicator — visible when scrolled past the active entry -->
|
|
73
|
+
<div class="ca-active-pill" data-anatomy-pill={instanceId} aria-hidden="true">
|
|
74
|
+
<span class="ca-active-pill-dot"></span>
|
|
75
|
+
<span class="ca-active-pill-name"></span>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div
|
|
79
|
+
class="ca-panel"
|
|
80
|
+
data-anatomy-panel={instanceId}
|
|
81
|
+
role="list"
|
|
82
|
+
aria-label="Anatomy parts"
|
|
83
|
+
>
|
|
84
|
+
{renderedParts.length > 0
|
|
85
|
+
? renderedParts.map((part) => (
|
|
86
|
+
<div
|
|
87
|
+
class="ca-part-entry"
|
|
88
|
+
data-anatomy-item={part.id}
|
|
89
|
+
role="listitem"
|
|
90
|
+
tabindex="0"
|
|
91
|
+
aria-label={part.name}
|
|
92
|
+
>
|
|
93
|
+
<div class="ca-part-header">
|
|
94
|
+
<span class="ca-part-indicator" aria-hidden="true"></span>
|
|
95
|
+
<span class="ca-part-name">{part.name}</span>
|
|
96
|
+
<code class="ca-part-id">{part.id}</code>
|
|
97
|
+
</div>
|
|
98
|
+
{part.html && (
|
|
99
|
+
<div class="ca-part-description" set:html={part.html} />
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
))
|
|
103
|
+
: (
|
|
104
|
+
<p class="ca-no-parts">
|
|
105
|
+
No parts defined. Add <code>data-part="name"</code> to your component elements.
|
|
106
|
+
</p>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<style>
|
|
114
|
+
/* ─── Layout ─── */
|
|
115
|
+
.ca-root {
|
|
116
|
+
display: grid;
|
|
117
|
+
grid-template-columns: 1fr 1fr;
|
|
118
|
+
gap: var(--ca-gap, 2rem);
|
|
119
|
+
align-items: start;
|
|
120
|
+
font-family: inherit;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ─── Preview ─── */
|
|
124
|
+
.ca-preview {
|
|
125
|
+
position: sticky;
|
|
126
|
+
top: 1.5rem;
|
|
127
|
+
border-radius: var(--ca-preview-radius, 10px);
|
|
128
|
+
border: 1px solid var(--ca-preview-border, #e5e7eb);
|
|
129
|
+
overflow: hidden;
|
|
130
|
+
background:
|
|
131
|
+
/* dot grid */
|
|
132
|
+
radial-gradient(circle, #d1d5db 1px, transparent 1px),
|
|
133
|
+
var(--ca-preview-bg, #f9fafb);
|
|
134
|
+
background-size: 20px 20px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.ca-preview-inner {
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
justify-content: center;
|
|
141
|
+
min-height: 160px;
|
|
142
|
+
padding: var(--ca-preview-padding, 2.5rem 2rem);
|
|
143
|
+
/* Slight backdrop so the component pops against the dot grid */
|
|
144
|
+
background: transparent;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ─── Panel wrapper ─── */
|
|
148
|
+
.ca-panel-wrapper {
|
|
149
|
+
position: relative;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ─── Optional header slot ─── */
|
|
153
|
+
.ca-panel-header {
|
|
154
|
+
margin-bottom: 0.75rem;
|
|
155
|
+
font-size: 0.875rem;
|
|
156
|
+
color: #4b5563;
|
|
157
|
+
}
|
|
158
|
+
.ca-panel-header :global(> :first-child) { margin-top: 0; }
|
|
159
|
+
.ca-panel-header :global(> :last-child) { margin-bottom: 0; }
|
|
160
|
+
|
|
161
|
+
/* ─── Sticky active pill ─── */
|
|
162
|
+
.ca-active-pill {
|
|
163
|
+
position: sticky;
|
|
164
|
+
top: 0;
|
|
165
|
+
z-index: 10;
|
|
166
|
+
display: none; /* shown via JS when scrolled */
|
|
167
|
+
align-items: center;
|
|
168
|
+
gap: 6px;
|
|
169
|
+
padding: 5px 10px;
|
|
170
|
+
margin-bottom: 4px;
|
|
171
|
+
border-radius: 6px;
|
|
172
|
+
background: var(--ca-label-bg, #4f46e5);
|
|
173
|
+
color: #fff;
|
|
174
|
+
font-size: 0.72rem;
|
|
175
|
+
font-weight: 600;
|
|
176
|
+
text-transform: uppercase;
|
|
177
|
+
letter-spacing: 0.07em;
|
|
178
|
+
pointer-events: none;
|
|
179
|
+
box-shadow: 0 2px 8px rgba(79,70,229,0.25);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.ca-active-pill.ca-pill--visible {
|
|
183
|
+
display: flex;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.ca-active-pill-dot {
|
|
187
|
+
width: 6px;
|
|
188
|
+
height: 6px;
|
|
189
|
+
border-radius: 50%;
|
|
190
|
+
background: rgba(255,255,255,0.7);
|
|
191
|
+
flex-shrink: 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* ─── Panel ─── */
|
|
195
|
+
.ca-panel {
|
|
196
|
+
display: flex;
|
|
197
|
+
flex-direction: column;
|
|
198
|
+
gap: 2px;
|
|
199
|
+
max-height: var(--ca-panel-max-height, 480px);
|
|
200
|
+
overflow-y: auto;
|
|
201
|
+
scroll-behavior: smooth;
|
|
202
|
+
padding-right: 2px; /* room for scrollbar */
|
|
203
|
+
|
|
204
|
+
/* Subtle scrollbar */
|
|
205
|
+
scrollbar-width: thin;
|
|
206
|
+
scrollbar-color: #e5e7eb transparent;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.ca-panel::-webkit-scrollbar { width: 4px; }
|
|
210
|
+
.ca-panel::-webkit-scrollbar-track { background: transparent; }
|
|
211
|
+
.ca-panel::-webkit-scrollbar-thumb { background: #e5e7eb; border-radius: 99px; }
|
|
212
|
+
|
|
213
|
+
/* ─── Part entries ─── */
|
|
214
|
+
.ca-part-entry {
|
|
215
|
+
padding: 0.8rem 0.9rem;
|
|
216
|
+
border-radius: 8px;
|
|
217
|
+
cursor: default;
|
|
218
|
+
transition: background 120ms ease, border-color 120ms ease;
|
|
219
|
+
border: 1px solid transparent;
|
|
220
|
+
outline: none;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.ca-part-entry:focus-visible {
|
|
224
|
+
outline: 2px solid var(--ca-overlay-border, rgba(99, 102, 241, 0.7));
|
|
225
|
+
outline-offset: 2px;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.ca-part-entry[data-active] {
|
|
229
|
+
background: var(--ca-item-active-bg, rgba(99, 102, 241, 0.06));
|
|
230
|
+
border-color: rgba(99, 102, 241, 0.15);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* ─── Part header row ─── */
|
|
234
|
+
.ca-part-header {
|
|
235
|
+
display: flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
gap: 8px;
|
|
238
|
+
margin-bottom: 0.4rem;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.ca-part-indicator {
|
|
242
|
+
width: 8px;
|
|
243
|
+
height: 8px;
|
|
244
|
+
border-radius: 50%;
|
|
245
|
+
border: 2px solid #d1d5db;
|
|
246
|
+
flex-shrink: 0;
|
|
247
|
+
transition: background 120ms ease, border-color 120ms ease;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.ca-part-entry[data-active] .ca-part-indicator {
|
|
251
|
+
background: var(--ca-label-bg, #4f46e5);
|
|
252
|
+
border-color: var(--ca-label-bg, #4f46e5);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.ca-part-name {
|
|
256
|
+
font-size: 0.82rem;
|
|
257
|
+
font-weight: 650;
|
|
258
|
+
color: var(--ca-part-name-color, #111827);
|
|
259
|
+
flex: 1;
|
|
260
|
+
letter-spacing: 0.01em;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.ca-part-entry[data-active] .ca-part-name {
|
|
264
|
+
color: var(--ca-label-bg, #4f46e5);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.ca-part-id {
|
|
268
|
+
font-family: ui-monospace, 'Cascadia Code', 'Fira Mono', monospace;
|
|
269
|
+
font-size: 0.68rem;
|
|
270
|
+
padding: 1px 6px;
|
|
271
|
+
border-radius: 4px;
|
|
272
|
+
background: #f3f4f6;
|
|
273
|
+
color: #6b7280;
|
|
274
|
+
border: 1px solid #e5e7eb;
|
|
275
|
+
flex-shrink: 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.ca-part-entry[data-active] .ca-part-id {
|
|
279
|
+
background: rgba(99,102,241,0.08);
|
|
280
|
+
border-color: rgba(99,102,241,0.2);
|
|
281
|
+
color: #4f46e5;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* ─── Markdown description ─── */
|
|
285
|
+
.ca-part-description {
|
|
286
|
+
font-size: 0.85rem;
|
|
287
|
+
line-height: 1.7;
|
|
288
|
+
color: var(--ca-part-desc-color, #4b5563);
|
|
289
|
+
padding-left: 16px; /* aligns with text after the indicator dot */
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.ca-part-description :global(p) { margin: 0 0 0.5em; }
|
|
293
|
+
.ca-part-description :global(p:last-child) { margin-bottom: 0; }
|
|
294
|
+
.ca-part-description :global(h1),
|
|
295
|
+
.ca-part-description :global(h2),
|
|
296
|
+
.ca-part-description :global(h3) {
|
|
297
|
+
font-size: 0.75rem;
|
|
298
|
+
font-weight: 700;
|
|
299
|
+
text-transform: uppercase;
|
|
300
|
+
letter-spacing: 0.06em;
|
|
301
|
+
color: #9ca3af;
|
|
302
|
+
margin: 0.9em 0 0.3em;
|
|
303
|
+
}
|
|
304
|
+
.ca-part-description :global(code) {
|
|
305
|
+
font-size: 0.78em;
|
|
306
|
+
padding: 0.15em 0.4em;
|
|
307
|
+
border-radius: 3px;
|
|
308
|
+
background: #f3f4f6;
|
|
309
|
+
border: 1px solid #e5e7eb;
|
|
310
|
+
font-family: ui-monospace, 'Cascadia Code', monospace;
|
|
311
|
+
color: #374151;
|
|
312
|
+
}
|
|
313
|
+
.ca-part-description :global(a) {
|
|
314
|
+
color: var(--ca-label-bg, #4f46e5);
|
|
315
|
+
text-decoration: underline;
|
|
316
|
+
text-underline-offset: 2px;
|
|
317
|
+
}
|
|
318
|
+
.ca-part-description :global(ul), .ca-part-description :global(ol) {
|
|
319
|
+
margin: 0.3em 0 0.5em;
|
|
320
|
+
padding-left: 1.3em;
|
|
321
|
+
}
|
|
322
|
+
.ca-part-description :global(li) { margin-bottom: 0.2em; }
|
|
323
|
+
.ca-part-description :global(strong) { color: #111827; font-weight: 600; }
|
|
324
|
+
|
|
325
|
+
.ca-no-parts {
|
|
326
|
+
font-size: 0.875rem;
|
|
327
|
+
color: #9ca3af;
|
|
328
|
+
font-style: italic;
|
|
329
|
+
padding: 0.75rem;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* ─── Responsive ─── */
|
|
333
|
+
@media (max-width: 720px) {
|
|
334
|
+
.ca-root { grid-template-columns: 1fr; }
|
|
335
|
+
.ca-preview { position: static; }
|
|
336
|
+
.ca-panel { max-height: 360px; }
|
|
337
|
+
}
|
|
338
|
+
</style>
|
|
339
|
+
|
|
340
|
+
<script>
|
|
341
|
+
import { createAnatomy } from '@component-anatomy/core';
|
|
342
|
+
|
|
343
|
+
function init() {
|
|
344
|
+
document.querySelectorAll<HTMLElement>('[data-anatomy-root]').forEach((root) => {
|
|
345
|
+
if (root.dataset.anatomyInit) return;
|
|
346
|
+
root.dataset.anatomyInit = '1';
|
|
347
|
+
|
|
348
|
+
const id = root.dataset.anatomyRoot!;
|
|
349
|
+
const preview = root.querySelector<HTMLElement>(`[data-anatomy-preview="${id}"]`);
|
|
350
|
+
const panel = root.querySelector<HTMLElement>(`[data-anatomy-panel="${id}"]`);
|
|
351
|
+
const pill = root.querySelector<HTMLElement>(`[data-anatomy-pill="${id}"]`);
|
|
352
|
+
const pillName = pill?.querySelector<HTMLElement>('.ca-active-pill-name');
|
|
353
|
+
|
|
354
|
+
if (!preview) return;
|
|
355
|
+
|
|
356
|
+
// The preview is a visual demo — hide it from the accessibility tree
|
|
357
|
+
// and remove all interactive elements from the tab order.
|
|
358
|
+
// We use tabindex="-1" rather than `inert` because `inert` also blocks
|
|
359
|
+
// pointer events, which would break our mouseenter/mouseleave detection.
|
|
360
|
+
preview.setAttribute('aria-hidden', 'true');
|
|
361
|
+
const FOCUSABLE = 'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
362
|
+
preview.querySelectorAll<HTMLElement>(FOCUSABLE).forEach((el) => {
|
|
363
|
+
el.setAttribute('tabindex', '-1');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
let config: { preset?: string; theme?: object; overlayLabel?: boolean } = {};
|
|
367
|
+
try {
|
|
368
|
+
config = JSON.parse(root.dataset.anatomyConfig ?? '{}');
|
|
369
|
+
} catch { /* malformed config — fall back to defaults */ }
|
|
370
|
+
|
|
371
|
+
const anatomy = createAnatomy({
|
|
372
|
+
root: preview,
|
|
373
|
+
panel: panel ?? undefined,
|
|
374
|
+
preset: config.preset as import('@component-anatomy/core').AnatomyPresetName | undefined,
|
|
375
|
+
theme: config.theme as import('@component-anatomy/core').AnatomyTheme | undefined,
|
|
376
|
+
overlay: { label: config.overlayLabel !== false },
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Sticky pill: show/hide based on whether the active panel entry is in view
|
|
380
|
+
if (pill && panel) {
|
|
381
|
+
const observer = new IntersectionObserver(
|
|
382
|
+
(entries) => {
|
|
383
|
+
const activeEntry = panel.querySelector<HTMLElement>('[data-anatomy-item][data-active]');
|
|
384
|
+
if (!activeEntry) { pill.classList.remove('ca-pill--visible'); return; }
|
|
385
|
+
const isVisible = entries.some(
|
|
386
|
+
(e) => e.target === activeEntry && e.isIntersecting
|
|
387
|
+
);
|
|
388
|
+
pill.classList.toggle('ca-pill--visible', !isVisible);
|
|
389
|
+
},
|
|
390
|
+
{ root: panel, threshold: 0.5 }
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
// Re-observe whenever a new part becomes active
|
|
394
|
+
anatomy.on('part:enter', (partId) => {
|
|
395
|
+
if (pillName) pillName.textContent = partId;
|
|
396
|
+
const activeEntry = panel.querySelector<HTMLElement>(`[data-anatomy-item="${partId}"]`);
|
|
397
|
+
if (activeEntry) observer.observe(activeEntry);
|
|
398
|
+
else pill.classList.remove('ca-pill--visible');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
anatomy.on('part:leave', () => {
|
|
402
|
+
pill.classList.remove('ca-pill--visible');
|
|
403
|
+
if (pillName) pillName.textContent = '';
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
init();
|
|
410
|
+
document.addEventListener('astro:page-load', init);
|
|
411
|
+
</script>
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { AnatomyPartDefinition } from '@component-anatomy/core';
|