@panphora/hyper-cms 0.4.2 → 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 +131 -5
- package/dist/hypercms.min.js +1204 -220
- package/package.json +5 -3
- package/src/events.js +298 -14
- package/src/form-builder.js +171 -19
- package/src/form-rules.js +23 -9
- package/src/hypercms-bundle.js +5 -2
- package/src/hypercms.js +197 -4
- package/src/refresh.js +2 -1
- package/src/shell.js +93 -18
- package/src/templates.js +431 -25
- package/src/theme/pixel-quiet.overrides.css +512 -0
- package/src/theme.generated.css +1175 -0
- package/src/vendor/mirk.vendor.js +197 -0
package/src/shell.js
CHANGED
|
@@ -19,7 +19,22 @@ export function markStylesBundled(doc) {
|
|
|
19
19
|
|
|
20
20
|
let titleIdCounter = 0
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
function escapeHtml(s) {
|
|
23
|
+
return String(s).replace(/[&<>"]/g, (c) => (
|
|
24
|
+
{ '&': '&', '<': '<', '>': '>', '"': '"' }[c]
|
|
25
|
+
))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function mountShell({
|
|
29
|
+
mountTo,
|
|
30
|
+
side = 'right',
|
|
31
|
+
overlay = false,
|
|
32
|
+
showSaveButton = false,
|
|
33
|
+
title = 'Page content',
|
|
34
|
+
eyebrow = 'Edit',
|
|
35
|
+
theme = null,
|
|
36
|
+
doc,
|
|
37
|
+
}) {
|
|
23
38
|
ensureStyles(doc)
|
|
24
39
|
const titleId = `hcms-shell-title-${++titleIdCounter}`
|
|
25
40
|
const root = doc.createElement('div')
|
|
@@ -33,17 +48,45 @@ export function mountShell({ mountTo, side = 'right', overlay = false, showSaveB
|
|
|
33
48
|
root.setAttribute('role', 'dialog')
|
|
34
49
|
root.setAttribute('aria-modal', 'true')
|
|
35
50
|
root.setAttribute('aria-labelledby', titleId)
|
|
36
|
-
|
|
51
|
+
// pixel-quiet is the baked-in look; an optional theme pins light/dark
|
|
52
|
+
// (otherwise the panel follows the OS via prefers-color-scheme).
|
|
53
|
+
const themeClass = theme === 'dark' ? ' dark' : theme === 'light' ? ' light' : ''
|
|
54
|
+
root.className =
|
|
55
|
+
'hcms-shell pixel-quiet hcms-side-' + side + (overlay ? ' hcms-overlay' : '') + themeClass
|
|
56
|
+
const titleHtml = escapeHtml(title)
|
|
57
|
+
const eyebrowHtml = escapeHtml(eyebrow)
|
|
58
|
+
// Pixel Quiet shell: a scroll-away header + in-flow Save inside .hcms-shell-body,
|
|
59
|
+
// with a condensed minibar pinned to the panel top once the header scrolls out.
|
|
60
|
+
// Close + Save are real mirk buttons. The .hcms-* hooks stay so the engine and
|
|
61
|
+
// events bind exactly as before; only the chrome shape changed.
|
|
62
|
+
// The Save button has no wiring here: [trigger-save] is the hyperclayjs save
|
|
63
|
+
// attribute — its save system (or its view-mode notice) handles the click.
|
|
64
|
+
// Standalone hosts delegate their own [trigger-save] click listener.
|
|
37
65
|
root.innerHTML = `
|
|
38
|
-
<
|
|
39
|
-
<
|
|
40
|
-
<button type="button" class="hcms-shell-close" data-hcms-action="close" aria-label="Close"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
|
|
66
|
+
<div class="hcms-shell-minibar" aria-hidden="true">
|
|
67
|
+
<span class="hcms-shell-minibar-title">${titleHtml}</span>
|
|
68
|
+
<button type="button" class="hcms-shell-close mirk-button mirk-button--small" data-hcms-action="close" aria-label="Close">
|
|
69
|
+
<span class="mirk-button__label">×</span>
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="hcms-shell-body">
|
|
73
|
+
<header class="hcms-shell-header">
|
|
74
|
+
<div class="hcms-shell-heading">
|
|
75
|
+
<div class="hcms-shell-eyebrow">${eyebrowHtml}</div>
|
|
76
|
+
<h2 class="hcms-shell-title" id="${titleId}">${titleHtml}</h2>
|
|
77
|
+
</div>
|
|
78
|
+
<button type="button" class="hcms-shell-close mirk-button mirk-button--small" data-hcms-action="close" aria-label="Close">
|
|
79
|
+
<span class="mirk-button__label">×</span>
|
|
80
|
+
</button>
|
|
81
|
+
</header>
|
|
82
|
+
<div class="hcms-shell-error" role="alert" hidden></div>
|
|
83
|
+
<div data-hcms-form-root class="hcms-form"></div>
|
|
84
|
+
<footer class="hcms-shell-footer"${showSaveButton ? '' : ' hidden'}>
|
|
85
|
+
<button type="button" class="hcms-shell-save mirk-button" trigger-save>
|
|
86
|
+
<span class="mirk-button__label">Save</span>
|
|
87
|
+
</button>
|
|
88
|
+
</footer>
|
|
89
|
+
</div>
|
|
47
90
|
`
|
|
48
91
|
|
|
49
92
|
// mountTo may be a nested element inside pageRoot. Use it as-is for the
|
|
@@ -58,6 +101,7 @@ export function mountShell({ mountTo, side = 'right', overlay = false, showSaveB
|
|
|
58
101
|
if (side === 'left') body.classList.add('hcms-side-left')
|
|
59
102
|
|
|
60
103
|
const focusTrap = installFocusTrap(root, doc)
|
|
104
|
+
const condense = installCondenseOnScroll(root)
|
|
61
105
|
|
|
62
106
|
return {
|
|
63
107
|
root,
|
|
@@ -66,6 +110,7 @@ export function mountShell({ mountTo, side = 'right', overlay = false, showSaveB
|
|
|
66
110
|
saveButton: root.querySelector('.hcms-shell-save'),
|
|
67
111
|
destroy() {
|
|
68
112
|
focusTrap.detach()
|
|
113
|
+
condense.detach()
|
|
69
114
|
root.remove()
|
|
70
115
|
body.classList.remove('hcms-open', 'hcms-overlay', 'hcms-side-left')
|
|
71
116
|
},
|
|
@@ -110,28 +155,58 @@ function ensureStyles(doc) {
|
|
|
110
155
|
const style = doc.createElement('style')
|
|
111
156
|
style.id = SHELL_STYLE_ID
|
|
112
157
|
style.setAttribute('save-remove', '')
|
|
158
|
+
// save-ignore mirrors the shell root so a full-document morph (e.g. a live-sync
|
|
159
|
+
// head-merge) preserves the stylesheet instead of stripping it; without it the
|
|
160
|
+
// style survives only via restoreChrome's reactive re-injection.
|
|
161
|
+
style.setAttribute('save-ignore', '')
|
|
113
162
|
style.textContent = cssText
|
|
114
163
|
;(doc.head || doc.documentElement).appendChild(style)
|
|
115
164
|
styledDocs.add(doc)
|
|
116
165
|
return
|
|
117
166
|
}
|
|
118
|
-
// No bundled CSS — try resolving the sibling
|
|
119
|
-
// Browsers ESM-resolved imports place a co-located
|
|
120
|
-
// hypercms.js. Falls back silently if resolution
|
|
121
|
-
// without an asset emitter).
|
|
167
|
+
// No bundled CSS — try resolving the sibling generated theme via
|
|
168
|
+
// import.meta.url. Browsers ESM-resolved imports place a co-located
|
|
169
|
+
// theme.generated.css next to hypercms.js. Falls back silently if resolution
|
|
170
|
+
// fails (e.g., bundlers without an asset emitter).
|
|
122
171
|
try {
|
|
123
|
-
const href = new URL('./
|
|
172
|
+
const href = new URL('./theme.generated.css', import.meta.url).href
|
|
124
173
|
const link = doc.createElement('link')
|
|
125
174
|
link.rel = 'stylesheet'
|
|
126
175
|
link.id = SHELL_STYLE_ID
|
|
127
176
|
link.setAttribute('save-remove', '')
|
|
177
|
+
link.setAttribute('save-ignore', '')
|
|
128
178
|
link.href = href
|
|
129
179
|
;(doc.head || doc.documentElement).appendChild(link)
|
|
130
180
|
styledDocs.add(doc)
|
|
131
181
|
} catch (_) {
|
|
132
|
-
//
|
|
133
|
-
//
|
|
182
|
+
// No bundled CSS and no resolvable co-located theme (e.g. the IIFE bundle,
|
|
183
|
+
// where import.meta is undefined). Warn loudly rather than mount a silently
|
|
184
|
+
// unstyled shell — the host must call installStyles(themeText) before opening.
|
|
185
|
+
console.warn(
|
|
186
|
+
'hypercms: shell stylesheet not applied — cssText is empty and the ' +
|
|
187
|
+
'co-located theme fallback is unavailable. Call installStyles(themeText) ' +
|
|
188
|
+
'before opening the CMS.'
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Reveal the condensed minibar once the full header scrolls out of the body
|
|
194
|
+
// (pixel-quiet decision 0003). Passive listener; toggles .is-condensed on the
|
|
195
|
+
// shell root when scrollTop passes the header height. No-op if the body or
|
|
196
|
+
// header is missing (e.g. a custom mount that omits them).
|
|
197
|
+
function installCondenseOnScroll(root) {
|
|
198
|
+
const bodyEl = root.querySelector('.hcms-shell-body')
|
|
199
|
+
const header = root.querySelector('.hcms-shell-header')
|
|
200
|
+
if (!bodyEl || !header || typeof bodyEl.addEventListener !== 'function') {
|
|
201
|
+
return { detach() {} }
|
|
202
|
+
}
|
|
203
|
+
const reveal = () => {
|
|
204
|
+
const trigger = (header.offsetHeight || 0) - 12
|
|
205
|
+
root.classList.toggle('is-condensed', bodyEl.scrollTop > trigger)
|
|
134
206
|
}
|
|
207
|
+
bodyEl.addEventListener('scroll', reveal, { passive: true })
|
|
208
|
+
reveal()
|
|
209
|
+
return { detach() { bodyEl.removeEventListener('scroll', reveal) } }
|
|
135
210
|
}
|
|
136
211
|
|
|
137
212
|
function installFocusTrap(root, doc) {
|