refactor: split color-strips.ts into focused modules under color-strips/ folder
Lint & Test / test (push) Successful in 2m6s

Monolithic 3060-line color-strips.ts split into 11 modules:
- index.ts (core orchestrator, modal, type switching, editor, save, CRUD)
- cards.ts (card rendering for all source types)
- game-event.ts (game event mappings, presets, UI)
- gradient.ts (gradient entity modal + CRUD)
- audio.ts (audio viz widgets, load/reset state)
- math-wave.ts (wave layers + waveform selects)
- mapped.ts (mapped zone helpers)
- color-cycle.ts (color cycle add/remove/render)
- composite.ts, notification.ts, test.ts (previously extracted, moved into folder)
This commit is contained in:
2026-04-05 12:54:15 +03:00
parent ce53ca6872
commit 7a9c368448
15 changed files with 3249 additions and 3160 deletions
+1 -1
View File
@@ -145,7 +145,7 @@ import {
testColorStrip, testCSPT, closeTestCssSourceModal, applyCssTestSettings, fireCssTestNotification, fireCssTestNotificationLayer, testColorStrip, testCSPT, closeTestCssSourceModal, applyCssTestSettings, fireCssTestNotification, fireCssTestNotificationLayer,
addCSSGameMapping, removeCSSGameMapping, onCSSGameMappingPresetChange, addCSSGameMapping, removeCSSGameMapping, onCSSGameMappingPresetChange,
mathWaveAddLayer, mathWaveRemoveLayer, mathWaveAddLayer, mathWaveRemoveLayer,
} from './features/color-strips.ts'; } from './features/color-strips/index.ts';
// Layer 5: audio sources // Layer 5: audio sources
import { import {
@@ -11,7 +11,7 @@ import { t } from '../core/i18n.ts';
import { showToast } from '../core/ui.ts'; import { showToast } from '../core/ui.ts';
import { Modal } from '../core/modal.ts'; import { Modal } from '../core/modal.ts';
import { closeTutorial, startCalibrationTutorial } from './tutorials.ts'; import { closeTutorial, startCalibrationTutorial } from './tutorials.ts';
import { startCSSOverlay, stopCSSOverlay } from './color-strips.ts'; import { startCSSOverlay, stopCSSOverlay } from './color-strips/index.ts';
import { ICON_WARNING, ICON_ROTATE_CW, ICON_ROTATE_CCW } from '../core/icons.ts'; import { ICON_WARNING, ICON_ROTATE_CW, ICON_ROTATE_CCW } from '../core/icons.ts';
import type { Calibration } from '../types.ts'; import type { Calibration } from '../types.ts';
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,207 @@
/**
* Color Strip Sources — Audio visualization helpers.
* Extracted from color-strips.ts to reduce file size.
*/
import { escapeHtml } from '../../core/api.ts';
import { _cachedValueSources, audioSourcesCache } from '../../core/state.ts';
import { t } from '../../core/i18n.ts';
import { getAudioSourceIcon } from '../../core/icons.ts';
import * as P from '../../core/icon-paths.ts';
import { IconSelect } from '../../core/icon-select.ts';
import { EntitySelect } from '../../core/entity-palette.ts';
import { BindableScalarWidget } from '../../core/bindable-scalar.ts';
import { BindableColorWidget } from '../../core/bindable-color.ts';
const _icon = (d: any) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
/* ── State ───────────────────────────────────────────────────── */
let _audioSensitivityWidget: BindableScalarWidget | null = null;
let _audioSmoothingWidget: BindableScalarWidget | null = null;
let _audioBeatDecayWidget: BindableScalarWidget | null = null;
let _audioColorWidget: BindableColorWidget | null = null;
let _audioColorPeakWidget: BindableColorWidget | null = null;
let _cssAudioSourceEntitySelect: any = null;
let _audioVizIconSelect: any = null;
let _audioPaletteEntitySelect: EntitySelect | null = null;
/* ── Destroy (called from modal onForceClose) ─────────────────── */
export function destroyAudioWidgets() {
if (_audioSensitivityWidget) { _audioSensitivityWidget.destroy(); _audioSensitivityWidget = null; }
if (_audioSmoothingWidget) { _audioSmoothingWidget.destroy(); _audioSmoothingWidget = null; }
if (_audioColorWidget) { _audioColorWidget.destroy(); _audioColorWidget = null; }
if (_audioColorPeakWidget) { _audioColorPeakWidget.destroy(); _audioColorPeakWidget = null; }
}
/* ── Widget ensure helpers ───────────────────────────────────── */
export function ensureAudioSensitivityWidget(): BindableScalarWidget {
if (!_audioSensitivityWidget) {
_audioSensitivityWidget = new BindableScalarWidget({
container: document.getElementById('css-editor-audio-sensitivity-container')!,
min: 0.1, max: 5.0, step: 0.1, default: 1.0,
idPrefix: 'css-editor-audio-sensitivity',
valueSources: () => _cachedValueSources,
format: (v) => v.toFixed(1),
});
}
return _audioSensitivityWidget;
}
export function ensureAudioSmoothingWidget(): BindableScalarWidget {
if (!_audioSmoothingWidget) {
_audioSmoothingWidget = new BindableScalarWidget({
container: document.getElementById('css-editor-audio-smoothing-container')!,
min: 0.0, max: 1.0, step: 0.05, default: 0.3,
idPrefix: 'css-editor-audio-smoothing',
valueSources: () => _cachedValueSources,
format: (v) => v.toFixed(2),
});
}
return _audioSmoothingWidget;
}
export function ensureAudioBeatDecayWidget(): BindableScalarWidget {
if (!_audioBeatDecayWidget) {
_audioBeatDecayWidget = new BindableScalarWidget({
container: document.getElementById('css-editor-audio-beat-decay-container')!,
min: 0.01, max: 0.5, step: 0.01, default: 0.15,
idPrefix: 'css-editor-audio-beat-decay',
valueSources: () => _cachedValueSources,
format: (v) => v.toFixed(2),
});
}
return _audioBeatDecayWidget;
}
export function ensureAudioColorWidget(): BindableColorWidget {
if (!_audioColorWidget) {
_audioColorWidget = new BindableColorWidget({
container: document.getElementById('css-editor-audio-color-container')!,
default: [0, 255, 0],
valueSources: () => _cachedValueSources,
idPrefix: 'css-editor-audio-color',
});
}
return _audioColorWidget;
}
export function ensureAudioColorPeakWidget(): BindableColorWidget {
if (!_audioColorPeakWidget) {
_audioColorPeakWidget = new BindableColorWidget({
container: document.getElementById('css-editor-audio-color-peak-container')!,
default: [255, 0, 0],
valueSources: () => _cachedValueSources,
idPrefix: 'css-editor-audio-color-peak',
});
}
return _audioColorPeakWidget;
}
/* ── IconSelect / EntitySelect ───────────────────────────────── */
export function ensureAudioVizIconSelect() {
const sel = document.getElementById('css-editor-audio-viz') as HTMLSelectElement | null;
if (!sel) return;
const items = [
{ value: 'spectrum', icon: _icon(P.activity), label: t('color_strip.audio.viz.spectrum'), desc: t('color_strip.audio.viz.spectrum.desc') },
{ value: 'beat_pulse', icon: _icon(P.zap), label: t('color_strip.audio.viz.beat_pulse'), desc: t('color_strip.audio.viz.beat_pulse.desc') },
{ value: 'vu_meter', icon: _icon(P.trendingUp), label: t('color_strip.audio.viz.vu_meter'), desc: t('color_strip.audio.viz.vu_meter.desc') },
{ value: 'pulse_on_beat', icon: _icon(P.heart), label: t('color_strip.audio.viz.pulse_on_beat'), desc: t('color_strip.audio.viz.pulse_on_beat.desc') },
{ value: 'energy_gradient', icon: _icon(P.flame), label: t('color_strip.audio.viz.energy_gradient'), desc: t('color_strip.audio.viz.energy_gradient.desc') },
{ value: 'spectrum_bands', icon: _icon(P.radio), label: t('color_strip.audio.viz.spectrum_bands'), desc: t('color_strip.audio.viz.spectrum_bands.desc') },
{ value: 'strobe_on_drop', icon: _icon(P.sparkles), label: t('color_strip.audio.viz.strobe_on_drop'), desc: t('color_strip.audio.viz.strobe_on_drop.desc') },
];
if (_audioVizIconSelect) { _audioVizIconSelect.updateItems(items); return; }
_audioVizIconSelect = new IconSelect({ target: sel, items, columns: 4 });
}
export function ensureAudioPaletteEntitySelect(buildGradientEntityItems: () => any[], syncSelectOptions: (sel: HTMLSelectElement, items: any[]) => void) {
const sel = document.getElementById('css-editor-audio-palette') as HTMLSelectElement | null;
if (!sel) return;
const items = buildGradientEntityItems();
syncSelectOptions(sel, items);
if (_audioPaletteEntitySelect) { _audioPaletteEntitySelect.refresh(); return; }
_audioPaletteEntitySelect = new EntitySelect({
target: sel,
getItems: buildGradientEntityItems,
placeholder: t('palette.search'),
});
}
export function refreshAudioPaletteEntitySelect() {
if (_audioPaletteEntitySelect) _audioPaletteEntitySelect.refresh();
}
export function getAudioVizIconSelect() { return _audioVizIconSelect; }
export function getAudioPaletteEntitySelect() { return _audioPaletteEntitySelect; }
/* ── Audio source loading ────────────────────────────────────── */
export async function loadAudioSources() {
const select = document.getElementById('css-editor-audio-source') as HTMLSelectElement | null;
if (!select) return;
try {
const sources: any[] = await audioSourcesCache.fetch();
select.innerHTML = sources.map(s => {
const badge = s.source_type === 'capture' ? ' [capture]' : ' [processed]';
return `<option value="${s.id}">${escapeHtml(s.name)}${badge}</option>`;
}).join('');
if (sources.length === 0) {
select.innerHTML = '';
}
if (_cssAudioSourceEntitySelect) _cssAudioSourceEntitySelect.destroy();
if (sources.length > 0) {
_cssAudioSourceEntitySelect = new EntitySelect({
target: select,
getItems: () => sources.map(s => ({
value: s.id,
label: s.name,
icon: getAudioSourceIcon(s.source_type),
desc: s.source_type,
})),
placeholder: t('palette.search'),
});
}
} catch {
select.innerHTML = '';
}
}
/* ── Load / Reset state ───────────────────────────────────────── */
export function loadAudioState(css: any) {
(document.getElementById('css-editor-audio-viz') as HTMLInputElement).value = css.visualization_mode || 'spectrum';
if (_audioVizIconSelect) _audioVizIconSelect.setValue(css.visualization_mode || 'spectrum');
ensureAudioSensitivityWidget().setValue(css.sensitivity ?? 1.0);
ensureAudioSmoothingWidget().setValue(css.smoothing ?? 0.3);
ensureAudioBeatDecayWidget().setValue(css.beat_decay ?? 0.15);
const audioGradientId = css.gradient_id || 'gr_builtin_rainbow';
(document.getElementById('css-editor-audio-palette') as HTMLInputElement).value = audioGradientId;
if (_audioPaletteEntitySelect) _audioPaletteEntitySelect.setValue(audioGradientId);
ensureAudioColorWidget().setValue(css.color);
ensureAudioColorPeakWidget().setValue(css.color_peak);
(document.getElementById('css-editor-audio-mirror') as HTMLInputElement).checked = css.mirror || false;
const select = document.getElementById('css-editor-audio-source') as HTMLSelectElement | null;
if (select && css.audio_source_id) {
select.value = css.audio_source_id;
}
}
export function resetAudioState() {
(document.getElementById('css-editor-audio-viz') as HTMLInputElement).value = 'spectrum';
if (_audioVizIconSelect) _audioVizIconSelect.setValue('spectrum');
ensureAudioSensitivityWidget().setValue(1.0);
ensureAudioSmoothingWidget().setValue(0.3);
ensureAudioBeatDecayWidget().setValue(0.15);
(document.getElementById('css-editor-audio-palette') as HTMLInputElement).value = 'gr_builtin_rainbow';
if (_audioPaletteEntitySelect) _audioPaletteEntitySelect.setValue('gr_builtin_rainbow');
ensureAudioColorWidget().setValue([0, 255, 0]);
ensureAudioColorPeakWidget().setValue([255, 0, 0]);
(document.getElementById('css-editor-audio-mirror') as HTMLInputElement).checked = false;
}
@@ -0,0 +1,335 @@
/**
* Color Strip Sources — Card rendering for the streams list.
* Extracted from color-strips.ts to reduce file size.
*/
import { escapeHtml } from '../../core/api.ts';
import {
_cachedSyncClocks, _cachedCSPTemplates, _cachedWeatherSources,
colorStripSourcesCache, gradientsCache, GradientEntity,
} from '../../core/state.ts';
import { t } from '../../core/i18n.ts';
import {
getColorStripIcon, getPictureSourceIcon,
ICON_CLONE, ICON_EDIT, ICON_CALIBRATION, ICON_OVERLAY,
ICON_LED, ICON_PALETTE, ICON_FPS, ICON_MAP_PIN, ICON_MUSIC,
ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM,
ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK, ICON_BELL, ICON_TEST,
ICON_AUTOMATION, ICON_FAST_FORWARD, ICON_THERMOMETER, ICON_PATTERN_TEMPLATE,
} from '../../core/icons.ts';
import { wrapCard } from '../../core/card-colors.ts';
import type { ColorStripSource } from '../../types.ts';
import { bindableValue, bindableColor } from '../../types.ts';
import { renderTagChips } from '../../core/tag-input.ts';
import { rgbArrayToHex } from '../css-gradient-editor.ts';
/* ── Types ────────────────────────────────────────────────────── */
type CardPropsRenderer = (source: ColorStripSource, opts: {
clockBadge: string;
animBadge: string;
audioSourceMap: Record<string, any>;
pictureSourceMap: Record<string, any>;
}) => string;
/* ── Gradient helpers ─────────────────────────────────────────── */
function _getGradients(): GradientEntity[] {
return gradientsCache.data || [];
}
function _gradientEntityStripHTML(stops: Array<{ position: number; color: number[] }>, w = 80, h = 16) {
const css = stops.map(s => `rgb(${s.color.join(',')}) ${(s.position * 100).toFixed(0)}%`).join(', ');
return `<span style="display:inline-block;width:${w}px;height:${h}px;border-radius:3px;background:linear-gradient(to right,${css});flex-shrink:0"></span>`;
}
/* ── Non-picture types set ────────────────────────────────────── */
const NON_PICTURE_TYPES = new Set([
'static', 'gradient', 'color_cycle', 'effect', 'composite', 'mapped',
'audio', 'api_input', 'notification', 'daylight', 'candlelight', 'weather', 'processed', 'key_colors',
'math_wave',
]);
/* ── Per-type card property renderers ─────────────────────────── */
const CSS_CARD_RENDERERS: Record<string, CardPropsRenderer> = {
static: (source, { clockBadge, animBadge }) => {
const hexColor = rgbArrayToHex(bindableColor(source.color, [255,255,255]));
return `
<span class="stream-card-prop" title="${t('color_strip.static_color')}">
<span style="display:inline-block;width:14px;height:14px;background:${hexColor};border:1px solid #888;border-radius:2px;vertical-align:middle;margin-right:4px"></span>${hexColor.toUpperCase()}
</span>
${animBadge}
${clockBadge}
`;
},
color_cycle: (source, { clockBadge }) => {
const colors = source.colors || [];
const swatches = colors.slice(0, 8).map((c: any) =>
`<span style="display:inline-block;width:12px;height:12px;background:${rgbArrayToHex(c)};border:1px solid #888;border-radius:2px;margin-right:2px"></span>`
).join('');
return `
<span class="stream-card-prop">${swatches}</span>
${clockBadge}
`;
},
gradient: (source, { clockBadge, animBadge }) => {
const stops = source.stops || [];
const sortedStops = [...stops].sort((a, b) => a.position - b.position);
let cssGradient = '';
if (sortedStops.length >= 2) {
const parts: string[] = [];
sortedStops.forEach(s => {
const pct = Math.round(s.position * 100);
parts.push(`${rgbArrayToHex(s.color)} ${pct}%`);
if (s.color_right) parts.push(`${rgbArrayToHex(s.color_right)} ${pct}%`);
});
cssGradient = `linear-gradient(to right, ${parts.join(', ')})`;
}
return `
${cssGradient ? `<span style="flex:1 1 100%;height:12px;background:${cssGradient};border-radius:3px;border:1px solid rgba(128,128,128,0.3)"></span>` : ''}
<span class="stream-card-prop">${ICON_PALETTE} ${stops.length} ${t('color_strip.gradient.stops_count')}</span>
${animBadge}
${clockBadge}
`;
},
effect: (source, { clockBadge }) => {
const effectLabel = t('color_strip.effect.' + (source.effect_type || 'fire')) || source.effect_type || 'fire';
const paletteLabel = source.palette ? (t('color_strip.palette.' + source.palette) || source.palette) : '';
return `
<span class="stream-card-prop">${ICON_FPS} ${escapeHtml(effectLabel)}</span>
${paletteLabel ? `<span class="stream-card-prop" title="${t('color_strip.effect.palette')}">${ICON_PALETTE} ${escapeHtml(paletteLabel)}</span>` : ''}
${clockBadge}
`;
},
composite: (source) => {
const layerCount = (source.layers || []).length;
const enabledCount = (source.layers || []).filter((l: any) => l.enabled !== false).length;
return `
<span class="stream-card-prop">${ICON_LINK} ${enabledCount}/${layerCount} ${t('color_strip.composite.layers_count')}</span>
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${source.led_count}</span>` : ''}
`;
},
mapped: (source) => {
const zoneCount = (source.zones || []).length;
return `
<span class="stream-card-prop">${ICON_MAP_PIN} ${zoneCount} ${t('color_strip.mapped.zones_count')}</span>
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${source.led_count}</span>` : ''}
`;
},
audio: (source, { audioSourceMap }) => {
const vizLabel = t('color_strip.audio.viz.' + (source.visualization_mode || 'spectrum')) || source.visualization_mode || 'spectrum';
const vizMode = source.visualization_mode || 'spectrum';
const showPalette = (vizMode === 'spectrum' || vizMode === 'beat_pulse' || vizMode === 'energy_gradient' || vizMode === 'spectrum_bands') && source.palette;
const audioPaletteLabel = showPalette ? (t('color_strip.palette.' + source.palette) || source.palette) : '';
return `
<span class="stream-card-prop">${ICON_MUSIC} ${escapeHtml(vizLabel)}</span>
${audioPaletteLabel ? `<span class="stream-card-prop" title="${t('color_strip.audio.palette')}">${ICON_PALETTE} ${escapeHtml(audioPaletteLabel)}</span>` : ''}
${source.audio_source_id ? (() => {
const as = audioSourceMap && audioSourceMap[source.audio_source_id];
const asName = as ? as.name : source.audio_source_id;
const asSection = as ? (as.source_type === 'processed' ? 'audio-processed' : 'audio-capture') : 'audio-capture';
const asTab = as ? (as.source_type === 'processed' ? 'audio_processed' : 'audio_capture') : 'audio_capture';
return `<span class="stream-card-prop${as ? ' stream-card-link' : ''}" title="${t('color_strip.audio.source')}"${as ? ` onclick="event.stopPropagation(); navigateToCard('streams','${asTab}','${asSection}','data-id','${source.audio_source_id}')"` : ''}>${ICON_AUDIO_LOOPBACK} ${escapeHtml(asName)}</span>`;
})() : ''}
${source.mirror ? `<span class="stream-card-prop">\u{1FA9E}</span>` : ''}
`;
},
api_input: (source) => {
const fbColor = rgbArrayToHex(bindableColor(source.fallback_color, [0, 0, 0]));
const timeoutVal = bindableValue(source.timeout, 5.0).toFixed(1);
const interpLabel = t('color_strip.api_input.interpolation.' + (source.interpolation || 'linear')) || source.interpolation || 'linear';
return `
<span class="stream-card-prop" title="${t('color_strip.api_input.fallback_color')}">
<span style="display:inline-block;width:14px;height:14px;background:${fbColor};border:1px solid #888;border-radius:2px;vertical-align:middle;margin-right:4px"></span>${fbColor.toUpperCase()}
</span>
<span class="stream-card-prop" title="${t('color_strip.api_input.timeout')}">${ICON_TIMER} ${timeoutVal}s</span>
<span class="stream-card-prop" title="${t('color_strip.api_input.interpolation')}">${escapeHtml(interpLabel)}</span>
`;
},
notification: (source) => {
const effectLabel = t('color_strip.notification.effect.' + (source.notification_effect || 'flash')) || source.notification_effect || 'flash';
const durationVal = source.duration_ms || 1500;
const defColorRgb = bindableColor(source.default_color as any, [255, 255, 255]);
const defColorHex = rgbArrayToHex(defColorRgb);
const appCount = source.app_colors ? Object.keys(source.app_colors).length : 0;
return `
<span class="stream-card-prop" title="${t('color_strip.notification.effect')}">${ICON_BELL} ${escapeHtml(effectLabel)}</span>
<span class="stream-card-prop" title="${t('color_strip.notification.duration')}">${ICON_TIMER} ${durationVal}ms</span>
<span class="stream-card-prop" title="${t('color_strip.notification.default_color')}">
<span style="display:inline-block;width:14px;height:14px;background:${defColorHex};border:1px solid #888;border-radius:2px;vertical-align:middle;margin-right:4px"></span>${defColorHex.toUpperCase()}
</span>
${appCount > 0 ? `<span class="stream-card-prop">${ICON_PALETTE} ${appCount} ${t('color_strip.notification.app_count')}</span>` : ''}
`;
},
daylight: (source, { clockBadge }) => {
const useRealTime = source.use_real_time;
const speedVal = bindableValue(source.speed, 1.0).toFixed(1);
return `
<span class="stream-card-prop">${useRealTime ? ICON_CLOCK + ' ' + t('color_strip.daylight.real_time') : ICON_FAST_FORWARD + ' ' + speedVal + 'x'}</span>
${clockBadge}
`;
},
candlelight: (source, { clockBadge }) => {
const hexColor = rgbArrayToHex(bindableColor(source.color, [255, 147, 41]));
const numCandles = source.num_candles ?? 3;
return `
<span class="stream-card-prop" title="${t('color_strip.candlelight.color')}">
<span style="display:inline-block;width:14px;height:14px;background:${hexColor};border:1px solid #888;border-radius:2px;vertical-align:middle;margin-right:4px"></span>${hexColor.toUpperCase()}
</span>
<span class="stream-card-prop">${numCandles} ${t('color_strip.candlelight.num_candles')}</span>
${clockBadge}
`;
},
weather: (source, { clockBadge }) => {
const speedVal = bindableValue(source.speed, 1.0).toFixed(1);
const tempInfl = bindableValue(source.temperature_influence, 0.5).toFixed(1);
const ws = (_cachedWeatherSources || []).find((w: any) => w.id === source.weather_source_id);
const wsName = ws?.name || '\u2014';
const wsLink = ws
? ` stream-card-link" onclick="event.stopPropagation(); navigateToCard('integrations','weather','weather-sources','data-id','${source.weather_source_id}')`
: '';
return `
<span class="stream-card-prop${wsLink}" title="${t('color_strip.weather.source')}">${ICON_LINK_SOURCE} ${escapeHtml(wsName)}</span>
<span class="stream-card-prop">${ICON_FAST_FORWARD} ${speedVal}x</span>
<span class="stream-card-prop">${ICON_THERMOMETER} ${tempInfl}</span>
${clockBadge}
`;
},
key_colors: (source, { pictureSourceMap }) => {
const rectCount = (source.rectangles || []).length;
const mode = source.interpolation_mode || 'average';
const ps = pictureSourceMap && source.picture_source_id ? pictureSourceMap[source.picture_source_id] : null;
const psName = ps?.name || '\u2014';
const psLink = ps
? ` stream-card-link" onclick="event.stopPropagation(); navigateToCard('streams','raw','raw-streams','data-stream-id','${source.picture_source_id}')`
: '';
return `
<span class="stream-card-prop${psLink}" title="${t('color_strip.key_colors.picture_source')}">${ICON_LINK_SOURCE} ${escapeHtml(psName)}</span>
<span class="stream-card-prop">${ICON_PALETTE} ${rectCount} region${rectCount !== 1 ? 's' : ''}</span>
<span class="stream-card-prop">${mode}</span>
`;
},
math_wave: (source, { clockBadge }) => {
const waveCount = (source.waves || []).length;
const speedVal = bindableValue(source.speed, 1.0).toFixed(1);
const gr = source.gradient_id ? _getGradients().find(g => g.id === source.gradient_id) : null;
const grName = gr?.name || '\u2014';
return `
<span class="stream-card-prop" title="${t('color_strip.math_wave.waves')}">${ICON_ACTIVITY} ${waveCount} wave${waveCount !== 1 ? 's' : ''}</span>
<span class="stream-card-prop" title="${t('color_strip.math_wave.gradient')}">${ICON_PALETTE} ${escapeHtml(grName)}</span>
<span class="stream-card-prop">${ICON_FAST_FORWARD} ${speedVal}x</span>
${clockBadge}
`;
},
processed: (source) => {
const inputSrc = ((colorStripSourcesCache.data || []) as any[]).find(s => s.id === source.input_source_id);
const inputName = inputSrc?.name || source.input_source_id || '\u2014';
const tplName = source.processing_template_id
? (_cachedCSPTemplates.find(t => t.id === source.processing_template_id)?.name || source.processing_template_id)
: '\u2014';
return `
<span class="stream-card-prop" title="${t('color_strip.processed.input')}">${ICON_LINK_SOURCE} ${escapeHtml(inputName)}</span>
<span class="stream-card-prop" title="${t('color_strip.processed.template')}">${ICON_SPARKLES} ${escapeHtml(tplName)}</span>
`;
},
picture_advanced: (source, { pictureSourceMap }) => {
const cal = source.calibration ?? {} as Partial<import('../../types.ts').Calibration>;
const lines = cal.lines || [];
const totalLeds = lines.reduce((s: any, l: any) => s + (l.led_count || 0), 0);
const ledCount = (source.led_count > 0) ? source.led_count : totalLeds;
const psIds: any[] = [...new Set(lines.map((l: any) => l.picture_source_id).filter(Boolean))];
const psNames = psIds.map((id: any) => {
const ps = pictureSourceMap && pictureSourceMap[id];
return ps ? ps.name : id;
});
return `
<span class="stream-card-prop" title="${t('calibration.advanced.lines_title')}">${ICON_MAP_PIN} ${lines.length} ${t('calibration.advanced.lines_title').toLowerCase()}</span>
${ledCount ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${ledCount}</span>` : ''}
${psNames.length ? `<span class="stream-card-prop stream-card-prop-full" title="${t('color_strip.picture_source')}">${ICON_LINK_SOURCE} ${escapeHtml(psNames.join(', '))}</span>` : ''}
`;
},
};
/* ── Fallback picture renderer ────────────────────────────────── */
function _renderPictureCardProps(source: ColorStripSource, pictureSourceMap: Record<string, any>): string {
const ps = pictureSourceMap && pictureSourceMap[source.picture_source_id!];
const srcName = ps ? ps.name : source.picture_source_id || '\u2014';
const cal = source.calibration ?? {} as Partial<import('../../types.ts').Calibration>;
const calLeds = (cal.leds_top || 0) + (cal.leds_right || 0) + (cal.leds_bottom || 0) + (cal.leds_left || 0);
const ledCount = (source.led_count > 0) ? source.led_count : calLeds;
let psSubTab = 'raw', psSection = 'raw-streams';
if (ps) {
if (ps.stream_type === 'static_image') { psSubTab = 'static_image'; psSection = 'static-streams'; }
else if (ps.stream_type === 'processed') { psSubTab = 'processed'; psSection = 'proc-streams'; }
}
return `
<span class="stream-card-prop stream-card-prop-full${ps ? ' stream-card-link' : ''}" title="${t('color_strip.picture_source')}"${ps ? ` onclick="event.stopPropagation(); navigateToCard('streams','${psSubTab}','${psSection}','data-stream-id','${source.picture_source_id}')"` : ''}>${ICON_LINK_SOURCE} ${escapeHtml(srcName)}</span>
${ledCount ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${ledCount}</span>` : ''}
`;
}
/* ── Main card builder ────────────────────────────────────────── */
export function createColorStripCard(source: ColorStripSource, pictureSourceMap: Record<string, any>, audioSourceMap: Record<string, any>) {
// Clock crosslink badge
const clockObj = source.clock_id ? _cachedSyncClocks.find(c => c.id === source.clock_id) : null;
const clockBadge = clockObj
? `<span class="stream-card-prop stream-card-link" title="${t('color_strip.clock')}" onclick="event.stopPropagation(); navigateToCard('streams','sync','sync-clocks','data-id','${source.clock_id}')">${ICON_CLOCK} ${escapeHtml(clockObj.name)}</span>`
: source.clock_id ? `<span class="stream-card-prop">${ICON_CLOCK} ${source.clock_id}</span>` : '';
const isAnimatable = source.source_type === 'static' || source.source_type === 'gradient';
const anim = isAnimatable && source.animation && source.animation.enabled ? source.animation : null;
const animBadge = anim
? `<span class="stream-card-prop" title="${t('color_strip.animation')}">${ICON_SPARKLES} ${t('color_strip.animation.type.' + anim.type) || anim.type}</span>`
: '';
const renderer = CSS_CARD_RENDERERS[source.source_type];
const propsHtml = renderer
? renderer(source, { clockBadge, animBadge, audioSourceMap, pictureSourceMap })
: _renderPictureCardProps(source, pictureSourceMap);
const icon = getColorStripIcon(source.source_type);
const isNotification = source.source_type === 'notification';
const isPictureKind = !NON_PICTURE_TYPES.has(source.source_type);
const calibrationBtn = isPictureKind
? `<button class="btn btn-icon btn-secondary" onclick="${source.source_type === 'picture_advanced' ? `showAdvancedCalibration('${source.id}')` : `showCSSCalibration('${source.id}')`}" title="${t('calibration.title')}">${ICON_CALIBRATION}</button>`
: '';
const overlayBtn = isPictureKind
? `<button class="btn btn-icon btn-secondary" onclick="event.stopPropagation(); toggleCSSOverlay('${source.id}')" title="${t('overlay.toggle')}">${ICON_OVERLAY}</button>`
: '';
const testNotifyBtn = isNotification
? `<button class="btn btn-icon btn-secondary" onclick="event.stopPropagation(); testNotification('${source.id}')" title="${t('color_strip.notification.test')}">${ICON_BELL}</button>`
: '';
const notifHistoryBtn = isNotification
? `<button class="btn btn-icon btn-secondary" onclick="event.stopPropagation(); showNotificationHistory()" title="${t('color_strip.notification.history.title')}">${ICON_AUTOMATION}</button>`
: '';
const isKeyColors = source.source_type === 'key_colors';
const regionsBtn = isKeyColors
? `<button class="btn btn-icon btn-secondary" onclick="event.stopPropagation(); configureKCRegions('${source.id}')" title="${t('color_strip.key_colors.configure_regions')}">${ICON_PATTERN_TEMPLATE}</button>`
: '';
const testPreviewBtn = `<button class="btn btn-icon btn-secondary" onclick="event.stopPropagation(); testColorStrip('${source.id}')" title="${t('color_strip.test.title')}">${ICON_TEST}</button>`;
return wrapCard({
dataAttr: 'data-css-id',
id: source.id,
removeOnclick: `deleteColorStrip('${source.id}')`,
removeTitle: t('common.delete'),
content: `
<div class="card-header">
<div class="card-title" title="${escapeHtml(source.name)}">
${icon} <span class="card-title-text">${escapeHtml(source.name)}</span>
</div>
</div>
<div class="stream-card-props">
${propsHtml}
</div>
${renderTagChips(source.tags)}`,
actions: `
<button class="btn btn-icon btn-secondary" onclick="cloneColorStrip('${source.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
<button class="btn btn-icon btn-secondary" onclick="showCSSEditor('${source.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>
${calibrationBtn}${overlayBtn}${regionsBtn}${testNotifyBtn}${notifHistoryBtn}${testPreviewBtn}`,
});
}
@@ -0,0 +1,68 @@
/**
* Color Strip Sources — Color Cycle helpers.
* Extracted from color-strips.ts to reduce file size.
*/
import { ICON_TRASH } from '../../core/icons.ts';
import { rgbArrayToHex, hexToRgbArray } from '../css-gradient-editor.ts';
/* ── State ────────────────────────────────────────────────────── */
const _DEFAULT_CYCLE_COLORS = ['#ff0000', '#ffff00', '#00ff00', '#00ffff', '#0000ff', '#ff00ff'];
let _colorCycleColors = [..._DEFAULT_CYCLE_COLORS];
/* ── DOM sync ─────────────────────────────────────────────────── */
function _syncColorCycleFromDom() {
const inputs = document.querySelectorAll<HTMLInputElement>('#color-cycle-colors-list input[type=color]');
if (inputs.length > 0) {
_colorCycleColors = Array.from(inputs).map(el => el.value);
}
}
/* ── Rendering ────────────────────────────────────────────────── */
function _colorCycleRenderList() {
const list = document.getElementById('color-cycle-colors-list') as HTMLElement | null;
if (!list) return;
const canRemove = _colorCycleColors.length > 2;
list.innerHTML = _colorCycleColors.map((hex, i) => `
<div class="color-cycle-item">
<input type="color" value="${hex}">
${canRemove
? `<button type="button" class="btn btn-secondary color-cycle-remove-btn"
onclick="colorCycleRemoveColor(${i})">${ICON_TRASH}</button>`
: `<div style="height:14px"></div>`}
</div>
`).join('') + `<div class="color-cycle-item"><button type="button" class="btn btn-secondary color-cycle-add-btn" onclick="colorCycleAddColor()">+</button></div>`;
}
/* ── Public actions ───────────────────────────────────────────── */
export function colorCycleAddColor() {
_syncColorCycleFromDom();
_colorCycleColors.push('#ffffff');
_colorCycleRenderList();
}
export function colorCycleRemoveColor(i: number) {
_syncColorCycleFromDom();
if (_colorCycleColors.length <= 2) return;
_colorCycleColors.splice(i, 1);
_colorCycleRenderList();
}
export function colorCycleGetColors() {
const inputs = document.querySelectorAll<HTMLInputElement>('#color-cycle-colors-list input[type=color]');
return Array.from(inputs).map(el => hexToRgbArray(el.value));
}
/* ── Load / Reset ─────────────────────────────────────────────── */
export function loadColorCycleState(css: any) {
const raw = css && css.colors;
_colorCycleColors = (raw && raw.length >= 2)
? raw.map((c: any) => rgbArrayToHex(c))
: [..._DEFAULT_CYCLE_COLORS];
_colorCycleRenderList();
}
@@ -3,17 +3,17 @@
* Extracted from color-strips.ts to reduce file size. * Extracted from color-strips.ts to reduce file size.
*/ */
import { escapeHtml } from '../core/api.ts'; import { escapeHtml } from '../../core/api.ts';
import { _cachedValueSources, _cachedCSPTemplates } from '../core/state.ts'; import { _cachedValueSources, _cachedCSPTemplates } from '../../core/state.ts';
import { t } from '../core/i18n.ts'; import { t } from '../../core/i18n.ts';
import { import {
getColorStripIcon, getValueSourceIcon, getColorStripIcon, getValueSourceIcon,
ICON_SPARKLES, ICON_TRASH, ICON_SPARKLES, ICON_TRASH,
} from '../core/icons.ts'; } from '../../core/icons.ts';
import * as P from '../core/icon-paths.ts'; import * as P from '../../core/icon-paths.ts';
import { IconSelect } from '../core/icon-select.ts'; import { IconSelect } from '../../core/icon-select.ts';
import { EntitySelect } from '../core/entity-palette.ts'; import { EntitySelect } from '../../core/entity-palette.ts';
import { BindableScalarWidget } from '../core/bindable-scalar.ts'; import { BindableScalarWidget } from '../../core/bindable-scalar.ts';
const _icon = (d: any) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`; const _icon = (d: any) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
@@ -0,0 +1,308 @@
/**
* Color Strip Sources — Game Event helpers.
* Extracted from color-strips.ts to reduce file size.
*/
import { escapeHtml } from '../../core/api.ts';
import { _cachedGameIntegrations, _cachedGameAdapters, _cachedValueSources } from '../../core/state.ts';
import { t } from '../../core/i18n.ts';
import { ICON_TRASH, ICON_GAMEPAD } from '../../core/icons.ts';
import * as P from '../../core/icon-paths.ts';
import { IconSelect, type IconSelectItem } from '../../core/icon-select.ts';
import { EntitySelect } from '../../core/entity-palette.ts';
import { BindableColorWidget } from '../../core/bindable-color.ts';
/* ── State ────────────────────────────────────────────────────── */
let _gameEventIdleColorWidget: BindableColorWidget | null = null;
let _cssGameIntegrationEntitySelect: EntitySelect | null = null;
let _cssGameMappings: any[] = [];
let _cssGameMappingIconSelects: IconSelect[] = [];
let _cssGamePresetIconSelect: IconSelect | null = null;
/* ── Destroy helpers (called from modal onForceClose) ─────────── */
export function destroyGameEventWidgets() {
if (_gameEventIdleColorWidget) { _gameEventIdleColorWidget.destroy(); _gameEventIdleColorWidget = null; }
if (_cssGameIntegrationEntitySelect) { _cssGameIntegrationEntitySelect.destroy(); _cssGameIntegrationEntitySelect = null; }
destroyCSSGameMappingIconSelects();
if (_cssGamePresetIconSelect) { _cssGamePresetIconSelect.destroy(); _cssGamePresetIconSelect = null; }
}
function destroyCSSGameMappingIconSelects() {
_cssGameMappingIconSelects.forEach(is => is.destroy());
_cssGameMappingIconSelects = [];
}
/* ── Color/hex helpers ────────────────────────────────────────── */
function _hexToRgbCSS(hex: string): number[] {
const m = hex.replace('#', '').match(/.{2}/g);
if (!m) return [255, 0, 0];
return m.map(c => parseInt(c, 16));
}
function _rgbToHexCSS(rgb: number[]): string {
return '#' + rgb.map(c => c.toString(16).padStart(2, '0')).join('');
}
/* ── Available event types ────────────────────────────────────── */
function _getCSSGameAvailableEventTypes(): string[] {
const giId = (document.getElementById('css-editor-game-integration') as HTMLSelectElement)?.value;
if (giId) {
const gi = (_cachedGameIntegrations || []).find(g => g.id === giId);
if (gi) {
const adapter = (_cachedGameAdapters || []).find(a => a.adapter_type === gi.adapter_type);
if (adapter && adapter.supported_events.length > 0) return adapter.supported_events;
}
}
return ['kill', 'death', 'health', 'armor', 'round_start', 'round_end', 'bomb_planted', 'bomb_defused', 'assist', 'headshot'];
}
/* ── Constants ────────────────────────────────────────────────── */
const _CSS_GE_EFFECT_TYPES: IconSelectItem[] = [
{ value: 'flash', label: 'Flash', icon: `<svg class="icon" viewBox="0 0 24 24">${P.zap}</svg>` },
{ value: 'pulse', label: 'Pulse', icon: `<svg class="icon" viewBox="0 0 24 24">${P.activity}</svg>` },
{ value: 'sweep', label: 'Sweep', icon: `<svg class="icon" viewBox="0 0 24 24">${P.fastForward}</svg>` },
{ value: 'color_shift', label: 'Color Shift', icon: `<svg class="icon" viewBox="0 0 24 24">${P.rainbow}</svg>` },
{ value: 'breathing', label: 'Breathing', icon: `<svg class="icon" viewBox="0 0 24 24">${P.heart}</svg>` },
];
const _CSS_GE_EVENT_ICONS: Record<string, string> = {
kill: P.crosshair, death: P.xIcon, health: P.heart, armor: P.shield,
round_start: P.play, round_end: P.square, bomb_planted: P.flame, bomb_defused: P.circleCheck,
assist: P.swords, headshot: P.target, damage: P.zap, gold: P.star,
level_up: P.trendingUp, respawn: P.refreshCw, item_pickup: P.packageIcon,
};
/* ── Build helpers ────────────────────────────────────────────── */
function _buildCSSGameEventTypeItems(): IconSelectItem[] {
return _getCSSGameAvailableEventTypes().map(et => ({
value: et,
label: et,
icon: `<svg class="icon" viewBox="0 0 24 24">${_CSS_GE_EVENT_ICONS[et] || P.circleDot}</svg>`,
}));
}
/* ── Rendering ────────────────────────────────────────────────── */
function _renderCSSGameMappingRow(mapping: any, index: number): string {
const eventTypes = _getCSSGameAvailableEventTypes();
const eventOptions = eventTypes.map(et =>
`<option value="${et}"${et === mapping.event_type ? ' selected' : ''}>${et}</option>`
).join('');
const effectOptions = _CSS_GE_EFFECT_TYPES.map(ef =>
`<option value="${ef.value}"${ef.value === mapping.effect_type ? ' selected' : ''}>${ef.label}</option>`
).join('');
const effectLabel = _CSS_GE_EFFECT_TYPES.find(ef => ef.value === mapping.effect_type)?.label || mapping.effect_type;
const hexColor = _rgbToHexCSS(mapping.color || [255, 0, 0]);
return `
<div class="gi-mapping-row" data-mapping-index="${index}">
<div class="gi-mapping-header">
<span class="gi-mapping-expand-btn">&#x25B6;</span>
<span class="gi-mapping-summary">
<span class="gi-mapping-summary-event">${escapeHtml(mapping.event_type)}</span>
<span class="gi-mapping-summary-effect">${escapeHtml(effectLabel)}</span>
<span class="gi-mapping-summary-color" style="background:${hexColor}"></span>
</span>
<button type="button" class="btn-remove-rule" onclick="event.stopPropagation(); removeCSSGameMapping(${index})" title="${t('common.delete')}">${ICON_TRASH}</button>
</div>
<div class="gi-mapping-body-wrapper">
<div class="gi-mapping-body">
<div class="gi-mapping-field-row">
<label>${t('game_integration.mapping.event_type')}</label>
<select data-field="event_type">${eventOptions}</select>
</div>
<div class="gi-mapping-field-row">
<label>${t('game_integration.mapping.effect_type')}</label>
<select data-field="effect_type">${effectOptions}</select>
</div>
<div class="gi-mapping-field-row">
<label>${t('game_integration.mapping.color')}</label>
<input type="color" data-field="color" value="${hexColor}">
</div>
<div class="gi-mapping-field-row">
<label>${t('game_integration.mapping.duration')}</label>
<input type="number" data-field="duration_ms" value="${mapping.duration_ms || 500}" min="50" max="10000" step="50">
</div>
<div class="gi-mapping-field-row">
<label>${t('game_integration.mapping.intensity')}</label>
<input type="range" data-field="intensity" value="${mapping.intensity ?? 1.0}" min="0" max="1" step="0.05"
oninput="this.title = this.value">
</div>
<div class="gi-mapping-field-row">
<label>${t('game_integration.mapping.priority')}</label>
<input type="number" data-field="priority" value="${mapping.priority || 5}" min="1" max="10">
</div>
</div>
</div>
</div>`;
}
function _wireCSSGameMappingRows(container: HTMLElement) {
container.querySelectorAll('.gi-mapping-header').forEach(header => {
const item = header.closest('.gi-mapping-row') as HTMLElement;
header.addEventListener('click', (e: Event) => {
if ((e.target as HTMLElement).closest('.btn-remove-rule')) return;
item.classList.toggle('gi-mapping-expanded');
});
});
container.querySelectorAll('.gi-mapping-row').forEach(row => {
const eventSel = row.querySelector('[data-field="event_type"]') as HTMLSelectElement | null;
const effectSel = row.querySelector('[data-field="effect_type"]') as HTMLSelectElement | null;
const colorInput = row.querySelector('input[data-field="color"]') as HTMLInputElement | null;
const summaryEvent = row.querySelector('.gi-mapping-summary-event') as HTMLElement | null;
const summaryEffect = row.querySelector('.gi-mapping-summary-effect') as HTMLElement | null;
const summaryColor = row.querySelector('.gi-mapping-summary-color') as HTMLElement | null;
if (eventSel) {
const is = new IconSelect({ target: eventSel, items: _buildCSSGameEventTypeItems(), columns: 4 });
_cssGameMappingIconSelects.push(is);
if (summaryEvent) {
eventSel.addEventListener('change', () => { summaryEvent.textContent = eventSel.value; });
}
}
if (effectSel) {
const is = new IconSelect({ target: effectSel, items: _CSS_GE_EFFECT_TYPES, columns: 3 });
_cssGameMappingIconSelects.push(is);
if (summaryEffect) {
effectSel.addEventListener('change', () => {
const label = _CSS_GE_EFFECT_TYPES.find(ef => ef.value === effectSel.value)?.label || effectSel.value;
summaryEffect.textContent = label;
});
}
}
if (colorInput && summaryColor) {
colorInput.addEventListener('input', () => { summaryColor.style.background = colorInput.value; });
}
});
}
function _renderCSSGameMappings(mappings: any[]) {
_cssGameMappings = [...mappings];
destroyCSSGameMappingIconSelects();
const container = document.getElementById('css-editor-ge-mappings-list');
if (!container) return;
container.innerHTML = mappings.map((m, i) => _renderCSSGameMappingRow(m, i)).join('');
_wireCSSGameMappingRows(container);
}
export function collectCSSGameMappings(): any[] {
const rows = document.querySelectorAll('#css-editor-ge-mappings-list .gi-mapping-row');
return Array.from(rows).map(row => {
const eventType = (row.querySelector('[data-field="event_type"]') as HTMLSelectElement)?.value || 'kill';
const effectType = (row.querySelector('[data-field="effect_type"]') as HTMLSelectElement)?.value || 'flash';
const colorInput = (row.querySelector('input[data-field="color"]') as HTMLInputElement)?.value || '#ff0000';
const duration = parseFloat((row.querySelector('[data-field="duration_ms"]') as HTMLInputElement)?.value) || 500;
const intensity = parseFloat((row.querySelector('[data-field="intensity"]') as HTMLInputElement)?.value) || 1.0;
const priority = parseInt((row.querySelector('[data-field="priority"]') as HTMLInputElement)?.value) || 5;
return { event_type: eventType, effect_type: effectType, color: _hexToRgbCSS(colorInput), duration_ms: duration, intensity, priority };
});
}
/* ── Public actions ───────────────────────────────────────────── */
export function addCSSGameMapping() {
const collected = collectCSSGameMappings();
collected.push({
event_type: _getCSSGameAvailableEventTypes()[0] || 'kill',
effect_type: 'flash',
color: [255, 0, 0],
duration_ms: 500,
intensity: 1.0,
priority: 5,
});
_renderCSSGameMappings(collected);
}
export function removeCSSGameMapping(index: number) {
const collected = collectCSSGameMappings();
collected.splice(index, 1);
_renderCSSGameMappings(collected);
}
export function onCSSGameMappingPresetChange() {
const sel = document.getElementById('css-editor-ge-mapping-preset') as HTMLSelectElement;
if (!sel.value) return;
const presets: Record<string, any[]> = {
fps_combat: [
{ event_type: 'kill', effect_type: 'flash', color: [0, 255, 0], duration_ms: 400, intensity: 1.0, priority: 8 },
{ event_type: 'death', effect_type: 'pulse', color: [255, 0, 0], duration_ms: 1500, intensity: 1.0, priority: 10 },
{ event_type: 'headshot', effect_type: 'flash', color: [255, 215, 0], duration_ms: 300, intensity: 1.0, priority: 9 },
{ event_type: 'health', effect_type: 'breathing', color: [255, 50, 50], duration_ms: 2000, intensity: 0.6, priority: 3 },
{ event_type: 'round_start', effect_type: 'sweep', color: [0, 100, 255], duration_ms: 800, intensity: 0.8, priority: 5 },
],
moba_health: [
{ event_type: 'health', effect_type: 'color_shift', color: [0, 255, 0], duration_ms: 1000, intensity: 0.7, priority: 4 },
{ event_type: 'kill', effect_type: 'flash', color: [255, 215, 0], duration_ms: 500, intensity: 1.0, priority: 8 },
{ event_type: 'death', effect_type: 'pulse', color: [255, 0, 0], duration_ms: 2000, intensity: 1.0, priority: 10 },
{ event_type: 'assist', effect_type: 'flash', color: [100, 200, 255], duration_ms: 300, intensity: 0.8, priority: 6 },
],
};
const preset = presets[sel.value];
if (preset) _renderCSSGameMappings(preset);
sel.value = '';
}
export function initCSSGamePresetIconSelect() {
const sel = document.getElementById('css-editor-ge-mapping-preset') as HTMLSelectElement | null;
if (!sel) return;
if (_cssGamePresetIconSelect) { _cssGamePresetIconSelect.destroy(); _cssGamePresetIconSelect = null; }
const items: IconSelectItem[] = [
{ value: '', label: t('game_integration.mapping.select_preset'), icon: '' },
{ value: 'fps_combat', label: t('game_integration.preset.fps_combat'), icon: `<svg class="icon" viewBox="0 0 24 24">${P.crosshair}</svg>` },
{ value: 'moba_health', label: t('game_integration.preset.moba_health'), icon: `<svg class="icon" viewBox="0 0 24 24">${P.heart}</svg>` },
];
_cssGamePresetIconSelect = new IconSelect({ target: sel, items, columns: 2 });
}
/* ── Widget ensure helpers ────────────────────────────────────── */
export function ensureGameEventIdleColorWidget(): BindableColorWidget {
if (!_gameEventIdleColorWidget) {
_gameEventIdleColorWidget = new BindableColorWidget({
container: document.getElementById('css-editor-game-event-idle-color-container')!,
default: [0, 0, 0],
valueSources: () => _cachedValueSources,
idPrefix: 'css-editor-ge-idle-color',
});
}
return _gameEventIdleColorWidget;
}
export function populateGameIntegrationDropdownCSS(selectedId: string = '') {
const sel = document.getElementById('css-editor-game-integration') as HTMLSelectElement;
const integrations = _cachedGameIntegrations || [];
const prev = selectedId || sel.value;
sel.innerHTML = `<option value="">${t('common.none_no_input')}</option>` +
integrations.map(gi => `<option value="${gi.id}"${gi.id === prev ? ' selected' : ''}>${escapeHtml(gi.name)}</option>`).join('');
sel.value = prev || '';
if (_cssGameIntegrationEntitySelect) _cssGameIntegrationEntitySelect.destroy();
_cssGameIntegrationEntitySelect = new EntitySelect({
target: sel,
getItems: () => integrations.map(gi => ({
value: gi.id,
label: gi.name,
icon: ICON_GAMEPAD,
desc: gi.adapter_type,
})),
allowNone: true,
noneLabel: t('common.none_no_input'),
placeholder: t('palette.search'),
});
}
export function renderCSSGameMappings(mappings: any[]) {
_renderCSSGameMappings(mappings);
}
/** Get raw mappings array (for snapshot). */
export function getCSSGameMappings(): any[] {
return _cssGameMappings;
}
@@ -0,0 +1,265 @@
/**
* Color Strip Sources — Gradient entity management (modal + CRUD).
* Extracted from color-strips.ts to reduce file size.
*/
import { fetchWithAuth } from '../../core/api.ts';
import { gradientsCache, GradientEntity } from '../../core/state.ts';
import { t } from '../../core/i18n.ts';
import { showToast, showConfirm } from '../../core/ui.ts';
import { Modal } from '../../core/modal.ts';
import { ICON_PALETTE, ICON_TRASH } from '../../core/icons.ts';
import { TagInput, renderTagChips } from '../../core/tag-input.ts';
import { escapeHtml } from '../../core/api.ts';
import {
gradientInit, gradientRenderAll, getGradientStops, gradientSetIdPrefix,
} from '../css-gradient-editor.ts';
/* ── Helpers ──────────────────────────────────────────────────── */
function _getGradients(): GradientEntity[] {
return gradientsCache.data || [];
}
function _findGradient(id: string): GradientEntity | undefined {
return _getGradients().find(g => g.id === id);
}
function _gradientEntityStripHTML(stops: Array<{ position: number; color: number[] }>, w = 80, h = 16) {
const css = stops.map(s => `rgb(${s.color.join(',')}) ${(s.position * 100).toFixed(0)}%`).join(', ');
return `<span style="display:inline-block;width:${w}px;height:${h}px;border-radius:3px;background:linear-gradient(to right,${css});flex-shrink:0"></span>`;
}
/* ── Gradient entity items builder (shared with palettes) ────── */
export function buildGradientEntityItems() {
const gradients = _getGradients();
return gradients.map(g => ({
value: g.id, icon: _gradientEntityStripHTML(g.stops), label: g.name,
}));
}
/** Sync <option> elements in a <select> from items array. */
export function syncSelectOptions(sel: HTMLSelectElement, items: Array<{ value: string; label: string }>) {
sel.innerHTML = '';
for (const item of items) {
const opt = document.createElement('option');
opt.value = item.value;
opt.textContent = item.label;
sel.appendChild(opt);
}
}
/* ── Gradient preset picker refresh ──────────────────────────── */
export { _getGradients as getGradients, _findGradient as findGradient, _gradientEntityStripHTML as gradientEntityStripHTML };
/* ── Custom preset list below gradient editor ─────────────────── */
export function renderCustomPresetList() {
const container = document.getElementById('css-editor-custom-presets-list') as HTMLElement | null;
if (!container) return;
const userGradients = _getGradients().filter(g => !g.is_builtin);
if (userGradients.length === 0) {
container.innerHTML = '';
return;
}
container.innerHTML = userGradients.map(g => {
const strip = _gradientEntityStripHTML(g.stops, 60, 14);
const safeName = escapeHtml(g.name);
return `<div class="custom-preset-row">
${strip}
<span class="custom-preset-name">${safeName}</span>
<button type="button" class="btn btn-icon btn-sm btn-secondary"
onclick="onGradientPresetChange('${g.id}')"
title="${t('color_strip.gradient.preset.apply')}">&#x2713;</button>
<button type="button" class="btn btn-icon btn-sm btn-danger"
onclick="deleteAndRefreshGradientPreset('${g.id}')"
title="${t('common.delete')}">${ICON_TRASH}</button>
</div>`;
}).join('');
}
/* ── Inline preset actions (from CSS editor) ──────────────────── */
export function onGradientPresetChange(value: any) {
if (!value) return;
const g = _findGradient(value);
if (g) {
gradientInit(g.stops);
}
}
export async function promptAndSaveGradientPreset() {
const name = window.prompt(t('color_strip.gradient.preset.save_prompt'), '');
if (!name || !name.trim()) return;
const stops = getGradientStops().map(s => ({
position: s.position,
color: s.color,
}));
try {
await fetchWithAuth('/gradients', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name.trim(), stops }),
});
await gradientsCache.fetch({ force: true });
showToast(t('color_strip.gradient.preset.saved'), 'success');
} catch (e: any) {
showToast(e.message || 'Failed to save gradient', 'error');
}
}
export async function deleteAndRefreshGradientPreset(gradientId: any) {
try {
await fetchWithAuth(`/gradients/${gradientId}`, { method: 'DELETE' });
await gradientsCache.fetch({ force: true });
showToast(t('color_strip.gradient.preset.deleted'), 'success');
} catch (e: any) {
showToast(e.message || 'Failed to delete gradient', 'error');
}
}
/* ── Gradient Editor Modal ────────────────────────────────────── */
class GradientEditorModal extends Modal {
constructor() { super('gradient-editor-modal'); }
snapshotValues() {
return {
name: (document.getElementById('gradient-editor-name') as HTMLInputElement).value,
description: (document.getElementById('gradient-editor-description') as HTMLInputElement).value,
stops: JSON.stringify(getGradientStops()),
tags: JSON.stringify(_gradientTagsInput ? _gradientTagsInput.getValue() : []),
};
}
onForceClose() {
if (_gradientTagsInput) { _gradientTagsInput.destroy(); _gradientTagsInput = null; }
gradientSetIdPrefix('');
}
}
const gradientEditorModal = new GradientEditorModal();
let _gradientTagsInput: any = null;
export async function showGradientModal(editId: string | null = null, cloneData: any = null) {
const titleEl = document.getElementById('gradient-editor-title')!;
const idInput = document.getElementById('gradient-editor-id') as HTMLInputElement;
const nameInput = document.getElementById('gradient-editor-name') as HTMLInputElement;
const descInput = document.getElementById('gradient-editor-description') as HTMLInputElement;
const errorEl = document.getElementById('gradient-editor-error') as HTMLElement;
idInput.value = '';
nameInput.value = '';
descInput.value = '';
errorEl.style.display = 'none';
if (_gradientTagsInput) { _gradientTagsInput.destroy(); _gradientTagsInput = null; }
const tagsContainer = document.getElementById('gradient-editor-tags-container')!;
_gradientTagsInput = new TagInput(tagsContainer, { placeholder: t('tags.placeholder') });
let stops = [{ position: 0.0, color: [255, 0, 0] }, { position: 1.0, color: [0, 0, 255] }];
if (editId) {
const g = _findGradient(editId);
if (!g) return;
idInput.value = g.id;
nameInput.value = g.name;
descInput.value = g.description || '';
_gradientTagsInput.setValue(g.tags || []);
stops = g.stops;
titleEl.innerHTML = `${ICON_PALETTE} ${t('gradient.edit')}`;
} else if (cloneData) {
nameInput.value = (cloneData.name || '') + ' (Copy)';
descInput.value = cloneData.description || '';
_gradientTagsInput.setValue(cloneData.tags || []);
stops = cloneData.stops || stops;
titleEl.innerHTML = `${ICON_PALETTE} ${t('gradient.add')}`;
} else {
titleEl.innerHTML = `${ICON_PALETTE} ${t('gradient.add')}`;
}
gradientEditorModal.open();
requestAnimationFrame(() => {
gradientSetIdPrefix('ge-');
gradientInit(stops);
gradientEditorModal.snapshot();
});
}
export async function closeGradientEditor() {
await gradientEditorModal.close();
}
export async function saveGradientEntity() {
const id = (document.getElementById('gradient-editor-id') as HTMLInputElement).value;
const name = (document.getElementById('gradient-editor-name') as HTMLInputElement).value.trim();
const description = (document.getElementById('gradient-editor-description') as HTMLInputElement).value.trim() || null;
const tags = _gradientTagsInput ? _gradientTagsInput.getValue() : [];
const errorEl = document.getElementById('gradient-editor-error') as HTMLElement;
if (!name) {
errorEl.textContent = t('gradient.error.name_required');
errorEl.style.display = '';
return;
}
const stops = getGradientStops().map(s => ({
position: s.position,
color: [...s.color],
}));
if (stops.length < 2) {
errorEl.textContent = t('gradient.error.min_stops');
errorEl.style.display = '';
return;
}
const payload: any = { name, stops, description, tags };
try {
const url = id ? `/gradients/${id}` : '/gradients';
const method = id ? 'PUT' : 'POST';
const res = await fetchWithAuth(url, { method, body: JSON.stringify(payload) });
if (!res!.ok) {
const err = await res!.json();
throw new Error(err.detail || 'Failed to save gradient');
}
showToast(id ? t('gradient.updated') : t('gradient.created'), 'success');
gradientsCache.invalidate();
gradientEditorModal.forceClose();
if (window.loadPictureSources) await window.loadPictureSources();
} catch (e: any) {
if (e.isAuth) return;
errorEl.textContent = e.message;
errorEl.style.display = '';
}
}
export async function cloneGradient(gradientId: string) {
const g = _findGradient(gradientId);
if (!g) return;
await showGradientModal(null, g);
}
export async function editGradient(gradientId: string) {
await showGradientModal(gradientId);
}
export async function deleteGradient(gradientId: string) {
const g = _findGradient(gradientId);
if (!g) return;
const ok = await showConfirm(t('gradient.confirm_delete', { name: g.name }));
if (!ok) return;
try {
await fetchWithAuth(`/gradients/${gradientId}`, { method: 'DELETE' });
gradientsCache.invalidate();
showToast(t('gradient.deleted'), 'success');
if (window.loadPictureSources) await window.loadPictureSources();
} catch (e: any) {
if (e.isAuth) return;
showToast(e.message || t('gradient.error.delete_failed'), 'error');
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,153 @@
/**
* Color Strip Sources — Mapped zone helpers.
* Extracted from color-strips.ts to reduce file size.
*/
import { escapeHtml } from '../../core/api.ts';
import { t } from '../../core/i18n.ts';
import { getColorStripIcon, ICON_TRASH } from '../../core/icons.ts';
import { EntitySelect } from '../../core/entity-palette.ts';
/* ── State ────────────────────────────────────────────────────── */
let _mappedZones: any[] = [];
let _mappedAvailableSources: any[] = [];
let _mappedZoneEntitySelects: any[] = [];
/* ── Source management ────────────────────────────────────────── */
export function mappedSetAvailableSources(sources: any[]) {
_mappedAvailableSources = sources;
}
function _getMappedSourceItems() {
return _mappedAvailableSources.map(s => ({
value: s.id,
label: s.name,
icon: getColorStripIcon(s.source_type),
}));
}
/* ── Destroy ──────────────────────────────────────────────────── */
function _mappedDestroyEntitySelects() {
_mappedZoneEntitySelects.forEach(es => es.destroy());
_mappedZoneEntitySelects = [];
}
/* ── Rendering ────────────────────────────────────────────────── */
export function mappedRenderList() {
const list = document.getElementById('mapped-zones-list') as HTMLElement | null;
if (!list) return;
_mappedDestroyEntitySelects();
list.innerHTML = _mappedZones.map((zone, i) => {
const srcOptions = _mappedAvailableSources.map(s =>
`<option value="${s.id}"${zone.source_id === s.id ? ' selected' : ''}>${escapeHtml(s.name)}</option>`
).join('');
return `
<div class="segment-row">
<div class="segment-row-header">
<span class="segment-index-label">#${i + 1}</span>
<button type="button" class="btn-icon-inline btn-danger-text"
onclick="mappedRemoveZone(${i})" title="${t('common.delete')}">${ICON_TRASH}</button>
</div>
<div class="segment-row-fields">
<select class="mapped-zone-source" data-idx="${i}">${srcOptions}</select>
<div class="segment-range-fields">
<label>${t('color_strip.mapped.zone_start')}</label>
<input type="number" class="mapped-zone-start" data-idx="${i}"
min="0" value="${zone.start}" placeholder="0">
<label>${t('color_strip.mapped.zone_end')}</label>
<input type="number" class="mapped-zone-end" data-idx="${i}"
min="0" value="${zone.end}" placeholder="0 = auto">
</div>
<label class="segment-reverse-label">
<input type="checkbox" class="mapped-zone-reverse" data-idx="${i}"${zone.reverse ? ' checked' : ''}>
<span>${t('color_strip.mapped.zone_reverse')}</span>
</label>
</div>
</div>
`;
}).join('');
// Attach EntitySelect to each zone's source dropdown
list.querySelectorAll<HTMLSelectElement>('.mapped-zone-source').forEach(sel => {
_mappedZoneEntitySelects.push(new EntitySelect({
target: sel,
getItems: _getMappedSourceItems,
placeholder: t('color_strip.mapped.select_source'),
}));
});
}
/* ── Actions ──────────────────────────────────────────────────── */
export function mappedAddZone() {
_mappedZonesSyncFromDom();
_mappedZones.push({
source_id: _mappedAvailableSources.length > 0 ? _mappedAvailableSources[0].id : '',
start: 0,
end: 0,
reverse: false,
});
mappedRenderList();
}
export function mappedRemoveZone(i: number) {
_mappedZonesSyncFromDom();
_mappedZones.splice(i, 1);
mappedRenderList();
}
/* ── DOM sync ─────────────────────────────────────────────────── */
function _mappedZonesSyncFromDom() {
const list = document.getElementById('mapped-zones-list') as HTMLElement | null;
if (!list) return;
const srcs = list.querySelectorAll<HTMLSelectElement>('.mapped-zone-source');
const starts = list.querySelectorAll<HTMLInputElement>('.mapped-zone-start');
const ends = list.querySelectorAll<HTMLInputElement>('.mapped-zone-end');
const reverses = list.querySelectorAll<HTMLInputElement>('.mapped-zone-reverse');
if (srcs.length === _mappedZones.length) {
for (let i = 0; i < srcs.length; i++) {
_mappedZones[i].source_id = srcs[i].value;
_mappedZones[i].start = parseInt(starts[i].value) || 0;
_mappedZones[i].end = parseInt(ends[i].value) || 0;
_mappedZones[i].reverse = reverses[i].checked;
}
}
}
export function mappedGetZones() {
_mappedZonesSyncFromDom();
return _mappedZones.map(z => ({
source_id: z.source_id,
start: z.start,
end: z.end,
reverse: z.reverse,
}));
}
export function loadMappedState(css: any) {
const raw = css && css.zones;
_mappedZones = (raw && raw.length > 0)
? raw.map((z: any) => ({
source_id: z.source_id || '',
start: z.start || 0,
end: z.end || 0,
reverse: z.reverse || false,
}))
: [];
mappedRenderList();
}
export function resetMappedState() {
_mappedZones = [];
mappedRenderList();
}
/** Get raw zones array (for snapshot). */
export function getMappedZones(): any[] {
return _mappedZones;
}
@@ -0,0 +1,165 @@
/**
* Color Strip Sources — Math Wave helpers.
* Extracted from color-strips.ts to reduce file size.
*/
import { _cachedValueSources } from '../../core/state.ts';
import { t } from '../../core/i18n.ts';
import { ICON_TRASH } from '../../core/icons.ts';
import * as P from '../../core/icon-paths.ts';
import { IconSelect, type IconSelectItem } from '../../core/icon-select.ts';
import { EntitySelect } from '../../core/entity-palette.ts';
import { BindableScalarWidget } from '../../core/bindable-scalar.ts';
const _icon = (d: any) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
/* ── State ────────────────────────────────────────────────────── */
let _mathWaveSpeedWidget: BindableScalarWidget | null = null;
let _mathWaveGradientEntitySelect: EntitySelect | null = null;
let _mathWaveWaveformIconSelects: IconSelect[] = [];
/* ── Destroy (called from modal onForceClose) ─────────────────── */
export function destroyMathWaveWidgets() {
// Waveform icon selects are destroyed in _mathWaveRenderLayers
_mathWaveWaveformIconSelects.forEach(s => s.destroy());
_mathWaveWaveformIconSelects = [];
}
/* ── Speed widget ─────────────────────────────────────────────── */
export function ensureMathWaveSpeedWidget(): BindableScalarWidget {
if (!_mathWaveSpeedWidget) {
_mathWaveSpeedWidget = new BindableScalarWidget({
container: document.getElementById('css-editor-math-wave-speed-container')!,
min: 0.1, max: 10.0, step: 0.1, default: 1.0,
idPrefix: 'css-editor-math-wave-speed',
valueSources: () => _cachedValueSources,
format: (v) => v.toFixed(1),
});
}
return _mathWaveSpeedWidget;
}
/* ── Gradient entity select ───────────────────────────────────── */
export function ensureMathWaveGradientEntitySelect(buildGradientEntityItems: () => any[], syncSelectOptions: (sel: HTMLSelectElement, items: any[]) => void) {
const sel = document.getElementById('css-editor-math-wave-gradient') as HTMLSelectElement | null;
if (!sel) return;
const items = buildGradientEntityItems();
syncSelectOptions(sel, items);
if (_mathWaveGradientEntitySelect) { _mathWaveGradientEntitySelect.refresh(); return; }
_mathWaveGradientEntitySelect = new EntitySelect({
target: sel,
getItems: buildGradientEntityItems,
placeholder: t('palette.search'),
});
}
export function refreshMathWaveGradientEntitySelect() {
if (_mathWaveGradientEntitySelect) _mathWaveGradientEntitySelect.refresh();
}
export function getMathWaveGradientEntitySelect(): EntitySelect | null {
return _mathWaveGradientEntitySelect;
}
/* ── Waveform items ───────────────────────────────────────────── */
function _buildMathWaveWaveformItems(): IconSelectItem[] {
return [
{ value: 'sine', icon: _icon(P.activity), label: t('color_strip.math_wave.waveform.sine'), desc: '' },
{ value: 'triangle', icon: _icon(P.trendingUp), label: t('color_strip.math_wave.waveform.triangle'), desc: '' },
{ value: 'sawtooth', icon: _icon(P.rotateCw), label: t('color_strip.math_wave.waveform.sawtooth'), desc: '' },
{ value: 'square', icon: _icon(P.layoutDashboard), label: t('color_strip.math_wave.waveform.square'), desc: '' },
];
}
/* ── Layer rendering ──────────────────────────────────────────── */
export type MathWaveLayer = { waveform: string; frequency: number; amplitude: number; phase: number; offset: number };
export function mathWaveRenderLayers(waves: MathWaveLayer[]) {
const container = document.getElementById('css-editor-math-wave-layers');
if (!container) return;
// Destroy old waveform icon selects
_mathWaveWaveformIconSelects.forEach(s => s.destroy());
_mathWaveWaveformIconSelects = [];
container.innerHTML = '';
waves.forEach((wave, idx) => {
const row = document.createElement('div');
row.className = 'math-wave-layer-row';
row.style.cssText = 'border:1px solid var(--border-color,#444);border-radius:6px;padding:8px;margin-bottom:6px;position:relative';
row.innerHTML = `
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
<strong>#${idx + 1}</strong>
<button type="button" class="btn btn-icon btn-danger btn-sm" onclick="mathWaveRemoveLayer(${idx})" title="${t('common.delete')}" style="margin-left:auto">${ICON_TRASH}</button>
</div>
<div class="form-group" style="margin-bottom:4px">
<label style="font-size:0.85em" data-i18n="color_strip.math_wave.waveform">${t('color_strip.math_wave.waveform')}</label>
<select class="mw-waveform" data-idx="${idx}">
<option value="sine">${t('color_strip.math_wave.waveform.sine')}</option>
<option value="triangle">${t('color_strip.math_wave.waveform.triangle')}</option>
<option value="sawtooth">${t('color_strip.math_wave.waveform.sawtooth')}</option>
<option value="square">${t('color_strip.math_wave.waveform.square')}</option>
</select>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">
<div class="form-group" style="margin-bottom:0">
<label style="font-size:0.85em" data-i18n="color_strip.math_wave.frequency">${t('color_strip.math_wave.frequency')}</label>
<input type="number" class="mw-frequency" min="0.1" max="20" step="0.1" value="${wave.frequency}">
</div>
<div class="form-group" style="margin-bottom:0">
<label style="font-size:0.85em" data-i18n="color_strip.math_wave.amplitude">${t('color_strip.math_wave.amplitude')}</label>
<input type="number" class="mw-amplitude" min="0.0" max="2.0" step="0.1" value="${wave.amplitude}">
</div>
<div class="form-group" style="margin-bottom:0">
<label style="font-size:0.85em" data-i18n="color_strip.math_wave.phase">${t('color_strip.math_wave.phase')}</label>
<input type="number" class="mw-phase" min="0.0" max="6.28" step="0.1" value="${wave.phase}">
</div>
<div class="form-group" style="margin-bottom:0">
<label style="font-size:0.85em" data-i18n="color_strip.math_wave.offset">${t('color_strip.math_wave.offset')}</label>
<input type="number" class="mw-offset" min="-1.0" max="1.0" step="0.1" value="${wave.offset}">
</div>
</div>
`;
container.appendChild(row);
// Set waveform value and attach IconSelect
const wfSelect = row.querySelector('.mw-waveform') as HTMLSelectElement;
wfSelect.value = wave.waveform || 'sine';
const iconSel = new IconSelect({
target: wfSelect,
items: _buildMathWaveWaveformItems(),
columns: 2,
});
_mathWaveWaveformIconSelects.push(iconSel);
});
}
export function mathWaveGetLayers(): MathWaveLayer[] {
const container = document.getElementById('css-editor-math-wave-layers');
if (!container) return [];
const rows = container.querySelectorAll('.math-wave-layer-row');
return Array.from(rows).map(row => ({
waveform: (row.querySelector('.mw-waveform') as HTMLSelectElement).value,
frequency: parseFloat((row.querySelector('.mw-frequency') as HTMLInputElement).value) || 1.0,
amplitude: parseFloat((row.querySelector('.mw-amplitude') as HTMLInputElement).value) || 1.0,
phase: parseFloat((row.querySelector('.mw-phase') as HTMLInputElement).value) || 0.0,
offset: parseFloat((row.querySelector('.mw-offset') as HTMLInputElement).value) || 0.0,
}));
}
export function mathWaveAddLayer() {
const current = mathWaveGetLayers();
current.push({ waveform: 'sine', frequency: 1.0, amplitude: 1.0, phase: 0.0, offset: 0.0 });
mathWaveRenderLayers(current);
}
export function mathWaveRemoveLayer(idx: number) {
const current = mathWaveGetLayers();
current.splice(idx, 1);
mathWaveRenderLayers(current);
}
@@ -3,20 +3,20 @@
* Extracted from color-strips.ts to reduce file size. * Extracted from color-strips.ts to reduce file size.
*/ */
import { fetchWithAuth, escapeHtml } from '../core/api.ts'; import { fetchWithAuth, escapeHtml } from '../../core/api.ts';
import { t } from '../core/i18n.ts'; import { t } from '../../core/i18n.ts';
import { showToast } from '../core/ui.ts'; import { showToast } from '../../core/ui.ts';
import { import {
ICON_SEARCH, ICON_CLONE, ICON_TRASH, getAssetTypeIcon, ICON_SEARCH, ICON_CLONE, ICON_TRASH, getAssetTypeIcon,
} from '../core/icons.ts'; } from '../../core/icons.ts';
import * as P from '../core/icon-paths.ts'; import * as P from '../../core/icon-paths.ts';
import { IconSelect } from '../core/icon-select.ts'; import { IconSelect } from '../../core/icon-select.ts';
import { EntitySelect } from '../core/entity-palette.ts'; import { EntitySelect } from '../../core/entity-palette.ts';
import { attachNotificationAppPicker, NotificationAppPalette } from '../core/process-picker.ts'; import { attachNotificationAppPicker, NotificationAppPalette } from '../../core/process-picker.ts';
import { _cachedAssets, _cachedValueSources, assetsCache } from '../core/state.ts'; import { _cachedAssets, _cachedValueSources, assetsCache } from '../../core/state.ts';
import { getBaseOrigin } from './settings.ts'; import { getBaseOrigin } from '../settings.ts';
import { BindableScalarWidget } from '../core/bindable-scalar.ts'; import { BindableScalarWidget } from '../../core/bindable-scalar.ts';
import { BindableColorWidget } from '../core/bindable-color.ts'; import { BindableColorWidget } from '../../core/bindable-color.ts';
const _icon = (d: any) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`; const _icon = (d: any) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
@@ -3,19 +3,19 @@
* Extracted from color-strips.ts to reduce file size. * Extracted from color-strips.ts to reduce file size.
*/ */
import { fetchWithAuth, escapeHtml } from '../core/api.ts'; import { fetchWithAuth, escapeHtml } from '../../core/api.ts';
import { colorStripSourcesCache } from '../core/state.ts'; import { colorStripSourcesCache } from '../../core/state.ts';
import { t } from '../core/i18n.ts'; import { t } from '../../core/i18n.ts';
import { showToast, openLightbox, closeLightbox } from '../core/ui.ts'; import { showToast, openLightbox, closeLightbox } from '../../core/ui.ts';
import { createFpsSparkline } from '../core/chart-utils.ts'; import { createFpsSparkline } from '../../core/chart-utils.ts';
import { import {
getColorStripIcon, getColorStripIcon,
ICON_WARNING, ICON_SUN_DIM, ICON_WARNING, ICON_SUN_DIM,
} from '../core/icons.ts'; } from '../../core/icons.ts';
import { EntitySelect } from '../core/entity-palette.ts'; import { EntitySelect } from '../../core/entity-palette.ts';
import { hexToRgbArray, getGradientStops } from './css-gradient-editor.ts'; import { hexToRgbArray, getGradientStops } from '../css-gradient-editor.ts';
import { testNotification, _getAnimationPayload, _colorCycleGetColors } from './color-strips.ts'; import { testNotification, _getAnimationPayload, _colorCycleGetColors } from './index.ts';
import { notificationGetAppColorsDict, notificationGetAppSoundsDict, getNotificationDurationValue, getNotificationVolumeValue } from './color-strips-notification.ts'; import { notificationGetAppColorsDict, notificationGetAppSoundsDict, getNotificationDurationValue, getNotificationVolumeValue } from './notification.ts';
/* ── Preview config builder ───────────────────────────────────── */ /* ── Preview config builder ───────────────────────────────────── */
@@ -53,7 +53,7 @@ import { getActiveSubTab, setActiveSubTab } from '../core/tab-registry.ts';
import { createValueSourceCard } from './value-sources.ts'; import { createValueSourceCard } from './value-sources.ts';
import { createSyncClockCard, initSyncClockDelegation } from './sync-clocks.ts'; import { createSyncClockCard, initSyncClockDelegation } from './sync-clocks.ts';
import { createAssetCard, initAssetDelegation } from './assets.ts'; import { createAssetCard, initAssetDelegation } from './assets.ts';
import { createColorStripCard } from './color-strips.ts'; import { createColorStripCard } from './color-strips/index.ts';
import { initAudioSourceDelegation } from './audio-sources.ts'; import { initAudioSourceDelegation } from './audio-sources.ts';
import { createAudioProcessingTemplateCard } from './audio-processing-templates.ts'; import { createAudioProcessingTemplateCard } from './audio-processing-templates.ts';
import { import {