foss-design 0.9.10 → 0.9.11

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.
@@ -1,7 +1,7 @@
1
1
  import { QueryClient, QueryClientProvider, useQuery, useQueryClient } from "@tanstack/react-query";
2
2
  import { AnimatePresence, MotionConfig, motion, useDragControls } from "motion/react";
3
3
  import * as React from "react";
4
- import { Children, createContext, forwardRef, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
+ import { Children, Component, createContext, forwardRef, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
5
  import { Link, Redirect, Route, Router, Switch as Switch$1, useLocation, useRouter } from "wouter";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import * as ReactDOM from "react-dom";
@@ -2444,6 +2444,8 @@ function viewerKeys(id) {
2444
2444
  ]
2445
2445
  };
2446
2446
  }
2447
+ /** What frames of the local viewer may use: the clipboard, both ways, and fullscreen. */
2448
+ var DEFAULT_FRAME_ALLOW = "clipboard-read; clipboard-write; fullscreen";
2447
2449
  var ids = /* @__PURE__ */ new WeakMap();
2448
2450
  var lastId = 0;
2449
2451
  /** A stable id per source object. */
@@ -3114,6 +3116,414 @@ function CommandPalette() {
3114
3116
  }) : null })
3115
3117
  });
3116
3118
  }
3119
+ //#endregion
3120
+ //#region viewer/src/ui/button.tsx
3121
+ var KIND = {
3122
+ primary: "border border-primary bg-primary text-primary-ink hover:opacity-90",
3123
+ secondary: "border border-rule-strong bg-surface text-ink hover:bg-soft",
3124
+ ghost: "border border-transparent bg-transparent text-ink2 hover:bg-soft2",
3125
+ dashed: "border border-dashed border-rule-strong bg-transparent text-ink2 hover:bg-soft",
3126
+ danger: "border border-danger bg-danger text-on-danger hover:opacity-90"
3127
+ };
3128
+ var buttonClass = (kind = "secondary", extra) => cn("inline-flex h-ctrl shrink-0 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-[6px] px-ctrl text-ctrl font-medium leading-none no-underline transition-[background-color,opacity,scale] duration-100 active:not-disabled:scale-[0.97]", "disabled:cursor-not-allowed disabled:opacity-45 aria-disabled:cursor-not-allowed aria-disabled:opacity-45", KIND[kind], extra);
3129
+ var Button = forwardRef(function Button({ kind = "secondary", icon, loading, disabled, className, children, type = "button", ...rest }, ref) {
3130
+ return /* @__PURE__ */ jsxs("button", {
3131
+ ref,
3132
+ type,
3133
+ disabled: disabled || loading,
3134
+ "aria-busy": loading || void 0,
3135
+ className: buttonClass(kind, className),
3136
+ ...rest,
3137
+ children: [loading ? /* @__PURE__ */ jsx(Icon, {
3138
+ name: "loader",
3139
+ size: 15,
3140
+ className: "animate-spin"
3141
+ }) : icon ? /* @__PURE__ */ jsx(Icon, {
3142
+ name: icon,
3143
+ size: 15
3144
+ }) : null, children]
3145
+ });
3146
+ });
3147
+ var ICON_KIND = {
3148
+ ghost: "border border-transparent bg-transparent text-muted hover:bg-soft2 hover:text-ink2",
3149
+ secondary: "border border-rule-strong bg-surface text-ink2 hover:bg-soft"
3150
+ };
3151
+ var iconButtonClass = (kind = "ghost", extra) => cn("inline-flex h-ctrl w-ctrl shrink-0 cursor-pointer items-center justify-center rounded-[6px] p-0 transition-[color,background-color,scale] duration-100 active:not-disabled:scale-[0.94] disabled:cursor-not-allowed disabled:opacity-45", ICON_KIND[kind], extra);
3152
+ var IconButton = forwardRef(function IconButton({ icon, label, kind = "ghost", size = 16, className, type = "button", ...rest }, ref) {
3153
+ return /* @__PURE__ */ jsx("button", {
3154
+ ref,
3155
+ type,
3156
+ "aria-label": label,
3157
+ title: label,
3158
+ className: iconButtonClass(kind, className),
3159
+ ...rest,
3160
+ children: /* @__PURE__ */ jsx(Icon, {
3161
+ name: icon,
3162
+ size
3163
+ })
3164
+ });
3165
+ });
3166
+ /** An in-app link styled as a button. */
3167
+ function ButtonLink({ to, kind = "secondary", icon, className, children }) {
3168
+ return /* @__PURE__ */ jsxs(Link, {
3169
+ href: to,
3170
+ className: buttonClass(kind, className),
3171
+ children: [icon ? /* @__PURE__ */ jsx(Icon, {
3172
+ name: icon,
3173
+ size: 15
3174
+ }) : null, children]
3175
+ });
3176
+ }
3177
+ /** A link out of the app (a frame in a new tab), styled as a button. */
3178
+ function ButtonAnchor({ kind = "secondary", icon, className, children, ...rest }) {
3179
+ return /* @__PURE__ */ jsxs("a", {
3180
+ className: buttonClass(kind, className),
3181
+ ...rest,
3182
+ children: [icon ? /* @__PURE__ */ jsx(Icon, {
3183
+ name: icon,
3184
+ size: 15
3185
+ }) : null, children]
3186
+ });
3187
+ }
3188
+ //#endregion
3189
+ //#region viewer/src/ui/badge.tsx
3190
+ var BADGE = {
3191
+ neutral: "bg-soft2 text-ink2",
3192
+ action: "bg-action-soft text-action",
3193
+ ok: "bg-ok-soft text-ok-text",
3194
+ danger: "bg-danger-soft text-danger-text"
3195
+ };
3196
+ function Badge({ tone = "neutral", children, className }) {
3197
+ return /* @__PURE__ */ jsx("span", {
3198
+ className: cn("inline-flex h-[22px] shrink-0 items-center gap-1 whitespace-nowrap rounded-[6px] px-2 text-[12px] font-medium", BADGE[tone], className),
3199
+ children
3200
+ });
3201
+ }
3202
+ function Count({ value, tone = "neutral" }) {
3203
+ return /* @__PURE__ */ jsx("span", {
3204
+ className: cn("inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-[4px] px-[5px] text-[11.5px] font-semibold tabular", tone === "action" ? "bg-action-soft text-action" : tone === "danger" ? "bg-danger-soft text-danger-text" : "bg-soft2 text-ink2"),
3205
+ children: value
3206
+ });
3207
+ }
3208
+ //#endregion
3209
+ //#region viewer/src/ui/panel.tsx
3210
+ function Panel({ children, className, style }) {
3211
+ return /* @__PURE__ */ jsx("section", {
3212
+ className: cn("min-w-0 shrink-0 overflow-hidden rounded-[10px] border border-rule bg-surface", className),
3213
+ style,
3214
+ children
3215
+ });
3216
+ }
3217
+ function PanelHead({ title, count, sub, right, as: Heading = "h2" }) {
3218
+ return /* @__PURE__ */ jsxs("div", {
3219
+ className: "flex min-h-[46px] items-center justify-between gap-3 px-4",
3220
+ children: [/* @__PURE__ */ jsxs(Heading, {
3221
+ className: "m-0 flex min-w-0 items-center gap-2 whitespace-nowrap text-[14px] font-semibold",
3222
+ children: [
3223
+ title,
3224
+ count !== void 0 ? /* @__PURE__ */ jsx(Count, { value: count }) : null,
3225
+ sub ? /* @__PURE__ */ jsx("span", {
3226
+ className: "truncate text-[12.5px] font-normal text-muted",
3227
+ children: sub
3228
+ }) : null
3229
+ ]
3230
+ }), right ? /* @__PURE__ */ jsx("div", {
3231
+ className: "flex items-center gap-2",
3232
+ children: right
3233
+ }) : null]
3234
+ });
3235
+ }
3236
+ function PanelTabs({ items, value, onChange, right }) {
3237
+ const indicator = useId();
3238
+ return /* @__PURE__ */ jsxs("div", {
3239
+ className: "flex h-[46px] items-center justify-between gap-3 px-4",
3240
+ children: [/* @__PURE__ */ jsx("div", {
3241
+ role: "tablist",
3242
+ className: "flex gap-5",
3243
+ children: items.map((item) => {
3244
+ const on = item.value === value;
3245
+ return /* @__PURE__ */ jsxs("button", {
3246
+ type: "button",
3247
+ role: "tab",
3248
+ "aria-selected": on,
3249
+ onClick: () => onChange(item.value),
3250
+ className: cn("relative flex h-[46px] cursor-pointer items-center gap-1.5 border-0 bg-transparent px-0.5 text-[14px] transition-colors", on ? "font-semibold text-ink" : "font-medium text-ink2 hover:text-ink"),
3251
+ children: [
3252
+ item.label,
3253
+ item.count !== void 0 ? /* @__PURE__ */ jsx(Count, { value: item.count }) : null,
3254
+ on ? /* @__PURE__ */ jsx(motion.span, {
3255
+ layoutId: indicator,
3256
+ className: "absolute inset-x-0 bottom-0 h-[2px] bg-ink",
3257
+ transition: {
3258
+ type: "spring",
3259
+ stiffness: 520,
3260
+ damping: 42
3261
+ }
3262
+ }) : null
3263
+ ]
3264
+ }, item.value);
3265
+ })
3266
+ }), right ? /* @__PURE__ */ jsx("div", {
3267
+ className: "flex items-center gap-2",
3268
+ children: right
3269
+ }) : null]
3270
+ });
3271
+ }
3272
+ function Cell({ align, children }) {
3273
+ return /* @__PURE__ */ jsx("div", {
3274
+ className: cn("flex min-w-0 items-center gap-2", align === "r" && "justify-end text-right", align === "c" && "justify-center"),
3275
+ children
3276
+ });
3277
+ }
3278
+ function cells(children, align) {
3279
+ return Children.toArray(children).map((child, index) => /* @__PURE__ */ jsx(Cell, {
3280
+ align: align?.[index] ?? "l",
3281
+ children: child
3282
+ }, index));
3283
+ }
3284
+ function THead({ cols, align, labels }) {
3285
+ return /* @__PURE__ */ jsx("div", {
3286
+ className: "grid h-8 items-center gap-x-4 border-t border-rule bg-soft px-4 text-[12.5px] text-muted",
3287
+ style: { gridTemplateColumns: cols },
3288
+ children: labels.map((label, index) => /* @__PURE__ */ jsx(Cell, {
3289
+ align: align?.[index] ?? "l",
3290
+ children: /* @__PURE__ */ jsx("span", {
3291
+ className: "whitespace-nowrap",
3292
+ children: label
3293
+ })
3294
+ }, index))
3295
+ });
3296
+ }
3297
+ var rowClass = (first, interactive, className) => cn("grid items-center gap-x-4 border-rule", !first && "border-t", interactive && "cursor-pointer transition-colors hover:bg-soft", className);
3298
+ function Row({ cols, align, minH = 44, pad = "10px 16px", children, first, className, onClick, title }) {
3299
+ return /* @__PURE__ */ jsx("div", {
3300
+ className: rowClass(first, !!onClick, className),
3301
+ style: {
3302
+ gridTemplateColumns: cols,
3303
+ minHeight: minH,
3304
+ padding: pad
3305
+ },
3306
+ onClick,
3307
+ title,
3308
+ children: cells(children, align)
3309
+ });
3310
+ }
3311
+ /** A table row that navigates somewhere in the app. */
3312
+ function RowLink({ to, cols, align, minH = 44, pad = "10px 16px", first, className, children }) {
3313
+ return /* @__PURE__ */ jsx(Link, {
3314
+ href: to,
3315
+ className: cn(rowClass(first, true, className), "text-inherit no-underline"),
3316
+ style: {
3317
+ gridTemplateColumns: cols,
3318
+ minHeight: minH,
3319
+ padding: pad
3320
+ },
3321
+ children: cells(children, align)
3322
+ });
3323
+ }
3324
+ function KV({ label, children, labelWidth = 180, minH = 40, end = false }) {
3325
+ return /* @__PURE__ */ jsxs("div", {
3326
+ className: "grid content-center items-baseline gap-x-4 border-t border-rule px-4 py-2",
3327
+ style: {
3328
+ gridTemplateColumns: `min(${labelWidth}px, 42%) minmax(0, 1fr)`,
3329
+ minHeight: minH
3330
+ },
3331
+ children: [/* @__PURE__ */ jsx("span", {
3332
+ className: "text-[13px] text-muted",
3333
+ children: label
3334
+ }), /* @__PURE__ */ jsx("div", {
3335
+ className: cn("flex min-w-0 flex-wrap items-center gap-2", end && "justify-end"),
3336
+ children
3337
+ })]
3338
+ });
3339
+ }
3340
+ function PanelBody({ children, pad = "16px", gap = 16, top = true, className }) {
3341
+ return /* @__PURE__ */ jsx("div", {
3342
+ className: cn("flex flex-col", top && "border-t border-rule", className),
3343
+ style: {
3344
+ padding: pad,
3345
+ gap
3346
+ },
3347
+ children
3348
+ });
3349
+ }
3350
+ function PanelFoot({ left, right }) {
3351
+ return /* @__PURE__ */ jsxs("div", {
3352
+ className: "flex items-center justify-between gap-3 border-t border-rule bg-soft px-4 py-3",
3353
+ children: [/* @__PURE__ */ jsx("div", {
3354
+ className: "flex min-w-0 items-center gap-2 text-[12.5px] text-muted",
3355
+ children: left
3356
+ }), /* @__PURE__ */ jsx("div", {
3357
+ className: "flex items-center gap-2",
3358
+ children: right
3359
+ })]
3360
+ });
3361
+ }
3362
+ /** Bottom summary strip. */
3363
+ function Strip({ children, className }) {
3364
+ return /* @__PURE__ */ jsx("div", {
3365
+ className: cn("flex min-h-[38px] shrink-0 items-center gap-2 border-t border-rule bg-soft px-4 py-2 text-[12.5px] text-muted", className),
3366
+ children
3367
+ });
3368
+ }
3369
+ /** Group header row inside tables. */
3370
+ function GroupRow({ children, sub }) {
3371
+ return /* @__PURE__ */ jsxs("div", {
3372
+ className: "flex h-9 items-center gap-2.5 border-t border-rule bg-soft px-4 text-[13px] font-medium",
3373
+ children: [children, sub ? /* @__PURE__ */ jsx("span", {
3374
+ className: "text-[12.5px] font-normal text-muted",
3375
+ children: sub
3376
+ }) : null]
3377
+ });
3378
+ }
3379
+ //#endregion
3380
+ //#region viewer/src/ui/page.tsx
3381
+ /** Page body under the top bar. */
3382
+ function Content({ children, className, gap = 20, wide = false }) {
3383
+ return /* @__PURE__ */ jsx("main", {
3384
+ className: cn("mx-auto flex w-full min-h-0 grow flex-col px-8 pt-6 pb-7 animate-[q-fade-in_160ms_ease-out] max-md:px-4 max-md:pt-4", wide ? "max-w-[1440px]" : "max-w-[1200px]", className),
3385
+ style: { gap },
3386
+ children
3387
+ });
3388
+ }
3389
+ function PageHead({ title, sub, right, badge, crumbs }) {
3390
+ return /* @__PURE__ */ jsxs("div", {
3391
+ className: "flex shrink-0 items-end justify-between gap-6 max-md:flex-col max-md:items-stretch max-md:gap-3",
3392
+ children: [/* @__PURE__ */ jsxs("div", {
3393
+ className: "flex min-w-0 flex-col gap-1",
3394
+ children: [
3395
+ crumbs ? /* @__PURE__ */ jsx("div", {
3396
+ className: "flex items-center gap-1.5 text-[13px] text-muted",
3397
+ children: crumbs
3398
+ }) : null,
3399
+ /* @__PURE__ */ jsxs("div", {
3400
+ className: "flex items-center gap-3",
3401
+ children: [/* @__PURE__ */ jsx("h1", {
3402
+ className: "m-0 text-[22px] font-semibold tracking-[-0.01em]",
3403
+ children: title
3404
+ }), badge]
3405
+ }),
3406
+ sub ? /* @__PURE__ */ jsx("div", {
3407
+ className: "text-[13px] [overflow-wrap:anywhere] text-muted",
3408
+ children: sub
3409
+ }) : null
3410
+ ]
3411
+ }), right ? /* @__PURE__ */ jsx("div", {
3412
+ className: "flex flex-wrap items-center gap-2",
3413
+ children: right
3414
+ }) : null]
3415
+ });
3416
+ }
3417
+ function SubNav({ items, label }) {
3418
+ const [location] = useLocation();
3419
+ return /* @__PURE__ */ jsx("nav", {
3420
+ "aria-label": label,
3421
+ className: "flex flex-col gap-0.5",
3422
+ children: items.map((item) => {
3423
+ const active = location === item.to;
3424
+ return /* @__PURE__ */ jsxs(Link, {
3425
+ href: item.to,
3426
+ "aria-current": active ? "page" : void 0,
3427
+ className: cn("relative flex h-8 items-center gap-2 whitespace-nowrap rounded-[6px] px-3 text-[13.5px] no-underline transition-colors focus-visible:outline-offset-[-2px]", active ? "font-medium text-ink" : "text-ink2 hover:bg-soft2"),
3428
+ children: [
3429
+ active ? /* @__PURE__ */ jsx(motion.span, {
3430
+ layoutId: "sub-nav-active",
3431
+ "aria-hidden": true,
3432
+ className: "absolute inset-0 z-0 rounded-[6px] bg-soft2",
3433
+ transition: {
3434
+ type: "spring",
3435
+ stiffness: 520,
3436
+ damping: 42
3437
+ }
3438
+ }) : null,
3439
+ /* @__PURE__ */ jsx("span", {
3440
+ className: "relative z-[1] min-w-0 truncate",
3441
+ children: item.label
3442
+ }),
3443
+ item.hint ? /* @__PURE__ */ jsx("span", {
3444
+ className: "relative z-[1] ml-auto text-[12px] font-normal text-muted",
3445
+ children: item.hint
3446
+ }) : null
3447
+ ]
3448
+ }, item.to);
3449
+ })
3450
+ });
3451
+ }
3452
+ function NavLabel({ children }) {
3453
+ return /* @__PURE__ */ jsx("div", {
3454
+ className: "px-3 pt-4 pb-1 text-[12px] text-muted",
3455
+ children
3456
+ });
3457
+ }
3458
+ /** Standalone message card: not found, empty, failed to load. */
3459
+ function NoticeCard({ title, text, action }) {
3460
+ return /* @__PURE__ */ jsxs(Panel, {
3461
+ className: "flex w-[480px] max-w-full flex-col items-center gap-2 px-6 py-8 text-center",
3462
+ children: [
3463
+ /* @__PURE__ */ jsx("h1", {
3464
+ className: "m-0 text-[16px] font-semibold",
3465
+ children: title
3466
+ }),
3467
+ text ? /* @__PURE__ */ jsx("div", {
3468
+ className: "m-0 max-w-[400px] text-[14px] text-ink2",
3469
+ children: text
3470
+ }) : null,
3471
+ action ? /* @__PURE__ */ jsx("div", {
3472
+ className: "mt-3 flex flex-wrap justify-center gap-2",
3473
+ children: action
3474
+ }) : null
3475
+ ]
3476
+ });
3477
+ }
3478
+ /** NoticeCard centred in the free space. */
3479
+ function Notice(props) {
3480
+ return /* @__PURE__ */ jsx("div", {
3481
+ className: "flex min-h-[50vh] grow items-center justify-center p-6 max-md:px-4",
3482
+ children: /* @__PURE__ */ jsx(NoticeCard, { ...props })
3483
+ });
3484
+ }
3485
+ //#endregion
3486
+ //#region viewer/src/components/error-boundary.tsx
3487
+ function Failed({ error }) {
3488
+ const scoped = !!useViewer().scope;
3489
+ return /* @__PURE__ */ jsx(Notice, {
3490
+ title: "This page failed to show",
3491
+ text: /* @__PURE__ */ jsx("span", {
3492
+ className: "font-mono text-[12.5px] break-words",
3493
+ children: error.message.slice(0, 500)
3494
+ }),
3495
+ action: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
3496
+ onClick: () => window.location.reload(),
3497
+ children: "Reload"
3498
+ }), scoped ? null : /* @__PURE__ */ jsx(ButtonLink, {
3499
+ to: "/",
3500
+ children: "All canvases"
3501
+ })] })
3502
+ });
3503
+ }
3504
+ var Boundary = class extends Component {
3505
+ state = { error: null };
3506
+ static getDerivedStateFromError(error) {
3507
+ return { error: error instanceof Error ? error : new Error(String(error)) };
3508
+ }
3509
+ componentDidCatch(error, info) {
3510
+ console.error("[design] a page failed to render", error, info.componentStack);
3511
+ }
3512
+ componentDidUpdate(previous) {
3513
+ if (this.state.error && previous.at !== this.props.at) this.setState({ error: null });
3514
+ }
3515
+ render() {
3516
+ return this.state.error ? /* @__PURE__ */ jsx(Failed, { error: this.state.error }) : this.props.children;
3517
+ }
3518
+ };
3519
+ /** One page that throws shows what went wrong in its place, instead of blanking the whole viewer. */
3520
+ function PageBoundary({ children }) {
3521
+ const [location] = useLocation();
3522
+ return /* @__PURE__ */ jsx(Boundary, {
3523
+ at: location,
3524
+ children
3525
+ });
3526
+ }
3117
3527
  /** The shortcut modifier as the user's keyboard labels it. */
