@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.
Files changed (69) hide show
  1. package/README.md +109 -64
  2. package/defaults.mjs +65 -0
  3. package/dist/core/virtual-module/virtual-routes.js +12 -1
  4. package/docs/PIN-CONTRACT.md +8 -0
  5. package/docs/PIN.md +29 -19
  6. package/index.d.ts +40 -16
  7. package/index.mjs +32 -10
  8. package/package.json +6 -2
  9. package/src/Home.astro +167 -264
  10. package/src/LabHead.astro +37 -1047
  11. package/src/chrome/ComponentCard.astro +69 -0
  12. package/src/chrome/Icon.astro +21 -0
  13. package/src/chrome/LICENSE-icons +43 -0
  14. package/src/chrome/Nav.astro +105 -0
  15. package/src/chrome/Panel.astro +104 -0
  16. package/src/chrome/Shell.astro +106 -0
  17. package/src/chrome/Sprite.astro +23 -0
  18. package/src/chrome/StoryView.astro +251 -0
  19. package/src/chrome/Tree.astro +83 -0
  20. package/src/chrome/ViewportControls.astro +98 -0
  21. package/src/chrome/ViewportStage.astro +32 -0
  22. package/src/chrome/fonts/OFL.txt +93 -0
  23. package/src/chrome/fonts/inter-latin-wght-normal.woff2 +0 -0
  24. package/src/chrome/icons.ts +59 -0
  25. package/src/chrome/marks-client.ts +102 -0
  26. package/src/chrome/model.ts +142 -0
  27. package/src/chrome/pins-data.ts +30 -0
  28. package/src/chrome/shell-client.ts +372 -0
  29. package/src/chrome/site-data.ts +230 -0
  30. package/src/chrome/trees.ts +152 -0
  31. package/src/chrome/viewport-client.ts +579 -0
  32. package/src/chrome/views/Assets.astro +110 -0
  33. package/src/chrome/views/Pages.astro +178 -0
  34. package/src/chrome/views/Placeholder.astro +37 -0
  35. package/src/chrome/views/Tasks.astro +107 -0
  36. package/src/core/LICENSE-astrobook +16 -0
  37. package/src/core/lib/components/home.astro +4 -2
  38. package/src/core/lib/pages/story.astro +12 -10
  39. package/src/core/virtual-module/virtual-routes.ts +20 -4
  40. package/src/pin/board.mjs +389 -175
  41. package/src/pin/index.mjs +33 -2
  42. package/src/pin/toolbar.js +1 -1
  43. package/src/shell/Browse.astro +106 -353
  44. package/src/shell/Viewport.astro +22 -1315
  45. package/src/shell/lab-index.ts +23 -14
  46. package/src/ui/components/app.astro +5 -7
  47. package/src/ui/components/theme-script.astro +13 -2
  48. package/src/ui/lab.css +2152 -370
  49. package/virtual.d.ts +13 -0
  50. package/src/shell/CardGrid.astro +0 -297
  51. package/src/ui/components/build-path.ts +0 -13
  52. package/src/ui/components/build-tree.ts +0 -108
  53. package/src/ui/components/collapse-duration.ts +0 -28
  54. package/src/ui/components/compress-terms.ts +0 -10
  55. package/src/ui/components/dashboard-layout.astro +0 -39
  56. package/src/ui/components/home.astro +0 -65
  57. package/src/ui/components/layout.astro +0 -110
  58. package/src/ui/components/sidebar-button-fullscreen.astro +0 -38
  59. package/src/ui/components/sidebar-button-search.astro +0 -23
  60. package/src/ui/components/sidebar-button-theme.astro +0 -9
  61. package/src/ui/components/sidebar-button.astro +0 -24
  62. package/src/ui/components/sidebar-resize-handle.astro +0 -74
  63. package/src/ui/components/sidebar-search-panel.astro +0 -41
  64. package/src/ui/components/sidebar-search-script.ts +0 -103
  65. package/src/ui/components/sidebar-title.astro +0 -17
  66. package/src/ui/components/sidebar-tree-node.astro +0 -143
  67. package/src/ui/components/sidebar-tree.astro +0 -84
  68. package/src/ui/components/sidebar.astro +0 -29
  69. package/src/ui/components/theme-toggle.astro +0 -63
