@panphora/hyper-cms 0.5.0 → 0.6.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.
- package/README.md +105 -1
- package/dist/hypercms.min.js +187 -23
- package/package.json +1 -1
- package/src/events.js +85 -8
- package/src/form-builder.js +143 -20
- package/src/form-rules.js +18 -6
- package/src/hypercms.js +9 -3
- package/src/refresh.js +2 -2
- package/src/shell.js +13 -2
- package/src/templates.js +294 -29
- package/src/theme/pixel-quiet.overrides.css +9 -0
- package/src/theme.generated.css +112 -11
- package/src/vendor/mirk.vendor.js +2 -2
package/src/templates.js
CHANGED
|
@@ -116,11 +116,89 @@ const DEFAULT_TEMPLATES = {
|
|
|
116
116
|
<div class="hcms-error" hidden></div>
|
|
117
117
|
</div>
|
|
118
118
|
`,
|
|
119
|
+
// Named built-in controls, opt-in via data-hcms-component on the page element
|
|
120
|
+
// a rule points at. Promoted from the demo custom templates (mirk chrome,
|
|
121
|
+
// already themed in theme.generated.css), so the easy path needs no template
|
|
122
|
+
// authoring. @checkbox/@toggle bind the same input[type=checkbox]@checked
|
|
123
|
+
// machinery; @select/@radio get their entries from data-hcms-options (the
|
|
124
|
+
// form-builder populates the empty <select> / clones the radio prototype).
|
|
125
|
+
'@checkbox': `
|
|
126
|
+
<div class="hcms-field hcms-field--row" data-hcms-shape="scalar">
|
|
127
|
+
<span class="hcms-label" data-hcms-label></span>
|
|
128
|
+
<label class="mirk-checkbox">
|
|
129
|
+
<input type="checkbox" class="mirk-sr-only" data-hcms-field />
|
|
130
|
+
<span class="mirk-checkbox__box"><span class="mirk-checkbox__mark"></span></span>
|
|
131
|
+
</label>
|
|
132
|
+
<div class="hcms-error" hidden></div>
|
|
133
|
+
</div>
|
|
134
|
+
`,
|
|
135
|
+
'@toggle': `
|
|
136
|
+
<div class="hcms-field hcms-field--row" data-hcms-shape="scalar">
|
|
137
|
+
<span class="hcms-label" data-hcms-label></span>
|
|
138
|
+
<label class="mirk-toggle">
|
|
139
|
+
<input type="checkbox" role="switch" class="mirk-sr-only" data-hcms-field />
|
|
140
|
+
<span class="mirk-toggle__track"><span class="mirk-toggle__thumb"></span></span>
|
|
141
|
+
</label>
|
|
142
|
+
<div class="hcms-error" hidden></div>
|
|
143
|
+
</div>
|
|
144
|
+
`,
|
|
145
|
+
'@select': `
|
|
146
|
+
<label class="hcms-field" data-hcms-shape="scalar">
|
|
147
|
+
<span class="hcms-label" data-hcms-label></span>
|
|
148
|
+
<div class="mirk-select">
|
|
149
|
+
<select class="mirk-select__field" data-hcms-field></select>
|
|
150
|
+
<span aria-hidden="true" class="mirk-select__chevron">›</span>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="hcms-error" hidden></div>
|
|
153
|
+
</label>
|
|
154
|
+
`,
|
|
155
|
+
'@radio': `
|
|
156
|
+
<div class="hcms-field" data-hcms-shape="scalar">
|
|
157
|
+
<span class="hcms-label" data-hcms-label></span>
|
|
158
|
+
<div class="hcms-radio-row">
|
|
159
|
+
<label class="mirk-radio">
|
|
160
|
+
<input type="radio" class="mirk-sr-only" data-hcms-field />
|
|
161
|
+
<span class="mirk-radio__ring"><span class="mirk-radio__fill"></span><span class="mirk-radio__dot"></span></span>
|
|
162
|
+
<span class="mirk-radio__label"></span>
|
|
163
|
+
</label>
|
|
164
|
+
</div>
|
|
165
|
+
<div class="hcms-error" hidden></div>
|
|
166
|
+
</div>
|
|
167
|
+
`,
|
|
168
|
+
'@textarea': `
|
|
169
|
+
<label class="hcms-field" data-hcms-shape="scalar">
|
|
170
|
+
<span class="hcms-label" data-hcms-label></span>
|
|
171
|
+
<textarea class="mirk-textarea" rows="3" data-hcms-field></textarea>
|
|
172
|
+
<div class="hcms-error" hidden></div>
|
|
173
|
+
</label>
|
|
174
|
+
`,
|
|
175
|
+
'@number': `
|
|
176
|
+
<label class="hcms-field" data-hcms-shape="scalar">
|
|
177
|
+
<span class="hcms-label" data-hcms-label></span>
|
|
178
|
+
<input class="mirk-input" type="number" data-hcms-field />
|
|
179
|
+
<div class="hcms-error" hidden></div>
|
|
180
|
+
</label>
|
|
181
|
+
`,
|
|
182
|
+
'@chips': `
|
|
183
|
+
<div class="hcms-field hcms-chips" data-hcms-shape="scalar-array">
|
|
184
|
+
<span class="hcms-label" data-hcms-label></span>
|
|
185
|
+
<div class="mirk-tags hcms-array-items"></div>
|
|
186
|
+
<button type="button" class="hcms-add mirk-button mirk-button--small" data-hcms-action="add"><span class="mirk-button__label">+ Add</span></button>
|
|
187
|
+
<div class="hcms-error" hidden></div>
|
|
188
|
+
</div>
|
|
189
|
+
`,
|
|
190
|
+
'@chips-item': `
|
|
191
|
+
<span class="mirk-tags__chip" data-hcms-array-item>
|
|
192
|
+
<input class="hcms-chip-field" data-hcms-field aria-label="Item" placeholder="…" />
|
|
193
|
+
<button type="button" class="hcms-remove" data-hcms-action="remove" aria-label="Remove">×</button>
|
|
194
|
+
</span>
|
|
195
|
+
`,
|
|
119
196
|
}
|
|
120
197
|
|
|
121
|
-
// Shape templates fill-the-gaps on every managed page. The opt-in
|
|
122
|
-
//
|
|
123
|
-
// selects them (the Slice-2 component seam), so pages using
|
|
198
|
+
// Shape templates fill-the-gaps on every managed page. The opt-in components
|
|
199
|
+
// (@file/@image uploads + the named controls) are injected on demand the first
|
|
200
|
+
// time a field selects them (the Slice-2 component seam), so pages using none
|
|
201
|
+
// stay clean.
|
|
124
202
|
const ALWAYS_INJECT_KEYS = [
|
|
125
203
|
'@scalar',
|
|
126
204
|
'@object',
|
|
@@ -136,9 +214,9 @@ export function injectDefaults(doc) {
|
|
|
136
214
|
for (const key of ALWAYS_INJECT_KEYS) injectTemplate(doc, head, key)
|
|
137
215
|
}
|
|
138
216
|
|
|
139
|
-
// Inject one opt-in component template
|
|
140
|
-
//
|
|
141
|
-
//
|
|
217
|
+
// Inject one opt-in component template if the page hasn't already defined its
|
|
218
|
+
// own. Called by the component-selection seam the first time a field resolves
|
|
219
|
+
// to a component.
|
|
142
220
|
export function injectComponentTemplate(doc, key) {
|
|
143
221
|
if (!DEFAULT_TEMPLATES[key]) return null
|
|
144
222
|
const head = doc && (doc.head || doc.documentElement)
|
|
@@ -146,33 +224,204 @@ export function injectComponentTemplate(doc, key) {
|
|
|
146
224
|
return injectTemplate(doc, head, key)
|
|
147
225
|
}
|
|
148
226
|
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
227
|
+
// Named-component selection. Explicit beats inferred: a data-hcms-component
|
|
228
|
+
// attribute on the page element a rule points at wins over the @prop-suffix
|
|
229
|
+
// inference, so a checked-bound field can still opt into the @toggle look.
|
|
230
|
+
// Inference stays narrow: @src means image upload (the dominant image-CMS
|
|
231
|
+
// workflow) and @checked means checkbox (a text input showing the literal word
|
|
232
|
+
// "true" helps nobody). @href is NOT inferred: editing a link/URL is far more
|
|
233
|
+
// common than uploading a file, so an a@href rule stays a plain URL field and
|
|
234
|
+
// the file upload is opt-in. Suffix is split like the engine (lastIndexOf('@'),
|
|
235
|
+
// see engine/extract.js). Returns a DEFAULT_TEMPLATES key.
|
|
236
|
+
const PROP_TO_COMPONENT = { src: '@image', checked: '@checkbox' }
|
|
237
|
+
// data-hcms-component values for scalar rules. "chips" is array-only (below).
|
|
238
|
+
const SCALAR_COMPONENT_BY_NAME = {
|
|
239
|
+
image: '@image',
|
|
240
|
+
file: '@file',
|
|
241
|
+
checkbox: '@checkbox',
|
|
242
|
+
toggle: '@toggle',
|
|
243
|
+
select: '@select',
|
|
244
|
+
radio: '@radio',
|
|
245
|
+
textarea: '@textarea',
|
|
246
|
+
number: '@number',
|
|
247
|
+
}
|
|
248
|
+
// Every opt-in component injects on demand; only the 6 shape templates are
|
|
249
|
+
// always present.
|
|
250
|
+
const ON_DEMAND_COMPONENT_KEYS = new Set([
|
|
251
|
+
...Object.values(SCALAR_COMPONENT_BY_NAME),
|
|
252
|
+
'@chips',
|
|
253
|
+
'@chips-item',
|
|
254
|
+
])
|
|
255
|
+
|
|
256
|
+
export function componentForScalarRule(rule, doc, pathArr) {
|
|
161
257
|
if (typeof rule !== 'string') return '@scalar'
|
|
162
258
|
const at = rule.lastIndexOf('@')
|
|
259
|
+
const override = readElementAttr(scalarSelectorOf(rule, at), doc, 'data-hcms-component')
|
|
260
|
+
if (override && SCALAR_COMPONENT_BY_NAME[override]) {
|
|
261
|
+
const key = SCALAR_COMPONENT_BY_NAME[override]
|
|
262
|
+
// Value guards check exactly the elements the engine binds: under an array
|
|
263
|
+
// the same rule binds one element per item (every match votes), while a
|
|
264
|
+
// plain scalar binds only the FIRST match — a decorative second element
|
|
265
|
+
// sharing the selector must not veto the control.
|
|
266
|
+
const multi = Array.isArray(pathArr) && pathArr.some((s) => s === '*' || typeof s === 'number')
|
|
267
|
+
if (key === '@number' && !currentScalarValues(rule, at, doc, multi).every(numberInputCanHold)) {
|
|
268
|
+
return '@scalar'
|
|
269
|
+
}
|
|
270
|
+
if (
|
|
271
|
+
(key === '@checkbox' || key === '@toggle') &&
|
|
272
|
+
(at < 0 || rule.slice(at + 1) !== 'checked') &&
|
|
273
|
+
!currentScalarValues(rule, at, doc, multi).every(checkboxCanHold)
|
|
274
|
+
) {
|
|
275
|
+
return '@scalar'
|
|
276
|
+
}
|
|
277
|
+
return key
|
|
278
|
+
}
|
|
163
279
|
if (at >= 0) {
|
|
164
280
|
const prop = rule.slice(at + 1)
|
|
165
281
|
if (PROP_TO_COMPONENT[prop]) return PROP_TO_COMPONENT[prop]
|
|
166
282
|
}
|
|
167
|
-
const override = readComponentOverride(rule, at, doc)
|
|
168
|
-
if (override === 'image') return '@image'
|
|
169
|
-
if (override === 'file') return '@file'
|
|
170
283
|
return '@scalar'
|
|
171
284
|
}
|
|
172
285
|
|
|
173
|
-
|
|
286
|
+
// The data-hcms-component key the page element explicitly declares for this
|
|
287
|
+
// rule, or null (absent / unknown name / value-guard fallback). Lets the
|
|
288
|
+
// builder tell "author asked for this" apart from suffix inference when
|
|
289
|
+
// logging shadowing notices.
|
|
290
|
+
export function declaredComponentKey(rule, doc) {
|
|
291
|
+
if (typeof rule !== 'string') return null
|
|
292
|
+
const at = rule.lastIndexOf('@')
|
|
293
|
+
const name = readElementAttr(scalarSelectorOf(rule, at), doc, 'data-hcms-component')
|
|
294
|
+
return (name && SCALAR_COMPONENT_BY_NAME[name]) || null
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// A number input physically rejects any value that isn't a plain number — it
|
|
298
|
+
// reads back '' — and the whole-form commit would then blank the page value on
|
|
299
|
+
// the next edit of ANY field. When any bound value isn't number-shaped, fall
|
|
300
|
+
// back to the plain text control. This runs inside componentForScalarRule so
|
|
301
|
+
// the builder and deriveFormRules stay in lockstep (both call it).
|
|
302
|
+
const NUMBER_VALUE_RE = /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/
|
|
303
|
+
|
|
304
|
+
function numberInputCanHold(value) {
|
|
305
|
+
if (value == null || value === '') return true
|
|
306
|
+
return NUMBER_VALUE_RE.test(String(value))
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// A checkbox/toggle extracts its checked property — 'true' or 'false' — so any
|
|
310
|
+
// other bound value ("yes", "on", "1") would be clobbered to 'false' on the
|
|
311
|
+
// next whole-form commit. @checked projections are exempt in the caller: they
|
|
312
|
+
// round-trip the checked state faithfully by construction.
|
|
313
|
+
function checkboxCanHold(value) {
|
|
314
|
+
return value == null || value === '' || value === 'true' || value === 'false'
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Read the page values the rule binds, the way the engine extracts them:
|
|
318
|
+
// trimmed textContent for text projections (adapter.text trims), the live
|
|
319
|
+
// property for @value, the raw attribute otherwise. Mirrors extraction's
|
|
320
|
+
// skips: cms-template seeds and the shell are never extracted, so their
|
|
321
|
+
// values don't get a vote. `multi` mirrors the engine's binding: all matches
|
|
322
|
+
// under an array (one element per item), only the first match otherwise.
|
|
323
|
+
function currentScalarValues(rule, at, doc, multi) {
|
|
324
|
+
if (!doc || !doc.querySelectorAll) return []
|
|
325
|
+
const selector = scalarSelectorOf(rule, at)
|
|
326
|
+
if (!selector || selector === '.') return []
|
|
327
|
+
let els = null
|
|
328
|
+
try {
|
|
329
|
+
els = doc.querySelectorAll(selector)
|
|
330
|
+
} catch {
|
|
331
|
+
return []
|
|
332
|
+
}
|
|
333
|
+
const prop = at >= 0 ? rule.slice(at + 1) : null
|
|
334
|
+
const out = []
|
|
335
|
+
for (const el of els) {
|
|
336
|
+
if (el.closest && el.closest('[cms-template], [data-hcms-shell]')) continue
|
|
337
|
+
if (prop) {
|
|
338
|
+
if (prop === 'value' && 'value' in el) out.push(el.value)
|
|
339
|
+
else out.push(el.getAttribute ? el.getAttribute(prop) : null)
|
|
340
|
+
} else {
|
|
341
|
+
out.push((el.textContent || '').trim())
|
|
342
|
+
}
|
|
343
|
+
if (!multi) break
|
|
344
|
+
}
|
|
345
|
+
return out
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Array-level component selection: data-hcms-component="chips" on the list
|
|
349
|
+
// container (or any ancestor of the items) swaps the scalar-array chrome for
|
|
350
|
+
// the chip list. Resolved through the first item the rule's selector matches,
|
|
351
|
+
// then closest(), so the natural authoring spot — the <ul> — works even though
|
|
352
|
+
// the selector targets the items. Growable-from-zero lists carry a cms-template
|
|
353
|
+
// item, so the item selector always has something to match.
|
|
354
|
+
export function componentForScalarArrayRule(rule, doc) {
|
|
355
|
+
if (typeof rule !== 'string' || !rule.endsWith('[]')) return null
|
|
356
|
+
if (!doc || !doc.querySelector) return null
|
|
357
|
+
const selector = rule.slice(0, -2).trim()
|
|
358
|
+
if (!selector) return null
|
|
359
|
+
let item = null
|
|
360
|
+
try {
|
|
361
|
+
item = doc.querySelector(selector)
|
|
362
|
+
} catch {
|
|
363
|
+
return null
|
|
364
|
+
}
|
|
365
|
+
const host = item && item.closest ? item.closest('[data-hcms-component]') : null
|
|
366
|
+
const name = host && host.getAttribute ? host.getAttribute('data-hcms-component') : null
|
|
367
|
+
if (name === 'chips') return { array: '@chips', item: '@chips-item' }
|
|
368
|
+
return null
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Template resolution shared by the builder and deriveFormRules: a custom
|
|
372
|
+
// template at the exact path wins, then a wildcard-path one, then the default.
|
|
373
|
+
export function resolveTemplate(pathArr, defaultKey, doc) {
|
|
374
|
+
const pathStr = pathArr.join('.')
|
|
375
|
+
const wildcardKey = pathArr.map((s) => (typeof s === 'number' ? '*' : s)).join('.')
|
|
376
|
+
return (
|
|
377
|
+
(pathStr && findTemplate(doc, pathStr)) ||
|
|
378
|
+
(wildcardKey && wildcardKey !== pathStr && findTemplate(doc, wildcardKey)) ||
|
|
379
|
+
findTemplate(doc, defaultKey)
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// The declared array component, but only when its array template actually
|
|
384
|
+
// wins at this path — a custom path/wildcard template that shadows it returns
|
|
385
|
+
// null. The form-builder (item chrome) and deriveFormRules (item leaf
|
|
386
|
+
// selector) MUST share this verdict: if they diverge, the derived selector
|
|
387
|
+
// stops matching the rendered item leaf, every item extracts null, and the
|
|
388
|
+
// whole-form commit writes those nulls back over real page data.
|
|
389
|
+
export function winningScalarArrayComponent(rule, pathArr, doc) {
|
|
390
|
+
const comp = componentForScalarArrayRule(rule, doc)
|
|
391
|
+
if (!comp) return null
|
|
392
|
+
const winner = resolveTemplate(pathArr, comp.array, doc)
|
|
393
|
+
if (winner && winner.getAttribute('data-hcms-tpl') === comp.array) return comp
|
|
394
|
+
return null
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Option list for @select/@radio: data-hcms-options="low medium high" on the
|
|
398
|
+
// same page element as data-hcms-component. Space-separated value tokens (the
|
|
399
|
+
// data-rules-name idiom); labels derive via humanize() like every other label.
|
|
400
|
+
export function readOptionsOverride(rule, doc) {
|
|
401
|
+
if (typeof rule !== 'string') return null
|
|
402
|
+
const at = rule.lastIndexOf('@')
|
|
403
|
+
const raw = readElementAttr(scalarSelectorOf(rule, at), doc, 'data-hcms-options')
|
|
404
|
+
if (raw == null) return null
|
|
405
|
+
const tokens = raw.trim().split(/\s+/).filter(Boolean)
|
|
406
|
+
return tokens.length ? tokens : null
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Crop opt-in for @image: data-hcms-crop="1:1" | "16:9" | "free" on the page
|
|
410
|
+
// element. The form-builder copies it onto the built field so the upload path
|
|
411
|
+
// reads it without a second page lookup.
|
|
412
|
+
export function readCropOverride(rule, doc) {
|
|
413
|
+
if (typeof rule !== 'string') return null
|
|
414
|
+
const at = rule.lastIndexOf('@')
|
|
415
|
+
return readElementAttr(scalarSelectorOf(rule, at), doc, 'data-hcms-crop')
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function scalarSelectorOf(rule, at) {
|
|
419
|
+
return at >= 0 ? rule.slice(0, at) : rule
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// The only branch that reads the page DOM during selection.
|
|
423
|
+
function readElementAttr(selector, doc, attrName) {
|
|
174
424
|
if (!doc || !doc.querySelector) return null
|
|
175
|
-
const selector = at >= 0 ? rule.slice(0, at) : rule
|
|
176
425
|
if (!selector || selector === '.') return null
|
|
177
426
|
let el = null
|
|
178
427
|
try {
|
|
@@ -180,13 +429,13 @@ function readComponentOverride(rule, at, doc) {
|
|
|
180
429
|
} catch {
|
|
181
430
|
return null
|
|
182
431
|
}
|
|
183
|
-
return el && el.getAttribute ? el.getAttribute(
|
|
432
|
+
return el && el.getAttribute ? el.getAttribute(attrName) : null
|
|
184
433
|
}
|
|
185
434
|
|
|
186
|
-
// Pre-inject the
|
|
435
|
+
// Pre-inject the component templates this page's rules actually select, so
|
|
187
436
|
// both deriveFormRules (selectors) and buildForm (DOM) resolve them. Walks the
|
|
188
437
|
// same rule shapes as buildTemplateMap; idempotent and author-template-safe.
|
|
189
|
-
export function
|
|
438
|
+
export function injectComponents(doc, pageRules) {
|
|
190
439
|
if (!doc || pageRules == null) return
|
|
191
440
|
walk(pageRules)
|
|
192
441
|
|
|
@@ -194,7 +443,15 @@ export function injectUploadComponents(doc, pageRules) {
|
|
|
194
443
|
const kind = shapeKindOf(rule)
|
|
195
444
|
if (kind === 'scalar') {
|
|
196
445
|
const key = componentForScalarRule(rule, doc)
|
|
197
|
-
if (
|
|
446
|
+
if (ON_DEMAND_COMPONENT_KEYS.has(key)) injectComponentTemplate(doc, key)
|
|
447
|
+
return
|
|
448
|
+
}
|
|
449
|
+
if (kind === 'scalar-array') {
|
|
450
|
+
const comp = componentForScalarArrayRule(rule, doc)
|
|
451
|
+
if (comp) {
|
|
452
|
+
injectComponentTemplate(doc, comp.array)
|
|
453
|
+
injectComponentTemplate(doc, comp.item)
|
|
454
|
+
}
|
|
198
455
|
return
|
|
199
456
|
}
|
|
200
457
|
if (kind === 'object') {
|
|
@@ -275,8 +532,14 @@ export function buildTemplateMap(pageRules, doc) {
|
|
|
275
532
|
const pathStr = pathArr.join('.')
|
|
276
533
|
const wildcardKey = pathArr.map((s) => (typeof s === 'number' ? '*' : s)).join('.')
|
|
277
534
|
const shape = shapeKindOf(rule)
|
|
535
|
+
const arrayComp = shape === 'scalar-array' ? componentForScalarArrayRule(rule, doc) : null
|
|
536
|
+
// Items follow the WINNING component (a shadowed chips array renders
|
|
537
|
+
// default items), matching buildScalarArray.
|
|
538
|
+
const winningComp = shape === 'scalar-array' ? winningScalarArrayComponent(rule, pathArr, doc) : null
|
|
278
539
|
const defaultKey =
|
|
279
|
-
shape === 'scalar'
|
|
540
|
+
shape === 'scalar'
|
|
541
|
+
? componentForScalarRule(rule, doc, pathArr)
|
|
542
|
+
: (arrayComp && arrayComp.array) || SHAPE_TO_DEFAULT_KEY[shape] || '@scalar'
|
|
280
543
|
|
|
281
544
|
let tpl =
|
|
282
545
|
(pathStr && findTemplate(doc, pathStr)) ||
|
|
@@ -292,7 +555,9 @@ export function buildTemplateMap(pageRules, doc) {
|
|
|
292
555
|
const itemKey = itemPath.map((s) => (typeof s === 'number' ? '*' : s)).join('.')
|
|
293
556
|
const itemShape = shape === 'object-array' ? 'object-array-item' : 'scalar-array-item'
|
|
294
557
|
const itemTpl =
|
|
295
|
-
findTemplate(doc, itemKey) ||
|
|
558
|
+
findTemplate(doc, itemKey) ||
|
|
559
|
+
(winningComp && findTemplate(doc, winningComp.item)) ||
|
|
560
|
+
findTemplate(doc, '@' + itemShape)
|
|
296
561
|
map.set(itemKey, itemTpl)
|
|
297
562
|
if (shape === 'object-array') {
|
|
298
563
|
const itemRule = rule[1]
|
|
@@ -264,6 +264,15 @@ body.hcms-open.hcms-overlay { overflow: hidden; }
|
|
|
264
264
|
/* inline radio row */
|
|
265
265
|
.hcms-radio-row { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; }
|
|
266
266
|
|
|
267
|
+
/* chip-field (the @chips built-in): a borderless inline input that sizes to its
|
|
268
|
+
text, so chips read like static chips but stay inline-editable. */
|
|
269
|
+
.hcms-shell .mirk-tags__chip { padding-right: 6px; }
|
|
270
|
+
.hcms-shell .hcms-chip-field {
|
|
271
|
+
border: 0; background: transparent; color: inherit; font: inherit;
|
|
272
|
+
outline: none; min-width: 2ch; field-sizing: content; padding: 0;
|
|
273
|
+
}
|
|
274
|
+
.hcms-shell .hcms-chips .hcms-add { margin-top: 4px; align-self: flex-start; }
|
|
275
|
+
|
|
267
276
|
/* ---------- OBJECT GROUP — a quiet framed band, not a heavy card ---------- */
|
|
268
277
|
.hcms-object {
|
|
269
278
|
display: flex;
|
package/src/theme.generated.css
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/* GENERATED by scripts/build-theme.js from mirk-
|
|
2
|
-
Source of truth: mirk-
|
|
1
|
+
/* GENERATED by scripts/build-theme.js from mirk-interface/mirk.css — DO NOT EDIT.
|
|
2
|
+
Source of truth: mirk-interface/mirk.css + src/theme/pixel-quiet.overrides.css.
|
|
3
3
|
Regenerate with: npm run build:theme */
|
|
4
4
|
|
|
5
|
-
/* ===== mirk-interface@2.0
|
|
5
|
+
/* ===== mirk-interface@2.2.0, scoped to .hcms-shell ===== */
|
|
6
6
|
/*
|
|
7
7
|
* mirk.css — the mirk UI kit, v2.
|
|
8
8
|
* Hand-written, no build. Fourteen form components as semantic BEM classes in
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
@font-face {
|
|
29
29
|
font-family: 'Departure Mono';
|
|
30
|
-
src: url('https://cdn.jsdelivr.net/npm/mirk-interface@2.0
|
|
30
|
+
src: url('https://cdn.jsdelivr.net/npm/mirk-interface@2.2.0/fonts/DepartureMono-1.500/DepartureMono-Regular.woff2') format('woff2');
|
|
31
31
|
font-weight: 400; font-style: normal; font-display: swap;
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -89,7 +89,44 @@
|
|
|
89
89
|
background: var(--mirk-canvas); color: var(--mirk-fg);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
/*
|
|
92
|
+
/* Built-in brand variant — "Pixel Quiet": mirk's warm soul with the volume
|
|
93
|
+
down. A softened, near-equal bevel, warmer ink, terracotta destructive, a
|
|
94
|
+
deeper navy-black dark. Authored with light-dark() like :root, so it follows
|
|
95
|
+
the OS and still flips with .dark / .light. Opt in: data-theme="pixel-quiet".
|
|
96
|
+
Sits after :root (equal specificity, source order wins). --mirk-radius /
|
|
97
|
+
--mirk-focus-offset are inherited unchanged; --mirk-ctrl-bg is unused. */
|
|
98
|
+
.hcms-shell[data-theme="pixel-quiet"] {
|
|
99
|
+
--mirk-fg: light-dark(#2B241B, #ECEAF2);
|
|
100
|
+
--mirk-bg: light-dark(#F7F2EA, #11131E);
|
|
101
|
+
--mirk-canvas: light-dark(#F7F2EA, #0B0C13);
|
|
102
|
+
--mirk-accent: light-dark(#efefe5, #1D1F2F);
|
|
103
|
+
--mirk-destructive: light-dark(#C24A3A, #ff5566);
|
|
104
|
+
--mirk-focus-color: light-dark(#C7AE93, #4A506B);
|
|
105
|
+
--mirk-bevel-bg: light-dark(#FCF8F1, #1A1D2C);
|
|
106
|
+
--mirk-bevel-fg: light-dark(#2B241B, #ECEAF2);
|
|
107
|
+
--mirk-bevel-tl: light-dark(#F0E7D8, #2A2E42);
|
|
108
|
+
--mirk-bevel-br: light-dark(#E2D4BF, #14182A);
|
|
109
|
+
--mirk-bevel-hover-bg: light-dark(#F4ECDF, #202436);
|
|
110
|
+
--mirk-pill-inner-top: light-dark(#FBF6EE, #202436);
|
|
111
|
+
--mirk-input-border: light-dark(#D8C8AF, #353B52);
|
|
112
|
+
--mirk-placeholder-color: light-dark(#A8987F, #6A7090);
|
|
113
|
+
--mirk-mark-fg: light-dark(#6B5942, #C9CDE0);
|
|
114
|
+
--mirk-toggle-bg: light-dark(#EFDBBD, #3E4660);
|
|
115
|
+
--mirk-toggle-hi: light-dark(#F4EADA, #4E567A);
|
|
116
|
+
--mirk-toggle-lo: light-dark(#C2A87E, #262B42);
|
|
117
|
+
--mirk-sortable-dot: light-dark(#DDCBB0, #353B52);
|
|
118
|
+
--mirk-sortable-shadow: light-dark(#C9B493, #0E1120);
|
|
119
|
+
--mirk-sortable-label: light-dark(#8C7B62, #8A90AB);
|
|
120
|
+
--mirk-sortable-placeholder: light-dark(#A8987F, #6A7090);
|
|
121
|
+
/* slider nub reuses the toggle family (identical pairing in :root); the fill
|
|
122
|
+
is the one value with no CMS precedent — a soft warm / muted navy track. */
|
|
123
|
+
--mirk-slider-fill: light-dark(#F2E0BD, #2A2E42);
|
|
124
|
+
--mirk-slider-nub-bg: light-dark(#EFDBBD, #3E4660);
|
|
125
|
+
--mirk-slider-nub-hi: light-dark(#F4EADA, #4E567A);
|
|
126
|
+
--mirk-slider-nub-lo: light-dark(#C2A87E, #262B42);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Roll your own the same way — an explicit [data-theme] block is the escape hatch:
|
|
93
130
|
[data-theme="sunset"] { color-scheme: light; --mirk-accent: #f0a868; … } */
|
|
94
131
|
}
|
|
95
132
|
|
|
@@ -291,6 +328,14 @@
|
|
|
291
328
|
outline: 1px solid var(--mirk-focus-color); outline-offset: var(--mirk-focus-offset);
|
|
292
329
|
}
|
|
293
330
|
|
|
331
|
+
.hcms-shell .mirk-checkbox--small { gap: 0.5rem; }
|
|
332
|
+
.hcms-shell .mirk-checkbox--small .mirk-checkbox__box { width: 18px; height: 18px; }
|
|
333
|
+
.hcms-shell .mirk-checkbox--small .mirk-checkbox__mark {
|
|
334
|
+
width: 5px; height: 10px;
|
|
335
|
+
border-right-width: 2px; border-bottom-width: 2px; translate: 0.5px -1px;
|
|
336
|
+
}
|
|
337
|
+
.hcms-shell .mirk-checkbox--small .mirk-checkbox__label { font-size: 14px; }
|
|
338
|
+
|
|
294
339
|
/* ============================ RADIO ============================ */
|
|
295
340
|
.hcms-shell .mirk-radio { display: inline-flex; align-items: center; gap: 0.75rem; cursor: pointer; width: fit-content; }
|
|
296
341
|
.hcms-shell .mirk-radio__ring {
|
|
@@ -317,6 +362,11 @@
|
|
|
317
362
|
outline: 1px solid var(--mirk-focus-color); outline-offset: var(--mirk-focus-offset);
|
|
318
363
|
}
|
|
319
364
|
|
|
365
|
+
.hcms-shell .mirk-radio--small { gap: 0.5rem; }
|
|
366
|
+
.hcms-shell .mirk-radio--small .mirk-radio__ring { width: 20px; height: 20px; }
|
|
367
|
+
.hcms-shell .mirk-radio--small .mirk-radio__dot { width: 7px; height: 7px; }
|
|
368
|
+
.hcms-shell .mirk-radio--small .mirk-radio__label { font-size: 14px; }
|
|
369
|
+
|
|
320
370
|
/* ============================ TOGGLE ============================ */
|
|
321
371
|
.hcms-shell .mirk-toggle { display: inline-flex; align-items: center; gap: 0.75rem; cursor: pointer; width: fit-content; }
|
|
322
372
|
.hcms-shell .mirk-toggle__track {
|
|
@@ -349,6 +399,15 @@
|
|
|
349
399
|
}
|
|
350
400
|
.hcms-shell .mirk-toggle--round:has(:checked) .mirk-toggle__thumb { translate: 21px; }
|
|
351
401
|
|
|
402
|
+
.hcms-shell .mirk-toggle--small { gap: 0.5rem; }
|
|
403
|
+
.hcms-shell .mirk-toggle--small .mirk-toggle__track { width: 42px; height: 23px; }
|
|
404
|
+
.hcms-shell .mirk-toggle--small .mirk-toggle__thumb { top: 2px; left: 2px; width: 17px; height: 17px; }
|
|
405
|
+
.hcms-shell .mirk-toggle--small:has(:checked) .mirk-toggle__thumb { translate: 19px; }
|
|
406
|
+
.hcms-shell .mirk-toggle--small .mirk-toggle__label { font-size: 14px; }
|
|
407
|
+
.hcms-shell .mirk-toggle--round.mirk-toggle--small .mirk-toggle__track { width: 43px; height: 25px; }
|
|
408
|
+
.hcms-shell .mirk-toggle--round.mirk-toggle--small .mirk-toggle__thumb { width: 19px; height: 19px; }
|
|
409
|
+
.hcms-shell .mirk-toggle--round.mirk-toggle--small:has(:checked) .mirk-toggle__thumb { translate: 18px; }
|
|
410
|
+
|
|
352
411
|
/* ============================ SLIDER ============================ */
|
|
353
412
|
.hcms-shell .mirk-slider { position: relative; height: 32px; width: 100%; --mirk-value: 0%; }
|
|
354
413
|
.hcms-shell .mirk-slider__input {
|
|
@@ -381,6 +440,12 @@
|
|
|
381
440
|
content: ""; position: absolute; inset: 2px; border-radius: 9999px; background: var(--mirk-slider-nub-bg);
|
|
382
441
|
}
|
|
383
442
|
|
|
443
|
+
.hcms-shell .mirk-slider--small { height: 24px; }
|
|
444
|
+
.hcms-shell .mirk-slider--small .mirk-slider__track { height: 8px; }
|
|
445
|
+
.hcms-shell .mirk-slider--small .mirk-slider__nub { width: 16px; height: 16px; }
|
|
446
|
+
.hcms-shell .mirk-slider--round.mirk-slider--small .mirk-slider__track { height: 7px; }
|
|
447
|
+
.hcms-shell .mirk-slider--round.mirk-slider--small .mirk-slider__nub { width: 18px; height: 18px; }
|
|
448
|
+
|
|
384
449
|
/* ============================ DATE ============================ */
|
|
385
450
|
.hcms-shell .mirk-date { position: relative; }
|
|
386
451
|
.hcms-shell .mirk-date__field {
|
|
@@ -402,6 +467,10 @@
|
|
|
402
467
|
}
|
|
403
468
|
.hcms-shell .mirk-date--rounded .mirk-date__field { border-radius: var(--mirk-radius); }
|
|
404
469
|
|
|
470
|
+
.hcms-shell .mirk-date--small .mirk-date__field { padding: 4px 36px 4px 12px; font-size: 14px; }
|
|
471
|
+
.hcms-shell .mirk-date--small .mirk-date__field::-webkit-calendar-picker-indicator { width: 36px; }
|
|
472
|
+
.hcms-shell .mirk-date--small .mirk-date__icon { right: 12px; }
|
|
473
|
+
|
|
405
474
|
/* The native file/image inputs are visually hidden; their styled label drives
|
|
406
475
|
them, and the focus ring rides :has(:focus-visible) on button or container. */
|
|
407
476
|
.hcms-shell .mirk-file__input, .hcms-shell .mirk-image__input {
|
|
@@ -432,17 +501,28 @@
|
|
|
432
501
|
.hcms-shell .mirk-file--compact .mirk-file__name { font-size: 16px; }
|
|
433
502
|
.hcms-shell .mirk-file--compact.mirk-file--round { border-radius: 15px; }
|
|
434
503
|
|
|
435
|
-
/* Filled: the name slot becomes a link to the chosen file, beside a
|
|
436
|
-
clear it
|
|
504
|
+
/* Filled: the name slot becomes a link to the chosen file, beside a circular ×
|
|
505
|
+
to clear it: a 1px ring over a bevel fill that turns destructive on hover.
|
|
506
|
+
Empty keeps the placeholder span. */
|
|
437
507
|
.hcms-shell a.mirk-file__name { text-decoration: underline; text-underline-offset: 2px; }
|
|
438
508
|
.hcms-shell .mirk-file__remove {
|
|
439
509
|
appearance: none; -webkit-appearance: none; flex-shrink: 0;
|
|
510
|
+
width: 18px; height: 18px; border-radius: 50%;
|
|
440
511
|
display: inline-flex; align-items: center; justify-content: center;
|
|
441
|
-
margin: 0; padding: 0;
|
|
442
|
-
|
|
512
|
+
margin: 0; padding: 0; cursor: pointer; line-height: 0;
|
|
513
|
+
color: var(--mirk-bevel-fg); background: var(--mirk-bevel-bg);
|
|
514
|
+
border: 1px solid var(--mirk-input-border);
|
|
443
515
|
}
|
|
444
|
-
.hcms-shell .mirk-file__remove svg { display: block; width:
|
|
445
|
-
.hcms-shell .mirk-file__remove:hover { color: var(--mirk-destructive); }
|
|
516
|
+
.hcms-shell .mirk-file__remove svg { display: block; width: 66%; height: 66%; }
|
|
517
|
+
.hcms-shell .mirk-file__remove:hover { border-color: var(--mirk-destructive); color: var(--mirk-destructive); }
|
|
518
|
+
|
|
519
|
+
/* Small: tighter name + gap + remove ×; pair the trigger with mirk-button--small.
|
|
520
|
+
Composes with --compact (densest) and --round. */
|
|
521
|
+
.hcms-shell .mirk-file--small { gap: 0.5rem; }
|
|
522
|
+
.hcms-shell .mirk-file--small .mirk-file__name { font-size: 14px; }
|
|
523
|
+
.hcms-shell .mirk-file--small .mirk-file__remove { width: 16px; height: 16px; }
|
|
524
|
+
.hcms-shell .mirk-file--small.mirk-file--compact { padding: 3px 8px; }
|
|
525
|
+
.hcms-shell .mirk-file--small.mirk-file--compact .mirk-file__name { font-size: 13px; }
|
|
446
526
|
|
|
447
527
|
/* ============================ IMAGE ============================ */
|
|
448
528
|
.hcms-shell .mirk-image { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
@@ -526,6 +606,13 @@
|
|
|
526
606
|
background-image: linear-gradient(to top in oklab, var(--mirk-bevel-bg), var(--mirk-pill-inner-top));
|
|
527
607
|
}
|
|
528
608
|
|
|
609
|
+
.hcms-shell .mirk-tags--small { gap: 0.375rem; padding: 0.375rem; }
|
|
610
|
+
.hcms-shell .mirk-tags--small .mirk-tags__chip { padding: 1px 6px 1px 10px; font-size: 12px; }
|
|
611
|
+
.hcms-shell .mirk-tags--small .mirk-tags__remove { font-size: 12px; }
|
|
612
|
+
.hcms-shell .mirk-tags--small .mirk-tags__input { font-size: 14px; min-width: 90px; }
|
|
613
|
+
.hcms-shell .mirk-tags--small.mirk-tags--round { border-radius: 12px; }
|
|
614
|
+
.hcms-shell .mirk-tags--small.mirk-tags--round .mirk-tags__chip-inner { padding: 1px 6px 1px 10px; }
|
|
615
|
+
|
|
529
616
|
/* ============================ SORTABLE ============================ */
|
|
530
617
|
.hcms-shell .mirk-sortable { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
531
618
|
.hcms-shell .mirk-sortable__item {
|
|
@@ -557,6 +644,11 @@
|
|
|
557
644
|
.hcms-shell .mirk-sortable__field:focus-visible {
|
|
558
645
|
outline: 1px solid var(--mirk-focus-color); outline-offset: var(--mirk-focus-offset);
|
|
559
646
|
}
|
|
647
|
+
.hcms-shell .mirk-sortable--small { gap: 0.375rem; }
|
|
648
|
+
.hcms-shell .mirk-sortable--small .mirk-sortable__grip { width: 24px; }
|
|
649
|
+
.hcms-shell .mirk-sortable--small .mirk-sortable__row { padding: 5px 13px 6px; }
|
|
650
|
+
.hcms-shell .mirk-sortable--small .mirk-sortable__label { font-size: 10px; margin-bottom: 1px; }
|
|
651
|
+
.hcms-shell .mirk-sortable--small .mirk-sortable__field { font-size: 14px; }
|
|
560
652
|
}
|
|
561
653
|
|
|
562
654
|
/* Respect the machine — outside @layer components so it always wins. */
|
|
@@ -835,6 +927,15 @@ body.hcms-open.hcms-overlay { overflow: hidden; }
|
|
|
835
927
|
/* inline radio row */
|
|
836
928
|
.hcms-radio-row { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; }
|
|
837
929
|
|
|
930
|
+
/* chip-field (the @chips built-in): a borderless inline input that sizes to its
|
|
931
|
+
text, so chips read like static chips but stay inline-editable. */
|
|
932
|
+
.hcms-shell .mirk-tags__chip { padding-right: 6px; }
|
|
933
|
+
.hcms-shell .hcms-chip-field {
|
|
934
|
+
border: 0; background: transparent; color: inherit; font: inherit;
|
|
935
|
+
outline: none; min-width: 2ch; field-sizing: content; padding: 0;
|
|
936
|
+
}
|
|
937
|
+
.hcms-shell .hcms-chips .hcms-add { margin-top: 4px; align-self: flex-start; }
|
|
938
|
+
|
|
838
939
|
/* ---------- OBJECT GROUP — a quiet framed band, not a heavy card ---------- */
|
|
839
940
|
.hcms-object {
|
|
840
941
|
display: flex;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/* VENDORED from mirk-
|
|
1
|
+
/* VENDORED from mirk-interface/mirk.js — DO NOT EDIT. Regenerate: npm run build:theme */
|
|
2
2
|
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
3
3
|
/*
|
|
4
4
|
* mirk.js — the delegated runtime for the mirk UI kit, v2.
|
|
5
|
-
* https://github.com/
|
|
5
|
+
* https://github.com/panphora/mirk-interface
|
|
6
6
|
*
|
|
7
7
|
* One listener per interaction on `document`. No init(), no MutationObserver:
|
|
8
8
|
* every current AND future element is handled, so injected / re-rendered /
|