3118
3528
  var MOD_KEY = /Mac|iPhone|iPad/.test(navigator.platform) ? "⌘" : "Ctrl";
3119
3529
  //#endregion
@@ -3142,7 +3552,7 @@ var oppositeSideMap = {
3142
3552
  bottom: "top",
3143
3553
  top: "bottom"
3144
3554
  };
3145
- function clamp(start, value, end) {
3555
+ function clamp$1(start, value, end) {
3146
3556
  return max(start, min(value, end));
3147
3557
  }
3148
3558
  function evaluate(value, param) {
@@ -3442,7 +3852,7 @@ var arrow$3 = (options) => ({
3442
3852
  const maxPadding = min(paddingObject[maxProp], largestPossiblePadding);
3443
3853
  const max = clientSize - arrowDimensions[length] - maxPadding;
3444
3854
  const center = clientSize / 2 - arrowDimensions[length] / 2 + centerToReference;
3445
- const offset = clamp(minPadding, center, max);
3855
+ const offset = clamp$1(minPadding, center, max);
3446
3856
  const shouldAddOffset = !middlewareData.arrow && getAlignment(placement) != null && center !== offset && rects.reference[length] / 2 - (center < minPadding ? minPadding : maxPadding) - arrowDimensions[length] / 2 < 0;
3447
3857
  const alignmentOffset = shouldAddOffset ? center < minPadding ? center - minPadding : center - max : 0;
3448
3858
  return {
@@ -3661,7 +4071,7 @@ var shift$2 = function(options) {
3661
4071
  const mainAxis = getOppositeAxis(crossAxis);
3662
4072
  let mainAxisCoord = coords[mainAxis];
3663
4073
  let crossAxisCoord = coords[crossAxis];
3664
- const clampCoord = (axis, coord) => clamp(coord + overflow[axis === "y" ? "top" : "left"], coord, coord - overflow[axis === "y" ? "bottom" : "right"]);
4074
+ const clampCoord = (axis, coord) => clamp$1(coord + overflow[axis === "y" ? "top" : "left"], coord, coord - overflow[axis === "y" ? "bottom" : "right"]);
3665
4075
  if (checkMainAxis) mainAxisCoord = clampCoord(mainAxis, mainAxisCoord);
3666
4076
  if (checkCrossAxis) crossAxisCoord = clampCoord(crossAxis, crossAxisCoord);
3667
4077
  const limitedCoords = limiter.fn({
@@ -5607,75 +6017,6 @@ function SearchButton({ width = 240, label = "Search and jump", compact }) {
5607
6017
  });
5608
6018
  }
5609
6019
  //#endregion
5610
- //#region viewer/src/ui/button.tsx
5611
- var KIND = {
5612
- primary: "border border-primary bg-primary text-primary-ink hover:opacity-90",
5613
- secondary: "border border-rule-strong bg-surface text-ink hover:bg-soft",
5614
- ghost: "border border-transparent bg-transparent text-ink2 hover:bg-soft2",
5615
- dashed: "border border-dashed border-rule-strong bg-transparent text-ink2 hover:bg-soft",
5616
- danger: "border border-danger bg-danger text-on-danger hover:opacity-90"
5617
- };
5618
- var buttonClass = (kind = "secondary", extra) => cn("inline-flex h-ctrl shrink-0 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-[6px] px-ctrl text-ctrl font-medium leading-none no-underline transition-[background-color,opacity] duration-100", "disabled:cursor-not-allowed disabled:opacity-45 aria-disabled:cursor-not-allowed aria-disabled:opacity-45", KIND[kind], extra);
5619
- var Button = forwardRef(function Button({ kind = "secondary", icon, loading, disabled, className, children, type = "button", ...rest }, ref) {
5620
- return /* @__PURE__ */ jsxs("button", {
5621
- ref,
5622
- type,
5623
- disabled: disabled || loading,
5624
- "aria-busy": loading || void 0,
5625
- className: buttonClass(kind, className),
5626
- ...rest,
5627
- children: [loading ? /* @__PURE__ */ jsx(Icon, {
5628
- name: "loader",
5629
- size: 15,
5630
- className: "animate-spin"
5631
- }) : icon ? /* @__PURE__ */ jsx(Icon, {
5632
- name: icon,
5633
- size: 15
5634
- }) : null, children]
5635
- });
5636
- });
5637
- var ICON_KIND = {
5638
- ghost: "border border-transparent bg-transparent text-muted hover:bg-soft2 hover:text-ink2",
5639
- secondary: "border border-rule-strong bg-surface text-ink2 hover:bg-soft"
5640
- };
5641
- var iconButtonClass = (kind = "ghost", extra) => cn("inline-flex h-ctrl w-ctrl shrink-0 cursor-pointer items-center justify-center rounded-[6px] p-0 transition-colors duration-100 disabled:cursor-not-allowed disabled:opacity-45", ICON_KIND[kind], extra);
5642
- var IconButton = forwardRef(function IconButton({ icon, label, kind = "ghost", size = 16, className, type = "button", ...rest }, ref) {
5643
- return /* @__PURE__ */ jsx("button", {
5644
- ref,
5645
- type,
5646
- "aria-label": label,
5647
- title: label,
5648
- className: iconButtonClass(kind, className),
5649
- ...rest,
5650
- children: /* @__PURE__ */ jsx(Icon, {
5651
- name: icon,
5652
- size
5653
- })
5654
- });
5655
- });
5656
- /** An in-app link styled as a button. */
5657
- function ButtonLink({ to, kind = "secondary", icon, className, children }) {
5658
- return /* @__PURE__ */ jsxs(Link, {
5659
- href: to,
5660
- className: buttonClass(kind, className),
5661
- children: [icon ? /* @__PURE__ */ jsx(Icon, {
5662
- name: icon,
5663
- size: 15
5664
- }) : null, children]
5665
- });
5666
- }
5667
- /** A link out of the app (a frame in a new tab), styled as a button. */
5668
- function ButtonAnchor({ kind = "secondary", icon, className, children, ...rest }) {
5669
- return /* @__PURE__ */ jsxs("a", {
5670
- className: buttonClass(kind, className),
5671
- ...rest,
5672
- children: [icon ? /* @__PURE__ */ jsx(Icon, {
5673
- name: icon,
5674
- size: 15
5675
- }) : null, children]
5676
- });
5677
- }
5678
- //#endregion
5679
6020
  //#region ../../node_modules/.pnpm/@radix-ui+react-collection@1.1.15_@types+react-dom@19.3.0_@types+react@19.3.0__@types+r_ed97e98f526881a1c9b799d335ea2adc/node_modules/@radix-ui/react-collection/dist/index.mjs
5680
6021
  var __defProp$6 = Object.defineProperty;
5681
6022
  var __name$6 = (target, value) => __defProp$6(target, "name", {
@@ -9521,14 +9862,44 @@ var SAFE_SCHEMES = /* @__PURE__ */ new Set([
9521
9862
  "https:",
9522
9863
  "mailto:"
9523
9864
  ]);
9524
- /** http, https, mailto or relative. Browsers drop whitespace and control characters inside a scheme, so do we. */
9865
+ /**
9866
+ * http, https, mailto or relative, read as the browser reads a URL (it drops whitespace and
9867
+ * control characters inside a scheme, and so does URL parsing).
9868
+ */
9525
9869
  function isSafeHref(href) {
9526
- const bare = href.replace(/[\u0000- \u007f]/g, "");
9527
- const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(bare)?.[1];
9528
- return !scheme || SAFE_SCHEMES.has(`${scheme.toLowerCase()}:`);
9870
+ try {
9871
+ return SAFE_SCHEMES.has(new URL(href, "http://relative.invalid/").protocol);
9872
+ } catch {
9873
+ return false;
9874
+ }
9529
9875
  }
9530
9876
  function escapeHtml(text) {
9531
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
9877
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
9878
+ }
9879
+ var NAMED = {
9880
+ amp: "&",
9881
+ lt: "<",
9882
+ gt: ">",
9883
+ quot: "\"",
9884
+ apos: "'",
9885
+ colon: ":",
9886
+ sol: "/",
9887
+ period: ".",
9888
+ num: "#",
9889
+ quest: "?",
9890
+ equals: "=",
9891
+ percnt: "%",
9892
+ tab: " ",
9893
+ newline: "\n",
9894
+ nbsp: "\xA0"
9895
+ };
9896
+ /** Character references in a link or image destination, which Markdown reads as the characters they name. */
9897
+ function decodeEntities(text) {
9898
+ return text.replace(/&(?:#(\d{1,7})|#x([0-9a-f]{1,6})|([a-z]{2,8}));/gi, (whole, dec, hex, name) => {
9899
+ if (name) return NAMED[name.toLowerCase()] ?? whole;
9900
+ const code = dec ? Number(dec) : Number.parseInt(hex, 16);
9901
+ return code > 0 && code <= 1114111 && (code < 55296 || code > 57343) ? String.fromCodePoint(code) : "�";
9902
+ });
9532
9903
  }
9533
9904
  var markdown = new F({
9534
9905
  gfm: true,
@@ -9536,14 +9907,22 @@ var markdown = new F({
9536
9907
  }, { renderer: {
9537
9908
  html: ({ text }) => escapeHtml(text),
9538
9909
  link(token) {
9539
- return isSafeHref(token.href) ? false : this.parser.parseInline(token.tokens);
9910
+ const text = this.parser.parseInline(token.tokens);
9911
+ const href = decodeEntities(token.href);
9912
+ if (!isSafeHref(href)) return text;
9913
+ const title = token.title ? ` title="${escapeHtml(decodeEntities(token.title))}"` : "";
9914
+ return `<a target="_blank" rel="noreferrer" href="${escapeHtml(href)}"${title}>${text}</a>`;
9540
9915
  },
9541
9916
  image(token) {
9542
- return isSafeHref(token.href) ? false : escapeHtml(token.text);
9917
+ const alt = escapeHtml(decodeEntities(token.text));
9918
+ const src = decodeEntities(token.href);
9919
+ if (!isSafeHref(src)) return alt;
9920
+ const title = token.title ? ` title="${escapeHtml(decodeEntities(token.title))}"` : "";
9921
+ return `<img src="${escapeHtml(src)}" alt="${alt}"${title}>`;
9543
9922
  }
9544
9923
  } });
9545
9924
  function renderMarkdown(text) {
9546
- return markdown.parse(text, { async: false }).replace(/<a href=/g, "<a target=\"_blank\" rel=\"noreferrer\" href=");
9925
+ return markdown.parse(text, { async: false });
9547
9926
  }
9548
9927
  /** Markdown from the project's own files (guidelines, notes, descriptions). */
9549
9928
  function Markdown({ text, className }) {
@@ -9554,13 +9933,155 @@ function Markdown({ text, className }) {
9554
9933
  });
9555
9934
  }
9556
9935
  //#endregion
9936
+ //#region viewer/src/lib/messages.ts
9937
+ /**
9938
+ * Frames run code anyone who can push wrote, and any of them may post the viewer anything: a
9939
+ * message only reaches the viewer's handlers with the fields its type has, of their types.
9940
+ * Strings are cut and sizes kept within reason, so a frame cannot swell the viewer's state.
9941
+ */
9942
+ var MAX_FRAME_SIZE = 1e5;
9943
+ var MAX_TEXT = 2e3;
9944
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9945
+ var isString = (value) => typeof value === "string";
9946
+ var isNumber = (value) => typeof value === "number" && Number.isFinite(value);
9947
+ var isBoolean = (value) => typeof value === "boolean";
9948
+ var optional = (check) => (value) => value === void 0 || check(value);
9949
+ var listOf = (check) => (value) => Array.isArray(value) && value.every(check);
9950
+ var shape = (fields) => (value) => isObject(value) && Object.entries(fields).every(([key, check]) => check(value[key]));
9951
+ var isPair = (value) => Array.isArray(value) && value.length === 2 && isString(value[0]) && isString(value[1]);
9952
+ var isBox = (value) => Array.isArray(value) && value.length === 4 && value.every(isNumber);
9953
+ var isCrumb = shape({
9954
+ ref: isNumber,
9955
+ label: isString
9956
+ });
9957
+ var isColor = shape({
9958
+ value: isString,
9959
+ css: isString,
9960
+ token: optional(isString)
9961
+ });
9962
+ var isTokenValue = shape({
9963
+ value: isString,
9964
+ token: optional(isString)
9965
+ });
9966
+ var isElementInfo = shape({
9967
+ ref: isNumber,
9968
+ tag: isString,
9969
+ label: isString,
9970
+ text: optional(isString),
9971
+ classes: listOf(isString),
9972
+ attributes: listOf(isPair),
9973
+ component: optional(shape({
9974
+ owners: listOf(isString),
9975
+ file: optional(isString)
9976
+ })),
9977
+ rect: shape({
9978
+ x: isNumber,
9979
+ y: isNumber,
9980
+ w: isNumber,
9981
+ h: isNumber
9982
+ }),
9983
+ margin: isBox,
9984
+ border: isBox,
9985
+ padding: isBox,
9986
+ layout: listOf(isPair),
9987
+ typography: optional(shape({
9988
+ family: isString,
9989
+ familyToken: optional(isString),
9990
+ size: isString,
9991
+ sizeToken: optional(isString),
9992
+ weight: isString,
9993
+ lineHeight: isString,
9994
+ letterSpacing: isString,
9995
+ color: isColor,
9996
+ align: isString,
9997
+ transform: optional(isString)
9998
+ })),
9999
+ appearance: shape({
10000
+ background: optional(isColor),
10001
+ backgroundImage: optional(isString),
10002
+ border: optional(shape({
10003
+ width: isString,
10004
+ style: isString,
10005
+ color: isColor
10006
+ })),
10007
+ radius: optional(isTokenValue),
10008
+ shadow: optional(isTokenValue),
10009
+ opacity: optional(isString)
10010
+ }),
10011
+ path: listOf(isCrumb),
10012
+ children: listOf(isCrumb),
10013
+ css: isString
10014
+ });
10015
+ /** What each message type carries besides `source`, `key` and `type`. */
10016
+ var FIELDS = {
10017
+ ready: {},
10018
+ blur: {},
10019
+ updated: {},
10020
+ "inspect-escape": {},
10021
+ size: {
10022
+ width: isNumber,
10023
+ height: isNumber
10024
+ },
10025
+ error: { message: isString },
10026
+ go: { target: isString },
10027
+ link: {
10028
+ href: isString,
10029
+ path: (value) => value === null || isString(value),
10030
+ form: isBoolean,
10031
+ raw: optional(isString),
10032
+ pushed: optional(isBoolean)
10033
+ },
10034
+ wheel: {
10035
+ deltaX: isNumber,
10036
+ deltaY: isNumber,
10037
+ x: isNumber,
10038
+ y: isNumber,
10039
+ zoom: isBoolean
10040
+ },
10041
+ keydown: { code: isString },
10042
+ pointer: {
10043
+ x: isNumber,
10044
+ y: isNumber,
10045
+ down: isBoolean
10046
+ },
10047
+ key: {
10048
+ name: isString,
10049
+ down: isBoolean
10050
+ },
10051
+ inspect: { info: (value) => value === null || isElementInfo(value) }
10052
+ };
10053
+ var clamp = (value) => Math.min(MAX_FRAME_SIZE, Math.max(0, value));
10054
+ /** `data` as a runtime message, with its sizes and texts bounded; null when it is none. */
10055
+ function readRuntimeMessage(data) {
10056
+ if (!isObject(data) || data.source !== "design-runtime" || !isString(data.key) || !isString(data.type)) return null;
10057
+ const fields = Object.hasOwn(FIELDS, data.type) ? FIELDS[data.type] : null;
10058
+ if (!fields || !shape(fields)(data)) return null;
10059
+ const message = data;
10060
+ if (message.type === "size") return {
10061
+ ...message,
10062
+ width: clamp(message.width),
10063
+ height: clamp(message.height)
10064
+ };
10065
+ if (message.type === "error") return {
10066
+ ...message,
10067
+ message: message.message.slice(0, MAX_TEXT)
10068
+ };
10069
+ return message;
10070
+ }
10071
+ /** A frame's error list with `message` added: once, and no more than `MAX_FRAME_ERRORS` of them. */
10072
+ function withError(list, message) {
10073
+ return list.includes(message) || list.length >= 20 ? list : [...list, message];
10074
+ }
10075
+ //#endregion
9557
10076
  //#region viewer/src/lib/frames.ts
9558
10077
  /** Routes runtime messages to whoever listens to the frame that sent them. */
9559
10078
  var handlers = /* @__PURE__ */ new Map();
9560
10079
  window.addEventListener("message", (event) => {
9561
- const data = event.data;
9562
- if (data?.source !== "design-runtime" || !event.source) return;
9563
- for (const handler of handlers.get(event.source) ?? []) handler(data);
10080
+ const listeners = event.source ? handlers.get(event.source) : void 0;
10081
+ if (!listeners) return;
10082
+ const message = readRuntimeMessage(event.data);
10083
+ if (!message) return;
10084
+ for (const handler of listeners) handler(message);
9564
10085
  });
9565
10086
  function listenToFrame(frame, handler) {
9566
10087
  let win = null;
@@ -9637,7 +10158,8 @@ function reloadFrame(frame) {
9637
10158
  * process, so frames served from the viewer's twin host (localhost ↔ 127.0.0.1,
9638
10159
  * both reach the same server) run their scripts and animations off the viewer's
9639
10160
  * thread: a canvas full of live screens still pans smoothly. Any other host,
9640
- * and static builds, keep frames on the viewer's origin.
10161
+ * and static builds, keep frames on the viewer's origin. The dev server keeps
10162
+ * its viewer on localhost and gives 127.0.0.1 to frames (server/hosts.ts).
9641
10163
  */
9642
10164
  var FRAME_ORIGIN = (() => {
9643
10165
  if (STATIC_SITE) return "";
@@ -9664,7 +10186,7 @@ function absoluteUrl(url) {
9664
10186
  * link to another site opens in a new tab. A push nobody's route matches belongs to the
9665
10187
  * screen's own router; a link or form that leads nowhere stays put with a note.
9666
10188
  */
9667
- function followLink(message, canvas, go, from) {
10189
+ function followLink(message, canvas, go, from, missing) {
9668
10190
  if (message.form) {
9669
10191
  toast("Forms are not sent from a screen", void 0, "info");
9670
10192
  return;
@@ -9677,7 +10199,7 @@ function followLink(message, canvas, go, from) {
9677
10199
  if (!window.open(message.href, "_blank", "noopener,noreferrer")) toast("Link to another site", message.href, "info");
9678
10200
  return;
9679
10201
  }
9680
- const frames = canvas.pages.flatMap((page) => page.sections.flatMap((section) => section.items.filter((item) => item.kind === "screen" || item.kind === "url")));
10202
+ const frames = Array.isArray(canvas) ? canvas : canvas.pages.flatMap((page) => page.sections.flatMap((section) => section.items.filter((item) => item.kind === "screen" || item.kind === "url")));
9681
10203
  const here = frames.find((item) => item.id === from);
9682
10204
  const path = message.raw === void 0 ? message.path : linkPath(message.raw, here?.routes?.[0]);
9683
10205
  if (path === null) return;
@@ -9695,7 +10217,9 @@ function followLink(message, canvas, go, from) {
9695
10217
  name = decodeURIComponent(name);
9696
10218
  } catch {}
9697
10219
  if (name && name !== from && frames.some((item) => item.id === name)) go(name);
9698
- else if (!message.pushed) toast("No screen for this link", path, "info");
10220
+ else if (message.pushed) return;
10221
+ else if (missing) missing(path);
10222
+ else toast("No screen for this link", path, "info");
9699
10223
  }
9700
10224
  //#endregion
9701
10225
  //#region viewer/src/components/inspector.tsx
@@ -9712,11 +10236,13 @@ function useInspection(frames, enabled, options) {
9712
10236
  const [selected, setSelected] = useState(null);
9713
10237
  const state = useRef({
9714
10238
  enabled,
9715
- tokens
10239
+ tokens,
10240
+ picked: null
9716
10241
  });
9717
10242
  state.current = {
9718
10243
  enabled,
9719
- tokens
10244
+ tokens,
10245
+ picked: selected?.frame ?? null
9720
10246
  };
9721
10247
  const onEscape = useRef(options.onEscape);
9722
10248
  onEscape.current = options.onEscape;
@@ -9751,7 +10277,7 @@ function useInspection(frames, enabled, options) {
9751
10277
  if (state.current.enabled) tell(frame, true);
9752
10278
  else told.current.set(frame, { on: false });
9753
10279
  } else if (message.type === "inspect") {
9754
- if (message.info) {
10280
+ if (message.info && (state.current.enabled || state.current.picked === frame)) {
9755
10281
  for (const other of framesRef.current) if (other !== frame) sendToFrame(other, {
9756
10282
  type: "inspect-select",
9757
10283
  ref: null
@@ -10220,233 +10746,42 @@ function InspectorPanel({ info, onSelect, onClose, active }) {
10220
10746
  children: info.attributes.map(([name, value]) => /* @__PURE__ */ jsx(Line, {
10221
10747
  label: name,
10222
10748
  children: /* @__PURE__ */ jsx(Value$1, {
10223
- title: value,
10224
- children: value
10225
- })
10226
- }, name))
10227
- }) : null,
10228
- info.children.length ? /* @__PURE__ */ jsx(Section, {
10229
- title: `Children · ${info.children.length}`,
10230
- children: /* @__PURE__ */ jsx("div", {
10231
- className: "flex flex-col px-2 pb-1",
10232
- children: info.children.map((child, index) => /* @__PURE__ */ jsxs("button", {
10233
- type: "button",
10234
- onClick: () => onSelect(child.ref),
10235
- className: "flex h-7 cursor-pointer items-center gap-2 rounded-[6px] border-0 bg-transparent px-2 text-left font-mono text-[12px] text-ink2 hover:bg-soft2",
10236
- children: [/* @__PURE__ */ jsx(Icon, {
10237
- name: "chevron-right",
10238
- size: 12,
10239
- className: "text-muted"
10240
- }), /* @__PURE__ */ jsx("span", {
10241
- className: "truncate",
10242
- children: child.label
10243
- })]
10244
- }, index))
10245
- })
10246
- }) : null
10247
- ]
10248
- }),
10249
- info ? /* @__PURE__ */ jsx("div", {
10250
- className: "flex shrink-0 items-center gap-2 border-t border-rule bg-soft px-4 py-2.5",
10251
- children: /* @__PURE__ */ jsx(Button, {
10252
- icon: "copy",
10253
- onClick: () => void copyText(info.css, "CSS copied"),
10254
- className: "grow",
10255
- children: "Copy CSS"
10256
- })
10257
- }) : null
10258
- ]
10259
- });
10260
- }
10261
- //#endregion
10262
- //#region viewer/src/ui/badge.tsx
10263
- var BADGE = {
10264
- neutral: "bg-soft2 text-ink2",
10265
- action: "bg-action-soft text-action",
10266
- ok: "bg-ok-soft text-ok-text",
10267
- danger: "bg-danger-soft text-danger-text"
10268
- };
10269
- function Badge({ tone = "neutral", children, className }) {
10270
- return /* @__PURE__ */ jsx("span", {
10271
- className: cn("inline-flex h-[22px] shrink-0 items-center gap-1 whitespace-nowrap rounded-[6px] px-2 text-[12px] font-medium", BADGE[tone], className),
10272
- children
10273
- });
10274
- }
10275
- function Count({ value, tone = "neutral" }) {
10276
- return /* @__PURE__ */ jsx("span", {
10277
- className: cn("inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-[4px] px-[5px] text-[11.5px] font-semibold tabular", tone === "action" ? "bg-action-soft text-action" : tone === "danger" ? "bg-danger-soft text-danger-text" : "bg-soft2 text-ink2"),
10278
- children: value
10279
- });
10280
- }
10281
- //#endregion
10282
- //#region viewer/src/ui/panel.tsx
10283
- function Panel({ children, className, style }) {
10284
- return /* @__PURE__ */ jsx("section", {
10285
- className: cn("min-w-0 shrink-0 overflow-hidden rounded-[10px] border border-rule bg-surface", className),
10286
- style,
10287
- children
10288
- });
10289
- }
10290
- function PanelHead({ title, count, sub, right, as: Heading = "h2" }) {
10291
- return /* @__PURE__ */ jsxs("div", {
10292
- className: "flex min-h-[46px] items-center justify-between gap-3 px-4",
10293
- children: [/* @__PURE__ */ jsxs(Heading, {
10294
- className: "m-0 flex min-w-0 items-center gap-2 whitespace-nowrap text-[14px] font-semibold",
10295
- children: [
10296
- title,
10297
- count !== void 0 ? /* @__PURE__ */ jsx(Count, { value: count }) : null,
10298
- sub ? /* @__PURE__ */ jsx("span", {
10299
- className: "truncate text-[12.5px] font-normal text-muted",
10300
- children: sub
10301
- }) : null
10302
- ]
10303
- }), right ? /* @__PURE__ */ jsx("div", {
10304
- className: "flex items-center gap-2",
10305
- children: right
10306
- }) : null]
10307
- });
10308
- }
10309
- function PanelTabs({ items, value, onChange, right }) {
10310
- const indicator = useId();
10311
- return /* @__PURE__ */ jsxs("div", {
10312
- className: "flex h-[46px] items-center justify-between gap-3 px-4",
10313
- children: [/* @__PURE__ */ jsx("div", {
10314
- role: "tablist",
10315
- className: "flex gap-5",
10316
- children: items.map((item) => {
10317
- const on = item.value === value;
10318
- return /* @__PURE__ */ jsxs("button", {
10319
- type: "button",
10320
- role: "tab",
10321
- "aria-selected": on,
10322
- onClick: () => onChange(item.value),
10323
- className: cn("relative flex h-[46px] cursor-pointer items-center gap-1.5 border-0 bg-transparent px-0.5 text-[14px] transition-colors", on ? "font-semibold text-ink" : "font-medium text-ink2 hover:text-ink"),
10324
- children: [
10325
- item.label,
10326
- item.count !== void 0 ? /* @__PURE__ */ jsx(Count, { value: item.count }) : null,
10327
- on ? /* @__PURE__ */ jsx(motion.span, {
10328
- layoutId: indicator,
10329
- className: "absolute inset-x-0 bottom-0 h-[2px] bg-ink",
10330
- transition: {
10331
- type: "spring",
10332
- stiffness: 520,
10333
- damping: 42
10334
- }
10335
- }) : null
10336
- ]
10337
- }, item.value);
10338
- })
10339
- }), right ? /* @__PURE__ */ jsx("div", {
10340
- className: "flex items-center gap-2",
10341
- children: right
10342
- }) : null]
10343
- });
10344
- }
10345
- function Cell({ align, children }) {
10346
- return /* @__PURE__ */ jsx("div", {
10347
- className: cn("flex min-w-0 items-center gap-2", align === "r" && "justify-end text-right", align === "c" && "justify-center"),
10348
- children
10349
- });
10350
- }
10351
- function cells(children, align) {
10352
- return Children.toArray(children).map((child, index) => /* @__PURE__ */ jsx(Cell, {
10353
- align: align?.[index] ?? "l",
10354
- children: child
10355
- }, index));
10356
- }
10357
- function THead({ cols, align, labels }) {
10358
- return /* @__PURE__ */ jsx("div", {
10359
- className: "grid h-8 items-center gap-x-4 border-t border-rule bg-soft px-4 text-[12.5px] text-muted",
10360
- style: { gridTemplateColumns: cols },
10361
- children: labels.map((label, index) => /* @__PURE__ */ jsx(Cell, {
10362
- align: align?.[index] ?? "l",
10363
- children: /* @__PURE__ */ jsx("span", {
10364
- className: "whitespace-nowrap",
10365
- children: label
10366
- })
10367
- }, index))
10368
- });
10369
- }
10370
- var rowClass = (first, interactive, className) => cn("grid items-center gap-x-4 border-rule", !first && "border-t", interactive && "cursor-pointer transition-colors hover:bg-soft", className);
10371
- function Row({ cols, align, minH = 44, pad = "10px 16px", children, first, className, onClick, title }) {
10372
- return /* @__PURE__ */ jsx("div", {
10373
- className: rowClass(first, !!onClick, className),
10374
- style: {
10375
- gridTemplateColumns: cols,
10376
- minHeight: minH,
10377
- padding: pad
10378
- },
10379
- onClick,
10380
- title,
10381
- children: cells(children, align)
10382
- });
10383
- }
10384
- /** A table row that navigates somewhere in the app. */
10385
- function RowLink({ to, cols, align, minH = 44, pad = "10px 16px", first, className, children }) {
10386
- return /* @__PURE__ */ jsx(Link, {
10387
- href: to,
10388
- className: cn(rowClass(first, true, className), "text-inherit no-underline"),
10389
- style: {
10390
- gridTemplateColumns: cols,
10391
- minHeight: minH,
10392
- padding: pad
10393
- },
10394
- children: cells(children, align)
10395
- });
10396
- }
10397
- function KV({ label, children, labelWidth = 180, minH = 40, end = false }) {
10398
- return /* @__PURE__ */ jsxs("div", {
10399
- className: "grid content-center items-baseline gap-x-4 border-t border-rule px-4 py-2",
10400
- style: {
10401
- gridTemplateColumns: `min(${labelWidth}px, 42%) minmax(0, 1fr)`,
10402
- minHeight: minH
10403
- },
10404
- children: [/* @__PURE__ */ jsx("span", {
10405
- className: "text-[13px] text-muted",
10406
- children: label
10407
- }), /* @__PURE__ */ jsx("div", {
10408
- className: cn("flex min-w-0 flex-wrap items-center gap-2", end && "justify-end"),
10409
- children
10410
- })]
10411
- });
10412
- }
10413
- function PanelBody({ children, pad = "16px", gap = 16, top = true, className }) {
10414
- return /* @__PURE__ */ jsx("div", {
10415
- className: cn("flex flex-col", top && "border-t border-rule", className),
10416
- style: {
10417
- padding: pad,
10418
- gap
10419
- },
10420
- children
10421
- });
10422
- }
10423
- function PanelFoot({ left, right }) {
10424
- return /* @__PURE__ */ jsxs("div", {
10425
- className: "flex items-center justify-between gap-3 border-t border-rule bg-soft px-4 py-3",
10426
- children: [/* @__PURE__ */ jsx("div", {
10427
- className: "flex min-w-0 items-center gap-2 text-[12.5px] text-muted",
10428
- children: left
10429
- }), /* @__PURE__ */ jsx("div", {
10430
- className: "flex items-center gap-2",
10431
- children: right
10432
- })]
10433
- });
10434
- }
10435
- /** Bottom summary strip. */
10436
- function Strip({ children, className }) {
10437
- return /* @__PURE__ */ jsx("div", {
10438
- className: cn("flex min-h-[38px] shrink-0 items-center gap-2 border-t border-rule bg-soft px-4 py-2 text-[12.5px] text-muted", className),
10439
- children
10440
- });
10441
- }
10442
- /** Group header row inside tables. */
10443
- function GroupRow({ children, sub }) {
10444
- return /* @__PURE__ */ jsxs("div", {
10445
- className: "flex h-9 items-center gap-2.5 border-t border-rule bg-soft px-4 text-[13px] font-medium",
10446
- children: [children, sub ? /* @__PURE__ */ jsx("span", {
10447
- className: "text-[12.5px] font-normal text-muted",
10448
- children: sub
10449
- }) : null]
10749
+ title: value,
10750
+ children: value
10751
+ })
10752
+ }, name))
10753
+ }) : null,
10754
+ info.children.length ? /* @__PURE__ */ jsx(Section, {
10755
+ title: `Children · ${info.children.length}`,
10756
+ children: /* @__PURE__ */ jsx("div", {
10757
+ className: "flex flex-col px-2 pb-1",
10758
+ children: info.children.map((child, index) => /* @__PURE__ */ jsxs("button", {
10759
+ type: "button",
10760
+ onClick: () => onSelect(child.ref),
10761
+ className: "flex h-7 cursor-pointer items-center gap-2 rounded-[6px] border-0 bg-transparent px-2 text-left font-mono text-[12px] text-ink2 hover:bg-soft2",
10762
+ children: [/* @__PURE__ */ jsx(Icon, {
10763
+ name: "chevron-right",
10764
+ size: 12,
10765
+ className: "text-muted"
10766
+ }), /* @__PURE__ */ jsx("span", {
10767
+ className: "truncate",
10768
+ children: child.label
10769
+ })]
10770
+ }, index))
10771
+ })
10772
+ }) : null
10773
+ ]
10774
+ }),
10775
+ info ? /* @__PURE__ */ jsx("div", {
10776
+ className: "flex shrink-0 items-center gap-2 border-t border-rule bg-soft px-4 py-2.5",
10777
+ children: /* @__PURE__ */ jsx(Button, {
10778
+ icon: "copy",
10779
+ onClick: () => void copyText(info.css, "CSS copied"),
10780
+ className: "grow",
10781
+ children: "Copy CSS"
10782
+ })
10783
+ }) : null
10784
+ ]
10450
10785
  });
10451
10786
  }
10452
10787
  //#endregion
@@ -10531,112 +10866,6 @@ function SlidePanel({ open, side, width, children }) {
10531
10866
  });
10532
10867
  }
