@1agh/maude 0.43.0 → 0.45.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/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 +396 -46
- package/apps/studio/api.ts +395 -2
- 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/_transcribe.mjs +419 -0
- package/apps/studio/bin/_transcribe.test.mjs +80 -0
- 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/photo-bg-remove.sh +119 -21
- package/apps/studio/bin/transcribe.sh +39 -0
- package/apps/studio/canvas-edit.ts +197 -2
- package/apps/studio/canvas-lib.tsx +115 -6
- package/apps/studio/client/app.jsx +1954 -685
- package/apps/studio/client/generate-dialog.jsx +352 -0
- package/apps/studio/client/github.js +14 -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/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/SettingsPanel.jsx +828 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
- package/apps/studio/client/photo-knobs.jsx +167 -172
- package/apps/studio/client/styles/3-shell-maude.css +334 -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/config.schema.json +70 -0
- package/apps/studio/context-menu.tsx +11 -2
- package/apps/studio/context.ts +16 -0
- package/apps/studio/design-setup-readiness.ts +115 -0
- package/apps/studio/dist/client.bundle.js +2097 -2088
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/draw/palette.ts +34 -0
- package/apps/studio/footage/schema.test.ts +92 -0
- package/apps/studio/footage/schema.ts +179 -1
- 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/service.ts +3 -1
- package/apps/studio/http.ts +867 -9
- package/apps/studio/media/intro.mp4 +0 -0
- package/apps/studio/media-commit-chain.ts +119 -0
- package/apps/studio/paths.ts +10 -0
- package/apps/studio/readiness.ts +108 -20
- package/apps/studio/scaffold-design.ts +1145 -0
- package/apps/studio/server.ts +13 -1
- package/apps/studio/test/_helpers.ts +15 -2
- package/apps/studio/test/acp-bridge.test.ts +34 -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/canvas-origin-gate.test.ts +37 -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/element-structural-edit.test.ts +108 -0
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
- package/apps/studio/test/generate-route.test.ts +106 -0
- 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/media-commit-chain.test.ts +210 -0
- package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
- package/apps/studio/test/readiness.test.ts +13 -11
- package/apps/studio/test/scaffold-design.test.ts +122 -0
- package/apps/studio/test/tour-overlay.test.tsx +16 -0
- package/apps/studio/tool-palette.tsx +11 -6
- package/apps/studio/ui-prefs.ts +130 -0
- package/apps/studio/use-canvas-media-drop.tsx +4 -2
- package/apps/studio/use-chrome-visibility.tsx +8 -2
- package/apps/studio/whats-new.json +96 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/bin/maude.mjs +6 -6
- package/cli/commands/design.mjs +33 -0
- package/cli/commands/init.mjs +7 -1
- package/cli/lib/gitignore-block.mjs +1 -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 +12 -10
- package/plugins/design/dependencies.json +18 -0
- package/plugins/flow/.claude-plugin/config.schema.json +5 -0
- package/plugins/flow/templates/ai-skeleton/scenario-guide.md +39 -0
- package/plugins/flow/templates/ai-skeleton/scenarios/README.md +2 -0
|
@@ -12,11 +12,18 @@
|
|
|
12
12
|
// (app.jsx broadcasts the updated edit down to the canvas). Reuses the shell's
|
|
13
13
|
// HSV `ColorPicker` when injected; falls back to a native color input.
|
|
14
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
|
+
//
|
|
15
21
|
// Sections + ranges mirror photo/schema.ts exactly:
|
|
16
22
|
// Adjustments (brightness/contrast/saturation/exposure −1..1, hue −180..180,
|
|
17
23
|
// sepia/grayscale/invert 0..1) · Duotone · Grain · Pattern · Mask · Background.
|
|
18
24
|
|
|
19
25
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
26
|
+
import { ColorField, PanelSection, Select, SliderField, Toggle } from './inspector-controls.jsx';
|
|
20
27
|
|
|
21
28
|
const PATTERN_TYPES = ['dots', 'grid', 'lines', 'diagonal', 'crosshatch'];
|
|
22
29
|
const PATTERN_BLENDS = ['normal', 'multiply', 'screen', 'overlay', 'soft-light'];
|
|
@@ -55,122 +62,103 @@ function prune(edit) {
|
|
|
55
62
|
// ── small inline-styled primitives ──────────────────────────────────────────
|
|
56
63
|
|
|
57
64
|
// Theme tokens — the REAL shell tokens (client/styles/1-tokens.css), defined
|
|
58
|
-
// for both `:root`/`[data-theme="light"]` and `[data-theme="dark"]`.
|
|
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.
|
|
65
|
+
// for both `:root`/`[data-theme="light"]` and `[data-theme="dark"]`.
|
|
64
66
|
const S = {
|
|
65
|
-
body: {
|
|
66
|
-
sec: { marginBottom:
|
|
67
|
+
body: { fontSize: 12, color: 'var(--fg-0)', overflowY: 'auto' },
|
|
68
|
+
sec: { marginBottom: 'var(--space-4)' },
|
|
67
69
|
secHead: {
|
|
68
70
|
display: 'flex',
|
|
69
71
|
alignItems: 'center',
|
|
70
72
|
justifyContent: 'space-between',
|
|
71
73
|
fontSize: 11,
|
|
72
74
|
textTransform: 'uppercase',
|
|
73
|
-
letterSpacing: '
|
|
75
|
+
letterSpacing: 'var(--tracking-wide)',
|
|
74
76
|
color: 'var(--fg-2)',
|
|
75
77
|
margin: '4px 0 8px',
|
|
76
78
|
},
|
|
77
|
-
row: { display: 'flex', alignItems: 'center', gap: 8, margin: '
|
|
78
|
-
label: { flex: '0 0
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
flex: '0 0 42px',
|
|
82
|
-
textAlign: 'right',
|
|
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
83
|
fontFamily: 'var(--font-mono, ui-monospace, monospace)',
|
|
84
|
-
fontSize:
|
|
84
|
+
fontSize: 10,
|
|
85
85
|
color: 'var(--fg-2)',
|
|
86
|
+
textAlign: 'left',
|
|
86
87
|
},
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
},
|
|
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 },
|
|
97
91
|
reset: {
|
|
98
92
|
font: 'inherit',
|
|
99
93
|
fontSize: 10,
|
|
100
94
|
padding: '1px 6px',
|
|
101
|
-
borderRadius:
|
|
95
|
+
borderRadius: 'var(--radius-xs)',
|
|
102
96
|
border: '1px solid transparent',
|
|
103
97
|
background: 'transparent',
|
|
104
98
|
color: 'var(--fg-2)',
|
|
105
99
|
cursor: 'pointer',
|
|
106
100
|
},
|
|
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
101
|
};
|
|
117
102
|
|
|
118
|
-
|
|
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 }) {
|
|
119
106
|
return (
|
|
120
|
-
<
|
|
121
|
-
<div style={S.secHead}>
|
|
122
|
-
<span>{title}</span>
|
|
123
|
-
{right}
|
|
124
|
-
</div>
|
|
107
|
+
<PanelSection title={title} right={right} onReset={onReset}>
|
|
125
108
|
{children}
|
|
126
|
-
</
|
|
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>
|
|
109
|
+
</PanelSection>
|
|
152
110
|
);
|
|
153
111
|
}
|
|
154
112
|
|
|
155
|
-
|
|
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 }) {
|
|
156
116
|
return (
|
|
157
|
-
|
|
158
|
-
<
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
</>
|
|
161
124
|
);
|
|
162
125
|
}
|
|
163
126
|
|
|
164
|
-
|
|
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);
|
|
165
134
|
const hex = value || fallback;
|
|
166
|
-
|
|
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]);
|
|
167
143
|
return (
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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)}`)}
|
|
174
162
|
/>
|
|
175
163
|
);
|
|
176
164
|
}
|
|
@@ -185,12 +173,15 @@ function ColorSwatch({ value, fallback, ColorPicker, onApply }) {
|
|
|
185
173
|
* @param {Function} [p.onEdit] (edit) => void — live-preview broadcast.
|
|
186
174
|
* @param {Function} [p.onRemoveBackground] async (asset) => { maskAsset } | null.
|
|
187
175
|
* @param {Function} [p.onRecordEdit] (before, after) => void — undo integration.
|
|
176
|
+
* @param {Function} [p.StIcon] Injected shell icon component (optional).
|
|
188
177
|
*/
|
|
189
|
-
export function PhotoKnobs({ asset, initialEdit, ColorPicker, onEdit, onRemoveBackground, onRecordEdit }) {
|
|
178
|
+
export function PhotoKnobs({ asset, initialEdit, ColorPicker, onEdit, onRemoveBackground, onRecordEdit, StIcon }) {
|
|
190
179
|
const [edit, setEditState] = useState(() => clone(initialEdit));
|
|
191
180
|
const [loading, setLoading] = useState(!initialEdit);
|
|
192
181
|
const [saveState, setSaveState] = useState('idle'); // idle | saving | saved | error
|
|
193
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('');
|
|
194
185
|
const putTimer = useRef(null);
|
|
195
186
|
const editRef = useRef(edit);
|
|
196
187
|
editRef.current = edit;
|
|
@@ -262,6 +253,29 @@ export function PhotoKnobs({ asset, initialEdit, ColorPicker, onEdit, onRemoveBa
|
|
|
262
253
|
}, { commit });
|
|
263
254
|
|
|
264
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
|
+
};
|
|
265
279
|
|
|
266
280
|
if (loading) return <div style={{ ...S.body, opacity: 0.6 }}>Loading photo edit…</div>;
|
|
267
281
|
|
|
@@ -274,49 +288,46 @@ export function PhotoKnobs({ asset, initialEdit, ColorPicker, onEdit, onRemoveBa
|
|
|
274
288
|
|
|
275
289
|
return (
|
|
276
290
|
<div style={S.body} data-testid="photo-knobs">
|
|
277
|
-
<div style={{ ...S.secHead, marginTop: 0, color: 'var(--fg-
|
|
291
|
+
<div style={{ ...S.secHead, marginTop: 0, color: 'var(--fg-2)', textTransform: 'none', letterSpacing: 0 }}>
|
|
278
292
|
<span style={{ fontFamily: 'var(--font-mono, monospace)', fontSize: 10 }}>{asset}</span>
|
|
279
293
|
<span style={{ fontSize: 10, color: saveState === 'error' ? 'var(--status-error)' : undefined }}>
|
|
280
294
|
{saveState === 'saving' ? 'saving…' : saveState === 'error' ? '⚠ save failed' : saveState === 'saved' ? 'saved' : ''}
|
|
281
295
|
</span>
|
|
282
296
|
</div>
|
|
283
297
|
|
|
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
|
-
>
|
|
298
|
+
<Section title="Adjustments" onReset={resetAdjustments}>
|
|
292
299
|
{ADJUSTMENTS.map((a) => (
|
|
293
|
-
<
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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>
|
|
304
312
|
))}
|
|
305
313
|
</Section>
|
|
306
314
|
|
|
307
315
|
<Section
|
|
308
316
|
title="Duotone"
|
|
309
317
|
right={<Toggle checked={duo.enabled} onChange={(v) => setSection('duotone', { enabled: v }, true)} label="on" />}
|
|
318
|
+
onReset={() => clearSection('duotone')}
|
|
310
319
|
>
|
|
311
320
|
{duo.enabled && (
|
|
312
321
|
<>
|
|
313
|
-
<
|
|
314
|
-
<
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
<
|
|
318
|
-
</
|
|
319
|
-
<
|
|
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>
|
|
320
331
|
</>
|
|
321
332
|
)}
|
|
322
333
|
</Section>
|
|
@@ -324,11 +335,20 @@ export function PhotoKnobs({ asset, initialEdit, ColorPicker, onEdit, onRemoveBa
|
|
|
324
335
|
<Section
|
|
325
336
|
title="Grain"
|
|
326
337
|
right={<Toggle checked={grain.enabled} onChange={(v) => setSection('grain', { enabled: v }, true)} label="on" />}
|
|
338
|
+
onReset={() => clearSection('grain')}
|
|
327
339
|
>
|
|
328
340
|
{grain.enabled && (
|
|
329
341
|
<>
|
|
330
|
-
<
|
|
331
|
-
|
|
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>
|
|
332
352
|
</>
|
|
333
353
|
)}
|
|
334
354
|
</Section>
|
|
@@ -336,56 +356,44 @@ export function PhotoKnobs({ asset, initialEdit, ColorPicker, onEdit, onRemoveBa
|
|
|
336
356
|
<Section
|
|
337
357
|
title="Pattern"
|
|
338
358
|
right={<Toggle checked={pat.enabled} onChange={(v) => setSection('pattern', { enabled: v }, true)} label="on" />}
|
|
359
|
+
onReset={() => clearSection('pattern')}
|
|
339
360
|
>
|
|
340
361
|
{pat.enabled && (
|
|
341
362
|
<>
|
|
342
|
-
<
|
|
343
|
-
<
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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)} />
|
|
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>
|
|
367
380
|
</>
|
|
368
381
|
)}
|
|
369
382
|
</Section>
|
|
370
383
|
|
|
371
|
-
<Section title="Mask">
|
|
372
|
-
<
|
|
373
|
-
<
|
|
374
|
-
|
|
375
|
-
{MASK_PRESETS.map((m) => (
|
|
376
|
-
<option key={m} value={m}>
|
|
377
|
-
{m}
|
|
378
|
-
</option>
|
|
379
|
-
))}
|
|
380
|
-
</select>
|
|
381
|
-
</div>
|
|
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>
|
|
382
388
|
{mask.preset && mask.preset !== 'none' && (
|
|
383
|
-
<
|
|
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>
|
|
384
392
|
)}
|
|
385
393
|
</Section>
|
|
386
394
|
|
|
387
|
-
<Section title="Background">
|
|
388
|
-
<div style={{ ...S.row, gap: 10 }}>
|
|
395
|
+
<Section title="Background" onReset={() => clearSection('backgroundRemoved')}>
|
|
396
|
+
<div style={{ ...S.row, gap: 10 }} aria-busy={bgBusy || undefined}>
|
|
389
397
|
{bg.maskAsset ? (
|
|
390
398
|
<>
|
|
391
399
|
<Toggle checked={bg.enabled} onChange={(v) => setSection('backgroundRemoved', { enabled: v }, true)} label="applied" />
|
|
@@ -394,16 +402,7 @@ export function PhotoKnobs({ asset, initialEdit, ColorPicker, onEdit, onRemoveBa
|
|
|
394
402
|
style={{ ...S.reset, opacity: bgBusy ? 0.6 : 1 }}
|
|
395
403
|
disabled={bgBusy || !onRemoveBackground}
|
|
396
404
|
title="Run background removal again"
|
|
397
|
-
onClick={
|
|
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
|
-
}}
|
|
405
|
+
onClick={() => runRemoveBackground()}
|
|
407
406
|
>
|
|
408
407
|
{bgBusy ? 'removing…' : 'redo'}
|
|
409
408
|
</button>
|
|
@@ -411,24 +410,20 @@ export function PhotoKnobs({ asset, initialEdit, ColorPicker, onEdit, onRemoveBa
|
|
|
411
410
|
) : (
|
|
412
411
|
<button
|
|
413
412
|
type="button"
|
|
414
|
-
|
|
413
|
+
className="st-btn"
|
|
414
|
+
style={{ opacity: bgBusy ? 0.6 : 1 }}
|
|
415
415
|
disabled={bgBusy || !onRemoveBackground}
|
|
416
416
|
data-testid="photo-remove-bg"
|
|
417
|
-
onClick={
|
|
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
|
-
}}
|
|
417
|
+
onClick={() => runRemoveBackground()}
|
|
427
418
|
>
|
|
428
|
-
{
|
|
419
|
+
{StIcon ? <StIcon name="sparkle" size={14} /> : null}
|
|
420
|
+
{bgBusy ? 'Removing…' : 'Remove Background'}
|
|
429
421
|
</button>
|
|
430
422
|
)}
|
|
431
423
|
</div>
|
|
424
|
+
<span aria-live="polite" className="sr-only">
|
|
425
|
+
{bgAnnounce}
|
|
426
|
+
</span>
|
|
432
427
|
</Section>
|
|
433
428
|
</div>
|
|
434
429
|
);
|