import { type CSSResultGroup, html, nothing, type PropertyValues, unsafeCSS } from 'lit'; import { HasSlotController } from '../../internal/slot'; import { ifDefined } from 'lit/directives/if-defined.js'; import { MutationController } from '@lit-labs/observers/mutation-controller.js'; import { property, query, queryAll, state } from 'lit/decorators.js'; import ZincElement from '../../internal/zinc-element'; import ZnButton from '../button'; import ZnButtonGroup from '../button-group'; import ZnCollapsible from '../collapsible'; import ZnIcon from '../icon'; import ZnNavbar from '../navbar'; import ZnOption from '../option'; import ZnPreviewFrame from '../preview-frame'; import ZnSelect from '../select'; import ZnTabs from '../tabs'; import type { ZnErrorEvent } from '../../events/zn-error'; import styles from './theme-editor.scss'; export type ThemeEditorMode = 'light' | 'dark'; export type ThemeEditorDevice = 'desktop' | 'tablet' | 'mobile'; export interface ThemeEditorGroup { /** The slot name controls are assigned to with `slot=""`. */ name: string; caption: string; description?: string; /** Renders expanded initially. */ open?: boolean; } export interface ThemeEditorSection extends ThemeEditorGroup { /** * Nests a collapsible per group inside this section's tab instead of the * section's own controls directly. A non-empty `groups` on ANY section * switches every section to `zn-tabs`, regardless of `section-layout`. */ groups?: ThemeEditorGroup[]; } export interface ThemeEditorSource { label: string; src: string; } // Controls whose state lives on `checked` rather than `value`. const BOOLEAN_CONTROLS = new Set(['zn-checkbox', 'zn-toggle']); // Matches theme-editor.scss's stacked breakpoint - keep both in sync. const STACKED_QUERY = '(max-width: 768px)'; const DEVICES: { id: ThemeEditorDevice; icon: string; label: string }[] = [ { id: 'desktop', icon: 'monitor', label: 'Desktop' }, { id: 'tablet', icon: 'tablet', label: 'Tablet' }, { id: 'mobile', icon: 'smartphone', label: 'Mobile' }, ]; interface HarvestableControl extends HTMLElement { name?: string; value?: unknown; checked?: boolean; disabled?: boolean; type?: string; } /** Turns a freeform `group`/`category` label into a slot-safe name. */ function slugify(label: string): string { return label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); } interface DerivedStructure { sections: ThemeEditorSection[]; /** The slot name each control must be assigned to. */ assignments: Map; } /** * @summary A theme editor: slotted form controls drive a live preview frame, * with a toolbar for the preview's light/dark mode and device width. * @documentation https://zinc.style/components/theme-editor * @status experimental * @since 1.0 * * @dependency zn-collapsible * @dependency zn-preview-frame * @dependency zn-icon * @dependency zn-button * @dependency zn-button-group * @dependency zn-tabs * @dependency zn-navbar * @dependency zn-select * * @event zn-theme-change - Emitted when the values, mode or device change. * @event zn-theme-submit - Emitted on submit (button click), carrying the * current values. With `action` set, only fires after a successful save. * @event zn-error - Emitted when a save fails. Also seen for preview render * failures: the frame's zn-error is composed and not stopped, so it bubbles * out through the editor too. * * @slot - Ungrouped theme controls, rendered above any sections. Controls * assigned `slot=""` matching a `sections` entry (or, when nested, a * `groups` entry) render inside that section/group instead. Harvesting and * change detection walk every slot's full assigned subtree, not just direct * children. * * With `sections` left unset, the structure is instead derived from the * controls' own attributes: `group="