10533
10868
  //#endregion
10534
- //#region viewer/src/ui/page.tsx
10535
- /** Page body under the top bar. */
10536
- function Content({ children, className, gap = 20, wide = false }) {
10537
- return /* @__PURE__ */ jsx("main", {
10538
- className: cn("mx-auto flex w-full min-h-0 grow flex-col px-8 pt-6 pb-7 animate-[q-fade-in_160ms_ease-out] max-md:px-4 max-md:pt-4", wide ? "max-w-[1440px]" : "max-w-[1200px]", className),
10539
- style: { gap },
10540
- children
10541
- });
10542
- }
10543
- function PageHead({ title, sub, right, badge, crumbs }) {
10544
- return /* @__PURE__ */ jsxs("div", {
10545
- className: "flex shrink-0 items-end justify-between gap-6 max-md:flex-col max-md:items-stretch max-md:gap-3",
10546
- children: [/* @__PURE__ */ jsxs("div", {
10547
- className: "flex min-w-0 flex-col gap-1",
10548
- children: [
10549
- crumbs ? /* @__PURE__ */ jsx("div", {
10550
- className: "flex items-center gap-1.5 text-[13px] text-muted",
10551
- children: crumbs
10552
- }) : null,
10553
- /* @__PURE__ */ jsxs("div", {
10554
- className: "flex items-center gap-3",
10555
- children: [/* @__PURE__ */ jsx("h1", {
10556
- className: "m-0 text-[22px] font-semibold tracking-[-0.01em]",
10557
- children: title
10558
- }), badge]
10559
- }),
10560
- sub ? /* @__PURE__ */ jsx("div", {
10561
- className: "text-[13px] [overflow-wrap:anywhere] text-muted",
10562
- children: sub
10563
- }) : null
10564
- ]
10565
- }), right ? /* @__PURE__ */ jsx("div", {
10566
- className: "flex flex-wrap items-center gap-2",
10567
- children: right
10568
- }) : null]
10569
- });
10570
- }
10571
- function SubNav({ items, label }) {
10572
- const [location] = useLocation();
10573
- return /* @__PURE__ */ jsx("nav", {
10574
- "aria-label": label,
10575
- className: "flex flex-col gap-0.5",
10576
- children: items.map((item) => {
10577
- const active = location === item.to;
10578
- return /* @__PURE__ */ jsxs(Link, {
10579
- href: item.to,
10580
- "aria-current": active ? "page" : void 0,
10581
- className: cn("relative flex h-8 items-center gap-2 whitespace-nowrap rounded-[6px] px-3 text-[13.5px] no-underline transition-colors focus-visible:outline-offset-[-2px]", active ? "font-medium text-ink" : "text-ink2 hover:bg-soft2"),
10582
- children: [
10583
- active ? /* @__PURE__ */ jsx(motion.span, {
10584
- layoutId: "sub-nav-active",
10585
- "aria-hidden": true,
10586
- className: "absolute inset-0 z-0 rounded-[6px] bg-soft2",
10587
- transition: {
10588
- type: "spring",
10589
- stiffness: 520,
10590
- damping: 42
10591
- }
10592
- }) : null,
10593
- /* @__PURE__ */ jsx("span", {
10594
- className: "relative z-[1] min-w-0 truncate",
10595
- children: item.label
10596
- }),
10597
- item.hint ? /* @__PURE__ */ jsx("span", {
10598
- className: "relative z-[1] ml-auto text-[12px] font-normal text-muted",
10599
- children: item.hint
10600
- }) : null
10601
- ]
10602
- }, item.to);
10603
- })
10604
- });
10605
- }
10606
- function NavLabel({ children }) {
10607
- return /* @__PURE__ */ jsx("div", {
10608
- className: "px-3 pt-4 pb-1 text-[12px] text-muted",
10609
- children
10610
- });
10611
- }
10612
- /** Standalone message card: not found, empty, failed to load. */
10613
- function NoticeCard({ title, text, action }) {
10614
- return /* @__PURE__ */ jsxs(Panel, {
10615
- className: "flex w-[480px] max-w-full flex-col items-center gap-2 px-6 py-8 text-center",
10616
- children: [
10617
- /* @__PURE__ */ jsx("h1", {
10618
- className: "m-0 text-[16px] font-semibold",
10619
- children: title
10620
- }),
10621
- text ? /* @__PURE__ */ jsx("div", {
10622
- className: "m-0 max-w-[400px] text-[14px] text-ink2",
10623
- children: text
10624
- }) : null,
10625
- action ? /* @__PURE__ */ jsx("div", {
10626
- className: "mt-3 flex flex-wrap justify-center gap-2",
10627
- children: action
10628
- }) : null
10629
- ]
10630
- });
10631
- }
10632
- /** NoticeCard centred in the free space. */
10633
- function Notice(props) {
10634
- return /* @__PURE__ */ jsx("div", {
10635
- className: "flex min-h-[50vh] grow items-center justify-center p-6 max-md:px-4",
10636
- children: /* @__PURE__ */ jsx(NoticeCard, { ...props })
10637
- });
10638
- }
10639
- //#endregion
10640
10869
  //#region viewer/src/pages/not-found.tsx
