refactor: remove inline gradient editor from CSS modal, use entity picker
Some checks failed
Lint & Test / test (push) Failing after 30s

Replace the gradient stop editor (canvas, markers, stop list) in the
CSS editor gradient section with a simple gradient entity selector.
Gradients are now created/edited exclusively in the Gradients tab.

Fix effect and audio palette pickers to populate from gradient entities
dynamically instead of hardcoded HTML options.
Unify all gradient/palette pickers via _buildGradientEntityItems().

Also: rename "None (use own speed)" → "None (no sync)" for sync clocks.
Add i18n keys for gradient selector and missing error messages.
This commit is contained in:
2026-03-24 14:09:49 +03:00
parent fc62d5d3b1
commit 178d115cc5
3 changed files with 32 additions and 109 deletions

View File

@@ -398,10 +398,7 @@ function _ensureEffectTypeIconSelect() {
function _ensureEffectPaletteIconSelect() { function _ensureEffectPaletteIconSelect() {
const sel = document.getElementById('css-editor-effect-palette') as HTMLSelectElement | null; const sel = document.getElementById('css-editor-effect-palette') as HTMLSelectElement | null;
if (!sel) return; if (!sel) return;
const gradients = _getGradients(); const items = _buildGradientEntityItems();
const items = gradients.map(g => ({
value: g.id, icon: _gradientEntityStripHTML(g.stops), label: g.name,
}));
if (_effectPaletteIconSelect) { _effectPaletteIconSelect.updateItems(items); return; } if (_effectPaletteIconSelect) { _effectPaletteIconSelect.updateItems(items); return; }
_effectPaletteIconSelect = new IconSelect({ target: sel, items, columns: 2 }); _effectPaletteIconSelect = new IconSelect({ target: sel, items, columns: 2 });
} }
@@ -435,10 +432,7 @@ function _ensureCandleTypeIconSelect() {
function _ensureAudioPaletteIconSelect() { function _ensureAudioPaletteIconSelect() {
const sel = document.getElementById('css-editor-audio-palette') as HTMLSelectElement | null; const sel = document.getElementById('css-editor-audio-palette') as HTMLSelectElement | null;
if (!sel) return; if (!sel) return;
const gradients = _getGradients(); const items = _buildGradientEntityItems();
const items = gradients.map(g => ({
value: g.id, icon: _gradientEntityStripHTML(g.stops), label: g.name,
}));
if (_audioPaletteIconSelect) { _audioPaletteIconSelect.updateItems(items); return; } if (_audioPaletteIconSelect) { _audioPaletteIconSelect.updateItems(items); return; }
_audioPaletteIconSelect = new IconSelect({ target: sel, items, columns: 2 }); _audioPaletteIconSelect = new IconSelect({ target: sel, items, columns: 2 });
} }
@@ -455,37 +449,27 @@ function _ensureAudioVizIconSelect() {
_audioVizIconSelect = new IconSelect({ target: sel, items, columns: 3 }); _audioVizIconSelect = new IconSelect({ target: sel, items, columns: 3 });
} }
function _buildGradientPresetItems() { function _buildGradientEntityItems() {
const gradients = _getGradients(); const gradients = _getGradients();
const builtInItems = gradients.filter(g => g.is_builtin).map(g => ({ return gradients.map(g => ({
value: g.id, icon: _gradientEntityStripHTML(g.stops), label: g.name, value: g.id, icon: _gradientEntityStripHTML(g.stops), label: g.name,
})); }));
const userItems = gradients.filter(g => !g.is_builtin).map(g => ({
value: g.id, icon: _gradientEntityStripHTML(g.stops), label: g.name,
isCustom: true,
}));
return [
{ value: '', icon: _icon(P.palette), label: t('color_strip.gradient.preset.custom') },
...builtInItems,
...userItems,
];
} }
function _ensureGradientPresetIconSelect() { function _ensureGradientPresetIconSelect() {
const sel = document.getElementById('css-editor-gradient-preset') as HTMLSelectElement | null; const sel = document.getElementById('css-editor-gradient-preset') as HTMLSelectElement | null;
if (!sel) return; if (!sel) return;
const items = _buildGradientPresetItems(); const items = _buildGradientEntityItems();
if (_gradientPresetIconSelect) { _gradientPresetIconSelect.updateItems(items); return; } if (_gradientPresetIconSelect) { _gradientPresetIconSelect.updateItems(items); return; }
_gradientPresetIconSelect = new IconSelect({ target: sel, items, columns: 3, onChange: (v) => onGradientPresetChange(v) }); _gradientPresetIconSelect = new IconSelect({ target: sel, items, columns: 3 });
} }
/** Rebuild the preset picker after adding/removing custom presets. */ /** Rebuild the gradient picker after entity changes. */
export function refreshGradientPresetPicker() { export function refreshGradientPresetPicker() {
if (_gradientPresetIconSelect) { const items = _buildGradientEntityItems();
_gradientPresetIconSelect.updateItems(_buildGradientPresetItems()); if (_gradientPresetIconSelect) _gradientPresetIconSelect.updateItems(items);
_gradientPresetIconSelect.setValue(''); if (_effectPaletteIconSelect) _effectPaletteIconSelect.updateItems(items);
} if (_audioPaletteIconSelect) _audioPaletteIconSelect.updateItems(items);
_renderCustomPresetList();
} }
/** Render the user-created gradient list below the save button. */ /** Render the user-created gradient list below the save button. */
@@ -1209,49 +1193,34 @@ const _typeHandlers: Record<string, { load: (...args: any[]) => any; reset: (...
}, },
gradient: { gradient: {
load(css) { load(css) {
const presetId = css.gradient_id || ''; const gradientId = css.gradient_id || '';
(document.getElementById('css-editor-gradient-preset') as HTMLInputElement).value = presetId; _ensureGradientPresetIconSelect();
if (_gradientPresetIconSelect) _gradientPresetIconSelect.setValue(presetId); (document.getElementById('css-editor-gradient-preset') as HTMLInputElement).value = gradientId;
// If gradient_id is set, load stops from the gradient entity if (_gradientPresetIconSelect) _gradientPresetIconSelect.setValue(gradientId);
let stops = css.stops || [
{ position: 0.0, color: [255, 0, 0] },
{ position: 1.0, color: [0, 0, 255] },
];
if (presetId) {
const g = _findGradient(presetId);
if (g) stops = g.stops;
}
gradientInit(stops);
_loadAnimationState(css.animation); _loadAnimationState(css.animation);
(document.getElementById('css-editor-gradient-easing') as HTMLSelectElement).value = css.easing || 'linear'; (document.getElementById('css-editor-gradient-easing') as HTMLSelectElement).value = css.easing || 'linear';
if (_gradientEasingIconSelect) _gradientEasingIconSelect.setValue(css.easing || 'linear'); if (_gradientEasingIconSelect) _gradientEasingIconSelect.setValue(css.easing || 'linear');
}, },
reset() { reset() {
(document.getElementById('css-editor-gradient-preset') as HTMLInputElement).value = ''; _ensureGradientPresetIconSelect();
if (_gradientPresetIconSelect) _gradientPresetIconSelect.setValue(''); // Default to first gradient
gradientInit([ const gradients = _getGradients();
{ position: 0.0, color: [255, 0, 0] }, const defaultId = gradients.length > 0 ? gradients[0].id : '';
{ position: 1.0, color: [0, 0, 255] }, (document.getElementById('css-editor-gradient-preset') as HTMLInputElement).value = defaultId;
]); if (_gradientPresetIconSelect) _gradientPresetIconSelect.setValue(defaultId);
_loadAnimationState(null); _loadAnimationState(null);
(document.getElementById('css-editor-gradient-easing') as HTMLSelectElement).value = 'linear'; (document.getElementById('css-editor-gradient-easing') as HTMLSelectElement).value = 'linear';
if (_gradientEasingIconSelect) _gradientEasingIconSelect.setValue('linear'); if (_gradientEasingIconSelect) _gradientEasingIconSelect.setValue('linear');
}, },
getPayload(name) { getPayload(name) {
const gStops = getGradientStops(); const gradientId = (document.getElementById('css-editor-gradient-preset') as HTMLInputElement).value;
if (gStops.length < 2) { if (!gradientId) {
cssEditorModal.showError(t('color_strip.gradient.min_stops')); cssEditorModal.showError(t('color_strip.gradient.error.no_gradient'));
return null; return null;
} }
const gradientId = (document.getElementById('css-editor-gradient-preset') as HTMLInputElement).value || null;
return { return {
name, name,
gradient_id: gradientId, gradient_id: gradientId,
stops: gStops.map(s => ({
position: s.position,
color: s.color,
...(s.colorRight ? { color_right: s.colorRight } : {}),
})),
animation: _getAnimationPayload(), animation: _getAnimationPayload(),
easing: (document.getElementById('css-editor-gradient-easing') as HTMLSelectElement).value, easing: (document.getElementById('css-editor-gradient-easing') as HTMLSelectElement).value,
}; };

View File

@@ -437,7 +437,7 @@
"common.none": "None", "common.none": "None",
"common.none_no_cspt": "None (no processing template)", "common.none_no_cspt": "None (no processing template)",
"common.none_no_input": "None (no input source)", "common.none_no_input": "None (no input source)",
"common.none_own_speed": "None (use own speed)", "common.none_own_speed": "None (no sync)",
"common.undo": "Undo", "common.undo": "Undo",
"validation.required": "This field is required", "validation.required": "This field is required",
"bulk.processing": "Processing…", "bulk.processing": "Processing…",
@@ -980,6 +980,9 @@
"color_strip.gradient.position": "Position (0.01.0)", "color_strip.gradient.position": "Position (0.01.0)",
"color_strip.gradient.bidir.hint": "Add a second color on the right side of this stop to create a hard edge in the gradient.", "color_strip.gradient.bidir.hint": "Add a second color on the right side of this stop to create a hard edge in the gradient.",
"color_strip.gradient.min_stops": "Gradient must have at least 2 stops", "color_strip.gradient.min_stops": "Gradient must have at least 2 stops",
"color_strip.gradient.select": "Gradient:",
"color_strip.gradient.select.hint": "Select a gradient from the library. Create and edit gradients in the Gradients tab.",
"color_strip.gradient.error.no_gradient": "Please select a gradient",
"color_strip.gradient.preset": "Preset:", "color_strip.gradient.preset": "Preset:",
"color_strip.gradient.preset.hint": "Load a predefined gradient palette. Selecting a preset replaces the current stops.", "color_strip.gradient.preset.hint": "Load a predefined gradient palette. Selecting a preset replaces the current stops.",
"color_strip.gradient.preset.custom": "— Custom —", "color_strip.gradient.preset.custom": "— Custom —",

View File

@@ -105,52 +105,12 @@
<div id="css-editor-gradient-section" style="display:none"> <div id="css-editor-gradient-section" style="display:none">
<div class="form-group"> <div class="form-group">
<div class="label-row"> <div class="label-row">
<label for="css-editor-gradient-preset" data-i18n="color_strip.gradient.preset">Preset:</label> <label for="css-editor-gradient-preset" data-i18n="color_strip.gradient.select">Gradient:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button> <button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div> </div>
<small class="input-hint" style="display:none" data-i18n="color_strip.gradient.preset.hint">Load a predefined gradient palette. Selecting a preset replaces the current stops.</small> <small class="input-hint" style="display:none" data-i18n="color_strip.gradient.select.hint">Select a gradient from the library. Create and edit gradients in the Gradients tab.</small>
<select id="css-editor-gradient-preset" onchange="onGradientPresetChange(this.value)"> <select id="css-editor-gradient-preset">
<option value="" data-i18n="color_strip.gradient.preset.custom">— Custom —</option>
<option value="rainbow" data-i18n="color_strip.gradient.preset.rainbow">Rainbow</option>
<option value="sunset" data-i18n="color_strip.gradient.preset.sunset">Sunset</option>
<option value="ocean" data-i18n="color_strip.gradient.preset.ocean">Ocean</option>
<option value="forest" data-i18n="color_strip.gradient.preset.forest">Forest</option>
<option value="fire" data-i18n="color_strip.gradient.preset.fire">Fire</option>
<option value="lava" data-i18n="color_strip.gradient.preset.lava">Lava</option>
<option value="aurora" data-i18n="color_strip.gradient.preset.aurora">Aurora</option>
<option value="ice" data-i18n="color_strip.gradient.preset.ice">Ice</option>
<option value="warm" data-i18n="color_strip.gradient.preset.warm">Warm</option>
<option value="cool" data-i18n="color_strip.gradient.preset.cool">Cool</option>
<option value="neon" data-i18n="color_strip.gradient.preset.neon">Neon</option>
<option value="pastel" data-i18n="color_strip.gradient.preset.pastel">Pastel</option>
</select> </select>
<div style="margin-top:6px;">
<button type="button" class="btn btn-secondary btn-sm"
onclick="promptAndSaveGradientPreset()"
data-i18n="color_strip.gradient.preset.save_button">Save as preset…</button>
</div>
<div id="css-editor-custom-presets-list" class="custom-presets-list"></div>
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.gradient.preview">Gradient:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.gradient.preview.hint">Visual preview. Click the marker track below to add a stop. Drag markers to reposition.</small>
<div class="gradient-editor">
<canvas id="gradient-canvas" height="44"></canvas>
<div id="gradient-markers-track" class="gradient-markers-track"></div>
</div>
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.gradient.stops">Color Stops:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.gradient.stops.hint">Each stop defines a color at a relative position (0.0 = start, 1.0 = end). The ↔ button adds a right-side color to create a hard edge at that stop.</small>
<div id="gradient-stops-list"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -199,16 +159,7 @@
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button> <button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div> </div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.palette.hint">Color palette used by the effect.</small> <small class="input-hint" style="display:none" data-i18n="color_strip.effect.palette.hint">Color palette used by the effect.</small>
<select id="css-editor-effect-palette" onchange="onEffectPaletteChange()"> <select id="css-editor-effect-palette">
<option value="fire" data-i18n="color_strip.palette.fire">Fire</option>
<option value="ocean" data-i18n="color_strip.palette.ocean">Ocean</option>
<option value="lava" data-i18n="color_strip.palette.lava">Lava</option>
<option value="forest" data-i18n="color_strip.palette.forest">Forest</option>
<option value="rainbow" data-i18n="color_strip.palette.rainbow">Rainbow</option>
<option value="aurora" data-i18n="color_strip.palette.aurora">Aurora</option>
<option value="sunset" data-i18n="color_strip.palette.sunset">Sunset</option>
<option value="ice" data-i18n="color_strip.palette.ice">Ice</option>
<option value="custom" data-i18n="color_strip.palette.custom">Custom</option>
</select> </select>
</div> </div>