@1agh/maude 0.42.0 → 0.44.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/acp/bridge.ts +44 -10
- package/apps/studio/acp/env.ts +23 -0
- package/apps/studio/acp/login-state.ts +263 -0
- package/apps/studio/acp/plugin-bootstrap.ts +31 -25
- package/apps/studio/acp/probe.ts +152 -1
- package/apps/studio/annotations-layer.tsx +1451 -324
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +568 -24
- package/apps/studio/bin/_audio-search.mjs +140 -0
- package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
- package/apps/studio/bin/_import-asset.mjs +815 -0
- package/apps/studio/bin/_import-brand.mjs +635 -0
- package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
- package/apps/studio/bin/_import-tokens.mjs +1201 -0
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_transcribe.mjs +419 -0
- package/apps/studio/bin/_transcribe.test.mjs +80 -0
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/audio-search.sh +28 -0
- package/apps/studio/bin/generate.sh +154 -0
- package/apps/studio/bin/import-asset.sh +34 -0
- package/apps/studio/bin/import-brand.sh +29 -0
- package/apps/studio/bin/import-tokens.sh +33 -0
- 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 +258 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/bin/transcribe.sh +39 -0
- package/apps/studio/canvas-edit.ts +656 -12
- package/apps/studio/canvas-lib.tsx +637 -10
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +2385 -699
- package/apps/studio/client/export-center.jsx +35 -1
- package/apps/studio/client/generate-dialog.jsx +352 -0
- package/apps/studio/client/github.js +18 -0
- package/apps/studio/client/inspector-controls.jsx +781 -0
- package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
- package/apps/studio/client/panels/ChatPanel.jsx +15 -33
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
- package/apps/studio/client/panels/ReadinessList.jsx +227 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
- 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 +432 -0
- package/apps/studio/client/styles/3-shell-maude.css +376 -28
- package/apps/studio/client/styles/4-components.css +223 -0
- package/apps/studio/client/styles/6-acp-chat.css +86 -0
- package/apps/studio/client/tour/quick-setup-tour.js +39 -0
- package/apps/studio/commands/annotation-strokes-command.ts +13 -3
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/config.schema.json +70 -0
- package/apps/studio/context-menu.tsx +42 -8
- package/apps/studio/context.ts +16 -0
- package/apps/studio/design-setup-readiness.ts +115 -0
- package/apps/studio/dist/client.bundle.js +5572 -21
- 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/draw/palette.ts +34 -0
- package/apps/studio/exporters/jobs.ts +47 -5
- package/apps/studio/exporters/video.ts +24 -4
- package/apps/studio/footage/schema.test.ts +287 -0
- package/apps/studio/footage/schema.ts +662 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
- package/apps/studio/generation/adapters/gemini.ts +542 -0
- package/apps/studio/generation/adapters/groq.test.ts +80 -0
- package/apps/studio/generation/adapters/groq.ts +247 -0
- package/apps/studio/generation/audio-library.test.ts +95 -0
- package/apps/studio/generation/audio-library.ts +156 -0
- package/apps/studio/generation/captions.test.ts +78 -0
- package/apps/studio/generation/captions.ts +132 -0
- package/apps/studio/generation/download.ts +76 -0
- package/apps/studio/generation/elevenlabs.test.ts +223 -0
- package/apps/studio/generation/gemini.test.ts +350 -0
- package/apps/studio/generation/jobs.test.ts +128 -0
- package/apps/studio/generation/jobs.ts +243 -0
- package/apps/studio/generation/keys.test.ts +75 -0
- package/apps/studio/generation/keys.ts +162 -0
- package/apps/studio/generation/prefs.test.ts +95 -0
- package/apps/studio/generation/prefs.ts +82 -0
- package/apps/studio/generation/registry.ts +88 -0
- package/apps/studio/generation/types.test.ts +123 -0
- package/apps/studio/generation/types.ts +266 -0
- package/apps/studio/generation/whisper-models.test.ts +152 -0
- package/apps/studio/generation/whisper-models.ts +257 -0
- package/apps/studio/generation/whisper-spike-results.md +61 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +199 -21
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +1039 -22
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/media/intro.mp4 +0 -0
- package/apps/studio/media-commit-chain.ts +119 -0
- package/apps/studio/paths.ts +20 -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/readiness.ts +108 -20
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/scaffold-design.ts +1145 -0
- package/apps/studio/server.ts +18 -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/_helpers.ts +15 -2
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-bridge.test.ts +34 -1
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/acp-env.test.ts +30 -0
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
- package/apps/studio/test/acp-session-plugins.test.ts +72 -1
- package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
- package/apps/studio/test/annotations-layer.test.ts +174 -0
- 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 +50 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
- package/apps/studio/test/csrf-write-guard.test.ts +23 -1
- package/apps/studio/test/design-setup-readiness.test.ts +155 -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 +154 -0
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/generate-route.test.ts +106 -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/import-asset-browser.test.ts +64 -0
- package/apps/studio/test/import-asset-route.test.ts +71 -0
- package/apps/studio/test/import-asset.test.ts +399 -0
- package/apps/studio/test/import-brand-browser.test.ts +88 -0
- package/apps/studio/test/import-brand-route.test.ts +120 -0
- package/apps/studio/test/import-brand.test.ts +206 -0
- package/apps/studio/test/import-tokens.test.ts +722 -0
- 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/media-commit-chain.test.ts +210 -0
- package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -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/readiness.test.ts +13 -11
- package/apps/studio/test/scaffold-design.test.ts +122 -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/tour-overlay.test.tsx +16 -0
- 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 +49 -21
- package/apps/studio/ui-prefs.ts +130 -0
- 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 +40 -1
- package/apps/studio/use-chrome-visibility.tsx +8 -2
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +149 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/bin/maude.mjs +6 -6
- package/cli/commands/design.mjs +67 -3
- package/cli/lib/gitignore-block.mjs +2 -0
- package/cli/lib/pkg-root.mjs +107 -0
- package/cli/lib/pkg-root.test.mjs +79 -0
- package/cli/lib/reconstruct-toolset.test.mjs +194 -0
- package/cli/lib/update-check.mjs +8 -0
- package/package.json +13 -11
- package/plugins/design/dependencies.json +18 -0
- package/plugins/design/templates/_shell.html +1 -0
|
@@ -0,0 +1,432 @@
|
|
|
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
|
+
// Controls come from the shared inspector-controls library
|
|
16
|
+
// (feature-inspector-controls-redesign) so bounded adjustments render as REAL
|
|
17
|
+
// sliders (track/thumb, keyboard-accessible) linked to an exact numeric field,
|
|
18
|
+
// and every row carries a single control (the old DUOTONE / Pattern rows doubled
|
|
19
|
+
// up two controls and overflowed the 304px panel).
|
|
20
|
+
//
|
|
21
|
+
// Sections + ranges mirror photo/schema.ts exactly:
|
|
22
|
+
// Adjustments (brightness/contrast/saturation/exposure −1..1, hue −180..180,
|
|
23
|
+
// sepia/grayscale/invert 0..1) · Duotone · Grain · Pattern · Mask · Background.
|
|
24
|
+
|
|
25
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
26
|
+
import { ColorField, PanelSection, Select, SliderField, Toggle } from './inspector-controls.jsx';
|
|
27
|
+
|
|
28
|
+
const PATTERN_TYPES = ['dots', 'grid', 'lines', 'diagonal', 'crosshatch'];
|
|
29
|
+
const PATTERN_BLENDS = ['normal', 'multiply', 'screen', 'overlay', 'soft-light'];
|
|
30
|
+
const MASK_PRESETS = ['none', 'vignette', 'radial-reveal', 'edge-fade'];
|
|
31
|
+
|
|
32
|
+
const ADJUSTMENTS = [
|
|
33
|
+
{ key: 'brightness', label: 'Brightness', min: -1, max: 1 },
|
|
34
|
+
{ key: 'contrast', label: 'Contrast', min: -1, max: 1 },
|
|
35
|
+
{ key: 'saturation', label: 'Saturation', min: -1, max: 1 },
|
|
36
|
+
{ key: 'exposure', label: 'Exposure', min: -1, max: 1 },
|
|
37
|
+
{ key: 'hue', label: 'Hue', min: -180, max: 180, step: 1, unit: '°' },
|
|
38
|
+
{ key: 'sepia', label: 'Sepia', min: 0, max: 1 },
|
|
39
|
+
{ key: 'grayscale', label: 'Grayscale', min: 0, max: 1 },
|
|
40
|
+
{ key: 'invert', label: 'Invert', min: 0, max: 1 },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Deep-clone a small plain edit object (structuredClone is available in the shell).
|
|
44
|
+
const clone = (o) => (o ? JSON.parse(JSON.stringify(o)) : {});
|
|
45
|
+
|
|
46
|
+
/** Prune empty sub-objects so a neutral edit serializes back to `{}`. */
|
|
47
|
+
function prune(edit) {
|
|
48
|
+
const out = {};
|
|
49
|
+
for (const [k, v] of Object.entries(edit)) {
|
|
50
|
+
if (v == null) continue;
|
|
51
|
+
if (typeof v === 'object' && !Array.isArray(v)) {
|
|
52
|
+
const inner = {};
|
|
53
|
+
for (const [ik, iv] of Object.entries(v)) if (iv != null && iv !== '') inner[ik] = iv;
|
|
54
|
+
if (Object.keys(inner).length) out[k] = inner;
|
|
55
|
+
} else {
|
|
56
|
+
out[k] = v;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── small inline-styled primitives ──────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
// Theme tokens — the REAL shell tokens (client/styles/1-tokens.css), defined
|
|
65
|
+
// for both `:root`/`[data-theme="light"]` and `[data-theme="dark"]`.
|
|
66
|
+
const S = {
|
|
67
|
+
body: { fontSize: 12, color: 'var(--fg-0)', overflowY: 'auto' },
|
|
68
|
+
sec: { marginBottom: 'var(--space-4)' },
|
|
69
|
+
secHead: {
|
|
70
|
+
display: 'flex',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
justifyContent: 'space-between',
|
|
73
|
+
fontSize: 11,
|
|
74
|
+
textTransform: 'uppercase',
|
|
75
|
+
letterSpacing: 'var(--tracking-wide)',
|
|
76
|
+
color: 'var(--fg-2)',
|
|
77
|
+
margin: '4px 0 8px',
|
|
78
|
+
},
|
|
79
|
+
row: { display: 'flex', alignItems: 'center', gap: 8, margin: 'var(--space-2) 0' },
|
|
80
|
+
label: { flex: '0 0 72px', fontSize: 11, color: 'var(--fg-1)' },
|
|
81
|
+
tip: {
|
|
82
|
+
flex: 1,
|
|
83
|
+
fontFamily: 'var(--font-mono, ui-monospace, monospace)',
|
|
84
|
+
fontSize: 10,
|
|
85
|
+
color: 'var(--fg-2)',
|
|
86
|
+
textAlign: 'left',
|
|
87
|
+
},
|
|
88
|
+
// handoff — tips go BELOW the control (help text), not inline right, aligned
|
|
89
|
+
// under the control column (past the 72px label).
|
|
90
|
+
tipBelow: { marginLeft: 80, marginTop: 2, marginBottom: 'var(--space-2)', fontSize: 10, color: 'var(--fg-2)', lineHeight: 1.4 },
|
|
91
|
+
reset: {
|
|
92
|
+
font: 'inherit',
|
|
93
|
+
fontSize: 10,
|
|
94
|
+
padding: '1px 6px',
|
|
95
|
+
borderRadius: 'var(--radius-xs)',
|
|
96
|
+
border: '1px solid transparent',
|
|
97
|
+
background: 'transparent',
|
|
98
|
+
color: 'var(--fg-2)',
|
|
99
|
+
cursor: 'pointer',
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// handoff — photo sections are now the shared collapsable PanelSection (caret +
|
|
104
|
+
// height animation), with the enable Toggle in the header's `right` slot.
|
|
105
|
+
function Section({ title, right, onReset, children }) {
|
|
106
|
+
return (
|
|
107
|
+
<PanelSection title={title} right={right} onReset={onReset}>
|
|
108
|
+
{children}
|
|
109
|
+
</PanelSection>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// One label + one control per row — the fix for the doubled-up DUOTONE / Pattern
|
|
114
|
+
// rows that overflowed the panel. SliderField/Select fill the control side.
|
|
115
|
+
function Row({ label, tip, children }) {
|
|
116
|
+
return (
|
|
117
|
+
<>
|
|
118
|
+
<div style={S.row}>
|
|
119
|
+
<span style={S.label}>{label}</span>
|
|
120
|
+
{children}
|
|
121
|
+
</div>
|
|
122
|
+
{tip ? <div style={S.tipBelow}>{tip}</div> : null}
|
|
123
|
+
</>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Compact colour field for the photo panel — the shared ColorField shell (swatch
|
|
128
|
+
// flush prefix + value) whose swatch opens a popover holding the injected HSV
|
|
129
|
+
// ColorPicker (falls back to a native colour input). 1:1 with the CSS panel's
|
|
130
|
+
// colour control, just with the pixel-level picker instead of the token popover.
|
|
131
|
+
function PhotoColorField({ value, fallback, label, ColorPicker, onApply }) {
|
|
132
|
+
const [open, setOpen] = useState(false);
|
|
133
|
+
const wrapRef = useRef(null);
|
|
134
|
+
const hex = value || fallback;
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!open) return undefined;
|
|
137
|
+
const onDoc = (e) => {
|
|
138
|
+
if (!wrapRef.current?.contains(e.target)) setOpen(false);
|
|
139
|
+
};
|
|
140
|
+
document.addEventListener('pointerdown', onDoc, true);
|
|
141
|
+
return () => document.removeEventListener('pointerdown', onDoc, true);
|
|
142
|
+
}, [open]);
|
|
143
|
+
return (
|
|
144
|
+
<ColorField
|
|
145
|
+
swatch={
|
|
146
|
+
<span className="st-cp-tokwrap" ref={wrapRef}>
|
|
147
|
+
<button type="button" className="st-cp-cf-sw" style={{ background: hex }} aria-haspopup="dialog" aria-expanded={open} aria-label={label} title={hex} onClick={() => setOpen((o) => !o)} />
|
|
148
|
+
{open ? (
|
|
149
|
+
<div className="st-cp-pop" role="dialog" aria-label={label} style={{ padding: 'var(--space-2)' }}>
|
|
150
|
+
{ColorPicker ? (
|
|
151
|
+
<ColorPicker seed={hex} label={label} onApply={onApply} />
|
|
152
|
+
) : (
|
|
153
|
+
<input type="color" value={hex} aria-label={label} onChange={(e) => onApply(e.target.value)} style={{ width: '100%', height: 32 }} />
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
) : null}
|
|
157
|
+
</span>
|
|
158
|
+
}
|
|
159
|
+
displayValue={(hex || '').replace('#', '')}
|
|
160
|
+
ariaLabel={label}
|
|
161
|
+
onValue={(v) => onApply(v.startsWith('#') ? v : `#${v.replace(/[^0-9a-fA-F]/g, '').slice(0, 6)}`)}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── the panel ────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {object} p
|
|
170
|
+
* @param {string} p.asset `assets/<sha8>.<ext>` source path.
|
|
171
|
+
* @param {object} [p.initialEdit] Seed PhotoEdit (else fetched on mount).
|
|
172
|
+
* @param {Function} [p.ColorPicker] Injected HSV picker component (optional).
|
|
173
|
+
* @param {Function} [p.onEdit] (edit) => void — live-preview broadcast.
|
|
174
|
+
* @param {Function} [p.onRemoveBackground] async (asset) => { maskAsset } | null.
|
|
175
|
+
* @param {Function} [p.onRecordEdit] (before, after) => void — undo integration.
|
|
176
|
+
* @param {Function} [p.StIcon] Injected shell icon component (optional).
|
|
177
|
+
*/
|
|
178
|
+
export function PhotoKnobs({ asset, initialEdit, ColorPicker, onEdit, onRemoveBackground, onRecordEdit, StIcon }) {
|
|
179
|
+
const [edit, setEditState] = useState(() => clone(initialEdit));
|
|
180
|
+
const [loading, setLoading] = useState(!initialEdit);
|
|
181
|
+
const [saveState, setSaveState] = useState('idle'); // idle | saving | saved | error
|
|
182
|
+
const [bgBusy, setBgBusy] = useState(false);
|
|
183
|
+
// sr-only live-region text for the background-removal busy state (Task 19).
|
|
184
|
+
const [bgAnnounce, setBgAnnounce] = useState('');
|
|
185
|
+
const putTimer = useRef(null);
|
|
186
|
+
const editRef = useRef(edit);
|
|
187
|
+
editRef.current = edit;
|
|
188
|
+
|
|
189
|
+
// Hydrate from the sidecar unless seeded.
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (initialEdit || !asset) return;
|
|
192
|
+
let dead = false;
|
|
193
|
+
setLoading(true);
|
|
194
|
+
fetch(`/_api/photo-edit?asset=${encodeURIComponent(asset)}`)
|
|
195
|
+
.then((r) => (r.ok ? r.json() : {}))
|
|
196
|
+
.then((j) => {
|
|
197
|
+
if (dead) return;
|
|
198
|
+
setEditState(j && typeof j === 'object' ? j : {});
|
|
199
|
+
setLoading(false);
|
|
200
|
+
})
|
|
201
|
+
.catch(() => !dead && setLoading(false));
|
|
202
|
+
return () => {
|
|
203
|
+
dead = true;
|
|
204
|
+
};
|
|
205
|
+
}, [asset, initialEdit]);
|
|
206
|
+
|
|
207
|
+
const put = useCallback(
|
|
208
|
+
(next) => {
|
|
209
|
+
clearTimeout(putTimer.current);
|
|
210
|
+
putTimer.current = setTimeout(async () => {
|
|
211
|
+
setSaveState('saving');
|
|
212
|
+
try {
|
|
213
|
+
const res = await fetch(`/_api/photo-edit?asset=${encodeURIComponent(asset)}`, {
|
|
214
|
+
method: 'PUT',
|
|
215
|
+
headers: { 'Content-Type': 'application/json' },
|
|
216
|
+
body: JSON.stringify(prune(next)),
|
|
217
|
+
});
|
|
218
|
+
setSaveState(res.ok ? 'saved' : 'error');
|
|
219
|
+
} catch {
|
|
220
|
+
setSaveState('error');
|
|
221
|
+
}
|
|
222
|
+
}, 160);
|
|
223
|
+
},
|
|
224
|
+
[asset]
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Apply a mutation, push the optimistic edit to the live preview, persist
|
|
228
|
+
// (debounced), and record undo (before → after).
|
|
229
|
+
const mutate = useCallback(
|
|
230
|
+
(fn, { commit = false } = {}) => {
|
|
231
|
+
const before = editRef.current;
|
|
232
|
+
const next = clone(before);
|
|
233
|
+
fn(next);
|
|
234
|
+
setEditState(next);
|
|
235
|
+
onEdit?.(next);
|
|
236
|
+
put(next);
|
|
237
|
+
if (commit) onRecordEdit?.(before, next);
|
|
238
|
+
},
|
|
239
|
+
[onEdit, put, onRecordEdit]
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const setAdj = (key, value, commit) =>
|
|
243
|
+
mutate((e) => {
|
|
244
|
+
e.adjustments = e.adjustments || {};
|
|
245
|
+
if (value === 0 || value == null) delete e.adjustments[key];
|
|
246
|
+
else e.adjustments[key] = value;
|
|
247
|
+
if (Object.keys(e.adjustments).length === 0) delete e.adjustments;
|
|
248
|
+
}, { commit });
|
|
249
|
+
|
|
250
|
+
const setSection = (section, patch, commit) =>
|
|
251
|
+
mutate((e) => {
|
|
252
|
+
e[section] = { ...(e[section] || {}), ...patch };
|
|
253
|
+
}, { commit });
|
|
254
|
+
|
|
255
|
+
const resetAdjustments = () => mutate((e) => delete e.adjustments, { commit: true });
|
|
256
|
+
// handoff — per-section reset (⟲): remove the section → back to its defaults.
|
|
257
|
+
const clearSection = (section) => mutate((e) => { delete e[section]; }, { commit: true });
|
|
258
|
+
|
|
259
|
+
// Shared by both the initial "Remove Background" and the "redo" button —
|
|
260
|
+
// drives the busy state + the sr-only live-region announcement (Task 19).
|
|
261
|
+
const runRemoveBackground = async () => {
|
|
262
|
+
if (!onRemoveBackground) return;
|
|
263
|
+
setBgBusy(true);
|
|
264
|
+
setBgAnnounce('Removing background…');
|
|
265
|
+
try {
|
|
266
|
+
const res = await onRemoveBackground(asset);
|
|
267
|
+
if (res?.maskAsset) {
|
|
268
|
+
setSection('backgroundRemoved', { enabled: true, maskAsset: res.maskAsset }, true);
|
|
269
|
+
setBgAnnounce('Background removed');
|
|
270
|
+
} else {
|
|
271
|
+
setBgAnnounce('Background removal failed');
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
setBgAnnounce('Background removal failed');
|
|
275
|
+
} finally {
|
|
276
|
+
setBgBusy(false);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
if (loading) return <div style={{ ...S.body, opacity: 0.6 }}>Loading photo edit…</div>;
|
|
281
|
+
|
|
282
|
+
const adj = edit.adjustments || {};
|
|
283
|
+
const duo = edit.duotone || {};
|
|
284
|
+
const grain = edit.grain || {};
|
|
285
|
+
const pat = edit.pattern || {};
|
|
286
|
+
const mask = edit.mask || {};
|
|
287
|
+
const bg = edit.backgroundRemoved || {};
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<div style={S.body} data-testid="photo-knobs">
|
|
291
|
+
<div style={{ ...S.secHead, marginTop: 0, color: 'var(--fg-2)', textTransform: 'none', letterSpacing: 0 }}>
|
|
292
|
+
<span style={{ fontFamily: 'var(--font-mono, monospace)', fontSize: 10 }}>{asset}</span>
|
|
293
|
+
<span style={{ fontSize: 10, color: saveState === 'error' ? 'var(--status-error)' : undefined }}>
|
|
294
|
+
{saveState === 'saving' ? 'saving…' : saveState === 'error' ? '⚠ save failed' : saveState === 'saved' ? 'saved' : ''}
|
|
295
|
+
</span>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
<Section title="Adjustments" onReset={resetAdjustments}>
|
|
299
|
+
{ADJUSTMENTS.map((a) => (
|
|
300
|
+
<Row key={a.key} label={a.label}>
|
|
301
|
+
<SliderField
|
|
302
|
+
value={adj[a.key]}
|
|
303
|
+
min={a.min}
|
|
304
|
+
max={a.max}
|
|
305
|
+
step={a.step ?? 0.01}
|
|
306
|
+
unit={a.unit ?? ''}
|
|
307
|
+
ariaLabel={a.label}
|
|
308
|
+
onInput={(v) => setAdj(a.key, v)}
|
|
309
|
+
onCommit={(v) => setAdj(a.key, v, true)}
|
|
310
|
+
/>
|
|
311
|
+
</Row>
|
|
312
|
+
))}
|
|
313
|
+
</Section>
|
|
314
|
+
|
|
315
|
+
<Section
|
|
316
|
+
title="Duotone"
|
|
317
|
+
right={<Toggle checked={duo.enabled} onChange={(v) => setSection('duotone', { enabled: v }, true)} label="on" />}
|
|
318
|
+
onReset={() => clearSection('duotone')}
|
|
319
|
+
>
|
|
320
|
+
{duo.enabled && (
|
|
321
|
+
<>
|
|
322
|
+
<Row label="Shadow">
|
|
323
|
+
<PhotoColorField value={duo.colorA} fallback="#111111" label="Shadow" ColorPicker={ColorPicker} onApply={(hex) => setSection('duotone', { colorA: hex }, true)} />
|
|
324
|
+
</Row>
|
|
325
|
+
<Row label="Highlight">
|
|
326
|
+
<PhotoColorField value={duo.colorB} fallback="#ffffff" label="Highlight" ColorPicker={ColorPicker} onApply={(hex) => setSection('duotone', { colorB: hex }, true)} />
|
|
327
|
+
</Row>
|
|
328
|
+
<Row label="Intensity">
|
|
329
|
+
<SliderField value={duo.intensity ?? 1} min={0} max={1} ariaLabel="Duotone intensity" onInput={(v) => setSection('duotone', { intensity: v })} onCommit={(v) => setSection('duotone', { intensity: v }, true)} />
|
|
330
|
+
</Row>
|
|
331
|
+
</>
|
|
332
|
+
)}
|
|
333
|
+
</Section>
|
|
334
|
+
|
|
335
|
+
<Section
|
|
336
|
+
title="Grain"
|
|
337
|
+
right={<Toggle checked={grain.enabled} onChange={(v) => setSection('grain', { enabled: v }, true)} label="on" />}
|
|
338
|
+
onReset={() => clearSection('grain')}
|
|
339
|
+
>
|
|
340
|
+
{grain.enabled && (
|
|
341
|
+
<>
|
|
342
|
+
<Row label="Amount">
|
|
343
|
+
<SliderField value={grain.amount ?? 0.4} min={0} max={1} ariaLabel="Grain amount" onInput={(v) => setSection('grain', { amount: v })} onCommit={(v) => setSection('grain', { amount: v }, true)} />
|
|
344
|
+
</Row>
|
|
345
|
+
<Row label="Size">
|
|
346
|
+
{/* Task 7 (feature-inspector-controls-redesign) — range reconciled
|
|
347
|
+
against photo/schema.ts's clamp (num(errors, g, 'size', 1, 32));
|
|
348
|
+
the panel used to cap at 8, silently hiding the top 3/4 of what
|
|
349
|
+
the server would actually accept. */}
|
|
350
|
+
<SliderField value={grain.size ?? 1} min={1} max={32} step={1} ariaLabel="Grain size" onInput={(v) => setSection('grain', { size: v })} onCommit={(v) => setSection('grain', { size: v }, true)} />
|
|
351
|
+
</Row>
|
|
352
|
+
</>
|
|
353
|
+
)}
|
|
354
|
+
</Section>
|
|
355
|
+
|
|
356
|
+
<Section
|
|
357
|
+
title="Pattern"
|
|
358
|
+
right={<Toggle checked={pat.enabled} onChange={(v) => setSection('pattern', { enabled: v }, true)} label="on" />}
|
|
359
|
+
onReset={() => clearSection('pattern')}
|
|
360
|
+
>
|
|
361
|
+
{pat.enabled && (
|
|
362
|
+
<>
|
|
363
|
+
<Row label="Type">
|
|
364
|
+
<Select value={pat.type || 'dots'} options={PATTERN_TYPES} ariaLabel="Pattern type" onChange={(v) => setSection('pattern', { type: v }, true)} />
|
|
365
|
+
</Row>
|
|
366
|
+
<Row label="Blend">
|
|
367
|
+
<Select value={pat.blend || 'normal'} options={PATTERN_BLENDS} ariaLabel="Pattern blend" onChange={(v) => setSection('pattern', { blend: v }, true)} />
|
|
368
|
+
</Row>
|
|
369
|
+
<Row label="Color" tip="tip: dark colour + Multiply blend reads best">
|
|
370
|
+
<PhotoColorField value={pat.color} fallback="#ffffff" label="Pattern color" ColorPicker={ColorPicker} onApply={(hex) => setSection('pattern', { color: hex }, true)} />
|
|
371
|
+
</Row>
|
|
372
|
+
<Row label="Scale">
|
|
373
|
+
{/* Task 7 — reconciled against schema.ts's clamp (num(errors, p,
|
|
374
|
+
'scale', 0.1, 16)); the panel used to cap at 4. */}
|
|
375
|
+
<SliderField value={pat.scale ?? 1} min={0.1} max={16} step={0.1} ariaLabel="Pattern scale" onInput={(v) => setSection('pattern', { scale: v })} onCommit={(v) => setSection('pattern', { scale: v }, true)} />
|
|
376
|
+
</Row>
|
|
377
|
+
<Row label="Opacity">
|
|
378
|
+
<SliderField value={pat.opacity ?? 0.5} min={0} max={1} ariaLabel="Pattern opacity" onInput={(v) => setSection('pattern', { opacity: v })} onCommit={(v) => setSection('pattern', { opacity: v }, true)} />
|
|
379
|
+
</Row>
|
|
380
|
+
</>
|
|
381
|
+
)}
|
|
382
|
+
</Section>
|
|
383
|
+
|
|
384
|
+
<Section title="Mask" onReset={() => clearSection('mask')}>
|
|
385
|
+
<Row label="Preset">
|
|
386
|
+
<Select value={mask.preset || 'none'} options={MASK_PRESETS} ariaLabel="Mask preset" onChange={(v) => setSection('mask', { preset: v }, true)} />
|
|
387
|
+
</Row>
|
|
388
|
+
{mask.preset && mask.preset !== 'none' && (
|
|
389
|
+
<Row label="Strength">
|
|
390
|
+
<SliderField value={mask.strength ?? 0.6} min={0} max={1} ariaLabel="Mask strength" onInput={(v) => setSection('mask', { strength: v })} onCommit={(v) => setSection('mask', { strength: v }, true)} />
|
|
391
|
+
</Row>
|
|
392
|
+
)}
|
|
393
|
+
</Section>
|
|
394
|
+
|
|
395
|
+
<Section title="Background" onReset={() => clearSection('backgroundRemoved')}>
|
|
396
|
+
<div style={{ ...S.row, gap: 10 }} aria-busy={bgBusy || undefined}>
|
|
397
|
+
{bg.maskAsset ? (
|
|
398
|
+
<>
|
|
399
|
+
<Toggle checked={bg.enabled} onChange={(v) => setSection('backgroundRemoved', { enabled: v }, true)} label="applied" />
|
|
400
|
+
<button
|
|
401
|
+
type="button"
|
|
402
|
+
style={{ ...S.reset, opacity: bgBusy ? 0.6 : 1 }}
|
|
403
|
+
disabled={bgBusy || !onRemoveBackground}
|
|
404
|
+
title="Run background removal again"
|
|
405
|
+
onClick={() => runRemoveBackground()}
|
|
406
|
+
>
|
|
407
|
+
{bgBusy ? 'removing…' : 'redo'}
|
|
408
|
+
</button>
|
|
409
|
+
</>
|
|
410
|
+
) : (
|
|
411
|
+
<button
|
|
412
|
+
type="button"
|
|
413
|
+
className="st-btn"
|
|
414
|
+
style={{ opacity: bgBusy ? 0.6 : 1 }}
|
|
415
|
+
disabled={bgBusy || !onRemoveBackground}
|
|
416
|
+
data-testid="photo-remove-bg"
|
|
417
|
+
onClick={() => runRemoveBackground()}
|
|
418
|
+
>
|
|
419
|
+
{StIcon ? <StIcon name="sparkle" size={14} /> : null}
|
|
420
|
+
{bgBusy ? 'Removing…' : 'Remove Background'}
|
|
421
|
+
</button>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
<span aria-live="polite" className="sr-only">
|
|
425
|
+
{bgAnnounce}
|
|
426
|
+
</span>
|
|
427
|
+
</Section>
|
|
428
|
+
</div>
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export default PhotoKnobs;
|