10641
10870
  function NotFound({ title = "Nothing here", text }) {
10642
10871
  const scoped = !!useViewer().scope;
@@ -10805,6 +11034,174 @@ function Segmented({ items, value, onChange, label, stretch, size = "ctrl" }) {
10805
11034
  });
10806
11035
  }
10807
11036
  //#endregion
11037
+ //#region viewer/src/components/corner-plate.tsx
11038
+ /** How long the plate stays after the screen loads, or after a tap shows it. */
11039
+ var LINGER_MS = 5500;
11040
+ /** A pointer that left the corner hides the plate after this, so passing by does not flicker it. */
11041
+ var LEAVE_MS = 350;
11042
+ /** The top-right quarter of the window, where the plate lives. */
11043
+ var inCorner = (x, y) => x >= window.innerWidth / 2 && y <= window.innerHeight / 2;
11044
+ /**
11045
+ * A plate in the top-right corner over a full-window frame. It shows while the screen loads and for a
11046
+ * few seconds after, then fades out; a pointer in the window's top-right quarter brings it back and
11047
+ * keeps it while it stays there, a tap there shows it for a while, and so does focus inside it.
11048
+ *
11049
+ * The page does not see the pointer over the frame, so the frame's runtime reports it (0.9.11+).
11050
+ * Until a frame does (an older runtime, a URL item), the plate shows whenever the pointer is over the
11051
+ * page, as before.
11052
+ */
11053
+ function CornerPlate({ frame, note, badge, children }) {
11054
+ const [lingering, setLingering] = useState(true);
11055
+ const [inside, setInside] = useState(false);
11056
+ const [focused, setFocused] = useState(false);
11057
+ const [reported, setReported] = useState(false);
11058
+ const [overPage, setOverPage] = useState(false);
11059
+ const [loads, setLoads] = useState(0);
11060
+ const linger = useRef(void 0);
11061
+ const leave = useRef(void 0);
11062
+ const showFor = (ms) => {
11063
+ setLingering(true);
11064
+ clearTimeout(linger.current);
11065
+ linger.current = setTimeout(() => setLingering(false), ms);
11066
+ };
11067
+ const at = (x, y) => {
11068
+ const corner = x >= 0 && inCorner(x, y);
11069
+ clearTimeout(leave.current);
11070
+ if (corner) setInside(true);
11071
+ else leave.current = setTimeout(() => setInside(false), LEAVE_MS);
11072
+ };
11073
+ useEffect(() => {
11074
+ if (!frame) return;
11075
+ const ask = () => {
11076
+ sendToFrame(frame, { type: "pointer" });
11077
+ showFor(LINGER_MS);
11078
+ setLoads((n) => n + 1);
11079
+ };
11080
+ frame.addEventListener("load", ask);
11081
+ const stop = listenToFrame(frame, (message) => {
11082
+ if (message.type !== "pointer") return;
11083
+ setReported(true);
11084
+ if (message.x < 0) return at(-1, -1);
11085
+ const rect = frame.getBoundingClientRect();
11086
+ const x = rect.left + message.x;
11087
+ const y = rect.top + message.y;
11088
+ if (message.down && inCorner(x, y)) showFor(LINGER_MS);
11089
+ else at(x, y);
11090
+ });
11091
+ return () => {
11092
+ frame.removeEventListener("load", ask);
11093
+ stop();
11094
+ };
11095
+ }, [frame]);
11096
+ useEffect(() => {
11097
+ const move = (event) => at(event.clientX, event.clientY);
11098
+ const out = (event) => {
11099
+ if (!event.relatedTarget) at(-1, -1);
11100
+ };
11101
+ window.addEventListener("pointermove", move, { passive: true });
11102
+ document.addEventListener("pointerout", out);
11103
+ return () => {
11104
+ window.removeEventListener("pointermove", move);
11105
+ document.removeEventListener("pointerout", out);
11106
+ clearTimeout(linger.current);
11107
+ clearTimeout(leave.current);
11108
+ };
11109
+ }, []);
11110
+ useEffect(() => {
11111
+ if (reported) return;
11112
+ const root = document.documentElement;
11113
+ const enter = () => setOverPage(true);
11114
+ const exit = () => setOverPage(false);
11115
+ root.addEventListener("pointerenter", enter);
11116
+ root.addEventListener("pointerleave", exit);
11117
+ return () => {
11118
+ root.removeEventListener("pointerenter", enter);
11119
+ root.removeEventListener("pointerleave", exit);
11120
+ };
11121
+ }, [reported]);
11122
+ const shown = lingering || inside || focused || !reported && overPage;
11123
+ return /* @__PURE__ */ jsxs(Fragment, { children: [badge ? /* @__PURE__ */ jsx(motion.button, {
11124
+ type: "button",
11125
+ "aria-label": "Show the page's notice",
11126
+ initial: false,
11127
+ animate: shown ? {
11128
+ opacity: 0,
11129
+ scale: .8
11130
+ } : {
11131
+ opacity: 1,
11132
+ scale: 1
11133
+ },
11134
+ transition: {
11135
+ duration: .2,
11136
+ delay: shown ? 0 : .25
11137
+ },
11138
+ onClick: () => showFor(LINGER_MS),
11139
+ tabIndex: shown ? -1 : 0,
11140
+ className: cn("absolute top-3 right-3 inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-full border border-rule bg-surface/90 px-2.5 text-[11.5px] text-muted shadow-[0_1px_3px_rgb(0_0_0/0.08)] backdrop-blur-sm hover:text-ink2", shown && "pointer-events-none"),
11141
+ children: badge
11142
+ }) : null, /* @__PURE__ */ jsx(motion.div, {
11143
+ initial: {
11144
+ opacity: 0,
11145
+ scale: .94,
11146
+ y: -4
11147
+ },
11148
+ animate: shown ? {
11149
+ opacity: 1,
11150
+ scale: 1,
11151
+ y: 0
11152
+ } : {
11153
+ opacity: 0,
11154
+ scale: .97,
11155
+ y: -2
11156
+ },
11157
+ transition: shown ? {
11158
+ type: "spring",
11159
+ stiffness: 420,
11160
+ damping: 30
11161
+ } : {
11162
+ duration: .45,
11163
+ ease: "easeOut"
11164
+ },
11165
+ style: { transformOrigin: "top right" },
11166
+ className: cn("absolute top-3 right-3 flex max-w-[calc(100%-24px)] flex-col items-end", !shown && "pointer-events-none"),
11167
+ "aria-hidden": !shown || void 0,
11168
+ onFocus: () => setFocused(true),
11169
+ onBlur: (event) => {
11170
+ if (!event.currentTarget.contains(event.relatedTarget)) setFocused(false);
11171
+ },
11172
+ children: /* @__PURE__ */ jsxs("div", {
11173
+ className: "relative flex max-w-full flex-col gap-1 rounded-[8px] border border-rule bg-surface p-1 shadow-pop",
11174
+ children: [
11175
+ loads > 0 ? /* @__PURE__ */ jsx(motion.span, {
11176
+ "aria-hidden": true,
11177
+ initial: {
11178
+ opacity: .55,
11179
+ scale: 1
11180
+ },
11181
+ animate: {
11182
+ opacity: 0,
11183
+ scale: 1.18
11184
+ },
11185
+ transition: {
11186
+ duration: 1.1,
11187
+ ease: "easeOut",
11188
+ delay: .15
11189
+ },
11190
+ className: "pointer-events-none absolute -inset-px rounded-[8px] border-2 border-action"
11191
+ }, loads) : null,
11192
+ children ? /* @__PURE__ */ jsx("div", {
11193
+ className: "flex items-center gap-1",
11194
+ children
11195
+ }) : null,
11196
+ note ? /* @__PURE__ */ jsx("div", {
11197
+ className: "px-2 pb-1 text-[11.5px] leading-snug text-muted",
11198
+ children: note
11199
+ }) : null
11200
+ ]
11201
+ })
11202
+ })] });
11203
+ }
11204
+ //#endregion
10808
11205
  //#region viewer/src/pages/canvas/full-page.tsx
