@panphora/hyper-cms 0.4.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/src/styles.css ADDED
@@ -0,0 +1,298 @@
1
+ .hcms-shell {
2
+ --hcms-primary: #2563eb;
3
+ --hcms-bg: #F6F7F9;
4
+ --hcms-text: #0f172a;
5
+ --hcms-muted: #64748b;
6
+ --hcms-border: #e2e8f0;
7
+ --hcms-error: #b91c1c;
8
+ --hcms-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
9
+ font-family: var(--hcms-font-family);
10
+ font-size: 14px;
11
+ line-height: 1.45;
12
+ color: var(--hcms-text);
13
+ background: var(--hcms-bg);
14
+ box-sizing: border-box;
15
+ position: fixed;
16
+ top: 0;
17
+ bottom: 0;
18
+ right: 0;
19
+ width: 100vw;
20
+ max-width: 400px;
21
+ overflow-y: auto;
22
+ border-left: 1px solid var(--hcms-border);
23
+ z-index: 2147483000;
24
+ display: flex;
25
+ flex-direction: column;
26
+ box-shadow: -4px 0 24px rgba(15, 23, 42, 0.08);
27
+ }
28
+
29
+ .hcms-shell.hcms-side-left {
30
+ left: 0;
31
+ right: auto;
32
+ border-left: 0;
33
+ border-right: 1px solid var(--hcms-border);
34
+ box-shadow: 4px 0 24px rgba(15, 23, 42, 0.08);
35
+ }
36
+
37
+ .hcms-shell * {
38
+ box-sizing: border-box;
39
+ }
40
+
41
+ .hcms-shell-header {
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: space-between;
45
+ padding: 16px;
46
+ border-bottom: 1px solid var(--hcms-border);
47
+ }
48
+
49
+ .hcms-shell-title {
50
+ margin: 0;
51
+ font-size: 16px;
52
+ font-weight: 600;
53
+ }
54
+
55
+ .hcms-shell-close {
56
+ background: transparent;
57
+ border: 0;
58
+ color: var(--hcms-muted);
59
+ font-size: 24px;
60
+ line-height: 1;
61
+ cursor: pointer;
62
+ padding: 4px 8px;
63
+ border-radius: 6px;
64
+ }
65
+ .hcms-shell-close:hover { background: rgba(15, 23, 42, 0.06); color: var(--hcms-text); }
66
+
67
+ .hcms-shell-error {
68
+ margin: 12px 16px;
69
+ padding: 10px 12px;
70
+ background: #fef2f2;
71
+ border: 1px solid #fecaca;
72
+ color: var(--hcms-error);
73
+ border-radius: 6px;
74
+ font-size: 13px;
75
+ }
76
+
77
+ .hcms-form {
78
+ flex: 1;
79
+ padding: 16px;
80
+ display: flex;
81
+ flex-direction: column;
82
+ gap: 16px;
83
+ }
84
+
85
+ .hcms-field {
86
+ display: flex;
87
+ flex-direction: column;
88
+ gap: 4px;
89
+ }
90
+
91
+ .hcms-label {
92
+ font-size: 12px;
93
+ font-weight: 600;
94
+ color: var(--hcms-muted);
95
+ text-transform: uppercase;
96
+ letter-spacing: 0.04em;
97
+ }
98
+
99
+ .hcms-input {
100
+ font-family: inherit;
101
+ font-size: 14px;
102
+ padding: 8px 10px;
103
+ border: 1px solid var(--hcms-border);
104
+ border-radius: 6px;
105
+ background: #fdfdfd;
106
+ color: var(--hcms-text);
107
+ width: 100%;
108
+ }
109
+ .hcms-input:focus {
110
+ outline: 2px solid var(--hcms-primary);
111
+ outline-offset: -1px;
112
+ border-color: var(--hcms-primary);
113
+ }
114
+
115
+ textarea.hcms-input { min-height: 80px; resize: vertical; }
116
+
117
+ .hcms-object {
118
+ display: flex;
119
+ flex-direction: column;
120
+ gap: 12px;
121
+ padding: 12px;
122
+ border: 1px solid var(--hcms-border);
123
+ border-radius: 8px;
124
+ background: #fdfdfd;
125
+ }
126
+
127
+ .hcms-object-title,
128
+ .hcms-array-title {
129
+ margin: 0 0 8px;
130
+ font-size: 13px;
131
+ font-weight: 600;
132
+ color: var(--hcms-muted);
133
+ text-transform: uppercase;
134
+ letter-spacing: 0.04em;
135
+ }
136
+
137
+ .hcms-array {
138
+ display: flex;
139
+ flex-direction: column;
140
+ gap: 8px;
141
+ }
142
+
143
+ .hcms-array-header {
144
+ display: flex;
145
+ justify-content: space-between;
146
+ align-items: baseline;
147
+ }
148
+
149
+ .hcms-array-items {
150
+ display: flex;
151
+ flex-direction: column;
152
+ gap: 8px;
153
+ list-style: none;
154
+ padding: 0;
155
+ margin: 0;
156
+ }
157
+
158
+ .hcms-array-item,
159
+ .hcms-card {
160
+ display: flex;
161
+ align-items: center;
162
+ gap: 8px;
163
+ padding: 8px;
164
+ background: #fdfdfd;
165
+ border: 1px solid var(--hcms-border);
166
+ border-radius: 6px;
167
+ }
168
+
169
+ .hcms-card {
170
+ flex-direction: column;
171
+ align-items: stretch;
172
+ padding: 12px;
173
+ gap: 10px;
174
+ }
175
+
176
+ .hcms-card-header {
177
+ display: flex;
178
+ justify-content: space-between;
179
+ align-items: center;
180
+ }
181
+
182
+ .hcms-card-fields {
183
+ display: flex;
184
+ flex-direction: column;
185
+ gap: 10px;
186
+ }
187
+
188
+ .hcms-drag-handle {
189
+ cursor: grab;
190
+ color: var(--hcms-muted);
191
+ user-select: none;
192
+ font-family: monospace;
193
+ font-size: 14px;
194
+ }
195
+
196
+ .hcms-remove,
197
+ .hcms-add {
198
+ font-family: inherit;
199
+ background: transparent;
200
+ border: 1px solid var(--hcms-border);
201
+ border-radius: 6px;
202
+ padding: 4px 10px;
203
+ cursor: pointer;
204
+ font-size: 13px;
205
+ color: var(--hcms-muted);
206
+ }
207
+ .hcms-remove:hover { color: var(--hcms-error); border-color: #fecaca; }
208
+ .hcms-add { align-self: flex-start; padding: 6px 12px; color: var(--hcms-text); }
209
+ .hcms-add:hover { border-color: var(--hcms-primary); color: var(--hcms-primary); }
210
+
211
+ .hcms-error {
212
+ margin: 6px 0;
213
+ padding: 6px 10px;
214
+ background: #fef2f2;
215
+ border: 1px solid #fecaca;
216
+ color: var(--hcms-error);
217
+ border-radius: 6px;
218
+ font-size: 12px;
219
+ }
220
+ .hcms-error[hidden] { display: none; }
221
+
222
+ .hcms-shell-footer {
223
+ border-top: 1px solid var(--hcms-border);
224
+ padding: 12px 16px;
225
+ display: flex;
226
+ justify-content: flex-end;
227
+ }
228
+
229
+ .hcms-shell-save {
230
+ font-family: inherit;
231
+ background: var(--hcms-primary);
232
+ color: #fdfdfd;
233
+ border: 0;
234
+ border-radius: 6px;
235
+ padding: 8px 16px;
236
+ font-size: 14px;
237
+ cursor: pointer;
238
+ font-weight: 500;
239
+ }
240
+ .hcms-shell-save:hover { filter: brightness(1.08); }
241
+
242
+ @media (max-width: 799px) {
243
+ .hcms-shell { max-width: 100vw; }
244
+ }
245
+
246
+ /* Scroll lock only when the sidebar covers the page (overlay + mobile fullscreen). In push mode the page keeps its own scroll. */
247
+ body.hcms-open.hcms-overlay {
248
+ overflow: hidden;
249
+ }
250
+
251
+ body.hcms-open:not(.hcms-overlay) {
252
+ padding-right: 400px;
253
+ }
254
+ body.hcms-open.hcms-side-left:not(.hcms-overlay) {
255
+ padding-right: 0;
256
+ padding-left: 400px;
257
+ }
258
+ @media (max-width: 799px) {
259
+ body.hcms-open:not(.hcms-overlay),
260
+ body.hcms-open.hcms-side-left:not(.hcms-overlay) {
261
+ padding-right: 0;
262
+ padding-left: 0;
263
+ }
264
+ body.hcms-open {
265
+ overflow: hidden;
266
+ }
267
+ }
268
+
269
+ /* sr-only: visually hidden but accessible to screen readers + becomes
270
+ visible on keyboard focus, so keyboard users can see move buttons land. */
271
+ .hcms-sr-only {
272
+ position: absolute;
273
+ width: 1px;
274
+ height: 1px;
275
+ padding: 0;
276
+ margin: -1px;
277
+ overflow: hidden;
278
+ clip: rect(0, 0, 0, 0);
279
+ white-space: nowrap;
280
+ border: 0;
281
+ }
282
+ .hcms-sr-only:focus {
283
+ position: static;
284
+ width: auto;
285
+ height: auto;
286
+ padding: 2px 6px;
287
+ margin: 0 2px;
288
+ overflow: visible;
289
+ clip: auto;
290
+ white-space: normal;
291
+ background: #F6F7F9;
292
+ border: 1px solid #c0c8d6;
293
+ border-radius: 3px;
294
+ color: #1f2532;
295
+ font-size: 12px;
296
+ cursor: pointer;
297
+ }
298
+ .hcms-sr-only[hidden] { display: none; }
@@ -0,0 +1,184 @@
1
+ import { getRuleAtPath } from './path.js'
2
+
3
+ export function humanize(key) {
4
+ return key
5
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
6
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
7
+ .replace(/[-_]/g, ' ')
8
+ .replace(/\s+/g, ' ')
9
+ .trim()
10
+ .replace(/^./, (c) => c.toUpperCase())
11
+ }
12
+
13
+ const DEFAULT_TEMPLATES = {
14
+ '@scalar': `
15
+ <label class="hcms-field" data-hcms-shape="scalar">
16
+ <span class="hcms-label" data-hcms-label></span>
17
+ <input class="hcms-input" data-hcms-field />
18
+ <div class="hcms-error" hidden></div>
19
+ </label>
20
+ `,
21
+ '@object': `
22
+ <section class="hcms-object" data-hcms-shape="object">
23
+ <h3 class="hcms-object-title" data-hcms-label></h3>
24
+ <div class="hcms-object-fields"></div>
25
+ <div class="hcms-error" hidden></div>
26
+ </section>
27
+ `,
28
+ '@scalar-array': `
29
+ <section class="hcms-array hcms-scalar-array" data-hcms-shape="scalar-array">
30
+ <header class="hcms-array-header">
31
+ <h3 class="hcms-array-title" data-hcms-label></h3>
32
+ </header>
33
+ <ul class="hcms-array-items"></ul>
34
+ <div class="hcms-error" hidden></div>
35
+ <button type="button" class="hcms-add" data-hcms-action="add">+ Add</button>
36
+ </section>
37
+ `,
38
+ '@scalar-array-item': `
39
+ <li class="hcms-array-item" draggable="true">
40
+ <span class="hcms-drag-handle" aria-hidden="true">::</span>
41
+ <input class="hcms-input" data-hcms-field />
42
+ <button type="button" class="hcms-move hcms-move-up hcms-sr-only" data-hcms-action="move-up" aria-label="Move up">↑</button>
43
+ <button type="button" class="hcms-move hcms-move-down hcms-sr-only" data-hcms-action="move-down" aria-label="Move down">↓</button>
44
+ <button type="button" class="hcms-remove" data-hcms-action="remove" aria-label="Remove">x</button>
45
+ <div class="hcms-error" hidden></div>
46
+ </li>
47
+ `,
48
+ '@object-array': `
49
+ <section class="hcms-array hcms-object-array" data-hcms-shape="object-array">
50
+ <header class="hcms-array-header">
51
+ <h3 class="hcms-array-title" data-hcms-label></h3>
52
+ </header>
53
+ <div class="hcms-array-items"></div>
54
+ <div class="hcms-error" hidden></div>
55
+ <button type="button" class="hcms-add" data-hcms-action="add">+ Add</button>
56
+ </section>
57
+ `,
58
+ '@object-array-item': `
59
+ <article class="hcms-card" draggable="true">
60
+ <header class="hcms-card-header">
61
+ <span class="hcms-drag-handle" aria-hidden="true">::</span>
62
+ <button type="button" class="hcms-move hcms-move-up hcms-sr-only" data-hcms-action="move-up" aria-label="Move up">↑</button>
63
+ <button type="button" class="hcms-move hcms-move-down hcms-sr-only" data-hcms-action="move-down" aria-label="Move down">↓</button>
64
+ <button type="button" class="hcms-remove" data-hcms-action="remove" aria-label="Remove">x</button>
65
+ </header>
66
+ <div class="hcms-card-fields"></div>
67
+ <div class="hcms-error" hidden></div>
68
+ </article>
69
+ `,
70
+ }
71
+
72
+ const DEFAULT_KEYS = Object.keys(DEFAULT_TEMPLATES)
73
+
74
+ export function injectDefaults(doc) {
75
+ const head = doc.head || doc.documentElement
76
+ if (!head) return
77
+ for (const key of DEFAULT_KEYS) {
78
+ if (findTemplate(doc, key)) continue
79
+ const tpl = doc.createElement('template')
80
+ tpl.setAttribute('data-hcms-tpl', key)
81
+ tpl.setAttribute('save-remove', '')
82
+ tpl.innerHTML = DEFAULT_TEMPLATES[key].trim()
83
+ head.appendChild(tpl)
84
+ }
85
+ }
86
+
87
+ export function findTemplate(doc, key) {
88
+ if (!doc || !doc.querySelector) return null
89
+ return doc.querySelector(`template[data-hcms-tpl="${cssEscape(key)}"]`)
90
+ }
91
+
92
+ function cssEscape(value) {
93
+ if (typeof CSS !== 'undefined' && CSS.escape) return CSS.escape(value)
94
+ return String(value).replace(/[^a-zA-Z0-9_\-.*]/g, (c) => '\\' + c)
95
+ }
96
+
97
+ export function shapeKindOf(rule) {
98
+ if (typeof rule === 'string') {
99
+ return rule.endsWith('[]') ? 'scalar-array' : 'scalar'
100
+ }
101
+ if (Array.isArray(rule)) return 'object-array'
102
+ if (typeof rule === 'object' && rule !== null) return 'object'
103
+ return 'scalar'
104
+ }
105
+
106
+ const SHAPE_TO_DEFAULT_KEY = {
107
+ scalar: '@scalar',
108
+ object: '@object',
109
+ 'scalar-array': '@scalar-array',
110
+ 'object-array': '@object-array',
111
+ }
112
+
113
+ const SHAPE_TO_ITEM_KEY = {
114
+ 'scalar-array': '@scalar-array-item',
115
+ 'object-array': '@object-array-item',
116
+ }
117
+
118
+ export function templateKeyForPath(pathArr, pageRules) {
119
+ const rule = getRuleAtPath(pageRules, pathArr)
120
+ const shape = shapeKindOf(rule)
121
+ return SHAPE_TO_DEFAULT_KEY[shape] || '@scalar'
122
+ }
123
+
124
+ export function itemTemplateKeyForArrayPath(pathArr, pageRules) {
125
+ const rule = getRuleAtPath(pageRules, pathArr)
126
+ const shape = shapeKindOf(rule)
127
+ return SHAPE_TO_ITEM_KEY[shape] || null
128
+ }
129
+
130
+ export function buildTemplateMap(pageRules, doc) {
131
+ const map = new Map()
132
+ walk([], pageRules)
133
+ return map
134
+
135
+ function walk(pathArr, rule) {
136
+ const pathStr = pathArr.join('.')
137
+ const wildcardKey = pathArr.map((s) => (typeof s === 'number' ? '*' : s)).join('.')
138
+ const shape = shapeKindOf(rule)
139
+
140
+ let tpl =
141
+ (pathStr && findTemplate(doc, pathStr)) ||
142
+ (wildcardKey && wildcardKey !== pathStr && findTemplate(doc, wildcardKey)) ||
143
+ findTemplate(doc, SHAPE_TO_DEFAULT_KEY[shape] || '@scalar')
144
+
145
+ map.set(pathStr, tpl)
146
+
147
+ if (shape === 'object') {
148
+ for (const [k, child] of Object.entries(rule)) walk([...pathArr, k], child)
149
+ } else if (shape === 'object-array' || shape === 'scalar-array') {
150
+ const itemPath = [...pathArr, '*']
151
+ const itemKey = itemPath.map((s) => (typeof s === 'number' ? '*' : s)).join('.')
152
+ const itemShape = shape === 'object-array' ? 'object-array-item' : 'scalar-array-item'
153
+ const itemTpl =
154
+ findTemplate(doc, itemKey) || findTemplate(doc, '@' + itemShape)
155
+ map.set(itemKey, itemTpl)
156
+ if (shape === 'object-array') {
157
+ const itemRule = rule[1]
158
+ if (itemRule && typeof itemRule === 'object' && !Array.isArray(itemRule)) {
159
+ for (const [k, child] of Object.entries(itemRule)) walk([...itemPath, k], child)
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ export function isInlineTemplate(tplEl) {
167
+ if (!tplEl) return false
168
+ const root = tplEl.content || tplEl
169
+ return !!root.querySelector('[data-hcms-field]')
170
+ }
171
+
172
+ export function slotSelectorFor(kind) {
173
+ switch (kind) {
174
+ case 'object':
175
+ return '.hcms-object-fields'
176
+ case 'object-array-item':
177
+ return '.hcms-card-fields'
178
+ case 'scalar-array':
179
+ case 'object-array':
180
+ return '.hcms-array-items'
181
+ default:
182
+ return null
183
+ }
184
+ }