@orbytes/astrolab 0.3.0 → 0.4.0-next.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/README.md +109 -64
- package/defaults.mjs +65 -0
- package/dist/core/virtual-module/virtual-routes.js +12 -1
- package/docs/PIN-CONTRACT.md +8 -0
- package/docs/PIN.md +29 -19
- package/index.d.ts +40 -16
- package/index.mjs +32 -10
- package/package.json +6 -2
- package/src/Home.astro +167 -264
- package/src/LabHead.astro +37 -1047
- package/src/chrome/ComponentCard.astro +69 -0
- package/src/chrome/Icon.astro +21 -0
- package/src/chrome/LICENSE-icons +43 -0
- package/src/chrome/Nav.astro +105 -0
- package/src/chrome/Panel.astro +104 -0
- package/src/chrome/Shell.astro +106 -0
- package/src/chrome/Sprite.astro +23 -0
- package/src/chrome/StoryView.astro +251 -0
- package/src/chrome/Tree.astro +83 -0
- package/src/chrome/ViewportControls.astro +98 -0
- package/src/chrome/ViewportStage.astro +32 -0
- package/src/chrome/fonts/OFL.txt +93 -0
- package/src/chrome/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/src/chrome/icons.ts +59 -0
- package/src/chrome/marks-client.ts +102 -0
- package/src/chrome/model.ts +142 -0
- package/src/chrome/pins-data.ts +30 -0
- package/src/chrome/shell-client.ts +372 -0
- package/src/chrome/site-data.ts +230 -0
- package/src/chrome/trees.ts +152 -0
- package/src/chrome/viewport-client.ts +579 -0
- package/src/chrome/views/Assets.astro +110 -0
- package/src/chrome/views/Pages.astro +178 -0
- package/src/chrome/views/Placeholder.astro +37 -0
- package/src/chrome/views/Tasks.astro +107 -0
- package/src/core/LICENSE-astrobook +16 -0
- package/src/core/lib/components/home.astro +4 -2
- package/src/core/lib/pages/story.astro +12 -10
- package/src/core/virtual-module/virtual-routes.ts +20 -4
- package/src/pin/board.mjs +389 -175
- package/src/pin/index.mjs +33 -2
- package/src/pin/toolbar.js +1 -1
- package/src/shell/Browse.astro +106 -353
- package/src/shell/Viewport.astro +22 -1315
- package/src/shell/lab-index.ts +23 -14
- package/src/ui/components/app.astro +5 -7
- package/src/ui/components/theme-script.astro +13 -2
- package/src/ui/lab.css +2152 -370
- package/virtual.d.ts +13 -0
- package/src/shell/CardGrid.astro +0 -297
- package/src/ui/components/build-path.ts +0 -13
- package/src/ui/components/build-tree.ts +0 -108
- package/src/ui/components/collapse-duration.ts +0 -28
- package/src/ui/components/compress-terms.ts +0 -10
- package/src/ui/components/dashboard-layout.astro +0 -39
- package/src/ui/components/home.astro +0 -65
- package/src/ui/components/layout.astro +0 -110
- package/src/ui/components/sidebar-button-fullscreen.astro +0 -38
- package/src/ui/components/sidebar-button-search.astro +0 -23
- package/src/ui/components/sidebar-button-theme.astro +0 -9
- package/src/ui/components/sidebar-button.astro +0 -24
- package/src/ui/components/sidebar-resize-handle.astro +0 -74
- package/src/ui/components/sidebar-search-panel.astro +0 -41
- package/src/ui/components/sidebar-search-script.ts +0 -103
- package/src/ui/components/sidebar-title.astro +0 -17
- package/src/ui/components/sidebar-tree-node.astro +0 -143
- package/src/ui/components/sidebar-tree.astro +0 -84
- package/src/ui/components/sidebar.astro +0 -29
- package/src/ui/components/theme-toggle.astro +0 -63
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A component's page — <subpath>/dashboard/<storyId>, the route Astrobook's core generates for every
|
|
3
|
+
// story (../core/lib/pages/story.astro renders this). REDESIGNED 2026-09-24:
|
|
4
|
+
//
|
|
5
|
+
// · the story is an <iframe> of its bare route on a canvas you size (./ViewportStage.astro), no
|
|
6
|
+
// longer rendered inline beside the chrome — so this page carries none of the site's CSS;
|
|
7
|
+
// · the tree stops at the component, and the component's stories are TABS here, a dropdown past
|
|
8
|
+
// four; previous / next (and j / k) step through every story of the tier in tree order;
|
|
9
|
+
// · the component's marks (responsive, marked for deletion) live in the status menu behind its
|
|
10
|
+
// pills, and everything that leaves the lab (VS Code, full screen, staging, a link) in ⋯.
|
|
11
|
+
//
|
|
12
|
+
// The navbar is interim — ops/navbar-brief.md is the design it will be replaced with.
|
|
13
|
+
import Shell from "./Shell.astro";
|
|
14
|
+
import Tree from "./Tree.astro";
|
|
15
|
+
import Icon from "./Icon.astro";
|
|
16
|
+
import ViewportControls from "./ViewportControls.astro";
|
|
17
|
+
import ViewportStage from "./ViewportStage.astro";
|
|
18
|
+
import { hrefs, labConfig, type Crumb, type TreeNode } from "./model";
|
|
19
|
+
import { componentsOf, tierTree } from "./trees";
|
|
20
|
+
import { pinCounts } from "./pins-data";
|
|
21
|
+
import { buildLabIndex, isSectionLike, livePillText, liveTitle, tierLabel } from "../shell/lab-index";
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
story: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const index = buildLabIndex();
|
|
28
|
+
const item = index.items.find((i) => i.storyId === Astro.props.story)!;
|
|
29
|
+
const mod = componentsOf(index).find((c) => c.moduleId === item.moduleId)!;
|
|
30
|
+
const tree = tierTree(index, item.tier, mod.moduleId);
|
|
31
|
+
|
|
32
|
+
// Every story of the tier, in the tree's own order, for previous / next.
|
|
33
|
+
const leaves = (nodes: TreeNode[]): TreeNode[] => nodes.flatMap((n) => (n.children ? leaves(n.children) : [n]));
|
|
34
|
+
const modules = new Map(componentsOf(index).map((c) => [`mod:${c.moduleId}`, c]));
|
|
35
|
+
const sequence = leaves(tree).flatMap((leaf) => modules.get(leaf.id)?.stories ?? []);
|
|
36
|
+
const at = sequence.findIndex((s) => s.storyId === item.storyId);
|
|
37
|
+
const prev = at > 0 ? sequence[at - 1] : null;
|
|
38
|
+
const next = at >= 0 && at < sequence.length - 1 ? sequence[at + 1] : null;
|
|
39
|
+
|
|
40
|
+
const pins = await pinCounts();
|
|
41
|
+
const openPins = pins.openFor(mod.componentFile);
|
|
42
|
+
const liveStory = mod.stories.find((s) => s.live);
|
|
43
|
+
const section = isSectionLike(item);
|
|
44
|
+
const tierDef = labConfig.tiers.find((t) => t.id === item.tier);
|
|
45
|
+
const cullable = Boolean(tierDef?.cullable);
|
|
46
|
+
const blocked = mod.live
|
|
47
|
+
? "live on the site — cannot be marked"
|
|
48
|
+
: mod.usedBy.length
|
|
49
|
+
? `used by ${mod.usedBy.length} live component${mod.usedBy.length === 1 ? "" : "s"} — cannot be marked`
|
|
50
|
+
: null;
|
|
51
|
+
|
|
52
|
+
// Level 2's breadcrumb names the PANEL's place — Site › Sections — as the design does; where the
|
|
53
|
+
// component sits inside the tier is the tree's job, and which component it is, the navbar's.
|
|
54
|
+
const crumbs: Crumb[] = [{ label: "Site" }, { label: tierLabel(item.tier), href: hrefs.tier(item.tier) }];
|
|
55
|
+
const TAB_LIMIT = 4;
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
<Shell
|
|
59
|
+
title={`${mod.moduleName} — ${item.storyName}`}
|
|
60
|
+
active={`tier:${item.tier}`}
|
|
61
|
+
panel={{
|
|
62
|
+
label: tierLabel(item.tier),
|
|
63
|
+
crumbs,
|
|
64
|
+
searchPlaceholder: `Search ${tierLabel(item.tier).toLowerCase()}`,
|
|
65
|
+
filterLabel: "Show components",
|
|
66
|
+
filters: [
|
|
67
|
+
{ value: "live", label: "Live on the site" },
|
|
68
|
+
{ value: "used", label: "Used by something live" },
|
|
69
|
+
{ value: "unused", label: "Unused" },
|
|
70
|
+
],
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<Tree slot="panel" nodes={tree} />
|
|
74
|
+
|
|
75
|
+
<Fragment slot="navbar">
|
|
76
|
+
<div class="lab-navbar__id">
|
|
77
|
+
<h1 class="lab-navbar__title">
|
|
78
|
+
{mod.moduleName}
|
|
79
|
+
{mod.version && <small>{mod.version}</small>}
|
|
80
|
+
</h1>
|
|
81
|
+
<button class="lab-status-btn" type="button" popovertarget="lab-status-menu" title="Status and marks">
|
|
82
|
+
{liveStory && <span class="lab-pill lab-pill--live" title={liveTitle(liveStory)}>{livePillText(liveStory)}</span>}
|
|
83
|
+
{!mod.live && mod.usedBy.length > 0 && <span class="lab-pill lab-pill--used">used by {mod.usedBy.length}</span>}
|
|
84
|
+
{!mod.live && !mod.usedBy.length && <span class="lab-pill">unused</span>}
|
|
85
|
+
{mod.responsive && (
|
|
86
|
+
<span class:list={["lab-pill", mod.responsive.done ? "lab-pill--responsive" : "lab-pill--unresponsive"]} data-lab-resp-pill>
|
|
87
|
+
{mod.responsive.done ? "responsive" : "not responsive"}
|
|
88
|
+
</span>
|
|
89
|
+
)}
|
|
90
|
+
<span class="lab-pill lab-pill--marked" data-lab-cull-pill hidden>marked for deletion</span>
|
|
91
|
+
{openPins > 0 && <span class="lab-pill lab-pill--brand">{openPins} open pin{openPins === 1 ? "" : "s"}</span>}
|
|
92
|
+
<Icon name="chevron-down" size="sm" />
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<nav class="lab-tabs" aria-label="Stories">
|
|
97
|
+
{
|
|
98
|
+
mod.stories.length <= TAB_LIMIT ? (
|
|
99
|
+
mod.stories.map((s) => (
|
|
100
|
+
<a
|
|
101
|
+
class="lab-tab"
|
|
102
|
+
href={s.dashboardUrl}
|
|
103
|
+
aria-current={s.storyId === item.storyId ? "page" : undefined}
|
|
104
|
+
data-lab-keep-viewport
|
|
105
|
+
>
|
|
106
|
+
{s.storyName}
|
|
107
|
+
</a>
|
|
108
|
+
))
|
|
109
|
+
) : (
|
|
110
|
+
<>
|
|
111
|
+
<button class="lab-btn lab-btn--sm" type="button" popovertarget="lab-story-menu">
|
|
112
|
+
{item.storyName}
|
|
113
|
+
<span class="lab-tabs__count">{mod.stories.indexOf(item) + 1} / {mod.stories.length}</span>
|
|
114
|
+
<Icon name="chevron-down" />
|
|
115
|
+
</button>
|
|
116
|
+
<div class="lab-menu" id="lab-story-menu" popover role="menu">
|
|
117
|
+
<p class="lab-menu__label">{mod.stories.length} stories</p>
|
|
118
|
+
{mod.stories.map((s) => (
|
|
119
|
+
<a
|
|
120
|
+
class="lab-menu__item"
|
|
121
|
+
role="menuitemradio"
|
|
122
|
+
aria-checked={String(s.storyId === item.storyId)}
|
|
123
|
+
href={s.dashboardUrl}
|
|
124
|
+
data-lab-keep-viewport
|
|
125
|
+
>
|
|
126
|
+
{s.storyName}
|
|
127
|
+
<Icon name="check" size="sm" class="lab-menu__tick" />
|
|
128
|
+
</a>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
</>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
</nav>
|
|
135
|
+
|
|
136
|
+
<div class="lab-navbar__tools">
|
|
137
|
+
<div class="lab-seg" role="group" aria-label="Previous and next story">
|
|
138
|
+
{
|
|
139
|
+
prev ? (
|
|
140
|
+
<a class="lab-btn lab-btn--sm lab-btn--icon" href={prev.dashboardUrl} title={`Previous: ${prev.moduleName} — ${prev.storyName} (k)`} data-lab-step="prev" data-lab-keep-viewport>
|
|
141
|
+
<Icon name="chevron-left" />
|
|
142
|
+
<span class="lab-visually-hidden">Previous story</span>
|
|
143
|
+
</a>
|
|
144
|
+
) : (
|
|
145
|
+
<span class="lab-btn lab-btn--sm lab-btn--icon" aria-disabled="true" data-lab-step><Icon name="chevron-left" /></span>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
{
|
|
149
|
+
next ? (
|
|
150
|
+
<a class="lab-btn lab-btn--sm lab-btn--icon" href={next.dashboardUrl} title={`Next: ${next.moduleName} — ${next.storyName} (j)`} data-lab-step="next" data-lab-keep-viewport>
|
|
151
|
+
<Icon name="chevron-right" />
|
|
152
|
+
<span class="lab-visually-hidden">Next story</span>
|
|
153
|
+
</a>
|
|
154
|
+
) : (
|
|
155
|
+
<span class="lab-btn lab-btn--sm lab-btn--icon" aria-disabled="true" data-lab-step><Icon name="chevron-right" /></span>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<ViewportControls defaultW={section ? 1440 : 960} defaultH={section ? 900 : 540} kind={section ? "section" : "component"} />
|
|
162
|
+
|
|
163
|
+
<div class="lab-navbar__tools">
|
|
164
|
+
<button class="lab-btn lab-btn--sm lab-btn--icon" type="button" popovertarget="lab-actions-menu" title="More">
|
|
165
|
+
<Icon name="more" />
|
|
166
|
+
<span class="lab-visually-hidden">More actions</span>
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div class="lab-menu" id="lab-actions-menu" popover role="menu" data-lab-align="end">
|
|
171
|
+
{
|
|
172
|
+
item.editorUrl && (
|
|
173
|
+
<a class="lab-menu__item" role="menuitem" href={item.editorUrl}>
|
|
174
|
+
<Icon name="code" />
|
|
175
|
+
Open in VS Code
|
|
176
|
+
</a>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
<a class="lab-menu__item" role="menuitem" href={item.storyUrl} target="_blank" rel="noopener">
|
|
180
|
+
<Icon name="expand" />
|
|
181
|
+
Open full screen
|
|
182
|
+
</a>
|
|
183
|
+
{
|
|
184
|
+
item.stagingUrl && (
|
|
185
|
+
<a class="lab-menu__item" role="menuitem" href={item.stagingUrl} target="_blank" rel="noopener">
|
|
186
|
+
<Icon name="external" />
|
|
187
|
+
View on staging
|
|
188
|
+
</a>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
<button class="lab-menu__item" type="button" role="menuitem" data-lab-copy-link>
|
|
192
|
+
<Icon name="link" />
|
|
193
|
+
<span data-lab-copy-label>Copy link to this size</span>
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div class="lab-menu lab-status-menu" id="lab-status-menu" popover role="menu">
|
|
198
|
+
<p class="lab-menu__label">On the site</p>
|
|
199
|
+
<p class="lab-status-menu__fact">
|
|
200
|
+
{
|
|
201
|
+
liveStory
|
|
202
|
+
? liveStory.liveOn.map((m) => `${m.page} · slot ${m.slot} of ${m.of}`).join(", ")
|
|
203
|
+
: mod.usedBy.length
|
|
204
|
+
? `Used by ${mod.usedBy.map((f) => f.split("/").pop()).join(", ")}`
|
|
205
|
+
: "Not mounted by any page, and nothing live uses it."
|
|
206
|
+
}
|
|
207
|
+
</p>
|
|
208
|
+
{
|
|
209
|
+
mod.componentFile && (
|
|
210
|
+
<p class="lab-status-menu__fact lab-mono">{mod.componentFile}</p>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
{
|
|
214
|
+
mod.responsive && (
|
|
215
|
+
<>
|
|
216
|
+
<hr />
|
|
217
|
+
<p class="lab-menu__label">Responsive</p>
|
|
218
|
+
<label class="lab-menu__item lab-menu__check">
|
|
219
|
+
<input type="checkbox" data-lab-mark="approved" data-file={mod.moduleFile} checked={mod.responsive.approved} disabled />
|
|
220
|
+
Approved for responsive work
|
|
221
|
+
</label>
|
|
222
|
+
<label class="lab-menu__item lab-menu__check">
|
|
223
|
+
<input type="checkbox" data-lab-mark="done" data-file={mod.moduleFile} checked={mod.responsive.done} disabled />
|
|
224
|
+
Made responsive
|
|
225
|
+
</label>
|
|
226
|
+
</>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
{
|
|
230
|
+
cullable && (
|
|
231
|
+
<>
|
|
232
|
+
<hr />
|
|
233
|
+
<p class="lab-menu__label">Clean-up</p>
|
|
234
|
+
<label class="lab-menu__item lab-menu__check" title={blocked ?? undefined}>
|
|
235
|
+
<input type="checkbox" data-lab-mark="cull" data-file={mod.moduleFile} data-blocked={blocked ?? undefined} disabled />
|
|
236
|
+
Mark for deletion
|
|
237
|
+
</label>
|
|
238
|
+
{blocked && <p class="lab-status-menu__fact">{blocked}</p>}
|
|
239
|
+
</>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
<p class="lab-status-menu__note" data-lab-marks-note>Marks can be changed on the dev server only.</p>
|
|
243
|
+
</div>
|
|
244
|
+
</Fragment>
|
|
245
|
+
|
|
246
|
+
<ViewportStage src={item.storyUrl} title={`${mod.moduleName} — ${item.storyName}`} />
|
|
247
|
+
</Shell>
|
|
248
|
+
|
|
249
|
+
<script>
|
|
250
|
+
import "./marks-client";
|
|
251
|
+
</script>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The level 2 tree (Figma `Tree view`, sm) — recursive, server-rendered, and usable with no script:
|
|
3
|
+
// groups are <details>, rows are links. The shell script adds memory (which groups you closed),
|
|
4
|
+
// search, the filter menu and j/k on top. Connector lines are CSS on the <li>, so the tree carries
|
|
5
|
+
// no connector markup at all.
|
|
6
|
+
import Icon from "./Icon.astro";
|
|
7
|
+
import type { TreeNode } from "./model";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
nodes: TreeNode[];
|
|
11
|
+
depth?: number;
|
|
12
|
+
/** Set on the outermost call only. */
|
|
13
|
+
root?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { nodes, depth = 0, root = true } = Astro.props;
|
|
17
|
+
|
|
18
|
+
/** Whether a node is, or holds, the current page — those groups render open whatever was stored. */
|
|
19
|
+
const holdsCurrent = (node: TreeNode): boolean =>
|
|
20
|
+
Boolean(node.current) || (node.children ?? []).some(holdsCurrent);
|
|
21
|
+
|
|
22
|
+
const searchText = (node: TreeNode) => `${node.label} ${node.search ?? ""}`.toLowerCase();
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<ul class={root ? "lab-tree" : "lab-tree__group"} role={root ? "tree" : "group"} data-lab-tree={root ? "" : undefined}>
|
|
26
|
+
{
|
|
27
|
+
nodes.map((node) => {
|
|
28
|
+
const hasChildren = (node.children?.length ?? 0) > 0;
|
|
29
|
+
const icon = node.icon ?? (hasChildren ? "folder" : "page");
|
|
30
|
+
const trailing = (
|
|
31
|
+
<>
|
|
32
|
+
{node.count !== undefined && <span class="lab-tree__count">{node.count}</span>}
|
|
33
|
+
{node.dot && <span class={`lab-dot lab-dot--${node.dot}`} title={node.dotTitle} />}
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
return (
|
|
37
|
+
<li
|
|
38
|
+
class="lab-tree__node"
|
|
39
|
+
role="none"
|
|
40
|
+
style={`--lab-d:${depth}`}
|
|
41
|
+
data-lab-search={searchText(node)}
|
|
42
|
+
data-lab-facets={node.facets}
|
|
43
|
+
>
|
|
44
|
+
{hasChildren ? (
|
|
45
|
+
<details open data-lab-tree-id={node.id} data-lab-holds-current={holdsCurrent(node) ? "" : undefined}>
|
|
46
|
+
<summary
|
|
47
|
+
class="lab-tree__row"
|
|
48
|
+
role="treeitem"
|
|
49
|
+
aria-current={node.current ? "page" : undefined}
|
|
50
|
+
title={node.title}
|
|
51
|
+
>
|
|
52
|
+
<Icon name={icon} size="sm" />
|
|
53
|
+
<span class="lab-tree__label">{node.href ? <a href={node.href}>{node.label}</a> : node.label}</span>
|
|
54
|
+
{trailing}
|
|
55
|
+
<Icon name="chevron-down" size="sm" class="lab-tree__chevron" />
|
|
56
|
+
</summary>
|
|
57
|
+
<Astro.self nodes={node.children!} depth={depth + 1} root={false} />
|
|
58
|
+
</details>
|
|
59
|
+
) : node.href && !node.disabled ? (
|
|
60
|
+
<a
|
|
61
|
+
class="lab-tree__row"
|
|
62
|
+
role="treeitem"
|
|
63
|
+
href={node.href}
|
|
64
|
+
aria-current={node.current ? "page" : undefined}
|
|
65
|
+
title={node.title}
|
|
66
|
+
data-lab-leaf
|
|
67
|
+
>
|
|
68
|
+
<Icon name={icon} size="sm" />
|
|
69
|
+
<span class="lab-tree__label">{node.label}</span>
|
|
70
|
+
{trailing}
|
|
71
|
+
</a>
|
|
72
|
+
) : (
|
|
73
|
+
<span class="lab-tree__row" role="treeitem" aria-disabled="true" title={node.title}>
|
|
74
|
+
<Icon name={icon} size="sm" />
|
|
75
|
+
<span class="lab-tree__label">{node.label}</span>
|
|
76
|
+
{trailing}
|
|
77
|
+
</span>
|
|
78
|
+
)}
|
|
79
|
+
</li>
|
|
80
|
+
);
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
</ul>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The viewport's controls, for the navbar: the device switch (the site's design widths), the
|
|
3
|
+
// W × H field with rotate, the presets menu (both sides of every breakpoint, and named devices), the
|
|
4
|
+
// band readout, zoom (Fit by default), and — only when the frame supports them — the preview's own
|
|
5
|
+
// light/dark switch and the parameters drawer's button. Decided 2026-09-24; the sizes are the
|
|
6
|
+
// `viewports` option (../../defaults.mjs). Behaviour: ./viewport-client.ts.
|
|
7
|
+
import Icon from "./Icon.astro";
|
|
8
|
+
import type { IconName } from "./icons";
|
|
9
|
+
import { labConfig } from "./model";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** The frame's size when neither the URL nor the last visit names one. */
|
|
13
|
+
defaultW: number;
|
|
14
|
+
defaultH: number;
|
|
15
|
+
/** Which remembered size to use: sections, components and pages each keep their own. */
|
|
16
|
+
kind: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { defaultW, defaultH, kind } = Astro.props;
|
|
20
|
+
const { design, breakpoints, devices } = labConfig.viewports;
|
|
21
|
+
const edges = breakpoints.filter((b) => b.min > 0).flatMap((b) => [b.min, b.min - 1]);
|
|
22
|
+
const zooms = [25, 50, 75, 100, 150, 200];
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<div
|
|
26
|
+
class="lab-navbar__tools"
|
|
27
|
+
data-lab-vp-controls
|
|
28
|
+
data-kind={kind}
|
|
29
|
+
data-default-w={defaultW}
|
|
30
|
+
data-default-h={defaultH}
|
|
31
|
+
data-breakpoints={JSON.stringify(breakpoints)}
|
|
32
|
+
data-step-widths={[...new Set([...design.map((d) => d.width), ...edges])].sort((a, b) => a - b).join(",")}
|
|
33
|
+
>
|
|
34
|
+
<div class="lab-seg" role="group" aria-label="Design widths">
|
|
35
|
+
{
|
|
36
|
+
design.map((d) => (
|
|
37
|
+
<button class="lab-btn lab-btn--sm lab-btn--icon" type="button" data-lab-vp-width={d.width} title={`${d.label} · ${d.width}px`}>
|
|
38
|
+
<Icon name={d.icon as IconName} />
|
|
39
|
+
<span class="lab-visually-hidden">{d.label}, {d.width} pixels wide</span>
|
|
40
|
+
</button>
|
|
41
|
+
))
|
|
42
|
+
}
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="lab-vp-size" role="group" aria-label="Frame size">
|
|
46
|
+
<input type="number" inputmode="numeric" min="240" step="1" aria-label="Width" data-lab-vp-w />
|
|
47
|
+
<span aria-hidden="true">×</span>
|
|
48
|
+
<input type="number" inputmode="numeric" min="240" step="1" aria-label="Height" data-lab-vp-h />
|
|
49
|
+
<button class="lab-icon-btn" type="button" title="Rotate — swap width and height (r)" data-lab-vp-rotate>
|
|
50
|
+
<Icon name="rotate" />
|
|
51
|
+
<span class="lab-visually-hidden">Rotate</span>
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<button class="lab-btn lab-btn--sm" type="button" popovertarget="lab-vp-presets" title="Breakpoints and devices ([ and ] step)">
|
|
56
|
+
Presets
|
|
57
|
+
<Icon name="chevron-down" />
|
|
58
|
+
</button>
|
|
59
|
+
<div class="lab-menu" id="lab-vp-presets" popover role="menu">
|
|
60
|
+
<p class="lab-menu__label">Breakpoint edges — width only</p>
|
|
61
|
+
{
|
|
62
|
+
edges.map((w) => (
|
|
63
|
+
<button class="lab-menu__item" type="button" role="menuitem" data-lab-vp-width={w}>
|
|
64
|
+
{w}px
|
|
65
|
+
<span class="lab-menu__hint">{breakpoints.find((b) => b.min <= w)?.name}</span>
|
|
66
|
+
</button>
|
|
67
|
+
))
|
|
68
|
+
}
|
|
69
|
+
<hr />
|
|
70
|
+
<p class="lab-menu__label">Devices</p>
|
|
71
|
+
{
|
|
72
|
+
devices.map((d) => (
|
|
73
|
+
<button class="lab-menu__item" type="button" role="menuitem" data-lab-vp-width={d.width} data-lab-vp-height={d.height}>
|
|
74
|
+
{d.label}
|
|
75
|
+
<span class="lab-menu__hint">{d.width} × {d.height}</span>
|
|
76
|
+
</button>
|
|
77
|
+
))
|
|
78
|
+
}
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<span class="lab-pill lab-vp-band" data-lab-vp-band aria-live="polite">—</span>
|
|
82
|
+
|
|
83
|
+
<select class="lab-select lab-select--sm" aria-label="Zoom" title="Zoom (f fits, 0 is 100%)" data-lab-vp-zoom>
|
|
84
|
+
<option value="fit">Fit</option>
|
|
85
|
+
{zooms.map((z) => <option value={String(z / 100)}>{z}%</option>)}
|
|
86
|
+
</select>
|
|
87
|
+
|
|
88
|
+
<button class="lab-btn lab-btn--sm lab-btn--icon lab-preview-theme-btn" type="button" hidden title="The preview's light or dark" data-lab-preview-theme>
|
|
89
|
+
<Icon name="sun" class="lab-icon--sun" />
|
|
90
|
+
<Icon name="moon" class="lab-icon--moon" />
|
|
91
|
+
<span class="lab-visually-hidden">Switch the preview between light and dark</span>
|
|
92
|
+
</button>
|
|
93
|
+
|
|
94
|
+
<button class="lab-btn lab-btn--sm" type="button" hidden aria-pressed="false" title="Tuning parameters" data-lab-params-toggle>
|
|
95
|
+
<Icon name="params" />
|
|
96
|
+
Parameters
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The canvas: the story or page in an <iframe> whose LOGICAL size is set by the controls or by
|
|
3
|
+
// dragging its edges. Media queries and vh units inside the frame follow the frame's own size,
|
|
4
|
+
// which is why it is an iframe and never an inline render. Zoom is a transform on the wrapper, so
|
|
5
|
+
// zooming never changes the frame's width — or its breakpoints. The parameters drawer sits on the
|
|
6
|
+
// canvas's right edge, over it, and exists only for a story that registered parameters.
|
|
7
|
+
interface Props {
|
|
8
|
+
src: string;
|
|
9
|
+
title: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { src, title } = Astro.props;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<div class="lab-vp" data-lab-vp>
|
|
16
|
+
<div class="lab-vp-stage" data-lab-vp-stage>
|
|
17
|
+
<div class="lab-vp-box" data-lab-vp-box>
|
|
18
|
+
<div class="lab-vp-wrapper" data-lab-vp-wrapper>
|
|
19
|
+
<iframe src={src} title={title} data-lab-vp-frame></iframe>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="lab-vp-handle lab-vp-handle--e" data-axis="x" title="Drag to resize the width" aria-hidden="true"></div>
|
|
22
|
+
<div class="lab-vp-handle lab-vp-handle--s" data-axis="y" title="Drag to resize the height" aria-hidden="true"></div>
|
|
23
|
+
<div class="lab-vp-handle lab-vp-handle--se" data-axis="xy" title="Drag to resize" aria-hidden="true"></div>
|
|
24
|
+
<span class="lab-vp-live" data-lab-vp-live aria-hidden="true"></span>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<aside class="lab-params" data-lab-params hidden aria-label="Parameters"></aside>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<script>
|
|
31
|
+
import "./viewport-client";
|
|
32
|
+
</script>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The chrome's icon set: Lucide 1.47.0 (lucide-static), ISC — a few shapes Lucide inherited from
|
|
2
|
+
// Feather are MIT. Both notices are in ./LICENSE-icons, which must travel with this file.
|
|
3
|
+
//
|
|
4
|
+
// WHY LUCIDE AND NOT THE DESIGN'S OWN ICONS (2026-09-24). The Figma file draws Untitled UI icons.
|
|
5
|
+
// Their npm package declares "MIT" in package.json, but the licence file it ships forbids
|
|
6
|
+
// distributing the icons in original or modified form — and this package is published to npm, so
|
|
7
|
+
// bundling them would be distributing them. Lucide draws the same 24px, 2px-stroke, round-cap
|
|
8
|
+
// family, so each design icon maps to its nearest Lucide shape by name below; the right-hand
|
|
9
|
+
// comment is the Lucide file each one came from.
|
|
10
|
+
//
|
|
11
|
+
// Every icon is `fill="none" stroke="currentColor"` (set once on the <symbol> in ./Sprite.astro), so
|
|
12
|
+
// it takes the colour of the text around it and themes with the chrome for free.
|
|
13
|
+
export const ICONS = {
|
|
14
|
+
"home": "<path d=\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\" /><path d=\"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\" />", // house
|
|
15
|
+
"site": "<path d=\"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z\" /><path d=\"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12\" /><path d=\"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17\" />", // layers
|
|
16
|
+
"sections": "<path d=\"M13 13.74a2 2 0 0 1-2 0L2.5 8.87a1 1 0 0 1 0-1.74L11 2.26a2 2 0 0 1 2 0l8.5 4.87a1 1 0 0 1 0 1.74z\" /><path d=\"m20 14.285 1.5.845a1 1 0 0 1 0 1.74L13 21.74a2 2 0 0 1-2 0l-8.5-4.87a1 1 0 0 1 0-1.74l1.5-.845\" />", // layers-2
|
|
17
|
+
"pages": "<path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\" /><path d=\"M14 2v5a1 1 0 0 0 1 1h5\" />", // file
|
|
18
|
+
"page": "<path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\" /><path d=\"M14 2v5a1 1 0 0 0 1 1h5\" />", // file
|
|
19
|
+
"components": "<rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\" /><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />", // copy
|
|
20
|
+
"assets": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\" /><circle cx=\"9\" cy=\"9\" r=\"2\" /><path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\" />", // image
|
|
21
|
+
"tasks": "<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\" ry=\"1\" /><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\" /><path d=\"m9 14 2 2 4-4\" />", // clipboard-check
|
|
22
|
+
"board": "<path d=\"m12.296 3.464 3.02 3.956\" /><path d=\"M20.2 6 3 11l-.9-2.4c-.3-1.1.3-2.2 1.3-2.5l13.5-4c1.1-.3 2.2.3 2.5 1.3z\" /><path d=\"M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\" /><path d=\"m6.18 5.276 3.1 3.899\" />", // clapperboard
|
|
23
|
+
"all-tasks": "<path d=\"m12 15 2 2 4-4\" /><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\" /><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />", // copy-check
|
|
24
|
+
"support": "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"m4.93 4.93 4.24 4.24\" /><path d=\"m14.83 9.17 4.24-4.24\" /><path d=\"m14.83 14.83 4.24 4.24\" /><path d=\"m9.17 14.83-4.24 4.24\" /><circle cx=\"12\" cy=\"12\" r=\"4\" />", // life-buoy
|
|
25
|
+
"settings": "<path d=\"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915\" /><circle cx=\"12\" cy=\"12\" r=\"3\" />", // settings
|
|
26
|
+
"chevron-down": "<path d=\"m6 9 6 6 6-6\" />", // chevron-down
|
|
27
|
+
"chevron-up": "<path d=\"m18 15-6-6-6 6\" />", // chevron-up
|
|
28
|
+
"chevron-right": "<path d=\"m9 18 6-6-6-6\" />", // chevron-right
|
|
29
|
+
"chevron-left": "<path d=\"m15 18-6-6 6-6\" />", // chevron-left
|
|
30
|
+
"chevrons-left": "<path d=\"m11 17-5-5 5-5\" /><path d=\"m18 17-5-5 5-5\" />", // chevrons-left
|
|
31
|
+
"chevrons-right": "<path d=\"m6 17 5-5-5-5\" /><path d=\"m13 17 5-5-5-5\" />", // chevrons-right
|
|
32
|
+
"search": "<path d=\"m21 21-4.34-4.34\" /><circle cx=\"11\" cy=\"11\" r=\"8\" />", // search
|
|
33
|
+
"filter": "<path d=\"M2 5h20\" /><path d=\"M6 12h12\" /><path d=\"M9 19h6\" />", // list-filter
|
|
34
|
+
"folder": "<path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\" />", // folder
|
|
35
|
+
"coffee": "<path d=\"M10 2v2\" /><path d=\"M14 2v2\" /><path d=\"M16 8a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1\" /><path d=\"M6 2v2\" />", // coffee
|
|
36
|
+
"panel-close": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" /><path d=\"M9 3v18\" /><path d=\"m16 15-3-3 3-3\" />", // panel-left-close
|
|
37
|
+
"panel-open": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" /><path d=\"M9 3v18\" /><path d=\"m14 9 3 3-3 3\" />", // panel-left-open
|
|
38
|
+
"sun": "<circle cx=\"12\" cy=\"12\" r=\"4\" /><path d=\"M12 2v2\" /><path d=\"M12 20v2\" /><path d=\"m4.93 4.93 1.41 1.41\" /><path d=\"m17.66 17.66 1.41 1.41\" /><path d=\"M2 12h2\" /><path d=\"M20 12h2\" /><path d=\"m6.34 17.66-1.41 1.41\" /><path d=\"m19.07 4.93-1.41 1.41\" />", // sun
|
|
39
|
+
"moon": "<path d=\"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401\" />", // moon
|
|
40
|
+
"code": "<path d=\"m18 16 4-4-4-4\" /><path d=\"m6 8-4 4 4 4\" /><path d=\"m14.5 4-5 16\" />", // code-xml
|
|
41
|
+
"link": "<path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\" /><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\" />", // link
|
|
42
|
+
"expand": "<path d=\"M15 3h6v6\" /><path d=\"m21 3-7 7\" /><path d=\"m3 21 7-7\" /><path d=\"M9 21H3v-6\" />", // maximize-2
|
|
43
|
+
"external": "<path d=\"M15 3h6v6\" /><path d=\"M10 14 21 3\" /><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />", // external-link
|
|
44
|
+
"reload": "<path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\" /><path d=\"M21 3v5h-5\" /><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\" /><path d=\"M8 16H3v5\" />", // refresh-cw
|
|
45
|
+
"rotate": "<path d=\"M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8\" /><path d=\"M21 3v5h-5\" />", // rotate-cw
|
|
46
|
+
"monitor": "<rect width=\"20\" height=\"14\" x=\"2\" y=\"3\" rx=\"2\" /><line x1=\"8\" x2=\"16\" y1=\"21\" y2=\"21\" /><line x1=\"12\" x2=\"12\" y1=\"17\" y2=\"21\" />", // monitor
|
|
47
|
+
"tablet": "<rect width=\"16\" height=\"20\" x=\"4\" y=\"2\" rx=\"2\" ry=\"2\" /><line x1=\"12\" x2=\"12.01\" y1=\"18\" y2=\"18\" />", // tablet
|
|
48
|
+
"phone": "<rect width=\"14\" height=\"20\" x=\"5\" y=\"2\" rx=\"2\" ry=\"2\" /><path d=\"M12 18h.01\" />", // smartphone
|
|
49
|
+
"phone-landscape": "<g transform=\"rotate(90 12 12)\"><rect width=\"14\" height=\"20\" x=\"5\" y=\"2\" rx=\"2\" ry=\"2\" /><path d=\"M12 18h.01\" /></g>", // smartphone, turned a quarter
|
|
50
|
+
"params": "<path d=\"M10 5H3\" /><path d=\"M12 19H3\" /><path d=\"M14 3v4\" /><path d=\"M16 17v4\" /><path d=\"M21 12h-9\" /><path d=\"M21 19h-5\" /><path d=\"M21 5h-7\" /><path d=\"M8 10v4\" /><path d=\"M8 12H3\" />", // sliders-horizontal
|
|
51
|
+
"close": "<path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />", // x
|
|
52
|
+
"more": "<circle cx=\"12\" cy=\"12\" r=\"1\" /><circle cx=\"19\" cy=\"12\" r=\"1\" /><circle cx=\"5\" cy=\"12\" r=\"1\" />", // ellipsis
|
|
53
|
+
"component": "<path d=\"M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z\" /><path d=\"M2.297 11.293a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0z\" /><path d=\"M8.916 17.912a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0z\" /><path d=\"M8.916 4.674a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z\" />", // component
|
|
54
|
+
"story": "<path d=\"M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z\" />", // bookmark
|
|
55
|
+
"pin": "<path d=\"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z\" />", // message-square
|
|
56
|
+
"check": "<path d=\"M20 6 9 17l-5-5\" />", // check
|
|
57
|
+
} as const;
|
|
58
|
+
|
|
59
|
+
export type IconName = keyof typeof ICONS;
|