10809
11206
  /** The full-window page of one screen, relative to the viewer's base. */
10810
11207
  var fullHref = (canvas, item, theme) => `/c/${encodeURIComponent(canvas)}/full/${encodeURIComponent(item)}?theme=${theme}`;
@@ -10820,13 +11217,18 @@ function useOpenUrl(canvasId, item, theme) {
10820
11217
  if (!frameSandbox) return absoluteUrl(frameSrc(item.url, item.theme ?? theme));
10821
11218
  return absoluteUrl(`${base}${fullHref(canvasId, item.id, item.theme ?? theme)}`);
10822
11219
  }
10823
- /** `/c/:canvas/full/:item`: one screen filling the window, a way out on hover and Esc. */
11220
+ /** `/c/:canvas/full/:item`: one screen filling the window, a way out in the top-right corner and on Esc. */
10824
11221
  function FullPage({ canvasId, itemId }) {
10825
11222
  const { data: canvas, error, isLoading } = useCanvas(canvasId);
10826
11223
  const [, navigate] = useLocation();
10827
- const { frameSandbox } = useViewer();
11224
+ const { frameSandbox, frameAllow, slots } = useViewer();
10828
11225
  const appTheme = useTheme();
10829
11226
  const frame = useRef(null);
11227
+ const [frameEl, setFrameEl] = useState(null);
11228
+ const setFrame = useCallback((el) => {
11229
+ frame.current = el;
11230
+ setFrameEl(el);
11231
+ }, []);
10830
11232
  const param = new URLSearchParams(window.location.search).get("theme");
10831
11233
  const items = canvas ? canvas.pages.flatMap((page) => page.sections.flatMap((section) => section.items.filter((item) => item.kind === "screen" || item.kind === "url"))) : [];
10832
11234
  const item = items.find((entry) => entry.id === itemId);
@@ -10864,31 +11266,35 @@ function FullPage({ canvasId, itemId }) {
10864
11266
  text: `No screen "${itemId}" on this canvas.`
10865
11267
  });
