import { html, nothing, type TemplateResult } from 'lit'; import { classifyPlanFile, encodePlanFile, renderBackdropGuard } from './backdrop-pick'; import { hasTranslation, langOf, t, type I18nKey } from './i18n'; import './hp-help'; import { DEFAULT_CUSTOM_FILL, DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY, DEFAULT_TEMP_MAX, DEFAULT_TEMP_MIN, SPACE_FILL_UI_MODES, customFillOf, spaceDisplayOf, stageBgOf, } from './logic'; import { bgModeOf, northDegOf } from './sun'; import { createEmptySpaceConfig, initialSpaceDisplayDraft, switchSpacePlanSource, touchSpaceDisplay, } from './space-dialog'; import { collectSpaceMarkerDependencies } from './space-deletion'; import { gridCellFieldToCm, gridCellFieldValue, newSpaceCellCm, } from './grid-scale'; import { zeroWallStyleOf } from './zero-walls'; import type { HouseplanEditorHostPort } from './houseplan-editor-runtime'; const BUILD_FINGERPRINT = '__HOUSEPLAN_SOURCE_FINGERPRINT__'; const CELL_CM_MIN = 0.1; const CELL_CM_MAX = 1000; const strictNumber = (value: string): number | null => { if (!value.trim()) return null; const parsed = Number(value.replace(',', '.')); return Number.isFinite(parsed) ? parsed : null; }; /** * Space creation/import is part of empty-install onboarding, not an editor. * It therefore has its own lazy boundary: normal View downloads neither this * module nor the editor, while a new empty installation can create its first * space without ever requesting the editor chunk. */ export const ONBOARDING_RUNTIME_FINGERPRINT = BUILD_FINGERPRINT; export class HouseplanOnboardingRuntime { public constructor(public readonly host: HouseplanEditorHostPort) {} private _help(key: Extract): TemplateResult | typeof nothing { const ariaKey = `${key}.aria` as I18nKey; const lang = langOf(this.host.hass, this.host._config?.language); if (!hasTranslation(lang, key) || !hasTranslation(lang, ariaKey)) return nothing; return html``; } public _openSpaceDialog(mode: 'edit' | 'create', spaceId?: string): void { if (!this.host._serverStorage || !this.host._serverCfg) { this.host._showToast(this.host._t('toast.integration_missing')); return; } if (mode === 'edit') { const sp = this.host._serverCfg.spaces.find((space) => space.id === spaceId); if (!sp) return; const disp = spaceDisplayOf(sp); const storedCustom = sp.settings?.custom_fill && typeof sp.settings.custom_fill === 'object' ? customFillOf(sp.settings.custom_fill) : null; const dialogCustom = disp.fill === 'none' ? { ...(storedCustom || DEFAULT_CUSTOM_FILL), a: 0 } : storedCustom; this.host._spaceDialog = { mode, spaceId, title: sp.title, planUrl: sp.plan_url || null, planFile: null, source: sp.plan_url ? 'file' : 'draw', showBorders: disp.showBorders, showNames: disp.showNames, zeroWallStyle: zeroWallStyleOf(sp), displayTouched: true, hideDecor: disp.hideDecor, hideOpenings: disp.hideOpenings, roomColor: disp.color, roomOpacity: disp.opacity, fillMode: disp.fill === 'none' ? 'custom' : disp.fill, customFill: dialogCustom, glowEnabled: disp.glow, bgColor: disp.bgColor, bgMode: sp.settings?.bg_mode === 'static' || sp.settings?.bg_mode === 'daynight' ? sp.settings.bg_mode : null, northDeg: northDegOf({}, sp.settings), sunRays: typeof sp.settings?.sun_rays === 'boolean' ? sp.settings.sun_rays : null, tempMin: disp.tempMin, tempMax: disp.tempMax, showLqi: disp.showLqi ?? this.host._config?.show_signal ?? true, cardFontScale: disp.cardFontScale, labelTemp: disp.labelTemp, labelHum: disp.labelHum, labelLqi: disp.labelLqi, labelLight: disp.labelLight, cellCm: Number(sp.cell_cm) > 0 ? Number(sp.cell_cm) : 5, cellCmInput: gridCellFieldValue( Number(sp.cell_cm) > 0 ? Number(sp.cell_cm) : 5, this.host._imperial, ), cellCmTouched: false, busy: false, }; return; } const cellCm = newSpaceCellCm(this.host._imperial); this.host._spaceDialog = { mode, title: '', planUrl: null, planFile: null, ...initialSpaceDisplayDraft(), hideDecor: false, hideOpenings: false, zeroWallStyle: 'dashed', roomColor: DEFAULT_ROOM_COLOR, roomOpacity: DEFAULT_ROOM_OPACITY, fillMode: 'custom', customFill: { ...DEFAULT_CUSTOM_FILL, a: 0 }, glowEnabled: true, bgColor: null, bgMode: 'daynight', northDeg: null, sunRays: null, tempMin: DEFAULT_TEMP_MIN, tempMax: DEFAULT_TEMP_MAX, showLqi: this.host._config?.show_signal ?? true, cardFontScale: 1, labelTemp: false, labelHum: false, labelLqi: false, labelLight: false, cellCm, cellCmInput: gridCellFieldValue(cellCm, this.host._imperial), cellCmTouched: false, busy: false, }; } public async _pickPlanFile(ev: Event): Promise { const input = ev.target as HTMLInputElement; const file = input.files?.[0]; if (!file || !this.host._spaceDialog) return; // #39: re-selecting the same file after a guard decision must fire again. input.value = ''; const classified = await classifyPlanFile(file); if (classified.kind === 'reject') { this.host._showToast(this.host._t('toast.plan_formats')); return; } if (classified.kind === 'guard') { this.host._backdropGuard = classified.state; return; } const payload = await encodePlanFile(file, classified.ext, file.name); if (!this.host._spaceDialog) return; this.host._spaceDialog = { ...this.host._spaceDialog, planFile: payload }; } public _renderBackdropGuard(): TemplateResult | typeof nothing { return renderBackdropGuard( this.host, (payload) => { if (this.host._spaceDialog) { this.host._spaceDialog = { ...this.host._spaceDialog, planFile: payload }; } }, () => { this.host._backdropGuard = null; }, this.host.hass, ) ?? nothing; } public _toggleServerPlans = async (): Promise => { const dialog = this.host._spaceDialog; if (!dialog) return; if (dialog.pickSaved) { this.host._spaceDialog = { ...dialog, pickSaved: false }; return; } this.host._spaceDialog = { ...dialog, pickSaved: true, savedBusy: true }; try { const response: { plans?: NonNullable } = await this.host.hass.callWS({ type: 'houseplan/plans/list', }); const current = this.host._spaceDialog; if (current) this.host._spaceDialog = { ...current, saved: response?.plans || [], savedBusy: false, }; } catch (error: unknown) { const current = this.host._spaceDialog; if (current) this.host._spaceDialog = { ...current, saved: [], savedBusy: false }; this.host._showToast(this.host._t('toast.plans_list_failed', { err: this.host._errText(error), })); } }; public _useServerPlan(url: string): void { const dialog = this.host._spaceDialog; if (!dialog) return; this.host._spaceDialog = { ...dialog, planUrl: url, planFile: null, pickSaved: false, savedAspect: undefined, }; this.host._aspectJob = this._readPlanAspect(url); } public async _readPlanAspect(url: string): Promise { for (let i = 0; i < 40; i++) { const src = this.host._display(url); if (src) { const ratio = await new Promise((resolve) => { const image = new Image(); image.onload = () => resolve(image.naturalWidth && image.naturalHeight ? image.naturalWidth / image.naturalHeight : 0); image.onerror = () => resolve(0); image.src = src; }); const current = this.host._spaceDialog; if (current && current.planUrl === url && Number.isFinite(ratio) && ratio > 0) { this.host._spaceDialog = { ...current, savedAspect: ratio }; return ratio; } return 0; } await new Promise((resolve) => setTimeout(resolve, 150)); if (this.host._spaceDialog?.planUrl !== url) return 0; } return 0; } public async _deleteServerPlan(name: string): Promise { const dialog = this.host._spaceDialog; const plan = dialog?.saved?.find((candidate) => candidate.name === name); if (!dialog || !plan || plan.used_by.length || plan.url === dialog.planUrl) return; const accepted = await this.host._confirmDanger({ key: 'delete-plan', kind: 'destructive', title: this.host._t('confirm.delete_plan_title'), message: this.host._t('confirm.delete_plan_body'), objectName: name, confirmLabel: this.host._t('btn.delete'), cancelLabel: this.host._t('btn.cancel'), }); const currentDialog = this.host._spaceDialog; const currentPlan = currentDialog?.saved?.find((candidate) => candidate.name === name); if (!accepted || !currentDialog || !currentPlan || currentPlan.url !== plan.url || currentPlan.modified !== plan.modified || currentPlan.used_by.length || currentPlan.url === currentDialog.planUrl) return; try { await this.host.hass.callWS({ type: 'houseplan/plans/delete', name }); const dialog = this.host._spaceDialog; if (dialog?.saved) this.host._spaceDialog = { ...dialog, saved: dialog.saved.filter((plan) => plan.name !== name), }; } catch (error: unknown) { this.host._showToast(this.host._t('toast.plan_delete_failed', { err: this.host._errText(error), })); } } public _renderServerPlans( dialog: NonNullable, ): TemplateResult { if (dialog.savedBusy) return html`
${this.host._t('space.loading')}
`; const list = dialog.saved || []; if (!list.length) return html`
${this.host._t('space.no_saved')}
`; const kb = (size: number) => (size >= 1048576 ? `${(size / 1048576).toFixed(1)} MB` : `${Math.round(size / 1024)} KB`); return html`
${list.map((plan) => html`
${plan.name} ${kb(plan.size)}${plan.used_by.length ? ` · ${this.host._t('space.used_by', { list: plan.used_by.join(', ') })}` : ''}
`)}
`; } private async _saveConfigNow(): Promise { this.host._cfgEpoch++; try { await this.host._writeConfig(); } catch (error: unknown) { const failure = error as { physicalGeometryRolledBack?: boolean; code?: string }; if (failure?.physicalGeometryRolledBack) await this.host._reloadRejectedPhysicalWrite(); else if (failure?.code === 'conflict') await this.host._reloadConfigOnly(); throw error; } } public async _saveSpaceDialog(): Promise { const dialog = this.host._spaceDialog; if (!dialog || dialog.busy || !dialog.title.trim()) return; if (dialog.source === 'file' && !dialog.planFile && !dialog.planUrl) { this.host._showToast(this.host._t('toast.plan_required')); return; } const wasFirst = dialog.mode === 'create' && (this.host._serverCfg?.spaces.length || 0) === 0; this.host._spaceDialog = { ...dialog, busy: true }; try { const spaceId = dialog.mode === 'create' ? `s${Date.now().toString(36)}` : dialog.spaceId!; let uploaded: { url: string; aspect: number } | null = null; if (dialog.source === 'file' && dialog.planFile) { const response: { url: string } = await this.host.hass.callWS({ type: 'houseplan/plan/set', space_id: spaceId, ext: dialog.planFile.ext, data: dialog.planFile.b64, }); uploaded = { url: response.url, aspect: dialog.planFile.aspect }; } let pickedAspect: number | null = dialog.savedAspect || null; if (!uploaded && dialog.source === 'file' && dialog.planUrl && !pickedAspect && this.host._aspectJob) { pickedAspect = (await this.host._aspectJob) || null; } const config = this.host._serverCfg!; let space = config.spaces.find((candidate) => candidate.id === spaceId); if (dialog.mode === 'create') { space = createEmptySpaceConfig(spaceId, dialog.title.trim()); config.spaces.push(space); } else { if (!space) throw new Error(`space ${spaceId} is gone from the config`); space.title = dialog.title.trim(); } if (!space) throw new Error(`space ${spaceId} is unavailable`); if (uploaded) { space.plan_url = uploaded.url; space.plan_aspect = uploaded.aspect; } else if (dialog.source === 'file' && dialog.planUrl && dialog.planUrl !== space.plan_url) { space.plan_url = dialog.planUrl; space.plan_aspect = pickedAspect; } if (dialog.source === 'draw') { space.plan_url = null; space.plan_aspect = null; delete space.plan_x; delete space.plan_y; delete space.plan_scale; delete space.plan_scale_x; delete space.plan_scale_y; delete space.plan_angle; } space.settings = { ...(space.settings || {}), show_borders: dialog.showBorders, show_names: dialog.showNames, hide_decor: dialog.hideDecor || undefined, hide_openings: dialog.hideOpenings || undefined, room_color: dialog.roomColor, room_opacity: dialog.roomOpacity, bg_color: dialog.bgColor || undefined, bg_mode: dialog.bgMode || undefined, north_deg: dialog.northDeg ?? undefined, sun_rays: dialog.sunRays ?? undefined, fill_mode: dialog.fillMode, custom_fill: dialog.customFill || undefined, glow_enabled: dialog.glowEnabled, temp_min: Number.isFinite(dialog.tempMin) ? Math.min(dialog.tempMin, dialog.tempMax) : DEFAULT_TEMP_MIN, temp_max: Number.isFinite(dialog.tempMax) ? Math.max(dialog.tempMin, dialog.tempMax) : DEFAULT_TEMP_MAX, show_lqi: dialog.showLqi, card_font_scale: dialog.cardFontScale !== 1 ? dialog.cardFontScale : undefined, label_temp: dialog.labelTemp, label_hum: dialog.labelHum, label_lqi: dialog.labelLqi, label_light: dialog.labelLight, }; space.zero_wall_style = dialog.zeroWallStyle; space.cell_cm = Number.isFinite(dialog.cellCm) && dialog.cellCm > 0 ? Math.max(CELL_CM_MIN, Math.min(CELL_CM_MAX, dialog.cellCm)) : 5; await this._saveConfigNow(); this.host._spaceDialog = null; if (dialog.mode === 'create') this.host._commitSpace(space.id); this.host._regSignature = ''; this.host._maybeRebuildDevices(); if (this.host._importQueue.length) { this._openNextImport(); } else if (wasFirst || this.host._importTotal > 0) { const wasImport = this.host._importTotal > 0; this.host._importTotal = 0; this.host._commitSpace(this.host._serverCfg!.spaces[0]?.id || this.host._space); await this.host._requestMode('plan'); this.host._tool = 'draw'; this.host._path = []; this.host._cursorPt = null; this.host._primeDrawWallField(); this.host._showToast(this.host._t(wasFirst && !wasImport ? 'toast.space_added_onboard' : 'import.done')); } else { this.host._showToast(this.host._t(dialog.mode === 'create' ? 'toast.space_added' : 'toast.space_saved')); if (dialog.mode === 'create') { if (this.host._mode !== 'plan') await this.host._requestMode('plan'); else { this.host._tool = 'draw'; this.host._path = []; this.host._cursorPt = null; this.host._primeDrawWallField(); this.host._saveNav(); } } } } catch (error: unknown) { const failure = error as { code?: string }; if (failure?.code !== 'conflict') await this.host._reloadConfigOnly(true); if (this.host._spaceDialog) this.host._spaceDialog = { ...this.host._spaceDialog, busy: false, }; this.host._showToast(this.host._t('toast.error', { err: this.host._errText(error) })); } } public async _deleteSpace(): Promise { const dialog = this.host._spaceDialog; if (!dialog || dialog.mode !== 'edit') return; const serverConfig = this.host._serverCfg; if (!serverConfig) return; const space = serverConfig.spaces.find((candidate) => candidate.id === dialog.spaceId); if (!space) return; const dependencies = collectSpaceMarkerDependencies( serverConfig, this.host._layout || {}, dialog.spaceId || '', ); const deletingLastSpace = serverConfig.spaces.length === 1 && serverConfig.spaces[0]?.id === dialog.spaceId; if (dependencies.count && !deletingLastSpace) { this.host._spaceDialog = { ...dialog, deleteBlockers: dependencies.count }; return; } const spaceId = dialog.spaceId!; const accepted = await this.host._confirmDanger({ key: 'delete-space', kind: 'destructive', title: this.host._t('confirm.delete_space_title'), message: this.host._t('confirm.delete_space_body'), objectName: space.title, confirmLabel: this.host._t('btn.delete'), cancelLabel: this.host._t('btn.cancel'), }); const currentDialog = this.host._spaceDialog; const currentConfig = this.host._serverCfg; if (!accepted || !currentDialog || currentDialog.mode !== 'edit' || currentDialog.busy || currentDialog.spaceId !== spaceId || !currentConfig) return; const currentSpace = currentConfig.spaces.find((candidate) => candidate.id === spaceId); if (!currentSpace) return; const currentDependencies = collectSpaceMarkerDependencies( currentConfig, this.host._layout || {}, spaceId, ); const currentlyDeletingLastSpace = currentConfig.spaces.length === 1 && currentConfig.spaces[0]?.id === spaceId; if (currentDependencies.count && !currentlyDeletingLastSpace) { this.host._spaceDialog = { ...currentDialog, deleteBlockers: currentDependencies.count, }; return; } this.host._spaceDialog = { ...currentDialog, deleteBlockers: 0, busy: true }; try { if (this.host._saveConfigDebounced.pending()) this.host._saveConfigDebounced.flush(); if (this.host._persistLayout.pending()) this.host._persistLayout.flush(); await this.host._writeChain; const response: { config_rev?: number; layout_rev?: number } = await this.host.hass.callWS({ type: 'houseplan/space/delete', space_id: spaceId, expected_config_rev: this.host._cfgRev, expected_layout_rev: this.host._layoutRev, }); const [configResponse, layoutResponse] = await Promise.all([ this.host._getAuthoritativeConfig(), this.host.hass.callWS({ type: 'houseplan/layout/get' }), ]); this.host._adoptStructuralResponses(configResponse, layoutResponse); this.host._cfgRev = response?.config_rev ?? this.host._cfgRev; this.host._layoutRev = response?.layout_rev ?? this.host._layoutRev; this.host._spaceDialog = null; if (this.host._space === spaceId) { this.host._commitSpace(this.host._serverCfg!.spaces[0]?.id || ''); } this.host._regSignature = ''; this.host._maybeRebuildDevices(); this.host._showToast(this.host._t('toast.space_deleted')); } catch (error: unknown) { const failure = error as { code?: string }; if (failure?.code === 'conflict' || failure?.code === 'space_in_use') { await Promise.all([ this.host._reloadConfigOnly(true), this.host._reloadLayoutOnly(), ]); } const refreshedConfig = this.host._serverCfg; if (this.host._spaceDialog && refreshedConfig) { const refreshed = collectSpaceMarkerDependencies( refreshedConfig, this.host._layout || {}, spaceId, ); const stillLastSpace = refreshedConfig.spaces.length === 1 && refreshedConfig.spaces[0]?.id === spaceId; this.host._spaceDialog = { ...this.host._spaceDialog, busy: false, deleteBlockers: stillLastSpace ? 0 : refreshed.count, }; } this.host._showToast(this.host._t('toast.delete_failed', { err: this.host._errText(error), })); } } public _startImport(): void { const dialog = this.host._importDialog; if (!dialog) return; const titles = dialog.floors.filter((floor) => floor.checked).map((floor) => floor.name); this.host._importDialog = null; if (!titles.length) { this._openSpaceDialog('create'); return; } this.host._importQueue = titles; this.host._importTotal = titles.length; this._openNextImport(); } public _openNextImport(): void { const title = this.host._importQueue.shift(); if (title === undefined) return; const cellCm = newSpaceCellCm(this.host._imperial); this.host._spaceDialog = { mode: 'create', title, planUrl: null, planFile: null, ...initialSpaceDisplayDraft(), hideDecor: false, hideOpenings: false, zeroWallStyle: 'dashed', roomColor: DEFAULT_ROOM_COLOR, roomOpacity: DEFAULT_ROOM_OPACITY, fillMode: 'custom', customFill: null, glowEnabled: true, bgColor: null, bgMode: 'daynight', northDeg: null, sunRays: null, tempMin: DEFAULT_TEMP_MIN, tempMax: DEFAULT_TEMP_MAX, showLqi: this.host._config?.show_signal ?? true, cardFontScale: 1, labelTemp: false, labelHum: false, labelLqi: false, labelLight: false, cellCm, cellCmInput: gridCellFieldValue(cellCm, this.host._imperial), cellCmTouched: false, busy: false, }; } public _skipImport(): void { this.host._spaceDialog = null; if (this.host._importQueue.length) this._openNextImport(); else if (this.host._importTotal > 0 && this.host._model.length) { this.host._importTotal = 0; this.host._commitSpace(this.host._serverCfg!.spaces[0]?.id || this.host._space); void this.host._requestMode('plan').then(() => { this.host._showToast(this.host._t('import.done')); }); } } public _renderImportDialog(): TemplateResult { const dialog = this.host._importDialog!; const selected = dialog.floors.filter((floor) => floor.checked).length; return html` (this.host._importDialog = null)}>
${this.host._t('import.hint')}
${dialog.floors.map((floor, index) => html``)}
`; } private _boolInput(value: boolean, change: (checked: boolean) => void): TemplateResult { return html` change((event.target as HTMLInputElement).checked)} />`; } private _rangeInput( min: number, max: number, step: number, value: number, change: (next: number) => void, ): TemplateResult { return html` change(Number((event.target as HTMLInputElement).value))} />`; } public _renderSpaceDialog(): TemplateResult { return this._renderSpaceDialogBody(this.host._spaceDialog!); } private _renderSpaceDialogBody( dialog: NonNullable, ): TemplateResult { const progress = this.host._importTotal > 0 && dialog.mode === 'create' ? this.host._t('import.progress', { i: this.host._importTotal - this.host._importQueue.length, n: this.host._importTotal, }) : ''; const close = () => { this.host._spaceDialog = null; this.host._importQueue = []; this.host._importTotal = 0; }; return html`
(this.host._spaceDialog = { ...dialog, title: (event.target as HTMLInputElement).value, })} /> ${dialog.source === 'file' ? html`
${dialog.planFile ? html`${dialog.planFile.name}` : dialog.planUrl ? html`${this.host._t('space.plan_alt')}` : html`${this.host._t('space.no_plan')}`} this._pickPlanFile(event)} />
${dialog.pickSaved ? this._renderServerPlans(dialog) : nothing}` : nothing}
${this._help('space.cell_cm.help')}
{ const raw = (event.target as HTMLInputElement).value; const parsed = strictNumber(raw); const canonical = parsed == null ? null : gridCellFieldToCm(parsed, this.host._imperial); this.host._spaceDialog = { ...dialog, cellCmInput: raw, cellCmTouched: true, cellCm: canonical != null && canonical > 0 ? Math.max(CELL_CM_MIN, Math.min(CELL_CM_MAX, canonical)) : dialog.cellCm, }; }} /> ${this.host._t(this.host._imperial ? 'space.scale_unit_imperial' : 'space.scale_unit')}
${this._help('space.zero_wall_style.help')}
${this.host._t('space.hide_decor_tip')}
${this.host._t('space.hide_openings_tip')}
${([ ['labelTemp', 'space.label_temp'], ['labelHum', 'space.label_hum'], ['labelLqi', 'space.label_lqi'], ['labelLight', 'space.label_light'], ] as const).map(([field, key]) => html``)}
${this._rangeInput(50, 300, 5, Math.round(dialog.cardFontScale * 100), (value) => { this.host._spaceDialog = { ...dialog, cardFontScale: value / 100 }; })} ${Math.round(dialog.cardFontScale * 100)}%
${this.host._renderCardPreview(dialog.cardFontScale, 1, 1)}
) => { this.host._spaceDialog = { ...dialog, roomColor: event.detail.color, roomOpacity: event.detail.opacity, }; }}>
${this._help('space.bg_mode.help')}
${(dialog.bgMode ?? bgModeOf(this.host._settings, {})) === 'static' ? html`
) => { this.host._spaceDialog = { ...dialog, bgColor: event.detail.color }; }}> ${dialog.bgColor ? html`` : html`${this.host._t('space.bg_inherited')}`}
` : nothing}
${this._help('space.north.help')}
{ const raw = (event.target as HTMLInputElement).value.trim(); const value = raw === '' ? null : Math.round(Number(raw)); this.host._spaceDialog = { ...dialog, northDeg: value !== null && Number.isFinite(value) ? Math.min(359, Math.max(0, value)) : null, }; }} /> ${dialog.northDeg === null ? this.host._t('space.north_inherited', { v: northDegOf(this.host._settings, {}) === null ? '—' : `${northDegOf(this.host._settings, {})}°`, }) : '°'}
${this.host._t('space.fill_label')} ${this._help('space.fill_mode.help')}
${SPACE_FILL_UI_MODES.map((value) => [value, `fill.${value}`] as const).map( ([value, key]) => html` ${value === 'custom' && dialog.fillMode === 'custom' ? html`
${this.host._t('space.custom_fill')} ) => { this.host._spaceDialog = { ...dialog, customFill: { c: event.detail.color, a: event.detail.opacity }, }; }}> ${dialog.customFill ? html`` : nothing}
` : nothing}`, )} ${dialog.deleteBlockers ? html`` : nothing}
`; } }