package/virtual.d.ts CHANGED
@@ -23,6 +23,19 @@ declare module "virtual:orbytes-lab/config.mjs" {
23
23
  cullDir: string | null;
24
24
  responsiveFile: string;
25
25
  cullFile: string;
26
+ /**
27
+ * The pin board, when it is running: dev only, and only when the board is on and found a git
28
+ * repository. Null in every build. Set by the pin half through the integration's shared state
29
+ * (../index.mjs), read when this module loads — after every integration's setup has run.
30
+ * `repoRoot` is an absolute local path, so this module must stay server-side only.
31
+ */
32
+ tasks: { base: string; api: string; assets: string; repoRoot: string; backlogDir: string } | null;
33
+ /** The viewport's screen sizes — defaults.mjs › DEFAULT_VIEWPORTS. */
34
+ viewports: {
35
+ design: { label: string; width: number; icon: string }[];
36
+ breakpoints: { name: string; min: number }[];
37
+ devices: { label: string; width: number; height: number }[];
38
+ };
26
39
  }
27
40
  const config: LabConfig;
28
41
  export default config;
@@ -1,297 +0,0 @@
1
- ---
2
- // One card per story: a scaled-down live <iframe> of the bare render, the module and story
3
- // names, the pills derived from the index — `live · slot N of M` when a page mounts the component
4
- // (`live · /about, slot 3` when that page is not the home page), `used by N` when a live component
5
- // imports it, plus the responsive mark on section versions — and a row of links: the viewport
6
- // configurator, the same story on staging, and (dev only) the component in VS Code. Factored out
7
- // of Home.astro so the folder pages (Browse.astro) draw the identical card.
8
- //
9
- // Thumbnails are the bare story routes themselves (/lab/stories/<id>) rendered in a virtual
10
- // viewport: sections at 1440×900 (page width), everything else at 960×540.
11
- //
12
- // The card is a <div> holding several links, not one <a>: the main link (thumb + label) opens the
13
- // dashboard and the link row sits under it. An <a> cannot nest in an <a>.
14
- import { isSectionLike, labBase, livePillText, liveTitle, type LabItem } from "./lab-index";
15
-
16
- interface Props {
17
- items: LabItem[];
18
- /** Shown when there is nothing to draw. */
19
- empty?: string;
20
- }
21
-
22
- const { items, empty = "No stories here." } = Astro.props;
23
-
24
- type Pill = { text: string; kind: string; title?: string };
25
-
26
- const pillsOf = (item: LabItem): Pill[] => {
27
- const pills: Pill[] = [];
28
- const live = livePillText(item);
29
- if (live) pills.push({ text: live, kind: "live", title: liveTitle(item) });
30
- else if (item.usedBy.length > 0)
31
- pills.push({ text: `used by ${item.usedBy.length}`, kind: "used", title: item.usedBy.join("\n") });
32
- if (item.responsive) {
33
- pills.push({
34
- text: item.responsive.done ? "responsive" : "not responsive",
35
- kind: item.responsive.done ? "responsive" : "unresponsive",
36
- title: item.responsive.approved
37
- ? "approved for responsive work"
38
- : "not approved for responsive work yet",
39
- });
40
- }
41
- return pills;
42
- };
43
-
44
- // What the page-level filter box matches against.
45
- const searchText = (item: LabItem) =>
46
- [
47
- item.moduleName,
48
- item.storyName,
49
- item.section,
50
- item.version,
51
- item.tier,
52
- item.livePage,
53
- item.live ? "live" : "",
54
- // "unresponsive", not "not responsive": the filter is a substring match per term, so a card
55
- // that said "not responsive" would still be a hit for "responsive". Same on the sidebar rows.
56
- item.responsive ? (item.responsive.done ? "responsive" : "unresponsive") : "",
57
- ...item.tags,
58
- ]
59
- .filter(Boolean)
60
- .join(" ")
61
- .toLowerCase();
62
- ---
63
-
64
- {
65
- items.length === 0 ? (
66
- <p class="lab-grid__empty">{empty}</p>
67
- ) : (
68
- <div class="lab-grid">
69
- {items.map((item) => {
70
- const section = isSectionLike(item);
71
- const pills = pillsOf(item);
72
- return (
73
- <div class="lab-card" data-search={searchText(item)}>
74
- <a class="lab-card__main" href={item.dashboardUrl}>
75
- <span
76
- class="lab-card__thumb"
77
- data-frame-w={section ? 1440 : 960}
78
- data-frame-h={section ? 900 : 540}
79
- >
80
- <iframe
81
- src={item.storyUrl}
82
- title={`${item.moduleName} — ${item.storyName}`}
83
- loading="lazy"
84
- tabindex="-1"
85
- />
86
- {pills.length > 0 && (
87
- <span class="lab-card__pills">
88
- {pills.map((pill) => (
89
- <span class:list={["lab-card__pill", `lab-card__pill--${pill.kind}`]} title={pill.title}>
90
- {pill.text}
91
- </span>
92
- ))}
93
- </span>
94
- )}
95
- </span>
96
- <span class="lab-card__label">
97
- <span class="lab-card__name">
98
- {item.moduleName}
99
- {item.version && <span class="lab-card__version">{item.version}</span>}
100
- </span>
101
- <span class="lab-card__variant">{item.storyName}</span>
102
- </span>
103
- </a>
104
- <span class="lab-card__links">
105
- <a
106
- href={`${labBase}/viewport/${item.storyId}`}
107
- title="Open in the viewport configurator"
108
- data-astro-reload
109
- >
110
- viewport <span aria-hidden="true">&#8690;</span>
111
- </a>
112
- {item.stagingUrl && (
113
- <a href={item.stagingUrl} title="The same story on the deployed lab" target="_blank" rel="noopener">
114
- staging <span aria-hidden="true">&#8599;</span>
115
- </a>
116
- )}
117
- {item.editorUrl && (
118
- <a href={item.editorUrl} title="Open the component in VS Code">
119
- code <span aria-hidden="true">&#8599;</span>
120
- </a>
121
- )}
122
- </span>
123
- </div>
124
- );
125
- })}
126
- </div>
127
- )
128
- }
129
-
130
- <script>
131
- // Scale each thumbnail iframe so its virtual viewport (data-frame-w × data-frame-h) fits the
132
- // card width exactly. Re-run on resize and after every Astro view-transition swap (the lab home
133
- // sits inside Astrobook's ClientRouter, where module scripts do not re-execute on navigation).
134
- const fit = () => {
135
- for (const thumb of document.querySelectorAll<HTMLElement>(".lab-card__thumb")) {
136
- const iframe = thumb.querySelector("iframe");
137
- if (!iframe) continue;
138
- const frameW = Number(thumb.dataset.frameW) || 1440;
139
- const frameH = Number(thumb.dataset.frameH) || 900;
140
- const scale = thumb.clientWidth / frameW;
141
- iframe.style.width = `${frameW}px`;
142
- iframe.style.height = `${frameH}px`;
143
- iframe.style.transform = `scale(${scale})`;
144
- thumb.style.height = `${Math.round(frameH * scale)}px`;
145
- }
146
- };
147
- fit();
148
- window.addEventListener("resize", fit);
149
- document.addEventListener("astro:page-load", fit);
150
- </script>
151
-
152
- <style>
153
- .lab-grid {
154
- display: grid;
155
- grid-template-columns: repeat(auto-fill, minmax(20em, 1fr));
156
- gap: 1.5em;
157
- }
158
-
159
- .lab-grid__empty {
160
- font-family: var(--lab-font-mono);
161
- font-size: 0.85em;
162
- color: var(--lab-color-text-faint);
163
- }
164
-
165
- .lab-card {
166
- position: relative;
167
- display: block;
168
- border: 1px solid var(--lab-color-border);
169
- background-color: var(--lab-color-surface);
170
- transition: border-color var(--lab-transition);
171
- }
172
-
173
- .lab-card:hover,
174
- .lab-card:focus-within {
175
- border-color: var(--lab-color-border-strong);
176
- }
177
-
178
- .lab-card__main {
179
- display: block;
180
- text-decoration: none;
181
- }
182
-
183
- .lab-card__main:focus-visible {
184
- outline: none;
185
- }
186
-
187
- /* The link row under the label. Three links no longer fit over the label's right end, so they
188
- get their own row rather than a cramped overlay. */
189
- .lab-card__links {
190
- display: flex;
191
- flex-wrap: wrap;
192
- gap: 1.25em;
193
- padding: 0 1em 0.85em;
194
- }
195
-
196
- .lab-card__links a {
197
- font-family: var(--lab-font-mono);
198
- font-weight: 500;
199
- font-size: 0.75em;
200
- line-height: 1.5;
201
- color: var(--lab-color-text-faint);
202
- text-decoration: none;
203
- border-bottom: 1px solid transparent;
204
- transition: color var(--lab-transition), border-color var(--lab-transition);
205
- }
206
-
207
- .lab-card__links a:hover,
208
- .lab-card__links a:focus-visible {
209
- color: var(--lab-color-text-strong);
210
- border-bottom-color: var(--lab-color-text-strong);
211
- outline: none;
212
- }
213
-
214
- .lab-card__thumb {
215
- display: block;
216
- position: relative;
217
- overflow: hidden;
218
- background-color: var(--lab-color-bg);
219
- border-bottom: 1px solid var(--lab-color-border);
220
- }
221
-
222
- .lab-card__thumb iframe {
223
- border: 0;
224
- transform-origin: 0 0;
225
- pointer-events: none; /* the card is the link; the live render is display-only */
226
- }
227
-
228
- .lab-card__pills {
229
- position: absolute;
230
- top: 0.6em;
231
- left: 0.6em;
232
- display: flex;
233
- flex-wrap: wrap;
234
- gap: 0.4em;
235
- }
236
-
237
- .lab-card__pill {
238
- font-family: var(--lab-font-mono);
239
- font-weight: 500;
240
- font-size: 0.7em;
241
- line-height: 1;
242
- padding: 0.5em 0.85em;
243
- border-radius: var(--lab-radius-pill);
244
- border: 1px solid var(--lab-color-border);
245
- background-color: var(--lab-color-surface-raised);
246
- color: var(--lab-color-text-muted);
247
- }
248
-
249
- .lab-card__pill--live {
250
- background-color: var(--lab-color-accent);
251
- border-color: var(--lab-color-accent);
252
- color: var(--lab-color-accent-text);
253
- }
254
-
255
- /* The responsive mark: solid border when the version has been made responsive, dashed when it
256
- has not — the same two-state reading as the sidebar's pill. */
257
- .lab-card__pill--responsive {
258
- border-color: var(--lab-color-border-strong);
259
- color: var(--lab-color-text-strong);
260
- }
261
-
262
- .lab-card__pill--unresponsive {
263
- border-style: dashed;
264
- color: var(--lab-color-text-faint);
265
- }
266
-
267
- .lab-card__label {
268
- display: flex;
269
- align-items: baseline;
270
- justify-content: space-between;
271
- gap: 1em;
272
- padding: 0.75em 1em 0.5em;
273
- }
274
-
275
- .lab-card__name {
276
- display: inline-flex;
277
- align-items: baseline;
278
- gap: 0.6em;
279
- color: var(--lab-color-text-strong);
280
- font-weight: 500;
281
- font-size: 0.95em;
282
- }
283
-
284
- .lab-card__version {
285
- font-family: var(--lab-font-mono);
286
- font-size: 0.75em;
287
- color: var(--lab-color-text-faint);
288
- border: 1px solid var(--lab-color-border);
289
- padding: 0.1em 0.5em;
290
- }
291
-
292
- .lab-card__variant {
293
- font-family: var(--lab-font-mono);
294
- font-size: 0.8em;
295
- color: var(--lab-color-text-faint);
296
- }
297
- </style>
@@ -1,13 +0,0 @@
1
- // Vendored from astrobook 0.13.3 (packages/ui/src/components/build-path.ts) — see
2
- // ../../core/LICENSE-astrobook.
3
- // Divergence: `@astrobook/core/client` → the vendored core's own path-builder, by relative path
4
- // with the .ts extension. The `import '@astrobook/types'` side-effect line is gone: the ambient
5
- // declarations it pulled in now live in ../../types/virtual.d.ts, referenced from the package's
6
- // own virtual.d.ts.
7
- import config from 'virtual:astrobook/global-config.mjs'
8
-
9
- import { createPathBuilder } from '../../core/utils/path-builder.ts'
10
-
11
- export const buildPath: (...parts: string[]) => string = createPathBuilder({
12
- trailingSlash: config.trailingSlash,
13
- })
@@ -1,108 +0,0 @@
1
- // Vendored from astrobook 0.13.3 (packages/ui/src/components/build-tree.ts) — see
2
- // ../../core/LICENSE-astrobook.
3
- // Divergence: relative imports carry the .ts extension, and the StoryModule type comes from the
4
- // vendored types (../../types/) rather than the `@astrobook/types` package.
5
- import type { StoryModule } from '../../types/types.ts'
6
-
7
- import { compressTerms } from './compress-terms.ts'
8
-
9
- export interface DirectoryNode {
10
- type: 'directory'
11
- name: string
12
- searchText: string
13
- children: TreeNode[]
14
- }
15
-
16
- export interface ModuleNode {
17
- type: 'module'
18
- id: string
19
- name: string
20
- searchText: string
21
- children: StoryNode[]
22
- }
23
-
24
- export interface StoryNode {
25
- type: 'story'
26
- id: string
27
- name: string
28
- searchText: string
29
- children?: undefined
30
- }
31
-
32
- export type TreeNode = DirectoryNode | ModuleNode | StoryNode
33
-
34
- export function isSingleStory(module: StoryModule): boolean {
35
- return module.stories.length === 1 && module.stories[0].name === module.name
36
- }
37
-
38
- function findOrCreateDirectory(
39
- children: TreeNode[],
40
- segments: string[],
41
- ): TreeNode[] {
42
- let current = children
43
- for (const segment of segments) {
44
- let dir: DirectoryNode | undefined
45
- for (const node of current) {
46
- if (node.type === 'directory' && node.name === segment) {
47
- dir = node
48
- break
49
- }
50
- }
51
- if (!dir) {
52
- dir = { type: 'directory', name: segment, children: [], searchText: '' }
53
- current.push(dir)
54
- }
55
- current = dir.children
56
- }
57
- return current
58
- }
59
-
60
- export function buildTree(modules: StoryModule[]): TreeNode[] {
61
- const root: TreeNode[] = []
62
-
63
- for (const module of modules) {
64
- const segments = module.directory ? module.directory.split('/') : []
65
- const children = findOrCreateDirectory(root, segments)
66
-
67
- const storyNodes: StoryNode[] = module.stories.map((story) => ({
68
- type: 'story',
69
- id: story.id,
70
- name: story.name,
71
- searchText: '',
72
- }))
73
-
74
- if (isSingleStory(module)) {
75
- children.push(storyNodes[0])
76
- } else {
77
- const moduleNode: ModuleNode = {
78
- type: 'module',
79
- id: module.id,
80
- name: module.name,
81
- searchText: '',
82
- children: storyNodes,
83
- }
84
- children.push(moduleNode)
85
- }
86
- }
87
-
88
- for (const node of root) {
89
- assignSearchText(node, [])
90
- }
91
-
92
- return root
93
- }
94
-
95
- function assignSearchText(
96
- node: TreeNode,
97
- ancestorTerms: readonly string[],
98
- ): readonly string[] {
99
- const currTerms = [...ancestorTerms, node.name]
100
- const subTreeTerms: Array<readonly string[]> = Array.from(
101
- node.children || [],
102
- (child) => assignSearchText(child, currTerms),
103
- )
104
-
105
- const mergedTerms = compressTerms([...currTerms, ...subTreeTerms.flat()])
106
- node.searchText = mergedTerms.join(' ')
107
- return mergedTerms
108
- }
@@ -1,28 +0,0 @@
1
- // Vendored from astrobook 0.13.3 (packages/ui/src/components/collapse-duration.ts) — see
2
- // ../../core/LICENSE-astrobook.
3
- // Divergence: the relative import carries the .ts extension.
4
- import type { TreeNode } from './build-tree.ts'
5
-
6
- function countDescendantRows(node: TreeNode): number {
7
- if (node.type === 'story') return 0
8
- let rows = node.children.length
9
- for (const child of node.children) {
10
- rows += countDescendantRows(child)
11
- }
12
- return rows
13
- }
14
-
15
- export function getCollapseDuration(node: TreeNode): number {
16
- if (node.type === 'story') return 0
17
- return computeCollapseDuration(countDescendantRows(node))
18
- }
19
-
20
- export function computeCollapseDuration(rows: number): number {
21
- const minMs = 100
22
- const maxMs = 500
23
- const scale = 35
24
- return Math.min(
25
- maxMs,
26
- Math.round(minMs + scale * Math.log(Math.max(1, rows))),
27
- )
28
- }
@@ -1,10 +0,0 @@
1
- // Vendored from astrobook 0.13.3 (packages/ui/src/components/compress-terms.ts) — see
2
- // ../../core/LICENSE-astrobook. Unchanged.
3
- export function compressTerms(terms: readonly string[]): readonly string[] {
4
- const unique = Array.from(new Set(terms.map((k) => k.toLowerCase())))
5
- return unique
6
- .filter(
7
- (term) => !unique.some((other) => other !== term && other.includes(term)),
8
- )
9
- .sort()
10
- }
@@ -1,39 +0,0 @@
1
- ---
2
- // The DASHBOARD document: the lab's chrome with the story rendered inside it. New in this
3
- // package — upstream had one layout for the dashboard, the bare story and the home page alike,
4
- // and gave all three the consumer's CSS.
5
- //
6
- // This is the one document that legitimately carries BOTH stylesheets, because it is both things
7
- // at once (ruled 2026-09-22):
8
- //
9
- // · the chrome — sidebar, search, toolbar — styled by ../lab.css and by nothing else;
10
- // · the preview — the story in <main> — which must be rendered exactly as the real site
11
- // renders it, so it gets the consumer's `css` list and the consumer's head component.
12
- //
13
- // Nothing crosses. The chrome reads only `--lab-*` tokens and is anchored on chrome classes and
14
- // ids, so the consumer's `body`/`a`/`*` rules cannot reach it; the chrome sets no font, colour or
15
- // size on `body`, so nothing of ours inherits down into the story. The full argument, and what it
16
- // costs, is at the top of ../lab.css.
17
- //
18
- // The consumer's head arrives via `virtual:orbytes-lab/user-head.mjs` — the lab shell's own
19
- // virtual module (../../../index.mjs), which is how it reached this package before too: the
20
- // package's LabHead used to mount it on EVERY lab page, which is what carried the site's fonts
21
- // into the chrome.
22
- import 'virtual:astrobook/user-css.mjs'
23
-
24
- import UserHead from 'virtual:orbytes-lab/user-head.mjs'
25
-
26
- import Layout from './layout.astro'
27
-
28
- interface Props {
29
- story: string | undefined
30
- hasSidebar: boolean
31
- }
32
- ---
33
-
34
- <Layout story={Astro.props.story} hasSidebar={Astro.props.hasSidebar}>
35
- <Fragment slot="head">
36
- <UserHead />
37
- </Fragment>
38
- <slot />
39
- </Layout>
@@ -1,65 +0,0 @@
1
- ---
2
- // Vendored from astrobook 0.13.3 (packages/ui/src/components/home.astro) — see
3
- // ../../core/LICENSE-astrobook.
4
- // Divergence: the UnoCSS utility strings (including the `@container` queries, which had no
5
- // container to query once the chrome stopped shipping Uno's preflight) are the `.lab-splash*`
6
- // classes in ../lab.css, and the star glyph is an inline SVG.
7
- //
8
- // This is Astrobook's BUILT-IN splash. The orbytes lab always passes its own `home`
9
- // (src/Home.astro — the tier cards and live thumbnails), so this renders only for a consumer
10
- // that leaves the option at its default. Kept working, not designed.
11
- import config from 'virtual:astrobook/global-config.mjs'
12
-
13
- const content = config.homeContent
14
- const showBadges = !!(content.version || content.repo)
15
- ---
16
-
17
- <div class="lab-splash lab-chrome">
18
- {
19
- content.title == null ? null : (
20
- <h1 class="lab-splash__title">{content.title}</h1>
21
- )
22
- }
23
- {
24
- content.subtitle == null ? null : (
25
- <p class="lab-splash__subtitle">{content.subtitle}</p>
26
- )
27
- }
28
-
29
- {
30
- showBadges && (
31
- <div class="lab-splash__badges">
32
- {content.version && (
33
- <a
34
- href={content.version.href}
35
- target="_blank"
36
- rel="noopener noreferrer"
37
- class="lab-splash__badge"
38
- >
39
- {content.version.label}
40
- </a>
41
- )}
42
- {content.repo && (
43
- <a
44
- href={content.repo.href}
45
- target="_blank"
46
- rel="noopener noreferrer"
47
- class="lab-splash__badge"
48
- >
49
- <svg
50
- viewBox="0 0 16 16"
51
- fill="none"
52
- stroke="currentColor"
53
- stroke-width="1.5"
54
- stroke-linejoin="round"
55
- aria-hidden="true"
56
- >
57
- <path d="M8 1.75 10 6l4.25.5-3.1 2.9.85 4.35L8 11.6l-4 2.15.85-4.35L1.75 6.5 6 6z" />
58
- </svg>
59
- <span>{content.repo.label}</span>
60
- </a>
61
- )}
62
- </div>
63
- )
64
- }
65
- </div>