10866
11268
  return /* @__PURE__ */ jsxs("div", {
10867
- className: "group fixed inset-0 bg-surface",
11269
+ className: "fixed inset-0 bg-surface",
10868
11270
  children: [/* @__PURE__ */ jsx("iframe", {
10869
- ref: frame,
11271
+ ref: setFrame,
10870
11272
  src: url,
10871
11273
  title: item.title,
10872
- allow: "clipboard-read; clipboard-write; fullscreen",
11274
+ allow: frameAllow,
10873
11275
  sandbox: frameSandbox,
10874
11276
  className: "block h-full w-full border-0",
10875
11277
  style: { colorScheme: theme },
10876
11278
  onLoad: () => item.kind === "screen" && sendTheme(frame.current, theme)
10877
- }, `${item.id}:${item.kind === "screen" ? item.rev : item.url}`), /* @__PURE__ */ jsx("div", {
10878
- className: "pointer-events-none absolute inset-x-0 top-0 flex justify-end p-3 opacity-0 transition-opacity duration-200 group-hover:opacity-100 focus-within:opacity-100",
10879
- children: /* @__PURE__ */ jsxs(Link, {
11279
+ }, `${item.id}:${item.kind === "screen" ? item.rev : item.url}`), /* @__PURE__ */ jsxs(CornerPlate, {
11280
+ frame: frameEl,
11281
+ note: slots.fullPlate?.note,
11282
+ badge: slots.fullPlate?.badge,
11283
+ children: [/* @__PURE__ */ jsxs(Link, {
10880
11284
  href: back,
10881
- className: "pointer-events-auto flex h-8 items-center gap-2 rounded-[8px] border border-rule bg-surface px-3 text-[12.5px] text-ink2 no-underline shadow-pop hover:text-ink",
11285
+ className: "flex h-8 min-w-0 items-center gap-2 rounded-[6px] px-2 text-[12.5px] text-ink2 no-underline hover:bg-soft2 hover:text-ink",
10882
11286
  children: [
10883
11287
  /* @__PURE__ */ jsx(Icon, {
10884
11288
  name: "x",
10885
11289
  size: 14
10886
11290
  }),
10887
- "Back to ",
10888
- canvas.title,
11291
+ /* @__PURE__ */ jsxs("span", {
11292
+ className: "truncate",
11293
+ children: ["Back to ", canvas.title]
11294
+ }),
10889
11295
  /* @__PURE__ */ jsx(Kbd, { children: "esc" })
10890
11296
  ]
10891
- })
11297
+ }), slots.fullPlate?.actions]
10892
11298
  })]
10893
11299
  });
