@1agh/maude 0.41.0 → 0.43.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/apps/studio/.ai/cache/_stats.json +7 -0
- package/apps/studio/acp/bootstrap-brief.ts +12 -2
- package/apps/studio/annotations-layer.tsx +1055 -278
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +173 -63
- package/apps/studio/bin/_html-playwright.mjs +26 -4
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_pdf-playwright.mjs +13 -2
- package/apps/studio/bin/_png-playwright.mjs +15 -2
- package/apps/studio/bin/_pptx-playwright.mjs +17 -4
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_screenshot-playwright.mjs +17 -2
- package/apps/studio/bin/_svg-playwright.mjs +26 -4
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/ingest-footage.sh +27 -0
- package/apps/studio/bin/photo-adjust.sh +163 -0
- package/apps/studio/bin/photo-bg-remove.sh +160 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/bin/screenshot.sh +53 -4
- package/apps/studio/canvas-edit.ts +459 -10
- package/apps/studio/canvas-lib.tsx +523 -5
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +476 -88
- package/apps/studio/client/export-center.jsx +460 -0
- package/apps/studio/client/github.js +4 -0
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/StickerPicker.jsx +160 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
- package/apps/studio/client/photo-knobs.jsx +437 -0
- package/apps/studio/client/styles/3-shell-maude.css +59 -0
- package/apps/studio/client/styles/4-components.css +150 -0
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/config.schema.json +2 -2
- package/apps/studio/context-menu.tsx +31 -6
- package/apps/studio/dist/client.bundle.js +5561 -19
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
- package/apps/studio/dist/runtime/pixi-js.js +519 -519
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +25 -0
- package/apps/studio/export-dialog.tsx +18 -25
- package/apps/studio/exporters/_runtime.ts +104 -0
- package/apps/studio/exporters/html.ts +12 -20
- package/apps/studio/exporters/index.ts +14 -2
- package/apps/studio/exporters/jobs.ts +376 -0
- package/apps/studio/exporters/pdf.ts +16 -20
- package/apps/studio/exporters/png.ts +12 -20
- package/apps/studio/exporters/pptx.ts +22 -23
- package/apps/studio/exporters/scope.ts +1 -0
- package/apps/studio/exporters/svg.ts +14 -22
- package/apps/studio/exporters/video.ts +37 -19
- package/apps/studio/footage/schema.test.ts +195 -0
- package/apps/studio/footage/schema.ts +484 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +198 -20
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +318 -64
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/paths.ts +10 -0
- package/apps/studio/photo/filters.ts +285 -0
- package/apps/studio/photo/pipeline.ts +607 -0
- package/apps/studio/photo/schema.ts +367 -0
- package/apps/studio/photo-store.ts +154 -0
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/server.ts +8 -2
- package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
- package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
- package/apps/studio/stickers/life-style/best.png +0 -0
- package/apps/studio/stickers/life-style/come-on.png +0 -0
- package/apps/studio/stickers/life-style/cut.png +0 -0
- package/apps/studio/stickers/life-style/detox.png +0 -0
- package/apps/studio/stickers/life-style/just.png +0 -0
- package/apps/studio/stickers/life-style/manifest.json +68 -0
- package/apps/studio/stickers/life-style/mirror.png +0 -0
- package/apps/studio/stickers/life-style/nice.png +0 -0
- package/apps/studio/stickers/life-style/objects.png +0 -0
- package/apps/studio/stickers/life-style/ok.png +0 -0
- package/apps/studio/stickers/life-style/queen.png +0 -0
- package/apps/studio/stickers/life-style/star.png +0 -0
- package/apps/studio/stickers/life-style/sun.png +0 -0
- package/apps/studio/stickers/life-style/water.png +0 -0
- package/apps/studio/stickers/life-style/yeah.png +0 -0
- package/apps/studio/stickers/life-style/you-can.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
- package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
- package/apps/studio/stickers/project-status/group-100.png +0 -0
- package/apps/studio/stickers/project-status/group-101.png +0 -0
- package/apps/studio/stickers/project-status/group-102.png +0 -0
- package/apps/studio/stickers/project-status/group-103.png +0 -0
- package/apps/studio/stickers/project-status/group-104.png +0 -0
- package/apps/studio/stickers/project-status/group-105.png +0 -0
- package/apps/studio/stickers/project-status/group-106.png +0 -0
- package/apps/studio/stickers/project-status/group-107.png +0 -0
- package/apps/studio/stickers/project-status/group-108.png +0 -0
- package/apps/studio/stickers/project-status/group-109.png +0 -0
- package/apps/studio/stickers/project-status/group-110.png +0 -0
- package/apps/studio/stickers/project-status/group-111.png +0 -0
- package/apps/studio/stickers/project-status/group-112.png +0 -0
- package/apps/studio/stickers/project-status/group-113.png +0 -0
- package/apps/studio/stickers/project-status/group-114.png +0 -0
- package/apps/studio/stickers/project-status/group-115.png +0 -0
- package/apps/studio/stickers/project-status/group-117.png +0 -0
- package/apps/studio/stickers/project-status/group-118.png +0 -0
- package/apps/studio/stickers/project-status/group-119.png +0 -0
- package/apps/studio/stickers/project-status/group-120.png +0 -0
- package/apps/studio/stickers/project-status/group-121.png +0 -0
- package/apps/studio/stickers/project-status/group-122.png +0 -0
- package/apps/studio/stickers/project-status/group-41.png +0 -0
- package/apps/studio/stickers/project-status/group-42.png +0 -0
- package/apps/studio/stickers/project-status/group-43.png +0 -0
- package/apps/studio/stickers/project-status/group-44.png +0 -0
- package/apps/studio/stickers/project-status/group-45.png +0 -0
- package/apps/studio/stickers/project-status/group-46.png +0 -0
- package/apps/studio/stickers/project-status/group-47.png +0 -0
- package/apps/studio/stickers/project-status/group-48.png +0 -0
- package/apps/studio/stickers/project-status/group-49.png +0 -0
- package/apps/studio/stickers/project-status/group-50.png +0 -0
- package/apps/studio/stickers/project-status/group-51.png +0 -0
- package/apps/studio/stickers/project-status/group-52.png +0 -0
- package/apps/studio/stickers/project-status/group-53.png +0 -0
- package/apps/studio/stickers/project-status/group-54.png +0 -0
- package/apps/studio/stickers/project-status/group-55.png +0 -0
- package/apps/studio/stickers/project-status/group-56.png +0 -0
- package/apps/studio/stickers/project-status/group-57.png +0 -0
- package/apps/studio/stickers/project-status/group-58.png +0 -0
- package/apps/studio/stickers/project-status/group-59.png +0 -0
- package/apps/studio/stickers/project-status/group-60.png +0 -0
- package/apps/studio/stickers/project-status/group-61.png +0 -0
- package/apps/studio/stickers/project-status/group-62.png +0 -0
- package/apps/studio/stickers/project-status/group-63.png +0 -0
- package/apps/studio/stickers/project-status/group-64.png +0 -0
- package/apps/studio/stickers/project-status/group-65.png +0 -0
- package/apps/studio/stickers/project-status/group-66.png +0 -0
- package/apps/studio/stickers/project-status/group-67.png +0 -0
- package/apps/studio/stickers/project-status/group-68.png +0 -0
- package/apps/studio/stickers/project-status/group-69.png +0 -0
- package/apps/studio/stickers/project-status/group-70.png +0 -0
- package/apps/studio/stickers/project-status/group-71.png +0 -0
- package/apps/studio/stickers/project-status/group-72.png +0 -0
- package/apps/studio/stickers/project-status/group-73.png +0 -0
- package/apps/studio/stickers/project-status/group-74.png +0 -0
- package/apps/studio/stickers/project-status/group-75.png +0 -0
- package/apps/studio/stickers/project-status/group-76.png +0 -0
- package/apps/studio/stickers/project-status/group-77.png +0 -0
- package/apps/studio/stickers/project-status/group-78.png +0 -0
- package/apps/studio/stickers/project-status/group-79.png +0 -0
- package/apps/studio/stickers/project-status/group-80.png +0 -0
- package/apps/studio/stickers/project-status/group-81.png +0 -0
- package/apps/studio/stickers/project-status/group-82.png +0 -0
- package/apps/studio/stickers/project-status/group-83.png +0 -0
- package/apps/studio/stickers/project-status/group-84.png +0 -0
- package/apps/studio/stickers/project-status/group-85.png +0 -0
- package/apps/studio/stickers/project-status/group-86.png +0 -0
- package/apps/studio/stickers/project-status/group-87.png +0 -0
- package/apps/studio/stickers/project-status/group-88.png +0 -0
- package/apps/studio/stickers/project-status/group-89.png +0 -0
- package/apps/studio/stickers/project-status/group-90.png +0 -0
- package/apps/studio/stickers/project-status/group-91.png +0 -0
- package/apps/studio/stickers/project-status/group-92.png +0 -0
- package/apps/studio/stickers/project-status/group-93.png +0 -0
- package/apps/studio/stickers/project-status/group-94.png +0 -0
- package/apps/studio/stickers/project-status/group-96.png +0 -0
- package/apps/studio/stickers/project-status/group-97.png +0 -0
- package/apps/studio/stickers/project-status/group-98.png +0 -0
- package/apps/studio/stickers/project-status/group-99.png +0 -0
- package/apps/studio/stickers/project-status/manifest.json +328 -0
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/asset-api.test.ts +3 -3
- package/apps/studio/test/canvas-edit.test.ts +17 -6
- package/apps/studio/test/canvas-origin-gate.test.ts +19 -0
- package/apps/studio/test/dns-rebinding-guard.test.ts +27 -0
- package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
- package/apps/studio/test/edit-source-command.test.ts +23 -0
- package/apps/studio/test/element-structural-edit.test.ts +46 -0
- package/apps/studio/test/export-center.test.tsx +287 -0
- package/apps/studio/test/export-shim-multi-capture.test.ts +83 -0
- package/apps/studio/test/exporters/history.test.ts +32 -3
- package/apps/studio/test/exporters/jobs.test.ts +263 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/git-branches.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +56 -1
- package/apps/studio/test/input-router.test.ts +33 -0
- package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
- package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
- package/apps/studio/test/photo-edit-api.test.ts +201 -0
- package/apps/studio/test/photo-filters.test.ts +149 -0
- package/apps/studio/test/photo-pipeline.test.ts +106 -0
- package/apps/studio/test/photo-taxonomy.test.ts +96 -0
- package/apps/studio/test/read-annotations.test.ts +164 -0
- package/apps/studio/test/shell-importmap.test.ts +49 -0
- package/apps/studio/test/text-editability-stamp.test.ts +131 -0
- package/apps/studio/test/timeline-parse.test.ts +57 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
- package/apps/studio/test/undo-stack.test.ts +33 -0
- package/apps/studio/test/video-asset.test.ts +5 -5
- package/apps/studio/test/video-render-bridge.test.ts +23 -1
- package/apps/studio/text-caret.ts +239 -0
- package/apps/studio/tool-palette.tsx +43 -20
- package/apps/studio/undo-stack.ts +94 -2
- package/apps/studio/use-annotation-resize.tsx +4 -4
- package/apps/studio/use-canvas-media-drop.tsx +38 -1
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +70 -0
- package/apps/studio/ws.ts +6 -0
- package/cli/commands/design.mjs +34 -3
- package/cli/lib/gitignore-block.mjs +2 -0
- package/package.json +10 -10
- package/plugins/design/templates/_shell.html +1 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
// photo-knobs.jsx — the Inspector "Photo" tab body (feature-photo-editor, Task 15).
|
|
2
|
+
//
|
|
3
|
+
// A self-contained editor for a PhotoEdit sidecar. Unlike CssKnobs (whose write
|
|
4
|
+
// path is /_api/edit-css keyed on a source `data-cd-id`), PhotoKnobs writes the
|
|
5
|
+
// non-destructive `assets/<sha8>.photo.json` sidecar through /_api/photo-edit —
|
|
6
|
+
// so it works for BOTH an artboard `<img>` and an annotation ImageStroke (both
|
|
7
|
+
// carry an `assets/<sha8>.<ext>` source), neither of which CssKnobs can express.
|
|
8
|
+
//
|
|
9
|
+
// Runs on the MAIN origin (the studio shell), so inline styles are fine
|
|
10
|
+
// (style-src 'self' 'unsafe-inline'). Renders the controls; the live pixel
|
|
11
|
+
// preview happens in the canvas iframe's <PhotoLayer>, driven by `onEdit`
|
|
12
|
+
// (app.jsx broadcasts the updated edit down to the canvas). Reuses the shell's
|
|
13
|
+
// HSV `ColorPicker` when injected; falls back to a native color input.
|
|
14
|
+
//
|
|
15
|
+
// Sections + ranges mirror photo/schema.ts exactly:
|
|
16
|
+
// Adjustments (brightness/contrast/saturation/exposure −1..1, hue −180..180,
|
|
17
|
+
// sepia/grayscale/invert 0..1) · Duotone · Grain · Pattern · Mask · Background.
|
|
18
|
+
|
|
19
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
20
|
+
|
|
21
|
+
const PATTERN_TYPES = ['dots', 'grid', 'lines', 'diagonal', 'crosshatch'];
|
|
22
|
+
const PATTERN_BLENDS = ['normal', 'multiply', 'screen', 'overlay', 'soft-light'];
|
|
23
|
+
const MASK_PRESETS = ['none', 'vignette', 'radial-reveal', 'edge-fade'];
|
|
24
|
+
|
|
25
|
+
const ADJUSTMENTS = [
|
|
26
|
+
{ key: 'brightness', label: 'Brightness', min: -1, max: 1 },
|
|
27
|
+
{ key: 'contrast', label: 'Contrast', min: -1, max: 1 },
|
|
28
|
+
{ key: 'saturation', label: 'Saturation', min: -1, max: 1 },
|
|
29
|
+
{ key: 'exposure', label: 'Exposure', min: -1, max: 1 },
|
|
30
|
+
{ key: 'hue', label: 'Hue', min: -180, max: 180, step: 1, unit: '°' },
|
|
31
|
+
{ key: 'sepia', label: 'Sepia', min: 0, max: 1 },
|
|
32
|
+
{ key: 'grayscale', label: 'Grayscale', min: 0, max: 1 },
|
|
33
|
+
{ key: 'invert', label: 'Invert', min: 0, max: 1 },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Deep-clone a small plain edit object (structuredClone is available in the shell).
|
|
37
|
+
const clone = (o) => (o ? JSON.parse(JSON.stringify(o)) : {});
|
|
38
|
+
|
|
39
|
+
/** Prune empty sub-objects so a neutral edit serializes back to `{}`. */
|
|
40
|
+
function prune(edit) {
|
|
41
|
+
const out = {};
|
|
42
|
+
for (const [k, v] of Object.entries(edit)) {
|
|
43
|
+
if (v == null) continue;
|
|
44
|
+
if (typeof v === 'object' && !Array.isArray(v)) {
|
|
45
|
+
const inner = {};
|
|
46
|
+
for (const [ik, iv] of Object.entries(v)) if (iv != null && iv !== '') inner[ik] = iv;
|
|
47
|
+
if (Object.keys(inner).length) out[k] = inner;
|
|
48
|
+
} else {
|
|
49
|
+
out[k] = v;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── small inline-styled primitives ──────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
// Theme tokens — the REAL shell tokens (client/styles/1-tokens.css), defined
|
|
58
|
+
// for both `:root`/`[data-theme="light"]` and `[data-theme="dark"]`. Earlier
|
|
59
|
+
// this used invented `--st-*` names (--st-fg/--st-border/--st-btn-bg/--st-mono)
|
|
60
|
+
// that were never actually declared anywhere in the stylesheet, so every rule
|
|
61
|
+
// silently fell back to its hardcoded dark-mode default — the panel looked
|
|
62
|
+
// fine in dark mode purely by coincidence and was low-contrast/washed out in
|
|
63
|
+
// light mode.
|
|
64
|
+
const S = {
|
|
65
|
+
body: { padding: '10px 12px', fontSize: 12, color: 'var(--fg-0)', overflowY: 'auto' },
|
|
66
|
+
sec: { marginBottom: 14 },
|
|
67
|
+
secHead: {
|
|
68
|
+
display: 'flex',
|
|
69
|
+
alignItems: 'center',
|
|
70
|
+
justifyContent: 'space-between',
|
|
71
|
+
fontSize: 11,
|
|
72
|
+
textTransform: 'uppercase',
|
|
73
|
+
letterSpacing: '.06em',
|
|
74
|
+
color: 'var(--fg-2)',
|
|
75
|
+
margin: '4px 0 8px',
|
|
76
|
+
},
|
|
77
|
+
row: { display: 'flex', alignItems: 'center', gap: 8, margin: '5px 0' },
|
|
78
|
+
label: { flex: '0 0 78px', fontSize: 11, color: 'var(--fg-1)' },
|
|
79
|
+
range: { flex: 1, minWidth: 0 },
|
|
80
|
+
num: {
|
|
81
|
+
flex: '0 0 42px',
|
|
82
|
+
textAlign: 'right',
|
|
83
|
+
fontFamily: 'var(--font-mono, ui-monospace, monospace)',
|
|
84
|
+
fontSize: 11,
|
|
85
|
+
color: 'var(--fg-2)',
|
|
86
|
+
},
|
|
87
|
+
btn: {
|
|
88
|
+
font: 'inherit',
|
|
89
|
+
fontSize: 11,
|
|
90
|
+
padding: '5px 10px',
|
|
91
|
+
borderRadius: 6,
|
|
92
|
+
border: '1px solid var(--border-default)',
|
|
93
|
+
background: 'var(--bg-3)',
|
|
94
|
+
color: 'var(--fg-0)',
|
|
95
|
+
cursor: 'pointer',
|
|
96
|
+
},
|
|
97
|
+
reset: {
|
|
98
|
+
font: 'inherit',
|
|
99
|
+
fontSize: 10,
|
|
100
|
+
padding: '1px 6px',
|
|
101
|
+
borderRadius: 4,
|
|
102
|
+
border: '1px solid transparent',
|
|
103
|
+
background: 'transparent',
|
|
104
|
+
color: 'var(--fg-2)',
|
|
105
|
+
cursor: 'pointer',
|
|
106
|
+
},
|
|
107
|
+
select: {
|
|
108
|
+
font: 'inherit',
|
|
109
|
+
fontSize: 11,
|
|
110
|
+
padding: '3px 6px',
|
|
111
|
+
borderRadius: 5,
|
|
112
|
+
border: '1px solid var(--border-default)',
|
|
113
|
+
background: 'var(--bg-3)',
|
|
114
|
+
color: 'var(--fg-0)',
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
function Section({ title, right, children }) {
|
|
119
|
+
return (
|
|
120
|
+
<section style={S.sec}>
|
|
121
|
+
<div style={S.secHead}>
|
|
122
|
+
<span>{title}</span>
|
|
123
|
+
{right}
|
|
124
|
+
</div>
|
|
125
|
+
{children}
|
|
126
|
+
</section>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function Slider({ label, value, min, max, step = 0.01, unit = '', onChange, onCommit }) {
|
|
131
|
+
const v = value ?? (min < 0 ? 0 : min);
|
|
132
|
+
return (
|
|
133
|
+
<div style={S.row}>
|
|
134
|
+
<span style={S.label}>{label}</span>
|
|
135
|
+
<input
|
|
136
|
+
type="range"
|
|
137
|
+
style={S.range}
|
|
138
|
+
min={min}
|
|
139
|
+
max={max}
|
|
140
|
+
step={step}
|
|
141
|
+
value={v}
|
|
142
|
+
onChange={(e) => onChange(Number(e.target.value))}
|
|
143
|
+
onPointerUp={onCommit}
|
|
144
|
+
onKeyUp={onCommit}
|
|
145
|
+
aria-label={label}
|
|
146
|
+
/>
|
|
147
|
+
<span style={S.num}>
|
|
148
|
+
{step >= 1 ? Math.round(v) : v.toFixed(2)}
|
|
149
|
+
{unit}
|
|
150
|
+
</span>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function Toggle({ checked, onChange, label }) {
|
|
156
|
+
return (
|
|
157
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, cursor: 'pointer' }}>
|
|
158
|
+
<input type="checkbox" checked={!!checked} onChange={(e) => onChange(e.target.checked)} />
|
|
159
|
+
{label}
|
|
160
|
+
</label>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function ColorSwatch({ value, fallback, ColorPicker, onApply }) {
|
|
165
|
+
const hex = value || fallback;
|
|
166
|
+
if (ColorPicker) return <ColorPicker seed={hex} onApply={onApply} />;
|
|
167
|
+
return (
|
|
168
|
+
<input
|
|
169
|
+
type="color"
|
|
170
|
+
value={hex}
|
|
171
|
+
onChange={(e) => onApply(e.target.value)}
|
|
172
|
+
style={{ width: 28, height: 20, padding: 0, border: 'none', background: 'none' }}
|
|
173
|
+
aria-label="color"
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── the panel ────────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {object} p
|
|
182
|
+
* @param {string} p.asset `assets/<sha8>.<ext>` source path.
|
|
183
|
+
* @param {object} [p.initialEdit] Seed PhotoEdit (else fetched on mount).
|
|
184
|
+
* @param {Function} [p.ColorPicker] Injected HSV picker component (optional).
|
|
185
|
+
* @param {Function} [p.onEdit] (edit) => void — live-preview broadcast.
|
|
186
|
+
* @param {Function} [p.onRemoveBackground] async (asset) => { maskAsset } | null.
|
|
187
|
+
* @param {Function} [p.onRecordEdit] (before, after) => void — undo integration.
|
|
188
|
+
*/
|
|
189
|
+
export function PhotoKnobs({ asset, initialEdit, ColorPicker, onEdit, onRemoveBackground, onRecordEdit }) {
|
|
190
|
+
const [edit, setEditState] = useState(() => clone(initialEdit));
|
|
191
|
+
const [loading, setLoading] = useState(!initialEdit);
|
|
192
|
+
const [saveState, setSaveState] = useState('idle'); // idle | saving | saved | error
|
|
193
|
+
const [bgBusy, setBgBusy] = useState(false);
|
|
194
|
+
const putTimer = useRef(null);
|
|
195
|
+
const editRef = useRef(edit);
|
|
196
|
+
editRef.current = edit;
|
|
197
|
+
|
|
198
|
+
// Hydrate from the sidecar unless seeded.
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (initialEdit || !asset) return;
|
|
201
|
+
let dead = false;
|
|
202
|
+
setLoading(true);
|
|
203
|
+
fetch(`/_api/photo-edit?asset=${encodeURIComponent(asset)}`)
|
|
204
|
+
.then((r) => (r.ok ? r.json() : {}))
|
|
205
|
+
.then((j) => {
|
|
206
|
+
if (dead) return;
|
|
207
|
+
setEditState(j && typeof j === 'object' ? j : {});
|
|
208
|
+
setLoading(false);
|
|
209
|
+
})
|
|
210
|
+
.catch(() => !dead && setLoading(false));
|
|
211
|
+
return () => {
|
|
212
|
+
dead = true;
|
|
213
|
+
};
|
|
214
|
+
}, [asset, initialEdit]);
|
|
215
|
+
|
|
216
|
+
const put = useCallback(
|
|
217
|
+
(next) => {
|
|
218
|
+
clearTimeout(putTimer.current);
|
|
219
|
+
putTimer.current = setTimeout(async () => {
|
|
220
|
+
setSaveState('saving');
|
|
221
|
+
try {
|
|
222
|
+
const res = await fetch(`/_api/photo-edit?asset=${encodeURIComponent(asset)}`, {
|
|
223
|
+
method: 'PUT',
|
|
224
|
+
headers: { 'Content-Type': 'application/json' },
|
|
225
|
+
body: JSON.stringify(prune(next)),
|
|
226
|
+
});
|
|
227
|
+
setSaveState(res.ok ? 'saved' : 'error');
|
|
228
|
+
} catch {
|
|
229
|
+
setSaveState('error');
|
|
230
|
+
}
|
|
231
|
+
}, 160);
|
|
232
|
+
},
|
|
233
|
+
[asset]
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// Apply a mutation, push the optimistic edit to the live preview, persist
|
|
237
|
+
// (debounced), and record undo (before → after).
|
|
238
|
+
const mutate = useCallback(
|
|
239
|
+
(fn, { commit = false } = {}) => {
|
|
240
|
+
const before = editRef.current;
|
|
241
|
+
const next = clone(before);
|
|
242
|
+
fn(next);
|
|
243
|
+
setEditState(next);
|
|
244
|
+
onEdit?.(next);
|
|
245
|
+
put(next);
|
|
246
|
+
if (commit) onRecordEdit?.(before, next);
|
|
247
|
+
},
|
|
248
|
+
[onEdit, put, onRecordEdit]
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const setAdj = (key, value, commit) =>
|
|
252
|
+
mutate((e) => {
|
|
253
|
+
e.adjustments = e.adjustments || {};
|
|
254
|
+
if (value === 0 || value == null) delete e.adjustments[key];
|
|
255
|
+
else e.adjustments[key] = value;
|
|
256
|
+
if (Object.keys(e.adjustments).length === 0) delete e.adjustments;
|
|
257
|
+
}, { commit });
|
|
258
|
+
|
|
259
|
+
const setSection = (section, patch, commit) =>
|
|
260
|
+
mutate((e) => {
|
|
261
|
+
e[section] = { ...(e[section] || {}), ...patch };
|
|
262
|
+
}, { commit });
|
|
263
|
+
|
|
264
|
+
const resetAdjustments = () => mutate((e) => delete e.adjustments, { commit: true });
|
|
265
|
+
|
|
266
|
+
if (loading) return <div style={{ ...S.body, opacity: 0.6 }}>Loading photo edit…</div>;
|
|
267
|
+
|
|
268
|
+
const adj = edit.adjustments || {};
|
|
269
|
+
const duo = edit.duotone || {};
|
|
270
|
+
const grain = edit.grain || {};
|
|
271
|
+
const pat = edit.pattern || {};
|
|
272
|
+
const mask = edit.mask || {};
|
|
273
|
+
const bg = edit.backgroundRemoved || {};
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<div style={S.body} data-testid="photo-knobs">
|
|
277
|
+
<div style={{ ...S.secHead, marginTop: 0, color: 'var(--fg-3)', textTransform: 'none', letterSpacing: 0 }}>
|
|
278
|
+
<span style={{ fontFamily: 'var(--font-mono, monospace)', fontSize: 10 }}>{asset}</span>
|
|
279
|
+
<span style={{ fontSize: 10, color: saveState === 'error' ? 'var(--status-error)' : undefined }}>
|
|
280
|
+
{saveState === 'saving' ? 'saving…' : saveState === 'error' ? '⚠ save failed' : saveState === 'saved' ? 'saved' : ''}
|
|
281
|
+
</span>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<Section
|
|
285
|
+
title="Adjustments"
|
|
286
|
+
right={
|
|
287
|
+
<button type="button" style={S.reset} onClick={resetAdjustments} title="Reset adjustments">
|
|
288
|
+
reset
|
|
289
|
+
</button>
|
|
290
|
+
}
|
|
291
|
+
>
|
|
292
|
+
{ADJUSTMENTS.map((a) => (
|
|
293
|
+
<Slider
|
|
294
|
+
key={a.key}
|
|
295
|
+
label={a.label}
|
|
296
|
+
value={adj[a.key]}
|
|
297
|
+
min={a.min}
|
|
298
|
+
max={a.max}
|
|
299
|
+
step={a.step ?? 0.01}
|
|
300
|
+
unit={a.unit ?? ''}
|
|
301
|
+
onChange={(v) => setAdj(a.key, v)}
|
|
302
|
+
onCommit={() => setAdj(a.key, adj[a.key] ?? 0, true)}
|
|
303
|
+
/>
|
|
304
|
+
))}
|
|
305
|
+
</Section>
|
|
306
|
+
|
|
307
|
+
<Section
|
|
308
|
+
title="Duotone"
|
|
309
|
+
right={<Toggle checked={duo.enabled} onChange={(v) => setSection('duotone', { enabled: v }, true)} label="on" />}
|
|
310
|
+
>
|
|
311
|
+
{duo.enabled && (
|
|
312
|
+
<>
|
|
313
|
+
<div style={S.row}>
|
|
314
|
+
<span style={S.label}>Shadow</span>
|
|
315
|
+
<ColorSwatch value={duo.colorA} fallback="#111111" ColorPicker={ColorPicker} onApply={(hex) => setSection('duotone', { colorA: hex }, true)} />
|
|
316
|
+
<span style={S.label}>Highlight</span>
|
|
317
|
+
<ColorSwatch value={duo.colorB} fallback="#ffffff" ColorPicker={ColorPicker} onApply={(hex) => setSection('duotone', { colorB: hex }, true)} />
|
|
318
|
+
</div>
|
|
319
|
+
<Slider label="Intensity" value={duo.intensity ?? 1} min={0} max={1} onChange={(v) => setSection('duotone', { intensity: v })} onCommit={() => setSection('duotone', { intensity: duo.intensity ?? 1 }, true)} />
|
|
320
|
+
</>
|
|
321
|
+
)}
|
|
322
|
+
</Section>
|
|
323
|
+
|
|
324
|
+
<Section
|
|
325
|
+
title="Grain"
|
|
326
|
+
right={<Toggle checked={grain.enabled} onChange={(v) => setSection('grain', { enabled: v }, true)} label="on" />}
|
|
327
|
+
>
|
|
328
|
+
{grain.enabled && (
|
|
329
|
+
<>
|
|
330
|
+
<Slider label="Amount" value={grain.amount ?? 0.4} min={0} max={1} onChange={(v) => setSection('grain', { amount: v })} onCommit={() => setSection('grain', { amount: grain.amount ?? 0.4 }, true)} />
|
|
331
|
+
<Slider label="Size" value={grain.size ?? 1} min={1} max={8} step={0.5} onChange={(v) => setSection('grain', { size: v })} onCommit={() => setSection('grain', { size: grain.size ?? 1 }, true)} />
|
|
332
|
+
</>
|
|
333
|
+
)}
|
|
334
|
+
</Section>
|
|
335
|
+
|
|
336
|
+
<Section
|
|
337
|
+
title="Pattern"
|
|
338
|
+
right={<Toggle checked={pat.enabled} onChange={(v) => setSection('pattern', { enabled: v }, true)} label="on" />}
|
|
339
|
+
>
|
|
340
|
+
{pat.enabled && (
|
|
341
|
+
<>
|
|
342
|
+
<div style={S.row}>
|
|
343
|
+
<span style={S.label}>Type</span>
|
|
344
|
+
<select style={S.select} value={pat.type || 'dots'} onChange={(e) => setSection('pattern', { type: e.target.value }, true)}>
|
|
345
|
+
{PATTERN_TYPES.map((t) => (
|
|
346
|
+
<option key={t} value={t}>
|
|
347
|
+
{t}
|
|
348
|
+
</option>
|
|
349
|
+
))}
|
|
350
|
+
</select>
|
|
351
|
+
<span style={S.label}>Blend</span>
|
|
352
|
+
<select style={S.select} value={pat.blend || 'normal'} onChange={(e) => setSection('pattern', { blend: e.target.value }, true)}>
|
|
353
|
+
{PATTERN_BLENDS.map((b) => (
|
|
354
|
+
<option key={b} value={b}>
|
|
355
|
+
{b}
|
|
356
|
+
</option>
|
|
357
|
+
))}
|
|
358
|
+
</select>
|
|
359
|
+
</div>
|
|
360
|
+
<div style={S.row}>
|
|
361
|
+
<span style={S.label}>Color</span>
|
|
362
|
+
<ColorSwatch value={pat.color} fallback="#ffffff" ColorPicker={ColorPicker} onApply={(hex) => setSection('pattern', { color: hex }, true)} />
|
|
363
|
+
<span style={{ ...S.num, flex: 1, textAlign: 'left', color: 'var(--fg-3)' }}>tip: dark + multiply</span>
|
|
364
|
+
</div>
|
|
365
|
+
<Slider label="Scale" value={pat.scale ?? 1} min={0.25} max={4} step={0.25} onChange={(v) => setSection('pattern', { scale: v })} onCommit={() => setSection('pattern', { scale: pat.scale ?? 1 }, true)} />
|
|
366
|
+
<Slider label="Opacity" value={pat.opacity ?? 0.5} min={0} max={1} onChange={(v) => setSection('pattern', { opacity: v })} onCommit={() => setSection('pattern', { opacity: pat.opacity ?? 0.5 }, true)} />
|
|
367
|
+
</>
|
|
368
|
+
)}
|
|
369
|
+
</Section>
|
|
370
|
+
|
|
371
|
+
<Section title="Mask">
|
|
372
|
+
<div style={S.row}>
|
|
373
|
+
<span style={S.label}>Preset</span>
|
|
374
|
+
<select style={S.select} value={mask.preset || 'none'} onChange={(e) => setSection('mask', { preset: e.target.value }, true)}>
|
|
375
|
+
{MASK_PRESETS.map((m) => (
|
|
376
|
+
<option key={m} value={m}>
|
|
377
|
+
{m}
|
|
378
|
+
</option>
|
|
379
|
+
))}
|
|
380
|
+
</select>
|
|
381
|
+
</div>
|
|
382
|
+
{mask.preset && mask.preset !== 'none' && (
|
|
383
|
+
<Slider label="Strength" value={mask.strength ?? 0.6} min={0} max={1} onChange={(v) => setSection('mask', { strength: v })} onCommit={() => setSection('mask', { strength: mask.strength ?? 0.6 }, true)} />
|
|
384
|
+
)}
|
|
385
|
+
</Section>
|
|
386
|
+
|
|
387
|
+
<Section title="Background">
|
|
388
|
+
<div style={{ ...S.row, gap: 10 }}>
|
|
389
|
+
{bg.maskAsset ? (
|
|
390
|
+
<>
|
|
391
|
+
<Toggle checked={bg.enabled} onChange={(v) => setSection('backgroundRemoved', { enabled: v }, true)} label="applied" />
|
|
392
|
+
<button
|
|
393
|
+
type="button"
|
|
394
|
+
style={{ ...S.reset, opacity: bgBusy ? 0.6 : 1 }}
|
|
395
|
+
disabled={bgBusy || !onRemoveBackground}
|
|
396
|
+
title="Run background removal again"
|
|
397
|
+
onClick={async () => {
|
|
398
|
+
if (!onRemoveBackground) return;
|
|
399
|
+
setBgBusy(true);
|
|
400
|
+
try {
|
|
401
|
+
const res = await onRemoveBackground(asset);
|
|
402
|
+
if (res?.maskAsset) setSection('backgroundRemoved', { enabled: true, maskAsset: res.maskAsset }, true);
|
|
403
|
+
} finally {
|
|
404
|
+
setBgBusy(false);
|
|
405
|
+
}
|
|
406
|
+
}}
|
|
407
|
+
>
|
|
408
|
+
{bgBusy ? 'removing…' : 'redo'}
|
|
409
|
+
</button>
|
|
410
|
+
</>
|
|
411
|
+
) : (
|
|
412
|
+
<button
|
|
413
|
+
type="button"
|
|
414
|
+
style={{ ...S.btn, opacity: bgBusy ? 0.6 : 1 }}
|
|
415
|
+
disabled={bgBusy || !onRemoveBackground}
|
|
416
|
+
data-testid="photo-remove-bg"
|
|
417
|
+
onClick={async () => {
|
|
418
|
+
if (!onRemoveBackground) return;
|
|
419
|
+
setBgBusy(true);
|
|
420
|
+
try {
|
|
421
|
+
const res = await onRemoveBackground(asset);
|
|
422
|
+
if (res?.maskAsset) setSection('backgroundRemoved', { enabled: true, maskAsset: res.maskAsset }, true);
|
|
423
|
+
} finally {
|
|
424
|
+
setBgBusy(false);
|
|
425
|
+
}
|
|
426
|
+
}}
|
|
427
|
+
>
|
|
428
|
+
{bgBusy ? 'Removing…' : '✦ Remove Background'}
|
|
429
|
+
</button>
|
|
430
|
+
)}
|
|
431
|
+
</div>
|
|
432
|
+
</Section>
|
|
433
|
+
</div>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export default PhotoKnobs;
|
|
@@ -86,6 +86,23 @@
|
|
|
86
86
|
border-radius: var(--radius-pill); background: var(--accent); border: 1.5px solid var(--bg-1);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/* feature-background-export-notification-center — menubar Exports button.
|
|
90
|
+
Mirrors .st-whatsnew's attribute-driven state (data-busy instead of
|
|
91
|
+
data-unseen — running/queued exports, not an unread count). */
|
|
92
|
+
.st-exports {
|
|
93
|
+
position: relative; appearance: none; border: 0; background: transparent; cursor: pointer;
|
|
94
|
+
width: 26px; height: 26px; display: grid; place-items: center; border-radius: var(--radius-sm);
|
|
95
|
+
color: var(--fg-1); font-size: 13px;
|
|
96
|
+
}
|
|
97
|
+
.st-exports:hover { background: var(--bg-3); color: var(--fg-0); }
|
|
98
|
+
.st-exports[data-busy="true"] { color: var(--accent); }
|
|
99
|
+
.st-exports__count {
|
|
100
|
+
position: absolute; top: -2px; right: -2px; min-width: 14px; height: 14px; padding: 0 3px;
|
|
101
|
+
display: grid; place-items: center; border-radius: var(--radius-pill);
|
|
102
|
+
background: var(--accent); color: var(--accent-fg); font-family: var(--font-mono);
|
|
103
|
+
font-size: 9px; font-weight: 700; line-height: 1; border: 1.5px solid var(--bg-1);
|
|
104
|
+
}
|
|
105
|
+
|
|
89
106
|
/* ─── Menubar dropdown ────────────────────────────────────────────────────── */
|
|
90
107
|
.st-dropdown {
|
|
91
108
|
position: absolute; top: 38px; min-width: 230px;
|
|
@@ -496,6 +513,28 @@
|
|
|
496
513
|
.st-ap-cell:hover { border-color: var(--accent, var(--border-strong, var(--border-default))); background: var(--bg-3); }
|
|
497
514
|
.st-ap-thumb { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: var(--radius-xs); background: var(--bg-4, var(--bg-3)); display: block; }
|
|
498
515
|
.st-ap-name { font-family: var(--font-mono); font-size: 10px; color: var(--fg-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
516
|
+
|
|
517
|
+
/* ─── StickerPicker (feature-whiteboard-annotation-improvements Phase 4) ──── */
|
|
518
|
+
.st-sticker-picker { width: 640px; }
|
|
519
|
+
.st-sticker-picker .input {
|
|
520
|
+
font-family: var(--font-body); font-size: var(--type-sm); color: var(--fg-0);
|
|
521
|
+
background: var(--bg-3); border: 1px solid var(--border-default); border-radius: var(--radius-sm);
|
|
522
|
+
padding: var(--space-3) var(--space-4); width: 100%;
|
|
523
|
+
transition: border-color var(--dur-soft) var(--ease-out), box-shadow var(--dur-soft) var(--ease-out);
|
|
524
|
+
}
|
|
525
|
+
.st-sticker-picker .input::placeholder { color: var(--fg-3); }
|
|
526
|
+
.st-sticker-picker .input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-tint); }
|
|
527
|
+
.st-sp-body { max-height: 56vh; overflow-y: auto; padding: 2px; display: flex; flex-direction: column; gap: var(--space-5); }
|
|
528
|
+
.st-sp-pack-hd { display: flex; align-items: baseline; gap: var(--space-2); padding: 0 2px; }
|
|
529
|
+
.st-sp-pack-name { font-family: var(--font-mono); font-size: var(--type-xs); font-weight: 600; color: var(--fg-1); text-transform: uppercase; letter-spacing: var(--tracking-wide); }
|
|
530
|
+
.st-sp-pack-author { font-family: var(--font-mono); font-size: var(--type-xs); color: var(--fg-3); }
|
|
531
|
+
.st-sp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(76px, 1fr)); gap: var(--space-2); }
|
|
532
|
+
.st-sp-cell { appearance: none; cursor: pointer; display: flex; flex-direction: column; align-items: center; padding: 6px; border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); background: var(--bg-2); transition: border-color var(--dur-soft) var(--ease-out), background var(--dur-soft) var(--ease-out); }
|
|
533
|
+
.st-sp-cell:hover { border-color: var(--accent, var(--border-strong, var(--border-default))); background: var(--bg-3); }
|
|
534
|
+
.st-sp-thumb { width: 100%; aspect-ratio: 1; object-fit: contain; display: block; }
|
|
535
|
+
.st-sp-empty { color: var(--fg-2); font-family: var(--font-mono); font-size: var(--type-xs); padding: var(--space-6) 0; text-align: center; }
|
|
536
|
+
.st-sp-ft { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); padding: var(--space-3) var(--space-5); border-top: 1px solid var(--border-subtle); font-family: var(--font-mono); font-size: 10px; color: var(--fg-3); }
|
|
537
|
+
.st-sp-ft a { color: var(--fg-2); }
|
|
499
538
|
.st-cp-replace { width: 100%; justify-content: center; }
|
|
500
539
|
/* Media "Replace…" — a full-width button, NOT inside the 3-col .st-cp-row grid
|
|
501
540
|
(which would drop it into the 10px provenance-dot column and clip it). */
|
|
@@ -1870,6 +1909,25 @@ body.st-scrubbing, body.st-scrubbing * { cursor: ew-resize !important; user-sele
|
|
|
1870
1909
|
box-shadow: 0 -10px 18px -12px color-mix(in oklab, var(--bg-0) 80%, transparent);
|
|
1871
1910
|
}
|
|
1872
1911
|
.rb-dock { position: relative; }
|
|
1912
|
+
/* Proactive "remote has updates" nudge — a full-width primary button above the dock
|
|
1913
|
+
trigger, shown when the tracking remote is ahead (status.remoteAhead). */
|
|
1914
|
+
.rb-getlatest { width: 100%; justify-content: flex-start; gap: var(--space-2); animation: rb-getlatest-in var(--dur-soft, 160ms) var(--ease-out, ease-out); }
|
|
1915
|
+
.rb-getlatest-tx { flex: 0 0 auto; }
|
|
1916
|
+
.rb-getlatest-sub { margin-left: auto; font-weight: 500; opacity: 0.9; font-size: var(--type-xs); white-space: nowrap; }
|
|
1917
|
+
@keyframes rb-getlatest-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
|
1918
|
+
@media (prefers-reduced-motion: reduce) { .rb-getlatest, .rb-resume { animation: none; } }
|
|
1919
|
+
/* "Continue on <draft>" resume nudge — a subtle secondary row (not the primary blue),
|
|
1920
|
+
shown above the dock trigger when you're on the Shared version but have a draft. */
|
|
1921
|
+
.rb-resume { display: flex; align-items: center; gap: var(--space-1); width: 100%; animation: rb-getlatest-in var(--dur-soft, 160ms) var(--ease-out, ease-out); }
|
|
1922
|
+
.rb-resume-go { flex: 1 1 auto; display: flex; align-items: center; gap: var(--space-2); min-width: 0; text-align: left; padding: var(--space-2) var(--space-3); border: 1px solid var(--border-default); border-radius: var(--radius-md); background: var(--bg-2); color: var(--fg-1); cursor: pointer; font-size: var(--type-sm); transition: border-color var(--dur-soft) var(--ease-out), background var(--dur-soft) var(--ease-out); }
|
|
1923
|
+
.rb-resume-go:hover { border-color: var(--border-strong); background: var(--bg-3); }
|
|
1924
|
+
.rb-resume-go:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
1925
|
+
.rb-resume-icon { color: var(--status-warn); flex: 0 0 auto; display: grid; place-items: center; }
|
|
1926
|
+
.rb-resume-tx { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1927
|
+
.rb-resume-arrow { color: var(--fg-3); flex: 0 0 auto; }
|
|
1928
|
+
.rb-resume-x { flex: 0 0 auto; display: grid; place-items: center; width: 28px; height: 28px; border: none; border-radius: var(--radius-md); background: transparent; color: var(--fg-3); cursor: pointer; }
|
|
1929
|
+
.rb-resume-x:hover { background: var(--bg-3); color: var(--fg-1); }
|
|
1930
|
+
.rb-resume-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
1873
1931
|
/* The compact one-line trigger — "📁 <project> · ◐ <version> ⌃" */
|
|
1874
1932
|
.rb-trigger {
|
|
1875
1933
|
display: flex; align-items: center; gap: var(--space-2);
|
|
@@ -1966,6 +2024,7 @@ body.st-scrubbing, body.st-scrubbing * { cursor: ew-resize !important; user-sele
|
|
|
1966
2024
|
.rb-sheet-body { margin: 0; color: var(--fg-1); font-size: var(--type-md); line-height: var(--lh-md); }
|
|
1967
2025
|
.rb-sheet-body b { color: var(--fg-0); font-weight: 600; }
|
|
1968
2026
|
.rb-sheet-meta { margin: 0; color: var(--fg-2); font-size: var(--type-sm); line-height: var(--lh-md); }
|
|
2027
|
+
.rb-pr-url { font-family: var(--font-mono, ui-monospace, monospace); font-size: var(--type-xs); word-break: break-all; user-select: all; opacity: 0.8; }
|
|
1969
2028
|
.rb-sheet-actions { display: flex; gap: var(--space-2); justify-content: flex-end; margin-top: var(--space-2); }
|
|
1970
2029
|
@media (prefers-reduced-motion: reduce) {
|
|
1971
2030
|
.rb-spin { animation-duration: 1ms; }
|
|
@@ -1306,6 +1306,156 @@
|
|
|
1306
1306
|
}
|
|
1307
1307
|
}
|
|
1308
1308
|
|
|
1309
|
+
/* ───────── Export notification center — feature-background-export-notification-center ─────────
|
|
1310
|
+
Panel shell reuses .help-modal-backdrop (above); list/item/progress are new
|
|
1311
|
+
— no prior job-row or progress-bar component existed in the client. */
|
|
1312
|
+
.st-export-panel {
|
|
1313
|
+
width: min(560px, 92vw);
|
|
1314
|
+
max-height: 80vh;
|
|
1315
|
+
display: flex;
|
|
1316
|
+
flex-direction: column;
|
|
1317
|
+
background: var(--u-bg-1);
|
|
1318
|
+
color: var(--u-fg-0);
|
|
1319
|
+
border: 1px solid var(--u-bg-3);
|
|
1320
|
+
border-radius: var(--radius-md);
|
|
1321
|
+
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.3);
|
|
1322
|
+
overflow: hidden;
|
|
1323
|
+
}
|
|
1324
|
+
.st-export-panel__body {
|
|
1325
|
+
overflow-y: auto;
|
|
1326
|
+
padding: 8px 14px 16px;
|
|
1327
|
+
}
|
|
1328
|
+
.st-export-list {
|
|
1329
|
+
list-style: none;
|
|
1330
|
+
margin: 0;
|
|
1331
|
+
padding: 0;
|
|
1332
|
+
}
|
|
1333
|
+
.st-export-item {
|
|
1334
|
+
padding: 12px 0;
|
|
1335
|
+
border-bottom: 1px solid var(--u-bg-3);
|
|
1336
|
+
}
|
|
1337
|
+
.st-export-item:last-child {
|
|
1338
|
+
border-bottom: none;
|
|
1339
|
+
}
|
|
1340
|
+
.st-export-item__hd {
|
|
1341
|
+
display: flex;
|
|
1342
|
+
align-items: center;
|
|
1343
|
+
justify-content: space-between;
|
|
1344
|
+
gap: 8px;
|
|
1345
|
+
margin-bottom: 6px;
|
|
1346
|
+
}
|
|
1347
|
+
.st-export-item__label {
|
|
1348
|
+
font-weight: 600;
|
|
1349
|
+
flex: 1;
|
|
1350
|
+
min-width: 0;
|
|
1351
|
+
}
|
|
1352
|
+
.st-export-item__ft {
|
|
1353
|
+
display: flex;
|
|
1354
|
+
align-items: center;
|
|
1355
|
+
justify-content: space-between;
|
|
1356
|
+
gap: 8px;
|
|
1357
|
+
margin-top: 6px;
|
|
1358
|
+
}
|
|
1359
|
+
.st-export-item__filename {
|
|
1360
|
+
color: var(--u-fg-2);
|
|
1361
|
+
font-size: 12px;
|
|
1362
|
+
font-family: var(--font-mono, monospace);
|
|
1363
|
+
overflow: hidden;
|
|
1364
|
+
text-overflow: ellipsis;
|
|
1365
|
+
white-space: nowrap;
|
|
1366
|
+
}
|
|
1367
|
+
.st-export-item__error {
|
|
1368
|
+
color: var(--u-status-error);
|
|
1369
|
+
font-size: 12px;
|
|
1370
|
+
}
|
|
1371
|
+
.st-export-pill {
|
|
1372
|
+
text-transform: uppercase;
|
|
1373
|
+
font-size: 9px;
|
|
1374
|
+
font-weight: 700;
|
|
1375
|
+
letter-spacing: 0.04em;
|
|
1376
|
+
padding: 2px 6px;
|
|
1377
|
+
border-radius: var(--radius-pill, 999px);
|
|
1378
|
+
white-space: nowrap;
|
|
1379
|
+
}
|
|
1380
|
+
.st-export-pill--queued {
|
|
1381
|
+
background: var(--u-bg-3);
|
|
1382
|
+
color: var(--u-fg-2);
|
|
1383
|
+
}
|
|
1384
|
+
.st-export-pill--running {
|
|
1385
|
+
background: var(--u-accent-bg);
|
|
1386
|
+
color: var(--u-accent);
|
|
1387
|
+
}
|
|
1388
|
+
.st-export-pill--done {
|
|
1389
|
+
background: color-mix(in oklab, var(--u-status-success) 18%, transparent);
|
|
1390
|
+
color: var(--u-status-success);
|
|
1391
|
+
}
|
|
1392
|
+
.st-export-pill--failed {
|
|
1393
|
+
background: color-mix(in oklab, var(--u-status-error) 18%, transparent);
|
|
1394
|
+
color: var(--u-status-error);
|
|
1395
|
+
}
|
|
1396
|
+
.st-export-progress {
|
|
1397
|
+
position: relative;
|
|
1398
|
+
height: 4px;
|
|
1399
|
+
border-radius: var(--radius-pill, 999px);
|
|
1400
|
+
background: var(--u-bg-3);
|
|
1401
|
+
overflow: hidden;
|
|
1402
|
+
}
|
|
1403
|
+
.st-export-progress-bar {
|
|
1404
|
+
height: 100%;
|
|
1405
|
+
background: var(--u-accent);
|
|
1406
|
+
transition: width 0.2s ease-out;
|
|
1407
|
+
}
|
|
1408
|
+
.st-export-progress--indeterminate::after {
|
|
1409
|
+
content: '';
|
|
1410
|
+
position: absolute;
|
|
1411
|
+
inset: 0;
|
|
1412
|
+
width: 40%;
|
|
1413
|
+
background: var(--u-accent);
|
|
1414
|
+
border-radius: var(--radius-pill, 999px);
|
|
1415
|
+
animation: st-export-indeterminate 1.2s ease-in-out infinite;
|
|
1416
|
+
}
|
|
1417
|
+
@keyframes st-export-indeterminate {
|
|
1418
|
+
0% {
|
|
1419
|
+
transform: translateX(-100%);
|
|
1420
|
+
}
|
|
1421
|
+
100% {
|
|
1422
|
+
transform: translateX(350%);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1426
|
+
.st-export-progress--indeterminate::after {
|
|
1427
|
+
animation-duration: 1ms;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
.st-toast .st-export-progress {
|
|
1431
|
+
margin-top: var(--space-1, 4px);
|
|
1432
|
+
}
|
|
1433
|
+
/* Busy state for the Save…/Download button — the bytes fetch + (native) the
|
|
1434
|
+
OS save-dialog handoff can take several seconds on a large export; without
|
|
1435
|
+
this the button just goes silent after the click. */
|
|
1436
|
+
.st-btn-spin {
|
|
1437
|
+
display: inline-block;
|
|
1438
|
+
width: 11px;
|
|
1439
|
+
height: 11px;
|
|
1440
|
+
margin-right: 5px;
|
|
1441
|
+
vertical-align: -1px;
|
|
1442
|
+
border: 1.5px solid currentColor;
|
|
1443
|
+
border-top-color: transparent;
|
|
1444
|
+
border-radius: 50%;
|
|
1445
|
+
opacity: 0.75;
|
|
1446
|
+
animation: st-btn-spin 0.7s linear infinite;
|
|
1447
|
+
}
|
|
1448
|
+
@keyframes st-btn-spin {
|
|
1449
|
+
to {
|
|
1450
|
+
transform: rotate(360deg);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1454
|
+
.st-btn-spin {
|
|
1455
|
+
animation-duration: 1ms;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1309
1459
|
/* ───────── Guided tour (overlay) — feature-in-app-whats-new-tour Phase 3 ───────── */
|
|
1310
1460
|
.mdcc-tour {
|
|
1311
1461
|
position: fixed;
|