@linktr.ee/arbor-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -0
- package/dist/index.js +1208 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
4
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// ../../apps/playroom/src/hash-code.js
|
|
8
|
+
var require_hash_code = __commonJS({
|
|
9
|
+
"../../apps/playroom/src/hash-code.js"(exports, module) {
|
|
10
|
+
"use strict";
|
|
11
|
+
function hashCode2(str) {
|
|
12
|
+
var s = String(str == null ? "" : str);
|
|
13
|
+
var h = 2166136261;
|
|
14
|
+
for (var i = 0; i < s.length; i++) {
|
|
15
|
+
h ^= s.charCodeAt(i);
|
|
16
|
+
h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
|
|
17
|
+
}
|
|
18
|
+
return h.toString(36);
|
|
19
|
+
}
|
|
20
|
+
module.exports = { hashCode: hashCode2 };
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// src/index.ts
|
|
25
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
26
|
+
|
|
27
|
+
// src/server.ts
|
|
28
|
+
import { readFileSync } from "node:fs";
|
|
29
|
+
import { dirname, join } from "node:path";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
32
|
+
import {
|
|
33
|
+
CallToolRequestSchema,
|
|
34
|
+
ListToolsRequestSchema
|
|
35
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
36
|
+
|
|
37
|
+
// ../../apps/playroom/src/canonical-snippet-lookup.ts
|
|
38
|
+
import { compressToEncodedURIComponent } from "lz-string";
|
|
39
|
+
|
|
40
|
+
// ../../apps/playroom/src/compositions.ts
|
|
41
|
+
var compositions_default = [
|
|
42
|
+
// ── Alert ──
|
|
43
|
+
{
|
|
44
|
+
group: "Alert",
|
|
45
|
+
name: "Alert (success)",
|
|
46
|
+
code: '<Alert status="success" title="Payment method updated" message="Your card ending in 4242 is now the default." />'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
group: "Alert",
|
|
50
|
+
name: "Alert (error)",
|
|
51
|
+
code: '<Alert status="error" title="Payment failed" message="Your card was declined. Try a different payment method." />'
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
group: "Alert",
|
|
55
|
+
name: "Alert (dismissible)",
|
|
56
|
+
code: '<Alert status="info" title="New feature available" message="You can now schedule links to go live at a specific time." onDismiss={() => {}} />'
|
|
57
|
+
},
|
|
58
|
+
// ── Actions ──
|
|
59
|
+
// Maps to figma-mappings `Actions` (node 8156-2311). The Figma component is
|
|
60
|
+
// a horizontal toolbar slot for grouping action buttons.
|
|
61
|
+
{
|
|
62
|
+
group: "Actions",
|
|
63
|
+
name: "Actions",
|
|
64
|
+
code: `<Actions>
|
|
65
|
+
<Button>Save changes</Button>
|
|
66
|
+
<Button variant="ghost">Cancel</Button>
|
|
67
|
+
</Actions>`
|
|
68
|
+
},
|
|
69
|
+
// ── Avatar ──
|
|
70
|
+
{
|
|
71
|
+
group: "Avatar",
|
|
72
|
+
name: "Avatar",
|
|
73
|
+
code: `<Avatar>
|
|
74
|
+
<AvatarImage src="https://i.pravatar.cc/150?u=1" alt="Alex Morgan" />
|
|
75
|
+
<AvatarFallback>AM</AvatarFallback>
|
|
76
|
+
</Avatar>`
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
group: "Avatar",
|
|
80
|
+
name: "Avatar with status badge",
|
|
81
|
+
code: `<Avatar>
|
|
82
|
+
<AvatarImage src="https://i.pravatar.cc/150?u=2" alt="Jordan Smith" />
|
|
83
|
+
<AvatarFallback>JS</AvatarFallback>
|
|
84
|
+
<AvatarBadge className="bg-green-600" />
|
|
85
|
+
</Avatar>`
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
group: "Avatar",
|
|
89
|
+
name: "Avatar group",
|
|
90
|
+
code: `<AvatarGroup>
|
|
91
|
+
<Avatar size="sm">
|
|
92
|
+
<AvatarImage src="https://i.pravatar.cc/150?u=1" alt="Alex" />
|
|
93
|
+
<AvatarFallback>AM</AvatarFallback>
|
|
94
|
+
</Avatar>
|
|
95
|
+
<Avatar size="sm">
|
|
96
|
+
<AvatarImage src="https://i.pravatar.cc/150?u=2" alt="Jordan" />
|
|
97
|
+
<AvatarFallback>JS</AvatarFallback>
|
|
98
|
+
</Avatar>
|
|
99
|
+
<Avatar size="sm">
|
|
100
|
+
<AvatarImage src="https://i.pravatar.cc/150?u=3" alt="Sam" />
|
|
101
|
+
<AvatarFallback>SK</AvatarFallback>
|
|
102
|
+
</Avatar>
|
|
103
|
+
<AvatarGroupCount count={5} size="sm" />
|
|
104
|
+
</AvatarGroup>`
|
|
105
|
+
},
|
|
106
|
+
// ── Badge ──
|
|
107
|
+
{
|
|
108
|
+
group: "Badge",
|
|
109
|
+
name: "Badge",
|
|
110
|
+
code: "<Badge>New</Badge>"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
group: "Badge",
|
|
114
|
+
name: "Badge (variants)",
|
|
115
|
+
code: `<div className="flex gap-2">
|
|
116
|
+
<Badge variant="success">Active</Badge>
|
|
117
|
+
<Badge variant="warning">Pending</Badge>
|
|
118
|
+
<Badge variant="danger">Expired</Badge>
|
|
119
|
+
<Badge variant="info">Beta</Badge>
|
|
120
|
+
</div>`
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
group: "Badge",
|
|
124
|
+
name: "StatusBadge",
|
|
125
|
+
code: '<StatusBadge status="active" />'
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
group: "Badge",
|
|
129
|
+
name: "PlanBadge",
|
|
130
|
+
code: '<PlanBadge plan="pro" />'
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
group: "Badge",
|
|
134
|
+
name: "NotificationBadge",
|
|
135
|
+
code: "<NotificationBadge count={3} />"
|
|
136
|
+
},
|
|
137
|
+
// ── Button ──
|
|
138
|
+
{
|
|
139
|
+
group: "Button",
|
|
140
|
+
name: "Button (primary)",
|
|
141
|
+
code: "<Button>Save changes</Button>"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
group: "Button",
|
|
145
|
+
name: "Button (secondary)",
|
|
146
|
+
code: '<Button variant="secondary">Cancel</Button>'
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
group: "Button",
|
|
150
|
+
name: "Button (danger)",
|
|
151
|
+
code: '<Button variant="danger">Delete account</Button>'
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
group: "Button",
|
|
155
|
+
name: "Button (outline)",
|
|
156
|
+
code: '<Button variant="outline">View analytics</Button>'
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
group: "Button",
|
|
160
|
+
name: "Button (loading)",
|
|
161
|
+
code: '<Button state="loading">Saving\u2026</Button>'
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
group: "Button",
|
|
165
|
+
name: "Button pair",
|
|
166
|
+
code: `<div className="flex gap-2">
|
|
167
|
+
<Button variant="secondary">Cancel</Button>
|
|
168
|
+
<Button>Save changes</Button>
|
|
169
|
+
</div>`
|
|
170
|
+
},
|
|
171
|
+
// ── CardSelect ──
|
|
172
|
+
{
|
|
173
|
+
group: "CardSelect",
|
|
174
|
+
name: "CardSelect",
|
|
175
|
+
code: `<CardSelect defaultValue="classic">
|
|
176
|
+
<CardSelectItem value="classic">Classic</CardSelectItem>
|
|
177
|
+
<CardSelectItem value="featured">Featured</CardSelectItem>
|
|
178
|
+
<CardSelectItem value="spotlight">Spotlight</CardSelectItem>
|
|
179
|
+
</CardSelect>`
|
|
180
|
+
},
|
|
181
|
+
// ── Checkbox ──
|
|
182
|
+
{
|
|
183
|
+
group: "Checkbox",
|
|
184
|
+
name: "Checkbox with label",
|
|
185
|
+
code: `<div className="flex items-center gap-2">
|
|
186
|
+
<Checkbox id="terms" />
|
|
187
|
+
<label htmlFor="terms">I agree to the terms and conditions</label>
|
|
188
|
+
</div>`
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
group: "Checkbox",
|
|
192
|
+
name: "Checkbox group",
|
|
193
|
+
code: `<div className="flex flex-col gap-3">
|
|
194
|
+
<div className="flex items-center gap-2">
|
|
195
|
+
<Checkbox id="email" defaultChecked />
|
|
196
|
+
<label htmlFor="email">Email notifications</label>
|
|
197
|
+
</div>
|
|
198
|
+
<div className="flex items-center gap-2">
|
|
199
|
+
<Checkbox id="push" />
|
|
200
|
+
<label htmlFor="push">Push notifications</label>
|
|
201
|
+
</div>
|
|
202
|
+
<div className="flex items-center gap-2">
|
|
203
|
+
<Checkbox id="sms" />
|
|
204
|
+
<label htmlFor="sms">SMS notifications</label>
|
|
205
|
+
</div>
|
|
206
|
+
</div>`
|
|
207
|
+
},
|
|
208
|
+
// ── Collapsible ──
|
|
209
|
+
{
|
|
210
|
+
group: "Collapsible",
|
|
211
|
+
name: "Collapsible",
|
|
212
|
+
code: `<Collapsible>
|
|
213
|
+
<CollapsibleTrigger asChild>
|
|
214
|
+
<Button variant="ghost">Show more</Button>
|
|
215
|
+
</CollapsibleTrigger>
|
|
216
|
+
<CollapsibleContent>
|
|
217
|
+
<Text>Here is the hidden content that appears when expanded.</Text>
|
|
218
|
+
</CollapsibleContent>
|
|
219
|
+
</Collapsible>`
|
|
220
|
+
},
|
|
221
|
+
// ── Dialog ──
|
|
222
|
+
{
|
|
223
|
+
group: "Dialog",
|
|
224
|
+
name: "Dialog",
|
|
225
|
+
code: `<Dialog>
|
|
226
|
+
<DialogTrigger asChild>
|
|
227
|
+
<Button>Edit profile</Button>
|
|
228
|
+
</DialogTrigger>
|
|
229
|
+
<DialogContent>
|
|
230
|
+
<DialogHeader>
|
|
231
|
+
<DialogTitle>Edit profile</DialogTitle>
|
|
232
|
+
<DialogDescription>Make changes to your profile here.</DialogDescription>
|
|
233
|
+
</DialogHeader>
|
|
234
|
+
<div className="flex flex-col gap-4 py-4">
|
|
235
|
+
<FieldRoot>
|
|
236
|
+
<FieldControl>
|
|
237
|
+
<FieldLabel>Display name</FieldLabel>
|
|
238
|
+
<FieldInput placeholder="Your name" />
|
|
239
|
+
</FieldControl>
|
|
240
|
+
</FieldRoot>
|
|
241
|
+
<FieldRoot>
|
|
242
|
+
<FieldControl>
|
|
243
|
+
<FieldLabel>Bio</FieldLabel>
|
|
244
|
+
<FieldTextarea placeholder="Tell us about yourself" />
|
|
245
|
+
</FieldControl>
|
|
246
|
+
</FieldRoot>
|
|
247
|
+
</div>
|
|
248
|
+
<DialogFooter>
|
|
249
|
+
<DialogClose asChild>
|
|
250
|
+
<Button variant="secondary">Cancel</Button>
|
|
251
|
+
</DialogClose>
|
|
252
|
+
<Button>Save changes</Button>
|
|
253
|
+
</DialogFooter>
|
|
254
|
+
</DialogContent>
|
|
255
|
+
</Dialog>`
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
group: "Dialog",
|
|
259
|
+
name: "Dialog (destructive)",
|
|
260
|
+
code: `<Dialog>
|
|
261
|
+
<DialogTrigger asChild>
|
|
262
|
+
<Button variant="danger">Delete link</Button>
|
|
263
|
+
</DialogTrigger>
|
|
264
|
+
<DialogContent>
|
|
265
|
+
<DialogHeader>
|
|
266
|
+
<DialogTitle>Delete link</DialogTitle>
|
|
267
|
+
<DialogDescription>
|
|
268
|
+
This action cannot be undone. This will permanently delete the link and remove it from your Linktree.
|
|
269
|
+
</DialogDescription>
|
|
270
|
+
</DialogHeader>
|
|
271
|
+
<DialogFooter>
|
|
272
|
+
<DialogClose asChild>
|
|
273
|
+
<Button variant="secondary">Cancel</Button>
|
|
274
|
+
</DialogClose>
|
|
275
|
+
<Button variant="danger">Delete</Button>
|
|
276
|
+
</DialogFooter>
|
|
277
|
+
</DialogContent>
|
|
278
|
+
</Dialog>`
|
|
279
|
+
},
|
|
280
|
+
// ── Drawer ──
|
|
281
|
+
{
|
|
282
|
+
group: "Drawer",
|
|
283
|
+
name: "Drawer (bottom)",
|
|
284
|
+
code: `<Drawer>
|
|
285
|
+
<DrawerTrigger asChild>
|
|
286
|
+
<Button>Open drawer</Button>
|
|
287
|
+
</DrawerTrigger>
|
|
288
|
+
<DrawerContent>
|
|
289
|
+
<DrawerHandle />
|
|
290
|
+
<DrawerHeader>
|
|
291
|
+
<DrawerTitle>Settings</DrawerTitle>
|
|
292
|
+
<DrawerDescription>Manage your preferences.</DrawerDescription>
|
|
293
|
+
</DrawerHeader>
|
|
294
|
+
<div className="p-4">
|
|
295
|
+
<Text>Drawer content goes here.</Text>
|
|
296
|
+
</div>
|
|
297
|
+
<DrawerFooter>
|
|
298
|
+
<DrawerClose asChild>
|
|
299
|
+
<Button variant="secondary">Close</Button>
|
|
300
|
+
</DrawerClose>
|
|
301
|
+
</DrawerFooter>
|
|
302
|
+
</DrawerContent>
|
|
303
|
+
</Drawer>`
|
|
304
|
+
},
|
|
305
|
+
// ── DropdownMenu ──
|
|
306
|
+
{
|
|
307
|
+
group: "DropdownMenu",
|
|
308
|
+
name: "DropdownMenu",
|
|
309
|
+
code: `<DropdownMenu>
|
|
310
|
+
<DropdownMenuTrigger asChild>
|
|
311
|
+
<Button variant="outline">Options</Button>
|
|
312
|
+
</DropdownMenuTrigger>
|
|
313
|
+
<DropdownMenuContent>
|
|
314
|
+
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
315
|
+
<DropdownMenuDivider />
|
|
316
|
+
<DropdownMenuItem>Edit</DropdownMenuItem>
|
|
317
|
+
<DropdownMenuItem>Duplicate</DropdownMenuItem>
|
|
318
|
+
<DropdownMenuItem>Share</DropdownMenuItem>
|
|
319
|
+
<DropdownMenuDivider />
|
|
320
|
+
<DropdownMenuItem className="text-danger">Delete</DropdownMenuItem>
|
|
321
|
+
</DropdownMenuContent>
|
|
322
|
+
</DropdownMenu>`
|
|
323
|
+
},
|
|
324
|
+
// ── Elevation ──
|
|
325
|
+
{
|
|
326
|
+
group: "Elevation",
|
|
327
|
+
name: "Elevation",
|
|
328
|
+
code: `<Elevation level={200} className="rounded-card p-6">
|
|
329
|
+
<Text variant="body-base-medium">Elevated card content</Text>
|
|
330
|
+
</Elevation>`
|
|
331
|
+
},
|
|
332
|
+
// ── Field ──
|
|
333
|
+
{
|
|
334
|
+
group: "Field",
|
|
335
|
+
name: "Field (input)",
|
|
336
|
+
code: `<FieldRoot>
|
|
337
|
+
<FieldControl>
|
|
338
|
+
<FieldLabel>Email address</FieldLabel>
|
|
339
|
+
<FieldInput placeholder="you@example.com" />
|
|
340
|
+
</FieldControl>
|
|
341
|
+
</FieldRoot>`
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
group: "Field",
|
|
345
|
+
name: "Field (textarea with counter)",
|
|
346
|
+
code: `<FieldRoot>
|
|
347
|
+
<FieldControl>
|
|
348
|
+
<FieldLabel>Bio</FieldLabel>
|
|
349
|
+
<FieldTextarea placeholder="Tell us about yourself" />
|
|
350
|
+
</FieldControl>
|
|
351
|
+
<FieldCounter max={200} />
|
|
352
|
+
</FieldRoot>`
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
group: "Field",
|
|
356
|
+
name: "Field (with addon)",
|
|
357
|
+
code: `<FieldRoot>
|
|
358
|
+
<FieldControl>
|
|
359
|
+
<FieldAddon align="start">https://</FieldAddon>
|
|
360
|
+
<FieldLabel>URL</FieldLabel>
|
|
361
|
+
<FieldInput placeholder="linktr.ee/yourname" />
|
|
362
|
+
</FieldControl>
|
|
363
|
+
</FieldRoot>`
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
group: "Field",
|
|
367
|
+
name: "Field (error state)",
|
|
368
|
+
code: `<FieldRoot state="error">
|
|
369
|
+
<FieldControl>
|
|
370
|
+
<FieldLabel>Username</FieldLabel>
|
|
371
|
+
<FieldInput value="ab" />
|
|
372
|
+
</FieldControl>
|
|
373
|
+
<FieldMessage>Username must be at least 3 characters.</FieldMessage>
|
|
374
|
+
</FieldRoot>`
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
group: "Field",
|
|
378
|
+
name: "Field (outline variant)",
|
|
379
|
+
code: `<FieldRoot variant="outline">
|
|
380
|
+
<FieldControl>
|
|
381
|
+
<FieldLabel>Link title</FieldLabel>
|
|
382
|
+
<FieldInput placeholder="My website" />
|
|
383
|
+
</FieldControl>
|
|
384
|
+
</FieldRoot>`
|
|
385
|
+
},
|
|
386
|
+
// ── Fieldset ──
|
|
387
|
+
{
|
|
388
|
+
group: "Fieldset",
|
|
389
|
+
name: "Fieldset",
|
|
390
|
+
code: `<Fieldset>
|
|
391
|
+
<FieldsetLegend>Notifications</FieldsetLegend>
|
|
392
|
+
<FieldsetDescription>Select how you'd like to be notified.</FieldsetDescription>
|
|
393
|
+
<RadioGroup defaultValue="email">
|
|
394
|
+
<RadioButton value="email">Email</RadioButton>
|
|
395
|
+
<RadioButton value="push">Push notification</RadioButton>
|
|
396
|
+
<RadioButton value="sms">SMS</RadioButton>
|
|
397
|
+
</RadioGroup>
|
|
398
|
+
</Fieldset>`
|
|
399
|
+
},
|
|
400
|
+
// ── HeaderBar ──
|
|
401
|
+
// Maps to figma-mappings `HeaderBar` (node 5223-5837). Title + back-button
|
|
402
|
+
// is the canonical small variant; HeaderBar.stories.tsx covers the full
|
|
403
|
+
// surface (actions, tabs, integration patterns).
|
|
404
|
+
{
|
|
405
|
+
group: "HeaderBar",
|
|
406
|
+
name: "HeaderBar",
|
|
407
|
+
code: '<HeaderBar title="My profile" showBackButton />'
|
|
408
|
+
},
|
|
409
|
+
// ── IconButton ──
|
|
410
|
+
{
|
|
411
|
+
group: "IconButton",
|
|
412
|
+
name: "IconButton",
|
|
413
|
+
code: '<IconButton icon={Gear} variant="secondary" aria-label="Settings" />'
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
group: "IconButton",
|
|
417
|
+
name: "IconButton (toolbar)",
|
|
418
|
+
code: '<IconButton icon={PencilSimple} variant="toolbar" size="sm" aria-label="Edit" />'
|
|
419
|
+
},
|
|
420
|
+
// ── Link ──
|
|
421
|
+
{
|
|
422
|
+
group: "Link",
|
|
423
|
+
name: "Link",
|
|
424
|
+
code: '<Link href="#">Learn more</Link>'
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
group: "Link",
|
|
428
|
+
name: "Link (external)",
|
|
429
|
+
code: '<Link href="https://linktr.ee" external>Visit Linktree</Link>'
|
|
430
|
+
},
|
|
431
|
+
// ── NativeSelect ──
|
|
432
|
+
{
|
|
433
|
+
group: "NativeSelect",
|
|
434
|
+
name: "NativeSelect",
|
|
435
|
+
code: `<NativeSelect>
|
|
436
|
+
<NativeSelectOption disabled value="">Select a country\u2026</NativeSelectOption>
|
|
437
|
+
<NativeSelectOption value="au">Australia</NativeSelectOption>
|
|
438
|
+
<NativeSelectOption value="us">United States</NativeSelectOption>
|
|
439
|
+
<NativeSelectOption value="uk">United Kingdom</NativeSelectOption>
|
|
440
|
+
</NativeSelect>`
|
|
441
|
+
},
|
|
442
|
+
// ── Popover ──
|
|
443
|
+
{
|
|
444
|
+
group: "Popover",
|
|
445
|
+
name: "Popover",
|
|
446
|
+
code: `<Popover>
|
|
447
|
+
<PopoverTrigger asChild>
|
|
448
|
+
<IconButton icon={Question} variant="ghost" aria-label="More info" />
|
|
449
|
+
</PopoverTrigger>
|
|
450
|
+
<PopoverContent className="w-80">
|
|
451
|
+
<Text variant="body-sm-medium">Help</Text>
|
|
452
|
+
<Text variant="body-sm-regular">This setting controls who can see your profile. Changes take effect immediately.</Text>
|
|
453
|
+
</PopoverContent>
|
|
454
|
+
</Popover>`
|
|
455
|
+
},
|
|
456
|
+
// ── RadioGroup ──
|
|
457
|
+
{
|
|
458
|
+
group: "RadioGroup",
|
|
459
|
+
name: "RadioGroup (compact)",
|
|
460
|
+
code: `<RadioGroup defaultValue="email">
|
|
461
|
+
<RadioButton variant="compact" value="email">Email</RadioButton>
|
|
462
|
+
<RadioButton variant="compact" value="push">Push notification</RadioButton>
|
|
463
|
+
<RadioButton variant="compact" value="sms">SMS</RadioButton>
|
|
464
|
+
</RadioGroup>`
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
group: "RadioGroup",
|
|
468
|
+
name: "RadioGroup (pill)",
|
|
469
|
+
code: `<RadioGroup defaultValue="monthly" orientation="horizontal">
|
|
470
|
+
<RadioButton variant="pill" value="monthly">Monthly</RadioButton>
|
|
471
|
+
<RadioButton variant="pill" value="yearly">Yearly</RadioButton>
|
|
472
|
+
</RadioGroup>`
|
|
473
|
+
},
|
|
474
|
+
// ── RadioButton ──
|
|
475
|
+
// Maps to figma-mappings `RadioButton` (node 5223-28096). Standalone-button
|
|
476
|
+
// entry distinct from the RadioGroup composition above — but RadioButton
|
|
477
|
+
// must always render inside a RadioGroup, so the canonical snippet wraps
|
|
478
|
+
// a 2-option pill group.
|
|
479
|
+
{
|
|
480
|
+
group: "RadioButton",
|
|
481
|
+
name: "RadioButton",
|
|
482
|
+
code: `<RadioGroup defaultValue="option-1">
|
|
483
|
+
<RadioButton value="option-1" variant="pill">Option 1</RadioButton>
|
|
484
|
+
<RadioButton value="option-2" variant="pill">Option 2</RadioButton>
|
|
485
|
+
</RadioGroup>`
|
|
486
|
+
},
|
|
487
|
+
// ── SearchInput ──
|
|
488
|
+
{
|
|
489
|
+
group: "SearchInput",
|
|
490
|
+
name: "SearchInput",
|
|
491
|
+
code: `<SearchInput>
|
|
492
|
+
<SearchInputRoot>
|
|
493
|
+
<SearchInputControl>
|
|
494
|
+
<SearchInputInput placeholder="Search links\u2026" />
|
|
495
|
+
</SearchInputControl>
|
|
496
|
+
<SearchInputContent>
|
|
497
|
+
<SearchInputItem value="my-website">My website</SearchInputItem>
|
|
498
|
+
<SearchInputItem value="instagram">Instagram</SearchInputItem>
|
|
499
|
+
<SearchInputItem value="shop">Shop</SearchInputItem>
|
|
500
|
+
<SearchInputEmpty>No results found.</SearchInputEmpty>
|
|
501
|
+
</SearchInputContent>
|
|
502
|
+
</SearchInputRoot>
|
|
503
|
+
</SearchInput>`
|
|
504
|
+
},
|
|
505
|
+
// ── Select ──
|
|
506
|
+
{
|
|
507
|
+
group: "Select",
|
|
508
|
+
name: "Select",
|
|
509
|
+
code: `<Select>
|
|
510
|
+
<SelectTrigger>
|
|
511
|
+
<SelectValue placeholder="Select a timezone\u2026" />
|
|
512
|
+
</SelectTrigger>
|
|
513
|
+
<SelectContent>
|
|
514
|
+
<SelectGroup>
|
|
515
|
+
<SelectLabel>Americas</SelectLabel>
|
|
516
|
+
<SelectItem value="est">Eastern (UTC\u22125)</SelectItem>
|
|
517
|
+
<SelectItem value="cst">Central (UTC\u22126)</SelectItem>
|
|
518
|
+
<SelectItem value="pst">Pacific (UTC\u22128)</SelectItem>
|
|
519
|
+
</SelectGroup>
|
|
520
|
+
<SelectDivider />
|
|
521
|
+
<SelectGroup>
|
|
522
|
+
<SelectLabel>Asia Pacific</SelectLabel>
|
|
523
|
+
<SelectItem value="aest">Sydney (UTC+10)</SelectItem>
|
|
524
|
+
<SelectItem value="nzst">Auckland (UTC+12)</SelectItem>
|
|
525
|
+
</SelectGroup>
|
|
526
|
+
</SelectContent>
|
|
527
|
+
</Select>`
|
|
528
|
+
},
|
|
529
|
+
// ── Divider ──
|
|
530
|
+
{
|
|
531
|
+
group: "Divider",
|
|
532
|
+
name: "Divider",
|
|
533
|
+
code: "<Divider />"
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
group: "Divider",
|
|
537
|
+
name: "Divider (with label)",
|
|
538
|
+
code: '<Divider label="OR" />'
|
|
539
|
+
},
|
|
540
|
+
// ── Skeleton ──
|
|
541
|
+
{
|
|
542
|
+
group: "Skeleton",
|
|
543
|
+
name: "Skeleton (text block)",
|
|
544
|
+
code: "<SkeletonText lines={3} />"
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
group: "Skeleton",
|
|
548
|
+
name: "Skeleton (profile)",
|
|
549
|
+
code: `<div className="flex items-center gap-3">
|
|
550
|
+
<SkeletonAvatar size="lg" />
|
|
551
|
+
<div className="flex flex-col gap-2">
|
|
552
|
+
<SkeletonText lines={1} className="w-32" />
|
|
553
|
+
<SkeletonText lines={1} className="w-48" />
|
|
554
|
+
</div>
|
|
555
|
+
</div>`
|
|
556
|
+
},
|
|
557
|
+
// ── Switch ──
|
|
558
|
+
{
|
|
559
|
+
group: "Switch",
|
|
560
|
+
name: "Switch",
|
|
561
|
+
code: '<Switch label="Enable notifications" />'
|
|
562
|
+
},
|
|
563
|
+
// ── Tabs ──
|
|
564
|
+
{
|
|
565
|
+
group: "Tabs",
|
|
566
|
+
name: "Tabs (underline)",
|
|
567
|
+
code: `<Tabs defaultValue="links">
|
|
568
|
+
<TabsList>
|
|
569
|
+
<TabsTrigger value="links">Links</TabsTrigger>
|
|
570
|
+
<TabsTrigger value="appearance">Appearance</TabsTrigger>
|
|
571
|
+
<TabsTrigger value="settings">Settings</TabsTrigger>
|
|
572
|
+
</TabsList>
|
|
573
|
+
<TabsContent value="links">Manage your links here.</TabsContent>
|
|
574
|
+
<TabsContent value="appearance">Customize your appearance.</TabsContent>
|
|
575
|
+
<TabsContent value="settings">Manage your settings.</TabsContent>
|
|
576
|
+
</Tabs>`
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
group: "Tabs",
|
|
580
|
+
name: "Tabs (segmented)",
|
|
581
|
+
code: `<Tabs defaultValue="monthly">
|
|
582
|
+
<TabsList variant="segmented">
|
|
583
|
+
<TabsTrigger value="monthly">Monthly</TabsTrigger>
|
|
584
|
+
<TabsTrigger value="yearly">Yearly</TabsTrigger>
|
|
585
|
+
</TabsList>
|
|
586
|
+
</Tabs>`
|
|
587
|
+
},
|
|
588
|
+
// ── Text ──
|
|
589
|
+
{
|
|
590
|
+
group: "Text",
|
|
591
|
+
name: "Text (title)",
|
|
592
|
+
code: '<Text variant="title-lg-bold" as="h1">Page title</Text>'
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
group: "Text",
|
|
596
|
+
name: "Text (body)",
|
|
597
|
+
code: '<Text variant="body-base-regular">Body text goes here.</Text>'
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
group: "Text",
|
|
601
|
+
name: "Text (small)",
|
|
602
|
+
code: '<Text variant="body-sm-regular">Supporting text or caption.</Text>'
|
|
603
|
+
},
|
|
604
|
+
// ── TextTitle ──
|
|
605
|
+
// figma-mappings splits Text into TextTitle / TextBody (different node IDs
|
|
606
|
+
// in Figma: 5618-182 and 5118-457). The canonical-snippet lookup matches
|
|
607
|
+
// by exact group name, so we mirror that split here.
|
|
608
|
+
{
|
|
609
|
+
group: "TextTitle",
|
|
610
|
+
name: "TextTitle",
|
|
611
|
+
code: '<Text variant="title-lg-bold">Section heading</Text>'
|
|
612
|
+
},
|
|
613
|
+
// ── TextBody ──
|
|
614
|
+
{
|
|
615
|
+
group: "TextBody",
|
|
616
|
+
name: "TextBody",
|
|
617
|
+
code: '<Text variant="body-base-regular">A body paragraph showing the typography scale.</Text>'
|
|
618
|
+
},
|
|
619
|
+
// ── Toast ──
|
|
620
|
+
// Snippet entries demo the Toast surface itself. Production usage is
|
|
621
|
+
// imperative via `sendToastSuccess`/`sendToastError`/etc. helpers
|
|
622
|
+
// (see @linktr.ee/arbor exports), which dispatch a CustomEvent picked
|
|
623
|
+
// up by `<ToastListener />`.
|
|
624
|
+
{
|
|
625
|
+
group: "Toast",
|
|
626
|
+
name: "Toast (success)",
|
|
627
|
+
code: '<Toast id="demo-1" title="Settings saved" description="Your preferences have been updated." icon={<CheckCircle size={20} weight="fill" />} />'
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
group: "Toast",
|
|
631
|
+
name: "Toast (with action)",
|
|
632
|
+
code: '<Toast id="demo-2" title="Link deleted" description="Your link is no longer visible." action={{ label: "Undo", onClick: () => {} }} />'
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
group: "Toast",
|
|
636
|
+
name: "Toast (inverted)",
|
|
637
|
+
code: '<Toast id="demo-3" inverted description="Copied to clipboard" />'
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
group: "Toast",
|
|
641
|
+
name: "Toast (stacked chin)",
|
|
642
|
+
code: '<Toast id="demo-4" stacked title="Multiple updates" description="Two new comments and one mention." />'
|
|
643
|
+
},
|
|
644
|
+
// ── Tooltip ──
|
|
645
|
+
{
|
|
646
|
+
group: "Tooltip",
|
|
647
|
+
name: "Tooltip",
|
|
648
|
+
code: `<TooltipProvider>
|
|
649
|
+
<Tooltip>
|
|
650
|
+
<TooltipTrigger asChild>
|
|
651
|
+
<IconButton icon={Question} variant="ghost" aria-label="More info" />
|
|
652
|
+
</TooltipTrigger>
|
|
653
|
+
<TooltipContent>More info</TooltipContent>
|
|
654
|
+
</Tooltip>
|
|
655
|
+
</TooltipProvider>`
|
|
656
|
+
},
|
|
657
|
+
// ═══════════════════════════════════
|
|
658
|
+
// ── Compositions (multi-component) ──
|
|
659
|
+
// ═══════════════════════════════════
|
|
660
|
+
{
|
|
661
|
+
group: "Compositions",
|
|
662
|
+
name: "Profile header",
|
|
663
|
+
code: `<div className="flex items-center gap-4">
|
|
664
|
+
<Avatar size="xl">
|
|
665
|
+
<AvatarImage src="https://i.pravatar.cc/150?u=4" alt="Alex Morgan" />
|
|
666
|
+
<AvatarFallback>AM</AvatarFallback>
|
|
667
|
+
</Avatar>
|
|
668
|
+
<div className="flex flex-col gap-1">
|
|
669
|
+
<Text variant="title-sm-bold">Alex Morgan</Text>
|
|
670
|
+
<Text variant="body-sm-regular">@alexmorgan</Text>
|
|
671
|
+
<div className="flex gap-2">
|
|
672
|
+
<PlanBadge plan="pro" />
|
|
673
|
+
<StatusBadge status="active" />
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
</div>`
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
group: "Compositions",
|
|
680
|
+
name: "Settings form",
|
|
681
|
+
code: `<div className="flex flex-col gap-6 max-w-lg">
|
|
682
|
+
<Text variant="title-sm-bold" as="h2">Profile settings</Text>
|
|
683
|
+
<FieldRoot>
|
|
684
|
+
<FieldControl>
|
|
685
|
+
<FieldLabel>Display name</FieldLabel>
|
|
686
|
+
<FieldInput placeholder="Your name" />
|
|
687
|
+
</FieldControl>
|
|
688
|
+
</FieldRoot>
|
|
689
|
+
<FieldRoot>
|
|
690
|
+
<FieldControl>
|
|
691
|
+
<FieldLabel>Bio</FieldLabel>
|
|
692
|
+
<FieldTextarea placeholder="Tell us about yourself" />
|
|
693
|
+
</FieldControl>
|
|
694
|
+
<FieldCounter max={150} />
|
|
695
|
+
</FieldRoot>
|
|
696
|
+
<FieldRoot variant="outline">
|
|
697
|
+
<FieldControl>
|
|
698
|
+
<FieldAddon align="start">https://</FieldAddon>
|
|
699
|
+
<FieldLabel>URL</FieldLabel>
|
|
700
|
+
<FieldInput placeholder="linktr.ee/yourname" />
|
|
701
|
+
</FieldControl>
|
|
702
|
+
</FieldRoot>
|
|
703
|
+
<Divider />
|
|
704
|
+
<div className="flex gap-2 justify-end">
|
|
705
|
+
<Button variant="secondary">Cancel</Button>
|
|
706
|
+
<Button>Save changes</Button>
|
|
707
|
+
</div>
|
|
708
|
+
</div>`
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
group: "Compositions",
|
|
712
|
+
name: "Card list",
|
|
713
|
+
code: `<div className="flex flex-col gap-3 max-w-md">
|
|
714
|
+
<Elevation level={100} className="rounded-card p-4">
|
|
715
|
+
<div className="flex items-center justify-between">
|
|
716
|
+
<div className="flex items-center gap-3">
|
|
717
|
+
<Avatar size="sm">
|
|
718
|
+
<AvatarFallback>\u{1F517}</AvatarFallback>
|
|
719
|
+
</Avatar>
|
|
720
|
+
<div>
|
|
721
|
+
<Text variant="body-base-medium">My website</Text>
|
|
722
|
+
<Text variant="body-sm-regular">linktr.ee/alex</Text>
|
|
723
|
+
</div>
|
|
724
|
+
</div>
|
|
725
|
+
<Badge variant="success">Active</Badge>
|
|
726
|
+
</div>
|
|
727
|
+
</Elevation>
|
|
728
|
+
<Elevation level={100} className="rounded-card p-4">
|
|
729
|
+
<div className="flex items-center justify-between">
|
|
730
|
+
<div className="flex items-center gap-3">
|
|
731
|
+
<Avatar size="sm">
|
|
732
|
+
<AvatarFallback>\u{1F4F8}</AvatarFallback>
|
|
733
|
+
</Avatar>
|
|
734
|
+
<div>
|
|
735
|
+
<Text variant="body-base-medium">Instagram</Text>
|
|
736
|
+
<Text variant="body-sm-regular">instagram.com/alex</Text>
|
|
737
|
+
</div>
|
|
738
|
+
</div>
|
|
739
|
+
<Badge variant="success">Active</Badge>
|
|
740
|
+
</div>
|
|
741
|
+
</Elevation>
|
|
742
|
+
<Elevation level={100} className="rounded-card p-4">
|
|
743
|
+
<div className="flex items-center justify-between">
|
|
744
|
+
<div className="flex items-center gap-3">
|
|
745
|
+
<Avatar size="sm">
|
|
746
|
+
<AvatarFallback>\u{1F6CD}\uFE0F</AvatarFallback>
|
|
747
|
+
</Avatar>
|
|
748
|
+
<div>
|
|
749
|
+
<Text variant="body-base-medium">Shop</Text>
|
|
750
|
+
<Text variant="body-sm-regular">shop.example.com</Text>
|
|
751
|
+
</div>
|
|
752
|
+
</div>
|
|
753
|
+
<Badge variant="secondary">Draft</Badge>
|
|
754
|
+
</div>
|
|
755
|
+
</Elevation>
|
|
756
|
+
</div>`
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
group: "Compositions",
|
|
760
|
+
name: "Notification list",
|
|
761
|
+
code: `<div className="flex flex-col gap-2 max-w-md">
|
|
762
|
+
<Alert status="success" title="Link added" message="Instagram was added to your Linktree." onDismiss={() => {}} />
|
|
763
|
+
<Alert status="warning" title="Approaching limit" message="You've used 9 of 10 links on the free plan." />
|
|
764
|
+
<Alert status="info" title="New feature" message="You can now schedule links to go live at a specific time." onDismiss={() => {}} />
|
|
765
|
+
</div>`
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
group: "Compositions",
|
|
769
|
+
name: "Empty state",
|
|
770
|
+
code: `<div className="flex flex-col items-center justify-center gap-4 py-12 text-center">
|
|
771
|
+
<Text variant="title-sm-bold">No links yet</Text>
|
|
772
|
+
<Text variant="body-base-regular">Add your first link to get started.</Text>
|
|
773
|
+
<Button>Add a link</Button>
|
|
774
|
+
</div>`
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
group: "Compositions",
|
|
778
|
+
name: "Pricing toggle",
|
|
779
|
+
code: `<div className="flex flex-col items-center gap-6">
|
|
780
|
+
<Tabs defaultValue="yearly">
|
|
781
|
+
<TabsList variant="segmented">
|
|
782
|
+
<TabsTrigger value="monthly">Monthly</TabsTrigger>
|
|
783
|
+
<TabsTrigger value="yearly">Yearly</TabsTrigger>
|
|
784
|
+
</TabsList>
|
|
785
|
+
</Tabs>
|
|
786
|
+
<div className="flex gap-4">
|
|
787
|
+
<Elevation level={100} className="rounded-card p-6 flex flex-col gap-3 w-56">
|
|
788
|
+
<Text variant="body-base-medium">Starter</Text>
|
|
789
|
+
<Text variant="title-lg-bold">$5/mo</Text>
|
|
790
|
+
<Text variant="body-sm-regular">For creators just getting started.</Text>
|
|
791
|
+
<Button variant="outline" className="w-full">Choose plan</Button>
|
|
792
|
+
</Elevation>
|
|
793
|
+
<Elevation level={200} className="rounded-card p-6 flex flex-col gap-3 w-56">
|
|
794
|
+
<div className="flex items-center gap-2">
|
|
795
|
+
<Text variant="body-base-medium">Pro</Text>
|
|
796
|
+
<Badge variant="accent">Popular</Badge>
|
|
797
|
+
</div>
|
|
798
|
+
<Text variant="title-lg-bold">$9/mo</Text>
|
|
799
|
+
<Text variant="body-sm-regular">For growing creators.</Text>
|
|
800
|
+
<Button className="w-full">Choose plan</Button>
|
|
801
|
+
</Elevation>
|
|
802
|
+
</div>
|
|
803
|
+
</div>`
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
group: "Compositions",
|
|
807
|
+
name: "Delete confirmation",
|
|
808
|
+
code: `<Dialog>
|
|
809
|
+
<DialogTrigger asChild>
|
|
810
|
+
<Button variant="danger">Delete account</Button>
|
|
811
|
+
</DialogTrigger>
|
|
812
|
+
<DialogContent>
|
|
813
|
+
<DialogHeader>
|
|
814
|
+
<DialogTitle>Delete account</DialogTitle>
|
|
815
|
+
<DialogDescription>
|
|
816
|
+
This will permanently delete your account and all your links. This action cannot be undone.
|
|
817
|
+
</DialogDescription>
|
|
818
|
+
</DialogHeader>
|
|
819
|
+
<FieldRoot state="error">
|
|
820
|
+
<FieldControl>
|
|
821
|
+
<FieldLabel>Type "DELETE" to confirm</FieldLabel>
|
|
822
|
+
<FieldInput placeholder="DELETE" />
|
|
823
|
+
</FieldControl>
|
|
824
|
+
</FieldRoot>
|
|
825
|
+
<DialogFooter>
|
|
826
|
+
<DialogClose asChild>
|
|
827
|
+
<Button variant="secondary">Cancel</Button>
|
|
828
|
+
</DialogClose>
|
|
829
|
+
<Button variant="danger">Delete my account</Button>
|
|
830
|
+
</DialogFooter>
|
|
831
|
+
</DialogContent>
|
|
832
|
+
</Dialog>`
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
group: "Compositions",
|
|
836
|
+
name: "Linktree profile",
|
|
837
|
+
code: `<div className="flex min-h-screen flex-col items-center bg-background px-4 py-12">
|
|
838
|
+
<div className="flex w-full max-w-md flex-col items-center gap-6">
|
|
839
|
+
<div className="flex flex-col items-center gap-3">
|
|
840
|
+
<Avatar size="xl">
|
|
841
|
+
<AvatarImage src="https://i.pravatar.cc/150?img=15" alt="Arbor" />
|
|
842
|
+
<AvatarFallback>\u{1F333}</AvatarFallback>
|
|
843
|
+
</Avatar>
|
|
844
|
+
<div className="flex flex-col items-center gap-1">
|
|
845
|
+
<Text variant="title-sm-bold">@arbor</Text>
|
|
846
|
+
<Text variant="body-sm-regular" className="text-center text-secondary">
|
|
847
|
+
Design system \xB7 Linktree
|
|
848
|
+
</Text>
|
|
849
|
+
</div>
|
|
850
|
+
</div>
|
|
851
|
+
|
|
852
|
+
<div className="flex w-full flex-col gap-3">
|
|
853
|
+
<Button variant="outline" className="w-full justify-center" size="lg">
|
|
854
|
+
\u{1F3B5} New single \u2014 Out now
|
|
855
|
+
</Button>
|
|
856
|
+
<Button variant="outline" className="w-full justify-center" size="lg">
|
|
857
|
+
\u{1F3A4} Upcoming shows
|
|
858
|
+
</Button>
|
|
859
|
+
<Button variant="outline" className="w-full justify-center" size="lg">
|
|
860
|
+
\u{1F6D2} Merch store
|
|
861
|
+
</Button>
|
|
862
|
+
<Button variant="outline" className="w-full justify-center" size="lg">
|
|
863
|
+
\u{1F4F8} Instagram
|
|
864
|
+
</Button>
|
|
865
|
+
<Button variant="outline" className="w-full justify-center" size="lg">
|
|
866
|
+
\u{1F3A7} Spotify
|
|
867
|
+
</Button>
|
|
868
|
+
</div>
|
|
869
|
+
|
|
870
|
+
<Divider />
|
|
871
|
+
|
|
872
|
+
<div className="flex flex-col items-center gap-2 w-full">
|
|
873
|
+
<Text variant="body-xs-regular" className="text-secondary">
|
|
874
|
+
Join my newsletter
|
|
875
|
+
</Text>
|
|
876
|
+
<div className="flex w-full items-center gap-2">
|
|
877
|
+
<FieldRoot variant="outline" className="flex-1">
|
|
878
|
+
<FieldControl>
|
|
879
|
+
<FieldLabel>Email</FieldLabel>
|
|
880
|
+
<FieldInput />
|
|
881
|
+
</FieldControl>
|
|
882
|
+
</FieldRoot>
|
|
883
|
+
<Button>Subscribe</Button>
|
|
884
|
+
</div>
|
|
885
|
+
</div>
|
|
886
|
+
</div>
|
|
887
|
+
</div>`
|
|
888
|
+
}
|
|
889
|
+
];
|
|
890
|
+
|
|
891
|
+
// ../../apps/playroom/src/canonical-snippet-lookup.ts
|
|
892
|
+
var PLAYROOM_HOMEPAGE_URL = "https://arbor.linktr.ee/playroom/";
|
|
893
|
+
function findCanonicalEntryByGroup(group) {
|
|
894
|
+
for (const entry of compositions_default) {
|
|
895
|
+
if (entry.group === group) return entry;
|
|
896
|
+
}
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
function buildPlayroomUrl(jsx) {
|
|
900
|
+
const compressed = compressToEncodedURIComponent(
|
|
901
|
+
JSON.stringify({ code: jsx })
|
|
902
|
+
);
|
|
903
|
+
return `${PLAYROOM_HOMEPAGE_URL}#?code=${compressed}`;
|
|
904
|
+
}
|
|
905
|
+
async function mintPlayroomUrl(jsx, slugClient) {
|
|
906
|
+
let result;
|
|
907
|
+
try {
|
|
908
|
+
result = await slugClient.mintOrReuse(jsx);
|
|
909
|
+
} catch {
|
|
910
|
+
return {
|
|
911
|
+
url: buildPlayroomUrl(jsx),
|
|
912
|
+
source: "lz-string-fallback",
|
|
913
|
+
reason: "network"
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
if (result.ok) {
|
|
917
|
+
return {
|
|
918
|
+
url: result.url,
|
|
919
|
+
source: result.source,
|
|
920
|
+
slug: result.slug
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
url: buildPlayroomUrl(jsx),
|
|
925
|
+
source: "lz-string-fallback",
|
|
926
|
+
reason: result.reason
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function lookupByArborName(arborName) {
|
|
930
|
+
const normalized = typeof arborName === "string" ? arborName.trim() : "";
|
|
931
|
+
if (!normalized) {
|
|
932
|
+
return {
|
|
933
|
+
coverage: "not_found",
|
|
934
|
+
figmaName: arborName,
|
|
935
|
+
url: PLAYROOM_HOMEPAGE_URL
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
const entry = findCanonicalEntryByGroup(normalized);
|
|
939
|
+
if (entry) {
|
|
940
|
+
return {
|
|
941
|
+
coverage: "mapped",
|
|
942
|
+
arborComponentName: normalized,
|
|
943
|
+
snippet: entry.code,
|
|
944
|
+
url: buildPlayroomUrl(entry.code)
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
coverage: "not_found",
|
|
949
|
+
figmaName: normalized,
|
|
950
|
+
url: PLAYROOM_HOMEPAGE_URL
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// ../../apps/playroom/src/slug-client.ts
|
|
955
|
+
var { hashCode } = require_hash_code();
|
|
956
|
+
var MAX_SNIPPET_BYTES = 200 * 1024;
|
|
957
|
+
var SLUG_PATTERN = /^[a-z]{3,11}-[a-z]{3,11}-[a-z]{3,11}-(?:[1-9]|[1-9][0-9])$/;
|
|
958
|
+
var PLAYROOM_BASE_URL = "https://arbor.linktr.ee/playroom/";
|
|
959
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
960
|
+
function createInMemoryCache() {
|
|
961
|
+
const store = /* @__PURE__ */ new Map();
|
|
962
|
+
return {
|
|
963
|
+
get: (key) => store.get(key),
|
|
964
|
+
set: (key, slug) => {
|
|
965
|
+
store.set(key, slug);
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
function assertValidEndpoint(endpoint) {
|
|
970
|
+
if (!(endpoint instanceof URL)) {
|
|
971
|
+
throw new Error(
|
|
972
|
+
"[slug-client] endpoint must be a URL instance \u2014 pass `new URL(...)`"
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
if (endpoint.protocol !== "https:") {
|
|
976
|
+
throw new Error(
|
|
977
|
+
`[slug-client] endpoint must be https://, got ${endpoint.protocol}`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function createSlugClient(opts) {
|
|
982
|
+
assertValidEndpoint(opts.endpoint);
|
|
983
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
984
|
+
if (typeof fetchImpl !== "function") {
|
|
985
|
+
throw new Error(
|
|
986
|
+
"[slug-client] no fetch implementation available \u2014 pass `fetch` in options or run in an environment with global fetch"
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
const cache = opts.cache ?? createInMemoryCache();
|
|
990
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
991
|
+
const endpointUrl = opts.endpoint.toString();
|
|
992
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
993
|
+
async function performMint(jsx, key) {
|
|
994
|
+
const controller = new AbortController();
|
|
995
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
996
|
+
const headers = {
|
|
997
|
+
"Content-Type": "application/json"
|
|
998
|
+
};
|
|
999
|
+
if (opts.bearerToken) {
|
|
1000
|
+
headers["Authorization"] = `Bearer ${opts.bearerToken}`;
|
|
1001
|
+
}
|
|
1002
|
+
if (opts.producerName) {
|
|
1003
|
+
headers["X-Arbor-Producer"] = opts.producerName;
|
|
1004
|
+
}
|
|
1005
|
+
let response;
|
|
1006
|
+
try {
|
|
1007
|
+
response = await fetchImpl(endpointUrl, {
|
|
1008
|
+
method: "POST",
|
|
1009
|
+
headers,
|
|
1010
|
+
body: JSON.stringify({ snippet: jsx }),
|
|
1011
|
+
signal: controller.signal
|
|
1012
|
+
});
|
|
1013
|
+
} catch (err) {
|
|
1014
|
+
const isAbort = err?.name === "AbortError";
|
|
1015
|
+
return { ok: false, reason: isAbort ? "timeout" : "network" };
|
|
1016
|
+
} finally {
|
|
1017
|
+
clearTimeout(timer);
|
|
1018
|
+
}
|
|
1019
|
+
if (!response.ok) {
|
|
1020
|
+
return { ok: false, reason: "http_error" };
|
|
1021
|
+
}
|
|
1022
|
+
let payload;
|
|
1023
|
+
try {
|
|
1024
|
+
payload = await response.json();
|
|
1025
|
+
} catch {
|
|
1026
|
+
return { ok: false, reason: "malformed_response" };
|
|
1027
|
+
}
|
|
1028
|
+
const slug = extractSlug(payload);
|
|
1029
|
+
if (!slug) {
|
|
1030
|
+
return { ok: false, reason: "malformed_response" };
|
|
1031
|
+
}
|
|
1032
|
+
const url = `${PLAYROOM_BASE_URL}?slug=${slug}`;
|
|
1033
|
+
const branded = slug;
|
|
1034
|
+
cache.set(key, branded);
|
|
1035
|
+
return { ok: true, source: "minted", slug: branded, url };
|
|
1036
|
+
}
|
|
1037
|
+
async function mintOrReuse(jsx) {
|
|
1038
|
+
const byteLength = new TextEncoder().encode(jsx).length;
|
|
1039
|
+
if (byteLength > MAX_SNIPPET_BYTES) {
|
|
1040
|
+
return { ok: false, reason: "oversize" };
|
|
1041
|
+
}
|
|
1042
|
+
const key = hashCode(jsx);
|
|
1043
|
+
const cached = cache.get(key);
|
|
1044
|
+
if (cached) {
|
|
1045
|
+
const url = `${PLAYROOM_BASE_URL}?slug=${cached}`;
|
|
1046
|
+
return { ok: true, source: "cached", slug: cached, url };
|
|
1047
|
+
}
|
|
1048
|
+
const existing = inflight.get(key);
|
|
1049
|
+
if (existing) return existing;
|
|
1050
|
+
const promise = performMint(jsx, key).finally(() => {
|
|
1051
|
+
inflight.delete(key);
|
|
1052
|
+
});
|
|
1053
|
+
inflight.set(key, promise);
|
|
1054
|
+
return promise;
|
|
1055
|
+
}
|
|
1056
|
+
return { mintOrReuse };
|
|
1057
|
+
}
|
|
1058
|
+
function extractSlug(payload) {
|
|
1059
|
+
if (!payload || typeof payload !== "object") return null;
|
|
1060
|
+
const slug = payload.slug;
|
|
1061
|
+
if (typeof slug !== "string" || slug.length === 0) return null;
|
|
1062
|
+
if (!SLUG_PATTERN.test(slug)) return null;
|
|
1063
|
+
return slug;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// src/tools/open-in-playroom.ts
|
|
1067
|
+
var TOOL_NAME = "arbor.open_in_playroom";
|
|
1068
|
+
var TOOL_DESCRIPTION = 'Given an Arbor component name (PascalCase), return the Playroom URL with the canonical snippet pre-loaded. Use coverage="mapped" responses with high confidence; coverage="fallback" responses are best-effort name matching; coverage="not_found" responses link to the Playroom homepage.';
|
|
1069
|
+
var TOOL_INPUT_SCHEMA = {
|
|
1070
|
+
type: "object",
|
|
1071
|
+
properties: {
|
|
1072
|
+
componentName: {
|
|
1073
|
+
type: "string",
|
|
1074
|
+
description: 'Arbor PascalCase component name, e.g. "Button" or "HeaderBar"'
|
|
1075
|
+
}
|
|
1076
|
+
},
|
|
1077
|
+
required: ["componentName"]
|
|
1078
|
+
};
|
|
1079
|
+
function buildMcpSlugClient() {
|
|
1080
|
+
const rawBase = process.env.SNIPPET_API_BASE;
|
|
1081
|
+
if (!rawBase) return null;
|
|
1082
|
+
let endpoint;
|
|
1083
|
+
try {
|
|
1084
|
+
endpoint = new URL(rawBase);
|
|
1085
|
+
} catch {
|
|
1086
|
+
throw new Error(`[arbor-mcp] SNIPPET_API_BASE is not a valid URL: ${rawBase}`);
|
|
1087
|
+
}
|
|
1088
|
+
if (endpoint.protocol !== "https:") {
|
|
1089
|
+
throw new Error(`[arbor-mcp] SNIPPET_API_BASE must use HTTPS: ${rawBase}`);
|
|
1090
|
+
}
|
|
1091
|
+
return createSlugClient({
|
|
1092
|
+
endpoint,
|
|
1093
|
+
// Unbounded — MCP per-session lifetime is short, no LRU needed. Cache key
|
|
1094
|
+
// is content-addressed (FNV-1a of the snippet) so duplicate JSX never
|
|
1095
|
+
// burns a second slug.
|
|
1096
|
+
cache: createInMemoryCache(),
|
|
1097
|
+
bearerToken: process.env.SNIPPET_MINT_TOKEN,
|
|
1098
|
+
producerName: "mcp",
|
|
1099
|
+
timeoutMs: 5e3
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
var mcpSlugClient = buildMcpSlugClient();
|
|
1103
|
+
async function runOpenInPlayroom(args, slugClientOverride) {
|
|
1104
|
+
const slugClient = slugClientOverride === void 0 ? mcpSlugClient : slugClientOverride;
|
|
1105
|
+
if (!args || typeof args !== "object") {
|
|
1106
|
+
throw new Error("arbor.open_in_playroom requires an object argument with `componentName`");
|
|
1107
|
+
}
|
|
1108
|
+
const { componentName } = args;
|
|
1109
|
+
if (typeof componentName !== "string" || componentName.trim() === "") {
|
|
1110
|
+
throw new Error("arbor.open_in_playroom: `componentName` is required and must be a non-empty string");
|
|
1111
|
+
}
|
|
1112
|
+
const lookup = lookupByArborName(componentName.trim());
|
|
1113
|
+
if (lookup.coverage === "not_found") {
|
|
1114
|
+
return {
|
|
1115
|
+
coverage: "not_found",
|
|
1116
|
+
figmaName: lookup.figmaName,
|
|
1117
|
+
url: lookup.url
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
if (!slugClient) {
|
|
1121
|
+
return {
|
|
1122
|
+
coverage: "mapped",
|
|
1123
|
+
arborComponentName: lookup.arborComponentName,
|
|
1124
|
+
snippet: lookup.snippet,
|
|
1125
|
+
url: lookup.url,
|
|
1126
|
+
// already buildPlayroomUrl output
|
|
1127
|
+
source: "lz-string-fallback",
|
|
1128
|
+
degraded: true,
|
|
1129
|
+
reason: "unconfigured"
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
const minted = await mintPlayroomUrl(lookup.snippet, slugClient);
|
|
1133
|
+
if (minted.source === "lz-string-fallback") {
|
|
1134
|
+
return {
|
|
1135
|
+
coverage: "mapped",
|
|
1136
|
+
arborComponentName: lookup.arborComponentName,
|
|
1137
|
+
snippet: lookup.snippet,
|
|
1138
|
+
url: minted.url,
|
|
1139
|
+
source: "lz-string-fallback",
|
|
1140
|
+
degraded: true,
|
|
1141
|
+
reason: minted.reason
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
return {
|
|
1145
|
+
coverage: "mapped",
|
|
1146
|
+
arborComponentName: lookup.arborComponentName,
|
|
1147
|
+
snippet: lookup.snippet,
|
|
1148
|
+
url: minted.url,
|
|
1149
|
+
source: minted.source,
|
|
1150
|
+
slug: minted.slug
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// src/server.ts
|
|
1155
|
+
var packageJsonPath = join(
|
|
1156
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
1157
|
+
"..",
|
|
1158
|
+
"package.json"
|
|
1159
|
+
);
|
|
1160
|
+
var { version: packageJsonVersion } = JSON.parse(
|
|
1161
|
+
readFileSync(packageJsonPath, "utf-8")
|
|
1162
|
+
);
|
|
1163
|
+
var SERVER_NAME = "arbor";
|
|
1164
|
+
var SERVER_VERSION = packageJsonVersion;
|
|
1165
|
+
function createArborMcpServer() {
|
|
1166
|
+
const server = new Server(
|
|
1167
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
1168
|
+
{ capabilities: { tools: {} } }
|
|
1169
|
+
);
|
|
1170
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1171
|
+
tools: [
|
|
1172
|
+
{
|
|
1173
|
+
name: TOOL_NAME,
|
|
1174
|
+
description: TOOL_DESCRIPTION,
|
|
1175
|
+
inputSchema: TOOL_INPUT_SCHEMA
|
|
1176
|
+
}
|
|
1177
|
+
]
|
|
1178
|
+
}));
|
|
1179
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
1180
|
+
const { name, arguments: args } = req.params;
|
|
1181
|
+
if (name === TOOL_NAME) {
|
|
1182
|
+
const result = await runOpenInPlayroom(args);
|
|
1183
|
+
return {
|
|
1184
|
+
content: [
|
|
1185
|
+
{
|
|
1186
|
+
type: "text",
|
|
1187
|
+
text: JSON.stringify(result, null, 2)
|
|
1188
|
+
}
|
|
1189
|
+
]
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1193
|
+
});
|
|
1194
|
+
return server;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// src/index.ts
|
|
1198
|
+
async function main() {
|
|
1199
|
+
const server = createArborMcpServer();
|
|
1200
|
+
const transport = new StdioServerTransport();
|
|
1201
|
+
await server.connect(transport);
|
|
1202
|
+
process.stderr.write("arbor-mcp: ready\n");
|
|
1203
|
+
}
|
|
1204
|
+
main().catch((err) => {
|
|
1205
|
+
process.stderr.write(`arbor-mcp: fatal: ${err instanceof Error ? err.message : String(err)}
|
|
1206
|
+
`);
|
|
1207
|
+
process.exit(1);
|
|
1208
|
+
});
|