10894
11300
  }
@@ -11453,7 +11859,7 @@ function deviceIcon(width) {
11453
11859
  var FrameItem = memo(function FrameItem({ placed, mode, thumb, theme, store, events, capture }) {
11454
11860
  const { item, x, y, w, h } = placed;
11455
11861
  const ref = useRef(null);
11456
- const { frameSandbox } = useViewer();
11862
+ const { frameSandbox, frameAllow } = useViewer();
11457
11863
  const setRef = useCallback((el) => {
11458
11864
  ref.current = el;
11459
11865
  setFrameEl(store, item.id, el);
@@ -11490,7 +11896,8 @@ var FrameItem = memo(function FrameItem({ placed, mode, thumb, theme, store, eve
11490
11896
  break;
11491
11897
  case "error": {
11492
11898
  const current = store.get().frames[item.id]?.errors ?? [];
11493
- if (!current.includes(message.message)) setFrame(store, item.id, { errors: [...current, message.message] });
11899
+ const errors = withError(current, message.message);
11900
+ if (errors !== current) setFrame(store, item.id, { errors });
11494
11901
  break;
11495
11902
  }
11496
11903
  case "updated":
@@ -11556,7 +11963,7 @@ var FrameItem = memo(function FrameItem({ placed, mode, thumb, theme, store, eve
11556
11963
  ref: setRef,
11557
11964
  src,
11558
11965
  title: item.title,
11559
- allow: "clipboard-read; clipboard-write; fullscreen",
11966
+ allow: frameAllow,
11560
11967
  sandbox: frameSandbox,
11561
11968
  className: cn("absolute top-0 left-0 block border-0 transition-opacity duration-200", ready ? "opacity-100" : "opacity-0"),
11562
11969
  style: {
@@ -12894,7 +13301,7 @@ function PlayPage({ canvasId, itemId }) {
12894
13301
  }
12895
13302
  function Player({ canvas, itemId }) {
12896
13303
  const [, navigate] = useLocation();
12897
- const { frameSandbox, slots } = useViewer();
13304
+ const { frameSandbox, frameAllow, slots } = useViewer();
12898
13305
  const pageParam = new URLSearchParams(window.location.search).get("page") ?? (window.location.hash.split("?")[1] ? new URLSearchParams(window.location.hash.split("?")[1]).get("page") : null);
12899
13306
  const all = useMemo(() => framesOf(canvas, null), [canvas]);
12900
13307
  const current = all.find((entry) => entry.item.id === itemId);
@@ -12970,7 +13377,7 @@ function Player({ canvas, itemId }) {
12970
13377
  if (!el || item?.kind !== "screen") return;
12971
13378
  setErrors([]);
12972
13379
  return listenToFrame(el, (message) => {
12973
- if (message.type === "error") setErrors((list) => list.includes(message.message) ? list : [...list, message.message]);
13380
+ if (message.type === "error") setErrors((list) => withError(list, message.message));
12974
13381
  else if (message.type === "updated") setErrors([]);
12975
13382
  else if (message.type === "keydown" && message.code === "Escape") navigate(back);
12976
13383
  else if (message.type === "go") {
@@ -13146,7 +13553,7 @@ function Player({ canvas, itemId }) {
13146
13553
  ref: setFrame,
13147
13554
  src,
13148
13555
  title: item.title,
13149
- allow: "clipboard-read; clipboard-write; fullscreen",
13556
+ allow: frameAllow,
13150
13557
  sandbox: frameSandbox,
13151
13558
  className: "block border-0",
13152
13559
  style: {
@@ -13202,7 +13609,7 @@ function PhoneCanvas({ canvasId, pageId, itemId }) {
13202
13609
  }
13203
13610
  function PhoneViewer({ canvas, pageId, itemId }) {
13204
13611
  const [, navigate] = useLocation();
13205
- const { frameSandbox, slots, scope } = useViewer();
13612
+ const { frameSandbox, frameAllow, slots, scope } = useViewer();
13206
13613
  const appTheme = useTheme();
13207
13614
  const all = useMemo(() => framesOf(canvas, null), [canvas]);
13208
13615
  const index = Math.max(0, itemId ? all.findIndex((entry) => entry.item.id === itemId) : pageId ? all.findIndex((entry) => entry.pageId === pageId) : 0);
@@ -13254,7 +13661,7 @@ function PhoneViewer({ canvas, pageId, itemId }) {
13254
13661
  if (!el || item?.kind !== "screen") return;
13255
13662
  return listenToFrame(el, (message) => {
13256
13663
  if (message.type === "size") setAutoHeight(message.height);
13257
- else if (message.type === "error") setErrors((list) => list.includes(message.message) ? list : [...list, message.message]);
13664
+ else if (message.type === "error") setErrors((list) => withError(list, message.message));
13258
13665
  else if (message.type === "updated") setErrors([]);
13259
13666
  else if (message.type === "go") goTo(message.target.includes("/") ? message.target.split("/")[1] : message.target);
13260
13667
  else if (message.type === "link") followLink(message, canvas, goTo, item.id);
@@ -13427,7 +13834,7 @@ function PhoneViewer({ canvas, pageId, itemId }) {
13427
13834
  ref: setFrame,
13428
13835
  src,
13429
13836
  title: item.title,
13430
- allow: "clipboard-read; clipboard-write; fullscreen",
13837
+ allow: frameAllow,
13431
13838
  sandbox: frameSandbox,
13432
13839
  className: "block border-0",
13433
13840
  style: {
@@ -14320,7 +14727,7 @@ function ColorsPage({ system }) {
14320
14727
  */
14321
14728
  function AutoFrame({ src, theme, title, minHeight = 120, width, className }) {
14322
14729
  const ref = useRef(null);
14323
- const { frameSandbox } = useViewer();
14730
+ const { frameSandbox, frameAllow } = useViewer();
14324
14731
  const [initial] = useState(() => ({
14325
14732
  src,
14326
14733
  theme
@@ -14337,7 +14744,7 @@ function AutoFrame({ src, theme, title, minHeight = 120, width, className }) {
14337
14744
  return listenToFrame(frame, (message) => {
14338
14745
  if (message.type === "size") setHeight(Math.max(minHeight, message.height));
14339
14746
  else if (message.type === "ready") setReady(true);
14340
- else if (message.type === "error") setErrors((list) => list.includes(message.message) ? list : [...list, message.message]);
14747
+ else if (message.type === "error") setErrors((list) => withError(list, message.message));
14341
14748
  else if (message.type === "updated") setErrors([]);
14342
14749
  else if (message.type === "wheel" && !message.zoom) scrollAround(frame, message);
14343
14750
  });
@@ -14364,6 +14771,7 @@ function AutoFrame({ src, theme, title, minHeight = 120, width, className }) {
14364
14771
  ref,
14365
14772
  src: url,
14366
14773
  title,
14774
+ allow: frameAllow,
14367
14775
  sandbox: frameSandbox,
14368
14776
  className: cn("block w-full border-0 transition-opacity duration-150", ready ? "opacity-100" : "opacity-0"),
14369
14777
  style: {
@@ -18597,7 +19005,7 @@ function ScopedRoutes({ canvas }) {
18597
19005
  ] });
18598
19006
  }
18599
19007
  /** The whole viewer: home, canvases, screens and the design system of one project. */
18600
- function DesignViewer({ source, base = "", frameSandbox, slots, scope, queryClient }) {
19008
+ function DesignViewer({ source, base = "", frameSandbox, frameAllow = DEFAULT_FRAME_ALLOW, slots, scope, queryClient }) {
18601
19009
  const [ownClient] = useState(() => queryClient ?? createQueryClient(source.live));
18602
19010
  const client = queryClient ?? ownClient;
18603
19011
  const keys = useMemo(() => viewerKeys(sourceId(source)), [source]);
@@ -18606,12 +19014,14 @@ function DesignViewer({ source, base = "", frameSandbox, slots, scope, queryClie
18606
19014
  source,
18607
19015
  keys,
18608
19016
  frameSandbox,
19017
+ frameAllow,
18609
19018
  slots: slots ?? {},
18610
19019
  scope: scopeCanvas === void 0 ? null : { canvas: scopeCanvas }
18611
19020
  }), [
18612
19021
  source,
18613
19022
  keys,
18614
19023
  frameSandbox,
19024
+ frameAllow,
18615
19025
  slots,
18616
19026
  scopeCanvas
18617
19027
  ]);
@@ -18630,7 +19040,7 @@ function DesignViewer({ source, base = "", frameSandbox, slots, scope, queryClie
18630
19040
  /* @__PURE__ */ jsx(Title, {}),
18631
19041
  /* @__PURE__ */ jsxs(Router, {
18632
19042
  base,
18633
- children: [viewer.scope ? /* @__PURE__ */ jsx(ScopedRoutes, { canvas: viewer.scope.canvas }) : /* @__PURE__ */ jsx(Routes, {}), /* @__PURE__ */ jsx(CommandPalette, {})]
19043
+ children: [/* @__PURE__ */ jsx(PageBoundary, { children: viewer.scope ? /* @__PURE__ */ jsx(ScopedRoutes, { canvas: viewer.scope.canvas }) : /* @__PURE__ */ jsx(Routes, {}) }), /* @__PURE__ */ jsx(CommandPalette, {})]
18634
19044
  }),
18635
19045
  /* @__PURE__ */ jsx(Toaster, {})
18636
19046
  ] })
@@ -18684,13 +19094,65 @@ function Avatar({ src, name, size = 28, shape = "circle", className }) {
18684
19094
  });
18685
19095
  }
18686
19096
  //#endregion
19097
+ //#region viewer/src/ui/copy-button.tsx
19098
+ /** A ghost "Copy" button that turns into "Copied" with a check for a moment after it copies. */
19099
+ function CopyButton({ text, label = "Copy", done = "Copied", className }) {
19100
+ const [copied, setCopied] = useState(false);
19101
+ const timer = useRef(void 0);
19102
+ useEffect(() => () => clearTimeout(timer.current), []);
19103
+ const copy = async () => {
19104
+ try {
19105
+ await navigator.clipboard.writeText(text);
19106
+ } catch {
19107
+ toast("Could not copy", "The browser blocked clipboard access", "error");
19108
+ return;
19109
+ }
19110
+ setCopied(true);
19111
+ clearTimeout(timer.current);
19112
+ timer.current = setTimeout(() => setCopied(false), 1600);
19113
+ };
19114
+ return /* @__PURE__ */ jsxs("button", {
19115
+ type: "button",
19116
+ onClick: () => void copy(),
19117
+ className: cn(buttonClass("ghost", "h-7 px-2"), copied && "!text-ok-text", className),
19118
+ children: [/* @__PURE__ */ jsx(AnimatePresence, {
19119
+ mode: "popLayout",
19120
+ initial: false,
19121
+ children: /* @__PURE__ */ jsxs(motion.span, {
19122
+ initial: {
19123
+ opacity: 0,
19124
+ scale: .6
19125
+ },
19126
+ animate: {
19127
+ opacity: 1,
19128
+ scale: 1
19129
+ },
19130
+ exit: {
19131
+ opacity: 0,
19132
+ scale: .6
19133
+ },
19134
+ transition: { duration: .14 },
19135
+ className: "inline-flex items-center gap-1.5",
19136
+ children: [/* @__PURE__ */ jsx(Icon, {
19137
+ name: copied ? "check" : "copy",
19138
+ size: 15
19139
+ }), copied ? done : label]
19140
+ }, copied ? "done" : "copy")
19141
+ }), /* @__PURE__ */ jsx("span", {
19142
+ "aria-live": "polite",
19143
+ className: "sr-only",
19144
+ children: copied ? done : ""
19145
+ })]
19146
+ });
19147
+ }
19148
+ //#endregion
18687
19149
  //#region viewer/src/ui/dialog.tsx
18688
19150
  /**
18689
19151
  * A modal over a dimmed page: focus stays inside until it closes, Escape and a
18690
19152
  * click outside close it. Head and foot are a panel's; `actions` go to the foot.
18691
19153
  * On a phone it is a sheet from the bottom edge with a handle: swiping it down closes it too.
18692
19154
  */
18693
- function Dialog({ open, onOpenChange, title, description, children, actions, footNote, below, width = 440 }) {
19155
+ function Dialog({ open, onOpenChange, title, description, children, actions, footNote, below, width = 440, step }) {
18694
19156
  const phone = useIsPhone();
18695
19157
  const drag = useDragControls();
18696
19158
  const autoFocus = (event) => {
@@ -18778,24 +19240,24 @@ function Dialog({ open, onOpenChange, title, description, children, actions, foo
18778
19240
  },
18779
19241
  tabIndex: -1,
18780
19242
  className: "fixed inset-x-0 bottom-0 z-50 flex max-h-[90dvh] flex-col rounded-t-[14px] border-t border-rule bg-surface pb-[env(safe-area-inset-bottom)] shadow-pop outline-none",
18781
- children: [
18782
- /* @__PURE__ */ jsxs("div", {
18783
- className: "shrink-0 touch-none",
18784
- onPointerDown: (event) => drag.start(event),
18785
- children: [/* @__PURE__ */ jsx("div", {
18786
- className: "flex justify-center pt-2",
18787
- children: /* @__PURE__ */ jsx("span", {
18788
- "aria-hidden": true,
18789
- className: "h-1 w-9 rounded-full bg-rule-strong"
18790
- })
18791
- }), head]
18792
- }),
18793
- /* @__PURE__ */ jsxs("div", {
19243
+ children: [/* @__PURE__ */ jsxs("div", {
19244
+ className: "shrink-0 touch-none",
19245
+ onPointerDown: (event) => drag.start(event),
19246
+ children: [/* @__PURE__ */ jsx("div", {
19247
+ className: "flex justify-center pt-2",
19248
+ children: /* @__PURE__ */ jsx("span", {
19249
+ "aria-hidden": true,
19250
+ className: "h-1 w-9 rounded-full bg-rule-strong"
19251
+ })
19252
+ }), head]
19253
+ }), /* @__PURE__ */ jsxs(Step, {
19254
+ step,
19255
+ className: "flex min-h-0 flex-col",
19256
+ children: [/* @__PURE__ */ jsxs("div", {
18794
19257
  className: "min-h-0 overflow-y-auto overscroll-contain",
18795
19258
  children: [body, list]
18796
- }),
18797
- foot
18798
- ]
19259
+ }), foot]
19260
+ })]
18799
19261
  })
18800
19262
  })]
18801
19263
  }) : open ? /* @__PURE__ */ jsx(DialogPortal, {
@@ -18814,7 +19276,7 @@ function Dialog({ open, onOpenChange, title, description, children, actions, foo
18814
19276
  forceMount: true,
18815
19277
  onOpenAutoFocus: autoFocus,
18816
19278
  ...described,
18817
- children: /* @__PURE__ */ jsxs(motion.div, {
19279
+ children: /* @__PURE__ */ jsx(motion.div, {
18818
19280
  initial: {
18819
19281
  opacity: 0,
18820
19282
  scale: .98,
@@ -18838,12 +19300,15 @@ function Dialog({ open, onOpenChange, title, description, children, actions, foo
18838
19300
  tabIndex: -1,
18839
19301
  className: cn(floatingSurface, "my-auto flex max-w-full flex-col outline-none"),
18840
19302
  style: { width },
18841
- children: [
18842
- head,
18843
- body,
18844
- list,
18845
- foot
18846
- ]
19303
+ children: /* @__PURE__ */ jsx(Resizing, { children: /* @__PURE__ */ jsxs(Step, {
19304
+ step,
19305
+ children: [
19306
+ head,
19307
+ body,
19308
+ list,
19309
+ foot
19310
+ ]
19311
+ }) })
18847
19312
  })
18848
19313
  })
18849
19314
  })
@@ -18851,6 +19316,53 @@ function Dialog({ open, onOpenChange, title, description, children, actions, foo
18851
19316
  }) : null })
18852
19317
  });
18853
19318
  }
19319
+ /** Eases its height to its content's, so a panel whose content changes grows and shrinks smoothly. */
19320
+ function Resizing({ children }) {
19321
+ const inner = useRef(null);
19322
+ const [height, setHeight] = useState("auto");
19323
+ useLayoutEffect(() => {
19324
+ const el = inner.current;
19325
+ if (!el) return;
19326
+ const observer = new ResizeObserver(() => setHeight(el.offsetHeight));
19327
+ observer.observe(el);
19328
+ return () => observer.disconnect();
19329
+ }, []);
19330
+ return /* @__PURE__ */ jsx(motion.div, {
19331
+ initial: false,
19332
+ animate: { height },
19333
+ transition: {
19334
+ type: "spring",
19335
+ stiffness: 520,
19336
+ damping: 44
19337
+ },
19338
+ className: "overflow-hidden",
19339
+ children: /* @__PURE__ */ jsx("div", {
19340
+ ref: inner,
19341
+ className: "flex flex-col",
19342
+ children
19343
+ })
19344
+ });
19345
+ }
19346
+ /** Fades the content in again when the dialog's step changes. */
19347
+ function Step({ step, className, children }) {
19348
+ const first = useRef(step);
19349
+ return /* @__PURE__ */ jsx(motion.div, {
19350
+ initial: step === first.current ? false : {
19351
+ opacity: 0,
19352
+ y: 4
19353
+ },
19354
+ animate: {
19355
+ opacity: 1,
19356
+ y: 0
19357
+ },
19358
+ transition: {
19359
+ duration: .18,
19360
+ ease: "easeOut"
19361
+ },
19362
+ className: className ?? "flex flex-col",
19363
+ children
19364
+ }, step ?? "");
19365
+ }
18854
19366
  //#endregion
18855
19367
  //#region viewer/src/ui/meter.tsx
18856
19368
  /**
@@ -18967,4 +19479,4 @@ function TextArea({ label, hint, error, className, id, disabled, rows = 4, ...re
18967
19479
  });
18968
19480
  }
18969
19481
  //#endregion
18970
- export { ApiError, Avatar, Badge, Bar, Button, ButtonAnchor, ButtonLink, Content, Count, DesignViewer, Dialog, GroupRow, Icon, IconButton, KV, Kbd, Mark, Menu, MenuItem, MenuLabel, MenuSeparator, Meter, Mono, Muted, NavLabel, Notice, NoticeCard, PageHead, Panel, PanelBody, PanelFoot, PanelHead, PanelTabs, Row, RowLink, Segmented, Sheet, Skeleton, SkeletonRows, SlidePanel, Strip, SubNav, Switch, THead, TextArea, TextField, ThemeMenu, Toaster, Tooltip, TooltipProvider, Two, Wordmark, buttonClass, cn, copyText, floatingSurface, iconButtonClass, initials, localSource, menuItemClass, menuSurface, resolveUrl, setThemePref, staticSource, toast, useBarCompact, useIsPhone, useTheme, useThemePref };
19482
+ export { ApiError, Avatar, Badge, Bar, Button, ButtonAnchor, ButtonLink, Content, CopyButton, CornerPlate, Count, DesignViewer, Dialog, GroupRow, Icon, IconButton, KV, Kbd, Mark, Menu, MenuItem, MenuLabel, MenuSeparator, Meter, Mono, Muted, NavLabel, Notice, NoticeCard, PageHead, Panel, PanelBody, PanelFoot, PanelHead, PanelTabs, Row, RowLink, Segmented, Sheet, Skeleton, SkeletonRows, SlidePanel, Strip, SubNav, Switch, THead, TextArea, TextField, ThemeMenu, Toaster, Tooltip, TooltipProvider, Two, Wordmark, buttonClass, cn, copyText, floatingSurface, followLink, frameSrc, iconButtonClass, initials, listenToFrame, localSource, menuItemClass, menuSurface, resolveUrl, sendTheme, setThemePref, staticSource, toast, useBarCompact, useIsPhone, useTheme, useThemePref };