@zodal/dials-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/widgets.ts","../src/registry.ts","../src/introspect.ts","../src/facets.ts","../src/search.ts","../src/lifecycle.ts","../src/form.ts","../src/store.ts","../src/profiles.ts"],"sourcesContent":["/**\n * @zodal/dials-ui — the headless settings UI layer for zodal-dials.\n *\n * Emits plain configuration objects (never DOM/React) that concrete renderers turn into a panel:\n * - `toSettingsForm(dials, options)` — ordered field configs + facet-projected groups.\n * - `toFieldStates(fields, result)` — value/provenance/managed/dirty state per field.\n * - widget classification (`widgetKindFor`) + the settings renderer registry\n * (`createSettingsRendererRegistry`, PRIORITY bands, composable testers incl. the terminal\n * `alwaysMatch` rawJson fallback).\n * - faceted organization (`toGroups`) and search (`toIndexableSettings`, `createSubstringSearchProvider`,\n * `parseScopedQuery`, `applyScopedFilters`, `searchSettings`).\n * - change-lifecycle helpers (`dirtyKeys`, `resetToDefault`, `unsetKey`, `recordLayerChange`, undo).\n */\n\nexport type {\n WidgetKind,\n SettingFieldConfig,\n SettingFieldState,\n SettingsGroup,\n SettingsForm,\n IndexableSetting,\n SearchProvider,\n} from './types.js';\n\nexport { widgetKindFor } from './widgets.js';\nexport type { WidgetInput } from './widgets.js';\n\nexport {\n createSettingsRendererRegistry,\n secretRoleIs,\n isStructuredValue,\n isBoolean,\n isEnum,\n isNumber,\n isString,\n alwaysMatch,\n // re-exported zodal renderer-registry primitives\n createRendererRegistry,\n PRIORITY,\n zodTypeIs,\n fieldNameMatches,\n metaMatches,\n hasRefinement,\n editWidgetIs,\n and,\n or,\n} from './registry.js';\nexport type { RendererRegistry, RendererTester } from './registry.js';\n\nexport { describeSettings } from './introspect.js';\nexport type { DescribeOptions } from './introspect.js';\n\nexport { toGroups } from './facets.js';\nexport type { FacetDef, GroupingOptions } from './facets.js';\n\nexport {\n toIndexableSettings,\n createSubstringSearchProvider,\n parseScopedQuery,\n applyScopedFilters,\n searchSettings,\n} from './search.js';\nexport type { IndexField, SubstringSearchOptions, ScopeFilter, ParsedQuery, FilterContext } from './search.js';\n\nexport { dirtyKeys, isDirty, resetToDefault, unsetKey, recordLayerChange, applyLayerPatch } from './lifecycle.js';\nexport type { ChangeRecord } from './lifecycle.js';\n\nexport { toSettingsForm, toFieldStates } from './form.js';\nexport type { ToSettingsFormOptions } from './form.js';\n\nexport { createSettingsStore } from './store.js';\nexport type { SettingsStore, SettingsState, CreateSettingsStoreOptions } from './store.js';\n\nexport {\n createProfileStore,\n createMemoryProfileStorage,\n createLocalStorageProfileStorage,\n} from './profiles.js';\nexport type { ProfileStore, ProfileStorage, ProfileStoreOptions, ProfileMeta, NamedProfile } from './profiles.js';\n","/**\n * Type -> widget classification for settings. Pure. Distinguishes a scalar leaf (switch/select/\n * slider/text/…) from an irreducible nested value (object/array), with `rawJson` as the terminal\n * fallback for anything unhandled — so coverage is total and degradation is honest. A `secret`\n * setting always maps to the `secret` widget regardless of its underlying type.\n */\n\nimport type { Sensitivity } from '@zodal/dials-core';\nimport type { WidgetKind } from './types.js';\n\nexport interface WidgetInput {\n zodType: string;\n sensitivity: Sensitivity;\n bounds?: { min?: number; max?: number };\n enumValues?: string[];\n /** An explicit `.meta({ editWidget })` override (wins over inference). */\n metaWidget?: unknown;\n}\n\nconst KNOWN_WIDGETS = new Set<WidgetKind>([\n 'switch', 'select', 'radio', 'slider', 'number', 'text', 'textarea',\n 'secret', 'color', 'date', 'path', 'object', 'array', 'rawJson',\n]);\n\n/** Choose the widget kind for a setting. */\nexport function widgetKindFor(input: WidgetInput): WidgetKind {\n if (typeof input.metaWidget === 'string' && KNOWN_WIDGETS.has(input.metaWidget as WidgetKind)) {\n return input.metaWidget as WidgetKind;\n }\n if (input.sensitivity === 'secret') return 'secret';\n switch (input.zodType) {\n case 'boolean':\n return 'switch';\n case 'enum':\n return input.enumValues && input.enumValues.length > 0 && input.enumValues.length <= 4 ? 'radio' : 'select';\n case 'number':\n return input.bounds && input.bounds.min !== undefined && input.bounds.max !== undefined ? 'slider' : 'number';\n case 'string':\n return 'text';\n case 'object':\n return 'object';\n case 'array':\n return 'array';\n default:\n return 'rawJson';\n }\n}\n","/**\n * The settings renderer registry — a thin specialization of zodal's capability-ranked\n * `RendererRegistry`. Concrete renderer packages (vanilla, shadcn, …) register `(tester, renderer)`\n * entries against the same open-closed API; selection is by PRIORITY band + composable testers, with\n * a terminal `alwaysMatch` entry (the rawJson fallback) guaranteeing total coverage and honest\n * degradation. Settings-specific testers read `sensitivity`/`structured` from the render context.\n */\n\nimport {\n createRendererRegistry,\n PRIORITY,\n zodTypeIs,\n fieldNameMatches,\n metaMatches,\n hasRefinement,\n editWidgetIs,\n and,\n or,\n} from '@zodal/ui';\nimport type { RendererRegistry, RendererTester } from '@zodal/ui';\n\n// Re-export the zodal renderer-registry primitives so renderer authors import everything from one place.\nexport {\n createRendererRegistry,\n PRIORITY,\n zodTypeIs,\n fieldNameMatches,\n metaMatches,\n hasRefinement,\n editWidgetIs,\n and,\n or,\n};\nexport type { RendererRegistry, RendererTester };\n\n/** Match a secret setting (its `sensitivity` is supplied via the render context). High priority so a\n * secret is always rendered with the masked widget regardless of its underlying type. */\nexport function secretRoleIs(): RendererTester {\n return (_field, ctx) => (ctx.sensitivity === 'secret' ? PRIORITY.OVERRIDE : -1);\n}\n\n/** Match an irreducible nested value (`structured: true` in context, or an object/array Zod type). */\nexport function isStructuredValue(): RendererTester {\n return (field, ctx) =>\n ctx.structured === true || field.zodType === 'object' || field.zodType === 'array' ? PRIORITY.LIBRARY : -1;\n}\n\nexport function isBoolean(): RendererTester {\n return zodTypeIs('boolean');\n}\nexport function isEnum(): RendererTester {\n return zodTypeIs('enum');\n}\nexport function isNumber(): RendererTester {\n return zodTypeIs('number');\n}\nexport function isString(): RendererTester {\n return zodTypeIs('string');\n}\n\n/**\n * Terminal renderer tester: ALWAYS matches, at the given priority (default FALLBACK). Pairing this\n * with a raw-JSON renderer guarantees every setting resolves to something (the honest-degradation\n * fallback).\n */\nexport function alwaysMatch(priority: number = PRIORITY.FALLBACK): RendererTester {\n return () => priority;\n}\n\n/** Create a settings renderer registry (a zodal `RendererRegistry`). Concrete renderer packages\n * populate it; remember to register a terminal `alwaysMatch` rawJson renderer for full coverage. */\nexport function createSettingsRendererRegistry<TComponent = unknown>(): RendererRegistry<TComponent> {\n return createRendererRegistry<TComponent>();\n}\n","/**\n * Build the static, value-independent `SettingFieldConfig[]` from a `DialsDefinition`, combining the\n * schema (via dials-core introspection helpers + `@zodal/core` enum/bounds helpers), the dials\n * classification (sensitivity, merge strategy, defaults), and `.meta()` annotations + an optional\n * external facet-assignment map. Pure.\n */\n\nimport type { z } from 'zod';\nimport { baseType, getObjectShape, readMeta } from '@zodal/dials-core';\nimport type { DialsDefinition } from '@zodal/dials-core';\nimport { getEnumValues, getNumericBounds, humanizeFieldName } from '@zodal/core';\nimport { widgetKindFor } from './widgets.js';\nimport type { SettingFieldConfig } from './types.js';\n\nexport interface DescribeOptions {\n /** Extra facet membership, merged with each setting's `.meta({ facets })`: key -> facet ids. */\n facets?: Record<string, string[]>;\n}\n\nfunction safe<T>(fn: () => T): T | undefined {\n try {\n return fn();\n } catch {\n return undefined;\n }\n}\n\nfunction uniqueStrings(values: string[]): string[] {\n return [...new Set(values)];\n}\n\n/** Describe every setting in a dials definition as a headless field config. */\nexport function describeSettings<T extends z.ZodObject<z.ZodRawShape>>(\n dials: DialsDefinition<T>,\n options: DescribeOptions = {},\n): SettingFieldConfig[] {\n const shape = getObjectShape(dials.schema);\n return dials.keys.map((key) => {\n const field = shape[key];\n const meta = field ? readMeta(field) : {};\n const zodType = field ? baseType(field) : 'unknown';\n const sensitivity = dials.sensitivityFor(key);\n\n const enumValues = field ? safe(() => getEnumValues(field) as unknown as string[]) : undefined;\n // getNumericBounds reports an absent bound as a non-finite value (±Infinity, or null in some\n // versions). Keep only FINITE bounds, so an unbounded / one-sided number is not mistaken for a\n // fully-bounded one (which would wrongly pick a slider).\n const rawBounds = field ? safe(() => getNumericBounds(field) as unknown as { min?: number | null; max?: number | null }) : undefined;\n const min = rawBounds != null && Number.isFinite(rawBounds.min) ? (rawBounds.min as number) : undefined;\n const max = rawBounds != null && Number.isFinite(rawBounds.max) ? (rawBounds.max as number) : undefined;\n const bounds = min !== undefined || max !== undefined ? { ...(min !== undefined ? { min } : {}), ...(max !== undefined ? { max } : {}) } : undefined;\n\n const metaFacets = Array.isArray(meta.facets) ? meta.facets.map((f) => String(f)) : [];\n const facets = uniqueStrings([...metaFacets, ...(options.facets?.[key] ?? [])]);\n const advanced = facets.includes('advanced') || meta.advanced === true;\n\n const required = field ? !field.safeParse(undefined).success && dials.defaults[key] === undefined : false;\n\n return {\n key,\n label: typeof meta.title === 'string' ? meta.title : humanizeFieldName(key),\n description: typeof meta.description === 'string' ? meta.description : undefined,\n widget: widgetKindFor({ zodType, sensitivity, bounds, enumValues, metaWidget: meta.editWidget }),\n zodType,\n required,\n readOnly: meta.readOnly === true || meta.editable === false,\n hidden: meta.hidden === true,\n sensitivity,\n mergeStrategy: dials.mergeStrategyFor(key),\n // Never put a secret's plaintext default into the headless config — a renderer that prefills\n // from `defaultValue` would expose it. Secrets carry no default here (the secret backend owns it).\n defaultValue: sensitivity === 'secret' ? undefined : dials.defaults[key],\n enumValues: enumValues && enumValues.length > 0 ? enumValues : undefined,\n bounds,\n facets,\n advanced,\n order: typeof meta.order === 'number' ? meta.order : undefined,\n isStructured: zodType === 'object' || zodType === 'array',\n };\n });\n}\n","/**\n * Faceted organization: project the flat settings surface into groups via a separate grouping layer\n * (facets are canonical; a tree is one projection). The forward index (facet -> keys) drives panels,\n * accordions, and bulk-operation scopes — the same model serves both the open-a-panel and the\n * expand-in-place gesture (the gesture is the renderer's choice, never encoded here). Computed\n * (\"smart\") groups are predicates over field config + resolution state. Pure.\n */\n\nimport type { EffectiveResult, SettingKey } from '@zodal/dials-core';\nimport type { SettingFieldConfig, SettingsGroup } from './types.js';\n\nexport interface FacetDef {\n id: string;\n title?: string;\n order?: number;\n}\n\nexport interface GroupingOptions {\n /** Declared facet titles/order. Facets used by fields but not declared get a humanized default. */\n facetDefs?: FacetDef[];\n /** Include computed/\"smart\" groups (@secret, @advanced, @modified, @managed). Default: true. */\n computedGroups?: boolean;\n /** Title for the catch-all group of settings with no facet. Default: 'Other'. */\n ungroupedTitle?: string;\n}\n\nfunction humanize(id: string): string {\n return id.replace(/^@/, '').replace(/[._-]+/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nfunction computedGroups(fields: SettingFieldConfig[], result?: EffectiveResult): SettingsGroup[] {\n const out: SettingsGroup[] = [];\n const add = (id: string, title: string, keys: SettingKey[], order: number): void => {\n if (keys.length > 0) out.push({ id, title, order, settingKeys: keys, computed: true });\n };\n const fieldKeys = new Set(fields.map((f) => f.key));\n add('@secret', 'Secrets', fields.filter((f) => f.sensitivity === 'secret').map((f) => f.key), 2000);\n add('@advanced', 'Advanced', fields.filter((f) => f.advanced).map((f) => f.key), 2001);\n if (result) {\n const modified = Object.keys(result.provenance).filter((k) => fieldKeys.has(k) && result.provenance[k].winningScope !== 'default');\n const managed = Object.keys(result.provenance).filter((k) => fieldKeys.has(k) && result.provenance[k].managed);\n add('@modified', 'Modified', modified, 2002);\n add('@managed', 'Managed by policy', managed, 2003);\n }\n return out;\n}\n\n/** Project field configs (in their incoming order) into facet groups, sorted by order then title. */\nexport function toGroups(\n fields: SettingFieldConfig[],\n result?: EffectiveResult,\n options: GroupingOptions = {},\n): SettingsGroup[] {\n const declared = new Map((options.facetDefs ?? []).map((f) => [f.id, f]));\n const forward = new Map<string, SettingKey[]>();\n for (const field of fields) {\n for (const facet of field.facets) {\n const list = forward.get(facet);\n if (list) list.push(field.key);\n else forward.set(facet, [field.key]);\n }\n }\n\n const groups: SettingsGroup[] = [];\n for (const [facet, keys] of forward) {\n const def = declared.get(facet);\n groups.push({ id: facet, title: def?.title ?? humanize(facet), order: def?.order ?? 100, settingKeys: keys, computed: false });\n }\n\n // Group ids must be unique (renderers key off them). A declared facet that collides with the\n // reserved catch-all (`_ungrouped`) or a computed id (`@…`) takes precedence; the reserved group\n // is then skipped rather than duplicated.\n const seenIds = new Set(groups.map((g) => g.id));\n const ungrouped = fields.filter((f) => f.facets.length === 0).map((f) => f.key);\n if (ungrouped.length > 0 && !seenIds.has('_ungrouped')) {\n groups.push({ id: '_ungrouped', title: options.ungroupedTitle ?? 'Other', order: 1000, settingKeys: ungrouped, computed: false });\n seenIds.add('_ungrouped');\n }\n\n if (options.computedGroups !== false) {\n for (const group of computedGroups(fields, result)) {\n if (!seenIds.has(group.id)) {\n groups.push(group);\n seenIds.add(group.id);\n }\n }\n }\n\n return groups.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));\n}\n","/**\n * Search over the settings surface: a declared, engine-agnostic `IndexableSetting[]` projection, a\n * pluggable `SearchProvider` (zero-dependency substring default; richer engines like MiniSearch or\n * a semantic provider plug in behind the same interface), and an engine-independent scoped `@`-filter\n * parser (`@modified`/`@managed`/`@secret`/`@advanced`/`@facet:<id>`/`@scope:<id>`) that narrows by\n * effective-value/provenance state BEFORE free text reaches the provider. Pure.\n */\n\nimport type { EffectiveResult, SettingKey } from '@zodal/dials-core';\nimport type { IndexableSetting, SearchProvider, SettingFieldConfig } from './types.js';\n\n/** Project field configs into the engine-agnostic indexable surface. */\nexport function toIndexableSettings(fields: SettingFieldConfig[]): IndexableSetting[] {\n return fields.map((f) => ({\n key: f.key,\n title: f.label,\n description: f.description ?? '',\n enumLabels: f.enumValues ?? [],\n facets: f.facets,\n keywords: [f.key, ...f.key.split(/[._\\-/]/).filter(Boolean)],\n }));\n}\n\n/** Which indexable text fields the substring provider searches (and their relative weight). */\nexport type IndexField = 'title' | 'description' | 'enumLabels' | 'facets' | 'keywords';\n\nexport interface SubstringSearchOptions {\n /** Which fields to match (default: all). */\n fields?: IndexField[];\n}\n\n/**\n * Zero-dependency substring search provider (the default). Lowercased-substring match over the\n * selected fields; title/keyword hits rank above description/facet/enum hits. Empty query returns all.\n */\nexport function createSubstringSearchProvider(\n settings: IndexableSetting[],\n options: SubstringSearchOptions = {},\n): SearchProvider {\n const fields: IndexField[] = options.fields ?? ['title', 'description', 'enumLabels', 'facets', 'keywords'];\n const haystacks = settings.map((s) => ({\n key: s.key,\n parts: {\n title: s.title.toLowerCase(),\n description: s.description.toLowerCase(),\n enumLabels: s.enumLabels.join(' ').toLowerCase(),\n facets: s.facets.join(' ').toLowerCase(),\n keywords: s.keywords.join(' ').toLowerCase(),\n } as Record<IndexField, string>,\n }));\n return {\n search(query: string): SettingKey[] {\n const q = query.trim().toLowerCase();\n if (!q) return settings.map((s) => s.key);\n const scored: Array<{ key: SettingKey; score: number }> = [];\n for (const h of haystacks) {\n let score = 0;\n for (const f of fields) {\n if (h.parts[f].includes(q)) score += f === 'title' || f === 'keywords' ? 3 : 1;\n }\n if (score > 0) scored.push({ key: h.key, score });\n }\n scored.sort((a, b) => b.score - a.score);\n return scored.map((s) => s.key);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Scoped @-filters (engine-independent)\n// ---------------------------------------------------------------------------\n\nexport type ScopeFilter =\n | { type: 'modified' }\n | { type: 'managed' }\n | { type: 'secret' }\n | { type: 'advanced' }\n | { type: 'facet'; value: string }\n | { type: 'scope'; value: string };\n\nexport interface ParsedQuery {\n filters: ScopeFilter[];\n text: string;\n}\n\n/** Parse a settings query into scoped `@`-filters + the residual free text. Unrecognized `@tokens`\n * fall back to free text. */\nexport function parseScopedQuery(query: string): ParsedQuery {\n const tokens = query.split(/\\s+/).filter(Boolean);\n const filters: ScopeFilter[] = [];\n const text: string[] = [];\n for (const tok of tokens) {\n const m = /^@([a-z]+)(?::(.+))?$/i.exec(tok);\n if (!m) {\n text.push(tok);\n continue;\n }\n const name = m[1].toLowerCase();\n const value = m[2];\n if (name === 'modified') filters.push({ type: 'modified' });\n else if (name === 'managed') filters.push({ type: 'managed' });\n else if (name === 'secret') filters.push({ type: 'secret' });\n else if (name === 'advanced') filters.push({ type: 'advanced' });\n else if (name === 'facet' && value) filters.push({ type: 'facet', value });\n else if (name === 'scope' && value) filters.push({ type: 'scope', value });\n else text.push(tok);\n }\n return { filters, text: text.join(' ') };\n}\n\nexport interface FilterContext {\n fields: SettingFieldConfig[];\n result?: EffectiveResult;\n}\n\n/** Apply scoped filters to a key set (keys must satisfy ALL filters), using field config + state. */\nexport function applyScopedFilters(keys: SettingKey[], filters: ScopeFilter[], context: FilterContext): SettingKey[] {\n if (filters.length === 0) return keys;\n const byKey = new Map(context.fields.map((f) => [f.key, f]));\n const prov = context.result?.provenance ?? {};\n return keys.filter((key) =>\n filters.every((filter) => {\n const f = byKey.get(key);\n switch (filter.type) {\n case 'secret':\n return f?.sensitivity === 'secret';\n case 'advanced':\n return f?.advanced === true;\n case 'facet':\n return f?.facets.includes(filter.value) ?? false;\n case 'modified':\n return prov[key] ? prov[key].winningScope !== 'default' : false;\n case 'managed':\n return prov[key]?.managed === true;\n case 'scope':\n return prov[key]?.winningScope === filter.value;\n default:\n return true;\n }\n }),\n );\n}\n\n/** End-to-end: parse a query, apply scoped filters, then free-text search the survivors (keeping the\n * provider's ranking order). */\nexport function searchSettings(query: string, provider: SearchProvider, context: FilterContext): SettingKey[] {\n const { filters, text } = parseScopedQuery(query);\n const allKeys = context.fields.map((f) => f.key);\n const filtered = new Set(applyScopedFilters(allKeys, filters, context));\n const ranked = text ? provider.search(text) : allKeys;\n return ranked.filter((k) => filtered.has(k));\n}\n","/**\n * Change-lifecycle helpers (headless): compute the dirty set, reset a key (remove it so a lower\n * scope re-wins) or explicitly UNSET it, and record/undo edits as reversible RFC 6902 patches over\n * the SERIALIZED layer (so the UNSET sentinel survives — `diffJsonPatch` only sees plain JSON). These\n * are thin wrappers over dials-core; consumers wire them to toasts/guards/undo stacks themselves.\n */\n\nimport {\n UNSET,\n diffJsonPatch,\n invertJsonPatch,\n applyJsonPatch,\n serializeLayer,\n deserializeLayer,\n} from '@zodal/dials-core';\nimport type { JsonPatchOp, Layer, SerializedLayer, SettingKey } from '@zodal/dials-core';\n\nfunction normalize(value: unknown): string {\n // Distinguish UNSET, undefined, null, and JSON values as four separate states. The control-char\n // (\u0000) sentinel prefixes cannot collide with any JSON-serialized value.\n if (value === UNSET) return '\u0000unset';\n if (value === undefined) return '\u0000undefined';\n return JSON.stringify(value);\n}\n\n/** Keys whose value in `current` differs from `baseline` (the dirty set). Absence, UNSET, `undefined`,\n * `null`, and a literal value are all distinguished. */\nexport function dirtyKeys(current: Layer, baseline: Layer): SettingKey[] {\n const keys = new Set<string>([...Object.keys(current), ...Object.keys(baseline)]);\n const out: SettingKey[] = [];\n for (const key of keys) {\n const inCurrent = Object.prototype.hasOwnProperty.call(current, key);\n const inBaseline = Object.prototype.hasOwnProperty.call(baseline, key);\n if (inCurrent !== inBaseline || normalize(current[key]) !== normalize(baseline[key])) out.push(key);\n }\n return out;\n}\n\n/** True if any key is dirty. */\nexport function isDirty(current: Layer, baseline: Layer): boolean {\n return dirtyKeys(current, baseline).length > 0;\n}\n\n/** Reset a key by removing it from the layer (a lower scope re-wins). Returns a new layer. */\nexport function resetToDefault(layer: Layer, key: SettingKey): Layer {\n const out = { ...layer };\n delete out[key];\n return out;\n}\n\n/** Explicitly UNSET a key (records an intentional reset, distinct from never-set). Returns a new layer. */\nexport function unsetKey(layer: Layer, key: SettingKey): Layer {\n return { ...layer, [key]: UNSET };\n}\n\n/** A reversible record of an edit (forward + inverse RFC 6902 patches over the serialized layer). */\nexport interface ChangeRecord {\n forward: JsonPatchOp[];\n inverse: JsonPatchOp[];\n}\n\n/** Record an edit from `before` to `after` as a reversible change over the serialized layers. */\nexport function recordLayerChange(before: Layer, after: Layer): ChangeRecord {\n const b = serializeLayer(before);\n const a = serializeLayer(after);\n const forward = diffJsonPatch(b, a);\n return { forward, inverse: invertJsonPatch(forward, b) };\n}\n\n/** Apply a change's `forward` (redo) or `inverse` (undo) patch to a layer, returning a new layer. */\nexport function applyLayerPatch(layer: Layer, ops: JsonPatchOp[]): Layer {\n const serialized = applyJsonPatch(serializeLayer(layer), ops) as SerializedLayer;\n return deserializeLayer(serialized);\n}\n","/**\n * `toSettingsForm` — the top-level headless generator: describe every (non-hidden) setting as a\n * field config, order them, and project them into facet groups. `toFieldStates` derives the\n * value-dependent state (effective value, provenance source, managed/shadowed flags, dirty) from a\n * cascade resolution — the input to provenance badges, locks, and reset affordances. Pure; emits\n * configuration objects only (never DOM).\n */\n\nimport type { z } from 'zod';\nimport { isSecretRef, makeSecretRef } from '@zodal/dials-core';\nimport type { DialsDefinition, EffectiveResult, SettingKey } from '@zodal/dials-core';\nimport { describeSettings } from './introspect.js';\nimport type { DescribeOptions } from './introspect.js';\nimport { toGroups } from './facets.js';\nimport type { GroupingOptions } from './facets.js';\nimport type { SettingFieldConfig, SettingFieldState, SettingsForm } from './types.js';\n\nexport interface ToSettingsFormOptions extends DescribeOptions, GroupingOptions {\n /** A resolution result, used for computed groups (@modified/@managed). */\n result?: EffectiveResult;\n /** Include hidden settings in the form. Default: false. */\n includeHidden?: boolean;\n}\n\n/** Build the full headless settings form (ordered field configs + facet groups). */\nexport function toSettingsForm<T extends z.ZodObject<z.ZodRawShape>>(\n dials: DialsDefinition<T>,\n options: ToSettingsFormOptions = {},\n): SettingsForm {\n const all = describeSettings(dials, { facets: options.facets });\n const visible = options.includeHidden ? all : all.filter((f) => !f.hidden);\n const fields = [...visible].sort((a, b) => (a.order ?? 1000) - (b.order ?? 1000) || a.label.localeCompare(b.label));\n const groups = toGroups(fields, options.result, {\n facetDefs: options.facetDefs,\n computedGroups: options.computedGroups,\n ungroupedTitle: options.ungroupedTitle,\n });\n return { fields, groups };\n}\n\n/** Derive value-dependent field state (value, provenance source, managed/shadowed, dirty) for each\n * field from a cascade resolution and an optional dirty set. */\nexport function toFieldStates(\n fields: SettingFieldConfig[],\n result: EffectiveResult,\n dirty: Iterable<SettingKey> = [],\n): Record<SettingKey, SettingFieldState> {\n const dirtySet = new Set<SettingKey>(dirty);\n const states: Record<SettingKey, SettingFieldState> = {};\n for (const field of fields) {\n const prov = result.provenance[field.key];\n const raw = result.effective[field.key];\n // Defense in depth: never surface a secret's plaintext to a renderer, even if the caller passed\n // an UNMASKED resolution (resolve defaults to maskSecrets:false). Emit a masked SecretRef.\n const value =\n field.sensitivity === 'secret' && !isSecretRef(raw)\n ? makeSecretRef(field.key, raw !== undefined && raw !== null && raw !== '')\n : raw;\n states[field.key] = {\n value,\n source: prov?.winningScope,\n managed: prov?.managed ?? false,\n shadowed: (prov?.shadowed.length ?? 0) > 0,\n dirty: dirtySet.has(field.key),\n };\n }\n return states;\n}\n","/**\n * `createSettingsStore` — a framework-agnostic reactive store of effective settings. It holds the\n * ordered lower-scope stack + the editable (user) layer; every mutation re-resolves the cascade\n * (effective + provenance + conflicts), recomputes the dirty set and validation, masks secrets, and\n * notifies subscribers. No framework dependency: `subscribe`/`getState` plug into React via\n * `useSyncExternalStore`, or into anything via the listener. Constraints/secret-masking are honored\n * by reusing dials-core (`resolve`, `validate`, `maskEffectiveResult`).\n */\n\nimport type { z } from 'zod';\nimport { UNSET, maskEffectiveResult } from '@zodal/dials-core';\nimport type {\n ConstraintResult,\n DialsDefinition,\n EffectiveResult,\n KeyProvenance,\n Layer,\n ScopedLayer,\n SettingKey,\n} from '@zodal/dials-core';\nimport { dirtyKeys } from './lifecycle.js';\n\nexport interface SettingsState {\n /** Effective value per key (secrets masked as `SecretRef`). */\n effective: Record<SettingKey, unknown>;\n /** Provenance per key (also masked for secrets). */\n provenance: Record<SettingKey, KeyProvenance>;\n /** Keys set by multiple layers to differing values. */\n conflicts: EffectiveResult['conflicts'];\n /** The editable (user) layer — holds RAW values (including secrets), as the source to persist.\n * Split secrets out (dials-core `splitBySensitivity`) before saving to a config store. The\n * display surfaces (`effective`/`provenance`) mask secrets; this does not. */\n layer: Layer;\n /** The ordered lower-scope stack. */\n scopes: ScopedLayer[];\n /** Keys whose editable-layer value differs from the last saved baseline. */\n dirty: SettingKey[];\n /** Validation of the (unmasked) effective values. */\n validation: ConstraintResult;\n}\n\nexport interface CreateSettingsStoreOptions {\n /** Initial lower-scope stack (defaults are prepended by resolve). */\n scopes?: ScopedLayer[];\n /** Initial editable layer. */\n layer?: Layer;\n /** Scope id of the editable layer. Default: 'user'. */\n scope?: string;\n /** Mask secret effective values + provenance. Default: true. */\n maskSecrets?: boolean;\n /** Called if a subscriber throws during notification (so one bad listener can't break the others\n * or escape a mutation). Default: rethrow asynchronously is avoided — errors are reported here. */\n onListenerError?: (error: unknown) => void;\n}\n\nexport interface SettingsStore {\n getState(): SettingsState;\n /** Subscribe to state changes; returns an unsubscribe function. */\n subscribe(listener: () => void): () => void;\n /** Set a key in the editable layer. */\n set(key: SettingKey, value: unknown): void;\n /** Explicitly UNSET a key in the editable layer (re-exposes a lower scope). */\n unset(key: SettingKey): void;\n /** Remove a key from the editable layer (reset — a lower scope re-wins). */\n reset(key: SettingKey): void;\n /** Replace the whole editable layer. */\n setLayer(layer: Layer): void;\n /** Replace the lower-scope stack. */\n setScopes(scopes: ScopedLayer[]): void;\n /** Mark the current editable layer as saved (clears the dirty set). */\n markSaved(): void;\n /** Current effective value for a key (masked for secrets). */\n get(key: SettingKey): unknown;\n /** Provenance for a key. */\n explain(key: SettingKey): KeyProvenance | undefined;\n}\n\n/** Create a reactive settings store over a dials definition. */\nexport function createSettingsStore<T extends z.ZodObject<z.ZodRawShape>>(\n dials: DialsDefinition<T>,\n options: CreateSettingsStoreOptions = {},\n): SettingsStore {\n const scopeId = options.scope ?? 'user';\n const maskSecrets = options.maskSecrets !== false;\n const listeners = new Set<() => void>();\n\n let scopes: ScopedLayer[] = [...(options.scopes ?? [])];\n let layer: Layer = { ...(options.layer ?? {}) };\n let baseline: Layer = { ...layer };\n let state: SettingsState;\n\n const recompute = (): void => {\n // Resolve UNMASKED for validation (a masked SecretRef would fail value constraints), then mask\n // for the exposed state.\n const raw = dials.resolve([...scopes, { scope: scopeId, layer }]);\n const validation = dials.validate(raw.effective);\n const shown = maskSecrets ? maskEffectiveResult(raw, dials.sensitivityFor) : raw;\n state = {\n effective: shown.effective,\n provenance: shown.provenance,\n conflicts: shown.conflicts,\n layer: { ...layer },\n scopes: [...scopes],\n dirty: dirtyKeys(layer, baseline),\n validation,\n };\n // Notify defensively: one throwing listener must not break the others or escape the mutation.\n for (const listener of [...listeners]) {\n try {\n listener();\n } catch (error) {\n options.onListenerError?.(error);\n }\n }\n };\n\n recompute();\n\n const mutate = (next: Layer): void => {\n layer = next;\n recompute();\n };\n\n return {\n getState: () => state,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n set: (key, value) => {\n // No-op guard: skip recompute+notify when a scalar value is unchanged (also breaks the naive\n // \"write back on change\" re-entrancy loop). Deep-equal object no-ops are not detected.\n if (Object.prototype.hasOwnProperty.call(layer, key) && Object.is(layer[key], value)) return;\n mutate({ ...layer, [key]: value });\n },\n unset: (key) => mutate({ ...layer, [key]: UNSET }),\n reset: (key) => {\n const next = { ...layer };\n delete next[key];\n mutate(next);\n },\n setLayer: (next) => mutate({ ...next }),\n setScopes: (next) => {\n scopes = [...next];\n recompute();\n },\n markSaved: () => {\n baseline = { ...layer };\n recompute();\n },\n get: (key) => state.effective[key],\n explain: (key) => state.provenance[key],\n };\n}\n","/**\n * Named profile management — the \"save / load / list named settings bundles\" capability (an app may\n * call these \"presets\", \"schemes\", or — for thoremin — \"instruments\"). A profile is a NAME + a\n * sparse `Layer` (persisted losslessly via `serializeLayer`, so `UNSET` survives) + optional metadata.\n * Persistence is pluggable (`ProfileStorage`): an in-memory default and a `localStorage` adapter are\n * provided. To apply a profile, put its layer into the cascade scope stack (e.g.\n * `store.setScopes([{ scope: 'profile', layer }])`) or replace the editable layer.\n *\n * SECURITY: profiles are plaintext at rest. Pass `sensitivityFor` so `secret` keys are REDACTED on\n * save (never persisted) — fail-closed, mirroring the jsonc store; otherwise split secrets out first.\n * Mutations are serialized per store so concurrent saves cannot lose updates.\n */\n\nimport { deserializeLayer, redactSecretsFromLayer, serializeLayer } from '@zodal/dials-core';\nimport type { Layer, SerializedLayer, Sensitivity, SettingKey } from '@zodal/dials-core';\n\n/** Lightweight profile descriptor (no layer payload) — for listing. */\nexport interface ProfileMeta {\n name: string;\n meta?: Record<string, unknown>;\n}\n\n/** A persisted named profile (a serialized sparse layer + metadata). */\nexport interface NamedProfile extends ProfileMeta {\n layer: SerializedLayer;\n}\n\n/** Pluggable persistence for the profile collection (reads/writes the whole list as JSON). */\nexport interface ProfileStorage {\n read(): Promise<NamedProfile[]>;\n write(profiles: NamedProfile[]): Promise<void>;\n}\n\nexport interface ProfileStoreOptions {\n /** Classify a setting's sensitivity. When provided, `secret` keys are REDACTED on save. */\n sensitivityFor?: (key: SettingKey) => Sensitivity;\n}\n\n/** An in-memory `ProfileStorage` (default; tests / ephemeral use). */\nexport function createMemoryProfileStorage(initial: NamedProfile[] = []): ProfileStorage {\n let profiles: NamedProfile[] = initial.map((p) => ({ ...p }));\n return {\n read: () => Promise.resolve(profiles.map((p) => ({ ...p }))),\n write: (next) => {\n profiles = next.map((p) => ({ ...p }));\n return Promise.resolve();\n },\n };\n}\n\n/** A `localStorage`-backed `ProfileStorage` (browser). Throws if `localStorage` is unavailable. A\n * corrupt stored value degrades to an empty list rather than throwing on every read. */\nexport function createLocalStorageProfileStorage(storageKey = 'zodal-dials.profiles'): ProfileStorage {\n const ls = (globalThis as { localStorage?: Storage }).localStorage;\n if (!ls) throw new Error('createLocalStorageProfileStorage: localStorage is not available');\n return {\n read: () => {\n const raw = ls.getItem(storageKey);\n if (!raw) return Promise.resolve([]);\n try {\n const parsed = JSON.parse(raw);\n return Promise.resolve(Array.isArray(parsed) ? (parsed as NamedProfile[]) : []);\n } catch {\n return Promise.resolve([]); // corrupt blob -> \"no profiles\" rather than a hard break\n }\n },\n write: (profiles) => {\n ls.setItem(storageKey, JSON.stringify(profiles));\n return Promise.resolve();\n },\n };\n}\n\nexport interface ProfileStore {\n /** All saved profiles (name + metadata only). */\n list(): Promise<ProfileMeta[]>;\n /** Save (or overwrite) a profile from a sparse layer. Rejects an empty/whitespace name. */\n save(name: string, layer: Layer, meta?: Record<string, unknown>): Promise<void>;\n /** Load a profile's layer, or undefined if absent. */\n load(name: string): Promise<Layer | undefined>;\n /** Remove a profile. */\n remove(name: string): Promise<void>;\n /** Rename a profile (no-op if `from` is absent or equals `to`; throws if `to` already exists). */\n rename(from: string, to: string): Promise<void>;\n /** Whether a profile exists. */\n has(name: string): Promise<boolean>;\n}\n\n/** Create a profile store over a pluggable storage backend. */\nexport function createProfileStore(storage: ProfileStorage, options: ProfileStoreOptions = {}): ProfileStore {\n const findIndex = (profiles: NamedProfile[], name: string): number => profiles.findIndex((p) => p.name === name);\n\n // Serialize all read-modify-write mutations so concurrent saves cannot lose updates.\n let queue: Promise<unknown> = Promise.resolve();\n const enqueue = <T>(run: () => Promise<T>): Promise<T> => {\n const result = queue.then(run, run);\n queue = result.then(\n () => undefined,\n () => undefined,\n );\n return result;\n };\n\n return {\n async list(): Promise<ProfileMeta[]> {\n const profiles = await storage.read();\n return profiles.map(({ name, meta }) => ({ name, ...(meta ? { meta } : {}) }));\n },\n\n save(name, layer, meta): Promise<void> {\n const trimmed = name.trim();\n if (!trimmed) return Promise.reject(new Error('profile name cannot be empty'));\n return enqueue(async () => {\n const profiles = await storage.read();\n let serialized = serializeLayer(layer);\n if (options.sensitivityFor) serialized = redactSecretsFromLayer(serialized, options.sensitivityFor);\n const entry: NamedProfile = { name: trimmed, layer: serialized, ...(meta ? { meta } : {}) };\n const index = findIndex(profiles, trimmed);\n if (index >= 0) profiles[index] = entry;\n else profiles.push(entry);\n await storage.write(profiles);\n });\n },\n\n async load(name): Promise<Layer | undefined> {\n const profiles = await storage.read();\n const found = profiles[findIndex(profiles, name)];\n return found ? deserializeLayer(found.layer) : undefined;\n },\n\n remove(name): Promise<void> {\n return enqueue(async () => {\n const profiles = await storage.read();\n await storage.write(profiles.filter((p) => p.name !== name));\n });\n },\n\n rename(from, to): Promise<void> {\n if (from === to) return Promise.resolve();\n return enqueue(async () => {\n const profiles = await storage.read();\n if (findIndex(profiles, to) >= 0) throw new Error(`profile \"${to}\" already exists`);\n const index = findIndex(profiles, from);\n if (index < 0) return;\n profiles[index] = { ...profiles[index], name: to };\n await storage.write(profiles);\n });\n },\n\n async has(name): Promise<boolean> {\n const profiles = await storage.read();\n return findIndex(profiles, name) >= 0;\n },\n };\n}\n"],"mappings":"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","names":["import_dials_core","import_dials_core","import_dials_core","import_dials_core"]}
@@ -0,0 +1,386 @@
1
+ import { SettingKey, Sensitivity, MergeStrategy, DialsDefinition, EffectiveResult, JsonPatchOp, Layer, ScopedLayer, KeyProvenance, ConstraintResult, SerializedLayer } from '@zodal/dials-core';
2
+ import { RendererTester, RendererRegistry } from '@zodal/ui';
3
+ export { PRIORITY, RendererRegistry, RendererTester, and, createRendererRegistry, editWidgetIs, fieldNameMatches, hasRefinement, metaMatches, or, zodTypeIs } from '@zodal/ui';
4
+ import { z } from 'zod';
5
+
6
+ /**
7
+ * Headless output types for the settings UI layer. These are plain configuration objects (never
8
+ * DOM/React) that any concrete renderer (vanilla, shadcn, …) turns into a settings panel.
9
+ */
10
+
11
+ /** The widget kind chosen for a setting's value type. A `secret` setting always maps to `secret`;
12
+ * an irreducible nested value maps to `object`/`array`, with `rawJson` as the terminal fallback. */
13
+ type WidgetKind = 'switch' | 'select' | 'radio' | 'slider' | 'number' | 'text' | 'textarea' | 'secret' | 'color' | 'date' | 'path' | 'object' | 'array' | 'rawJson';
14
+ /** A headless field configuration for one setting (the static, value-independent shape). */
15
+ interface SettingFieldConfig {
16
+ key: SettingKey;
17
+ label: string;
18
+ description?: string;
19
+ widget: WidgetKind;
20
+ /** The setting's base Zod type ('string'/'number'/'boolean'/'enum'/'object'/'array'/…). */
21
+ zodType: string;
22
+ required: boolean;
23
+ readOnly: boolean;
24
+ hidden: boolean;
25
+ sensitivity: Sensitivity;
26
+ mergeStrategy: MergeStrategy;
27
+ defaultValue?: unknown;
28
+ enumValues?: string[];
29
+ bounds?: {
30
+ min?: number;
31
+ max?: number;
32
+ };
33
+ /** Facet ids this setting belongs to (multi-membership). */
34
+ facets: string[];
35
+ /** Advanced-disclosure flag (membership in the `advanced` facet). */
36
+ advanced: boolean;
37
+ order?: number;
38
+ /** The value is an irreducible nested object/array (use a sub-editor or the rawJson fallback). */
39
+ isStructured: boolean;
40
+ }
41
+ /** The value-dependent state of a setting field, derived from cascade resolution. */
42
+ interface SettingFieldState {
43
+ /** The current effective value (a masked `SecretRef` for secrets). */
44
+ value: unknown;
45
+ /** The winning scope (provenance). */
46
+ source?: string;
47
+ /** True if the effective value comes from a managed/policy scope (lock the control). */
48
+ managed: boolean;
49
+ /** True if another scope also set this key (shadowed). */
50
+ shadowed: boolean;
51
+ /** True if the active layer overrides the baseline (dirty). */
52
+ dirty: boolean;
53
+ }
54
+ /** A group projected from a facet (or a computed/"smart" facet). The gesture (open a panel vs.
55
+ * expand in place) is NOT encoded here — both are driven by this one model. */
56
+ interface SettingsGroup {
57
+ id: string;
58
+ title: string;
59
+ order: number;
60
+ settingKeys: SettingKey[];
61
+ /** True for a computed/"smart" group (e.g. `@modified`, `@secret`) vs. a declared facet. */
62
+ computed: boolean;
63
+ }
64
+ /** The full headless settings form: field configs + facet-projected groups. */
65
+ interface SettingsForm {
66
+ fields: SettingFieldConfig[];
67
+ groups: SettingsGroup[];
68
+ }
69
+ /** The engine-agnostic, indexable projection of a setting for search providers. */
70
+ interface IndexableSetting {
71
+ key: SettingKey;
72
+ title: string;
73
+ description: string;
74
+ enumLabels: string[];
75
+ facets: string[];
76
+ keywords: string[];
77
+ }
78
+ /** A pluggable search provider over the indexable surface. */
79
+ interface SearchProvider {
80
+ /** Return the matching setting keys for a free-text query (best match first). */
81
+ search(query: string): SettingKey[];
82
+ }
83
+
84
+ /**
85
+ * Type -> widget classification for settings. Pure. Distinguishes a scalar leaf (switch/select/
86
+ * slider/text/…) from an irreducible nested value (object/array), with `rawJson` as the terminal
87
+ * fallback for anything unhandled — so coverage is total and degradation is honest. A `secret`
88
+ * setting always maps to the `secret` widget regardless of its underlying type.
89
+ */
90
+
91
+ interface WidgetInput {
92
+ zodType: string;
93
+ sensitivity: Sensitivity;
94
+ bounds?: {
95
+ min?: number;
96
+ max?: number;
97
+ };
98
+ enumValues?: string[];
99
+ /** An explicit `.meta({ editWidget })` override (wins over inference). */
100
+ metaWidget?: unknown;
101
+ }
102
+ /** Choose the widget kind for a setting. */
103
+ declare function widgetKindFor(input: WidgetInput): WidgetKind;
104
+
105
+ /**
106
+ * The settings renderer registry — a thin specialization of zodal's capability-ranked
107
+ * `RendererRegistry`. Concrete renderer packages (vanilla, shadcn, …) register `(tester, renderer)`
108
+ * entries against the same open-closed API; selection is by PRIORITY band + composable testers, with
109
+ * a terminal `alwaysMatch` entry (the rawJson fallback) guaranteeing total coverage and honest
110
+ * degradation. Settings-specific testers read `sensitivity`/`structured` from the render context.
111
+ */
112
+
113
+ /** Match a secret setting (its `sensitivity` is supplied via the render context). High priority so a
114
+ * secret is always rendered with the masked widget regardless of its underlying type. */
115
+ declare function secretRoleIs(): RendererTester;
116
+ /** Match an irreducible nested value (`structured: true` in context, or an object/array Zod type). */
117
+ declare function isStructuredValue(): RendererTester;
118
+ declare function isBoolean(): RendererTester;
119
+ declare function isEnum(): RendererTester;
120
+ declare function isNumber(): RendererTester;
121
+ declare function isString(): RendererTester;
122
+ /**
123
+ * Terminal renderer tester: ALWAYS matches, at the given priority (default FALLBACK). Pairing this
124
+ * with a raw-JSON renderer guarantees every setting resolves to something (the honest-degradation
125
+ * fallback).
126
+ */
127
+ declare function alwaysMatch(priority?: number): RendererTester;
128
+ /** Create a settings renderer registry (a zodal `RendererRegistry`). Concrete renderer packages
129
+ * populate it; remember to register a terminal `alwaysMatch` rawJson renderer for full coverage. */
130
+ declare function createSettingsRendererRegistry<TComponent = unknown>(): RendererRegistry<TComponent>;
131
+
132
+ /**
133
+ * Build the static, value-independent `SettingFieldConfig[]` from a `DialsDefinition`, combining the
134
+ * schema (via dials-core introspection helpers + `@zodal/core` enum/bounds helpers), the dials
135
+ * classification (sensitivity, merge strategy, defaults), and `.meta()` annotations + an optional
136
+ * external facet-assignment map. Pure.
137
+ */
138
+
139
+ interface DescribeOptions {
140
+ /** Extra facet membership, merged with each setting's `.meta({ facets })`: key -> facet ids. */
141
+ facets?: Record<string, string[]>;
142
+ }
143
+ /** Describe every setting in a dials definition as a headless field config. */
144
+ declare function describeSettings<T extends z.ZodObject<z.ZodRawShape>>(dials: DialsDefinition<T>, options?: DescribeOptions): SettingFieldConfig[];
145
+
146
+ /**
147
+ * Faceted organization: project the flat settings surface into groups via a separate grouping layer
148
+ * (facets are canonical; a tree is one projection). The forward index (facet -> keys) drives panels,
149
+ * accordions, and bulk-operation scopes — the same model serves both the open-a-panel and the
150
+ * expand-in-place gesture (the gesture is the renderer's choice, never encoded here). Computed
151
+ * ("smart") groups are predicates over field config + resolution state. Pure.
152
+ */
153
+
154
+ interface FacetDef {
155
+ id: string;
156
+ title?: string;
157
+ order?: number;
158
+ }
159
+ interface GroupingOptions {
160
+ /** Declared facet titles/order. Facets used by fields but not declared get a humanized default. */
161
+ facetDefs?: FacetDef[];
162
+ /** Include computed/"smart" groups (@secret, @advanced, @modified, @managed). Default: true. */
163
+ computedGroups?: boolean;
164
+ /** Title for the catch-all group of settings with no facet. Default: 'Other'. */
165
+ ungroupedTitle?: string;
166
+ }
167
+ /** Project field configs (in their incoming order) into facet groups, sorted by order then title. */
168
+ declare function toGroups(fields: SettingFieldConfig[], result?: EffectiveResult, options?: GroupingOptions): SettingsGroup[];
169
+
170
+ /**
171
+ * Search over the settings surface: a declared, engine-agnostic `IndexableSetting[]` projection, a
172
+ * pluggable `SearchProvider` (zero-dependency substring default; richer engines like MiniSearch or
173
+ * a semantic provider plug in behind the same interface), and an engine-independent scoped `@`-filter
174
+ * parser (`@modified`/`@managed`/`@secret`/`@advanced`/`@facet:<id>`/`@scope:<id>`) that narrows by
175
+ * effective-value/provenance state BEFORE free text reaches the provider. Pure.
176
+ */
177
+
178
+ /** Project field configs into the engine-agnostic indexable surface. */
179
+ declare function toIndexableSettings(fields: SettingFieldConfig[]): IndexableSetting[];
180
+ /** Which indexable text fields the substring provider searches (and their relative weight). */
181
+ type IndexField = 'title' | 'description' | 'enumLabels' | 'facets' | 'keywords';
182
+ interface SubstringSearchOptions {
183
+ /** Which fields to match (default: all). */
184
+ fields?: IndexField[];
185
+ }
186
+ /**
187
+ * Zero-dependency substring search provider (the default). Lowercased-substring match over the
188
+ * selected fields; title/keyword hits rank above description/facet/enum hits. Empty query returns all.
189
+ */
190
+ declare function createSubstringSearchProvider(settings: IndexableSetting[], options?: SubstringSearchOptions): SearchProvider;
191
+ type ScopeFilter = {
192
+ type: 'modified';
193
+ } | {
194
+ type: 'managed';
195
+ } | {
196
+ type: 'secret';
197
+ } | {
198
+ type: 'advanced';
199
+ } | {
200
+ type: 'facet';
201
+ value: string;
202
+ } | {
203
+ type: 'scope';
204
+ value: string;
205
+ };
206
+ interface ParsedQuery {
207
+ filters: ScopeFilter[];
208
+ text: string;
209
+ }
210
+ /** Parse a settings query into scoped `@`-filters + the residual free text. Unrecognized `@tokens`
211
+ * fall back to free text. */
212
+ declare function parseScopedQuery(query: string): ParsedQuery;
213
+ interface FilterContext {
214
+ fields: SettingFieldConfig[];
215
+ result?: EffectiveResult;
216
+ }
217
+ /** Apply scoped filters to a key set (keys must satisfy ALL filters), using field config + state. */
218
+ declare function applyScopedFilters(keys: SettingKey[], filters: ScopeFilter[], context: FilterContext): SettingKey[];
219
+ /** End-to-end: parse a query, apply scoped filters, then free-text search the survivors (keeping the
220
+ * provider's ranking order). */
221
+ declare function searchSettings(query: string, provider: SearchProvider, context: FilterContext): SettingKey[];
222
+
223
+ /**
224
+ * Change-lifecycle helpers (headless): compute the dirty set, reset a key (remove it so a lower
225
+ * scope re-wins) or explicitly UNSET it, and record/undo edits as reversible RFC 6902 patches over
226
+ * the SERIALIZED layer (so the UNSET sentinel survives — `diffJsonPatch` only sees plain JSON). These
227
+ * are thin wrappers over dials-core; consumers wire them to toasts/guards/undo stacks themselves.
228
+ */
229
+
230
+ /** Keys whose value in `current` differs from `baseline` (the dirty set). Absence, UNSET, `undefined`,
231
+ * `null`, and a literal value are all distinguished. */
232
+ declare function dirtyKeys(current: Layer, baseline: Layer): SettingKey[];
233
+ /** True if any key is dirty. */
234
+ declare function isDirty(current: Layer, baseline: Layer): boolean;
235
+ /** Reset a key by removing it from the layer (a lower scope re-wins). Returns a new layer. */
236
+ declare function resetToDefault(layer: Layer, key: SettingKey): Layer;
237
+ /** Explicitly UNSET a key (records an intentional reset, distinct from never-set). Returns a new layer. */
238
+ declare function unsetKey(layer: Layer, key: SettingKey): Layer;
239
+ /** A reversible record of an edit (forward + inverse RFC 6902 patches over the serialized layer). */
240
+ interface ChangeRecord {
241
+ forward: JsonPatchOp[];
242
+ inverse: JsonPatchOp[];
243
+ }
244
+ /** Record an edit from `before` to `after` as a reversible change over the serialized layers. */
245
+ declare function recordLayerChange(before: Layer, after: Layer): ChangeRecord;
246
+ /** Apply a change's `forward` (redo) or `inverse` (undo) patch to a layer, returning a new layer. */
247
+ declare function applyLayerPatch(layer: Layer, ops: JsonPatchOp[]): Layer;
248
+
249
+ /**
250
+ * `toSettingsForm` — the top-level headless generator: describe every (non-hidden) setting as a
251
+ * field config, order them, and project them into facet groups. `toFieldStates` derives the
252
+ * value-dependent state (effective value, provenance source, managed/shadowed flags, dirty) from a
253
+ * cascade resolution — the input to provenance badges, locks, and reset affordances. Pure; emits
254
+ * configuration objects only (never DOM).
255
+ */
256
+
257
+ interface ToSettingsFormOptions extends DescribeOptions, GroupingOptions {
258
+ /** A resolution result, used for computed groups (@modified/@managed). */
259
+ result?: EffectiveResult;
260
+ /** Include hidden settings in the form. Default: false. */
261
+ includeHidden?: boolean;
262
+ }
263
+ /** Build the full headless settings form (ordered field configs + facet groups). */
264
+ declare function toSettingsForm<T extends z.ZodObject<z.ZodRawShape>>(dials: DialsDefinition<T>, options?: ToSettingsFormOptions): SettingsForm;
265
+ /** Derive value-dependent field state (value, provenance source, managed/shadowed, dirty) for each
266
+ * field from a cascade resolution and an optional dirty set. */
267
+ declare function toFieldStates(fields: SettingFieldConfig[], result: EffectiveResult, dirty?: Iterable<SettingKey>): Record<SettingKey, SettingFieldState>;
268
+
269
+ /**
270
+ * `createSettingsStore` — a framework-agnostic reactive store of effective settings. It holds the
271
+ * ordered lower-scope stack + the editable (user) layer; every mutation re-resolves the cascade
272
+ * (effective + provenance + conflicts), recomputes the dirty set and validation, masks secrets, and
273
+ * notifies subscribers. No framework dependency: `subscribe`/`getState` plug into React via
274
+ * `useSyncExternalStore`, or into anything via the listener. Constraints/secret-masking are honored
275
+ * by reusing dials-core (`resolve`, `validate`, `maskEffectiveResult`).
276
+ */
277
+
278
+ interface SettingsState {
279
+ /** Effective value per key (secrets masked as `SecretRef`). */
280
+ effective: Record<SettingKey, unknown>;
281
+ /** Provenance per key (also masked for secrets). */
282
+ provenance: Record<SettingKey, KeyProvenance>;
283
+ /** Keys set by multiple layers to differing values. */
284
+ conflicts: EffectiveResult['conflicts'];
285
+ /** The editable (user) layer — holds RAW values (including secrets), as the source to persist.
286
+ * Split secrets out (dials-core `splitBySensitivity`) before saving to a config store. The
287
+ * display surfaces (`effective`/`provenance`) mask secrets; this does not. */
288
+ layer: Layer;
289
+ /** The ordered lower-scope stack. */
290
+ scopes: ScopedLayer[];
291
+ /** Keys whose editable-layer value differs from the last saved baseline. */
292
+ dirty: SettingKey[];
293
+ /** Validation of the (unmasked) effective values. */
294
+ validation: ConstraintResult;
295
+ }
296
+ interface CreateSettingsStoreOptions {
297
+ /** Initial lower-scope stack (defaults are prepended by resolve). */
298
+ scopes?: ScopedLayer[];
299
+ /** Initial editable layer. */
300
+ layer?: Layer;
301
+ /** Scope id of the editable layer. Default: 'user'. */
302
+ scope?: string;
303
+ /** Mask secret effective values + provenance. Default: true. */
304
+ maskSecrets?: boolean;
305
+ /** Called if a subscriber throws during notification (so one bad listener can't break the others
306
+ * or escape a mutation). Default: rethrow asynchronously is avoided — errors are reported here. */
307
+ onListenerError?: (error: unknown) => void;
308
+ }
309
+ interface SettingsStore {
310
+ getState(): SettingsState;
311
+ /** Subscribe to state changes; returns an unsubscribe function. */
312
+ subscribe(listener: () => void): () => void;
313
+ /** Set a key in the editable layer. */
314
+ set(key: SettingKey, value: unknown): void;
315
+ /** Explicitly UNSET a key in the editable layer (re-exposes a lower scope). */
316
+ unset(key: SettingKey): void;
317
+ /** Remove a key from the editable layer (reset — a lower scope re-wins). */
318
+ reset(key: SettingKey): void;
319
+ /** Replace the whole editable layer. */
320
+ setLayer(layer: Layer): void;
321
+ /** Replace the lower-scope stack. */
322
+ setScopes(scopes: ScopedLayer[]): void;
323
+ /** Mark the current editable layer as saved (clears the dirty set). */
324
+ markSaved(): void;
325
+ /** Current effective value for a key (masked for secrets). */
326
+ get(key: SettingKey): unknown;
327
+ /** Provenance for a key. */
328
+ explain(key: SettingKey): KeyProvenance | undefined;
329
+ }
330
+ /** Create a reactive settings store over a dials definition. */
331
+ declare function createSettingsStore<T extends z.ZodObject<z.ZodRawShape>>(dials: DialsDefinition<T>, options?: CreateSettingsStoreOptions): SettingsStore;
332
+
333
+ /**
334
+ * Named profile management — the "save / load / list named settings bundles" capability (an app may
335
+ * call these "presets", "schemes", or — for thoremin — "instruments"). A profile is a NAME + a
336
+ * sparse `Layer` (persisted losslessly via `serializeLayer`, so `UNSET` survives) + optional metadata.
337
+ * Persistence is pluggable (`ProfileStorage`): an in-memory default and a `localStorage` adapter are
338
+ * provided. To apply a profile, put its layer into the cascade scope stack (e.g.
339
+ * `store.setScopes([{ scope: 'profile', layer }])`) or replace the editable layer.
340
+ *
341
+ * SECURITY: profiles are plaintext at rest. Pass `sensitivityFor` so `secret` keys are REDACTED on
342
+ * save (never persisted) — fail-closed, mirroring the jsonc store; otherwise split secrets out first.
343
+ * Mutations are serialized per store so concurrent saves cannot lose updates.
344
+ */
345
+
346
+ /** Lightweight profile descriptor (no layer payload) — for listing. */
347
+ interface ProfileMeta {
348
+ name: string;
349
+ meta?: Record<string, unknown>;
350
+ }
351
+ /** A persisted named profile (a serialized sparse layer + metadata). */
352
+ interface NamedProfile extends ProfileMeta {
353
+ layer: SerializedLayer;
354
+ }
355
+ /** Pluggable persistence for the profile collection (reads/writes the whole list as JSON). */
356
+ interface ProfileStorage {
357
+ read(): Promise<NamedProfile[]>;
358
+ write(profiles: NamedProfile[]): Promise<void>;
359
+ }
360
+ interface ProfileStoreOptions {
361
+ /** Classify a setting's sensitivity. When provided, `secret` keys are REDACTED on save. */
362
+ sensitivityFor?: (key: SettingKey) => Sensitivity;
363
+ }
364
+ /** An in-memory `ProfileStorage` (default; tests / ephemeral use). */
365
+ declare function createMemoryProfileStorage(initial?: NamedProfile[]): ProfileStorage;
366
+ /** A `localStorage`-backed `ProfileStorage` (browser). Throws if `localStorage` is unavailable. A
367
+ * corrupt stored value degrades to an empty list rather than throwing on every read. */
368
+ declare function createLocalStorageProfileStorage(storageKey?: string): ProfileStorage;
369
+ interface ProfileStore {
370
+ /** All saved profiles (name + metadata only). */
371
+ list(): Promise<ProfileMeta[]>;
372
+ /** Save (or overwrite) a profile from a sparse layer. Rejects an empty/whitespace name. */
373
+ save(name: string, layer: Layer, meta?: Record<string, unknown>): Promise<void>;
374
+ /** Load a profile's layer, or undefined if absent. */
375
+ load(name: string): Promise<Layer | undefined>;
376
+ /** Remove a profile. */
377
+ remove(name: string): Promise<void>;
378
+ /** Rename a profile (no-op if `from` is absent or equals `to`; throws if `to` already exists). */
379
+ rename(from: string, to: string): Promise<void>;
380
+ /** Whether a profile exists. */
381
+ has(name: string): Promise<boolean>;
382
+ }
383
+ /** Create a profile store over a pluggable storage backend. */
384
+ declare function createProfileStore(storage: ProfileStorage, options?: ProfileStoreOptions): ProfileStore;
385
+
386
+ export { type ChangeRecord, type CreateSettingsStoreOptions, type DescribeOptions, type FacetDef, type FilterContext, type GroupingOptions, type IndexField, type IndexableSetting, type NamedProfile, type ParsedQuery, type ProfileMeta, type ProfileStorage, type ProfileStore, type ProfileStoreOptions, type ScopeFilter, type SearchProvider, type SettingFieldConfig, type SettingFieldState, type SettingsForm, type SettingsGroup, type SettingsState, type SettingsStore, type SubstringSearchOptions, type ToSettingsFormOptions, type WidgetInput, type WidgetKind, alwaysMatch, applyLayerPatch, applyScopedFilters, createLocalStorageProfileStorage, createMemoryProfileStorage, createProfileStore, createSettingsRendererRegistry, createSettingsStore, createSubstringSearchProvider, describeSettings, dirtyKeys, isBoolean, isDirty, isEnum, isNumber, isString, isStructuredValue, parseScopedQuery, recordLayerChange, resetToDefault, searchSettings, secretRoleIs, toFieldStates, toGroups, toIndexableSettings, toSettingsForm, unsetKey, widgetKindFor };