webspresso 0.0.77 → 0.0.78

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.
Files changed (33) hide show
  1. package/README.md +5 -0
  2. package/bin/commands/db-scaffold.js +81 -0
  3. package/bin/utils/model-migrations.js +211 -0
  4. package/bin/webspresso.js +2 -0
  5. package/core/content/cache.js +64 -0
  6. package/core/content/field-types.js +180 -0
  7. package/core/content/index.js +30 -0
  8. package/core/content/renderer.js +84 -0
  9. package/core/content/schema.js +75 -0
  10. package/core/content/service.js +400 -0
  11. package/core/content/types.js +59 -0
  12. package/index.d.ts +17 -0
  13. package/index.js +7 -0
  14. package/package.json +1 -1
  15. package/plugins/admin-panel/app.js +7 -7
  16. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +41 -0
  17. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +2 -2
  18. package/plugins/admin-panel/index.js +17 -18
  19. package/plugins/admin-panel/modules/menu.js +1 -0
  20. package/plugins/content/admin/content-entries-component.js +291 -0
  21. package/plugins/content/admin/content-types-component.js +250 -0
  22. package/plugins/content/api-handlers.js +157 -0
  23. package/plugins/content/client/inline-edit.css +296 -0
  24. package/plugins/content/client/inline-edit.js +366 -0
  25. package/plugins/content/helpers.js +77 -0
  26. package/plugins/content/index.js +231 -0
  27. package/plugins/content/migration-template.js +54 -0
  28. package/plugins/content/models/content-entry.js +45 -0
  29. package/plugins/content/models/content-type.js +36 -0
  30. package/plugins/index.js +2 -0
  31. package/src/file-router.js +21 -1
  32. package/templates/skills/webspresso-usage/REFERENCE-framework.md +1 -1
  33. package/templates/skills/webspresso-usage/SKILL.md +5 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Content plugin API handlers
3
+ * @module plugins/content/api-handlers
4
+ */
5
+
6
+ /**
7
+ * @param {Object} deps
8
+ * @param {ReturnType<import('../../core/content/service').createContentService>} deps.contentService
9
+ */
10
+ function createContentApiHandlers({ contentService }) {
11
+ function sendError(res, err, status = 400) {
12
+ const message = err?.message || 'Request failed';
13
+ if (message.includes('not found')) {
14
+ return res.status(404).json({ error: message });
15
+ }
16
+ return res.status(status).json({ error: message });
17
+ }
18
+
19
+ async function listTypesHandler(req, res) {
20
+ try {
21
+ const data = await contentService.listTypes();
22
+ res.json({ data });
23
+ } catch (err) {
24
+ sendError(res, err);
25
+ }
26
+ }
27
+
28
+ async function createTypeHandler(req, res) {
29
+ try {
30
+ const row = await contentService.createType(req.body);
31
+ res.status(201).json({ data: row });
32
+ } catch (err) {
33
+ sendError(res, err);
34
+ }
35
+ }
36
+
37
+ async function getTypeHandler(req, res) {
38
+ try {
39
+ const row = await contentService.getTypeById(req.params.id);
40
+ if (!row) return res.status(404).json({ error: 'Content type not found' });
41
+ res.json({ data: row });
42
+ } catch (err) {
43
+ sendError(res, err);
44
+ }
45
+ }
46
+
47
+ async function updateTypeHandler(req, res) {
48
+ try {
49
+ const row = await contentService.updateType(req.params.id, req.body);
50
+ res.json({ data: row });
51
+ } catch (err) {
52
+ sendError(res, err);
53
+ }
54
+ }
55
+
56
+ async function deleteTypeHandler(req, res) {
57
+ try {
58
+ await contentService.deleteType(req.params.id);
59
+ res.json({ success: true });
60
+ } catch (err) {
61
+ sendError(res, err);
62
+ }
63
+ }
64
+
65
+ async function getTypeSchemaHandler(req, res) {
66
+ try {
67
+ const row = await contentService.getTypeBySlug(req.params.typeSlug);
68
+ if (!row) return res.status(404).json({ error: 'Content type not found' });
69
+ res.json({ data: { slug: row.slug, name: row.name, schema: row.schema } });
70
+ } catch (err) {
71
+ sendError(res, err);
72
+ }
73
+ }
74
+
75
+ async function listEntriesHandler(req, res) {
76
+ try {
77
+ const data = await contentService.listEntries(req.params.typeSlug, {
78
+ status: req.query.status,
79
+ locale: req.query.locale !== undefined ? req.query.locale : undefined,
80
+ });
81
+ res.json({ data });
82
+ } catch (err) {
83
+ sendError(res, err);
84
+ }
85
+ }
86
+
87
+ async function createEntryHandler(req, res) {
88
+ try {
89
+ const row = await contentService.createEntry(req.params.typeSlug, req.body);
90
+ res.status(201).json({ data: row });
91
+ } catch (err) {
92
+ sendError(res, err);
93
+ }
94
+ }
95
+
96
+ async function getEntryHandler(req, res) {
97
+ try {
98
+ const bundle = await contentService.getEntryById(req.params.id);
99
+ if (!bundle) return res.status(404).json({ error: 'Content entry not found' });
100
+ res.json({ data: bundle.entry, type: bundle.type });
101
+ } catch (err) {
102
+ sendError(res, err);
103
+ }
104
+ }
105
+
106
+ async function updateEntryHandler(req, res) {
107
+ try {
108
+ const row = await contentService.updateEntry(req.params.id, req.body);
109
+ res.json({ data: row });
110
+ } catch (err) {
111
+ sendError(res, err);
112
+ }
113
+ }
114
+
115
+ async function deleteEntryHandler(req, res) {
116
+ try {
117
+ await contentService.deleteEntry(req.params.id);
118
+ res.json({ success: true });
119
+ } catch (err) {
120
+ sendError(res, err);
121
+ }
122
+ }
123
+
124
+ async function publicGetEntryHandler(req, res) {
125
+ try {
126
+ const result = await contentService.getEntry(req.params.typeSlug, req.params.entrySlug, {
127
+ locale: req.query.locale ?? null,
128
+ });
129
+ if (!result) return res.status(404).json({ error: 'Content not found' });
130
+ res.json({
131
+ type: result.meta.typeSlug,
132
+ slug: result.meta.slug,
133
+ data: result.data,
134
+ meta: result.meta,
135
+ });
136
+ } catch (err) {
137
+ sendError(res, err);
138
+ }
139
+ }
140
+
141
+ return {
142
+ listTypesHandler,
143
+ createTypeHandler,
144
+ getTypeHandler,
145
+ updateTypeHandler,
146
+ deleteTypeHandler,
147
+ getTypeSchemaHandler,
148
+ listEntriesHandler,
149
+ createEntryHandler,
150
+ getEntryHandler,
151
+ updateEntryHandler,
152
+ deleteEntryHandler,
153
+ publicGetEntryHandler,
154
+ };
155
+ }
156
+
157
+ module.exports = { createContentApiHandlers };
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Inline edit — popover editor for admin users on public pages.
3
+ */
4
+ .ws-content-host,
5
+ .ws-content-block {
6
+ position: relative;
7
+ margin-block: 0.25rem;
8
+ }
9
+
10
+ .ws-content-toolbar {
11
+ display: flex;
12
+ justify-content: flex-end;
13
+ margin-bottom: 0.375rem;
14
+ min-height: 1.75rem;
15
+ }
16
+
17
+ .ws-content-edit-btn {
18
+ display: inline-flex;
19
+ align-items: center;
20
+ gap: 0.35rem;
21
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
22
+ font-size: 0.75rem;
23
+ font-weight: 600;
24
+ line-height: 1;
25
+ padding: 0.35rem 0.65rem;
26
+ border-radius: 9999px;
27
+ border: 1px solid #bfdbfe;
28
+ background: #eff6ff;
29
+ color: #1d4ed8;
30
+ cursor: pointer;
31
+ box-shadow: 0 1px 2px rgba(37, 99, 235, 0.12);
32
+ transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
33
+ }
34
+
35
+ .ws-content-edit-btn:hover {
36
+ background: #dbeafe;
37
+ border-color: #93c5fd;
38
+ box-shadow: 0 2px 6px rgba(37, 99, 235, 0.18);
39
+ }
40
+
41
+ .ws-content-edit-btn:focus-visible {
42
+ outline: 2px solid #2563eb;
43
+ outline-offset: 2px;
44
+ }
45
+
46
+ .ws-content-edit-btn-icon {
47
+ font-size: 0.85em;
48
+ line-height: 1;
49
+ }
50
+
51
+ .ws-content-editable {
52
+ outline: 1px dashed transparent;
53
+ outline-offset: 3px;
54
+ border-radius: 0.2rem;
55
+ transition: outline-color 0.15s, background-color 0.15s;
56
+ }
57
+
58
+ .ws-content-host:hover .ws-content-editable,
59
+ .ws-content-host.is-editing .ws-content-editable {
60
+ outline-color: rgba(37, 99, 235, 0.4);
61
+ background: rgba(37, 99, 235, 0.05);
62
+ }
63
+
64
+ /* Editor shell — id kept for compatibility */
65
+ #ws-content-modal {
66
+ display: none;
67
+ position: fixed;
68
+ inset: 0;
69
+ z-index: 99999;
70
+ pointer-events: none;
71
+ }
72
+
73
+ #ws-content-modal.is-open {
74
+ display: block;
75
+ }
76
+
77
+ .ws-content-modal-backdrop {
78
+ position: absolute;
79
+ inset: 0;
80
+ background: rgba(15, 23, 42, 0.2);
81
+ pointer-events: auto;
82
+ }
83
+
84
+ .ws-content-modal-panel {
85
+ position: fixed;
86
+ z-index: 1;
87
+ width: min(22rem, calc(100vw - 1.5rem));
88
+ max-height: min(24rem, calc(100vh - 2rem));
89
+ display: flex;
90
+ flex-direction: column;
91
+ background: #fff;
92
+ border: 1px solid #e2e8f0;
93
+ border-radius: 0.75rem;
94
+ box-shadow:
95
+ 0 4px 6px -1px rgba(15, 23, 42, 0.08),
96
+ 0 16px 32px -8px rgba(15, 23, 42, 0.18);
97
+ pointer-events: auto;
98
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
99
+ font-size: 0.875rem;
100
+ color: #0f172a;
101
+ box-sizing: border-box;
102
+ }
103
+
104
+ .ws-content-modal-panel *,
105
+ .ws-content-modal-panel *::before,
106
+ .ws-content-modal-panel *::after {
107
+ box-sizing: border-box;
108
+ }
109
+
110
+ .ws-content-popover-arrow {
111
+ position: absolute;
112
+ width: 12px;
113
+ height: 12px;
114
+ background: #fff;
115
+ border: 1px solid #e2e8f0;
116
+ transform: rotate(45deg);
117
+ top: -7px;
118
+ left: 50%;
119
+ margin-left: -6px;
120
+ pointer-events: none;
121
+ }
122
+
123
+ .ws-content-modal-panel.is-above .ws-content-popover-arrow {
124
+ top: auto;
125
+ bottom: -7px;
126
+ border-top-color: transparent;
127
+ border-left-color: transparent;
128
+ }
129
+
130
+ .ws-content-modal-panel:not(.is-above) .ws-content-popover-arrow {
131
+ border-bottom-color: transparent;
132
+ border-right-color: transparent;
133
+ }
134
+
135
+ .ws-content-modal-header {
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: space-between;
139
+ gap: 0.75rem;
140
+ padding: 0.75rem 1rem;
141
+ border-bottom: 1px solid #f1f5f9;
142
+ flex-shrink: 0;
143
+ }
144
+
145
+ .ws-content-modal-title {
146
+ margin: 0;
147
+ font-size: 0.875rem;
148
+ font-weight: 600;
149
+ line-height: 1.3;
150
+ color: #0f172a;
151
+ }
152
+
153
+ .ws-content-modal-close {
154
+ flex-shrink: 0;
155
+ width: 1.75rem;
156
+ height: 1.75rem;
157
+ display: inline-flex;
158
+ align-items: center;
159
+ justify-content: center;
160
+ border: none;
161
+ border-radius: 0.375rem;
162
+ background: transparent;
163
+ font-size: 1.25rem;
164
+ line-height: 1;
165
+ cursor: pointer;
166
+ color: #64748b;
167
+ }
168
+
169
+ .ws-content-modal-close:hover {
170
+ background: #f1f5f9;
171
+ color: #334155;
172
+ }
173
+
174
+ .ws-content-modal-body {
175
+ padding: 0.75rem 1rem;
176
+ overflow: auto;
177
+ flex: 1;
178
+ min-height: 0;
179
+ }
180
+
181
+ .ws-content-field {
182
+ margin-bottom: 0.75rem;
183
+ }
184
+
185
+ .ws-content-field:last-child {
186
+ margin-bottom: 0;
187
+ }
188
+
189
+ .ws-content-field label {
190
+ display: block;
191
+ font-size: 0.75rem;
192
+ font-weight: 600;
193
+ margin-bottom: 0.3rem;
194
+ color: #475569;
195
+ letter-spacing: 0.01em;
196
+ }
197
+
198
+ .ws-content-field input,
199
+ .ws-content-field textarea,
200
+ .ws-content-field select {
201
+ width: 100%;
202
+ border: 1px solid #cbd5e1;
203
+ border-radius: 0.5rem;
204
+ padding: 0.5rem 0.625rem;
205
+ font: inherit;
206
+ font-size: 0.875rem;
207
+ color: #0f172a;
208
+ background: #fff;
209
+ transition: border-color 0.15s, box-shadow 0.15s;
210
+ }
211
+
212
+ .ws-content-field input:focus,
213
+ .ws-content-field textarea:focus,
214
+ .ws-content-field select:focus {
215
+ outline: none;
216
+ border-color: #3b82f6;
217
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
218
+ }
219
+
220
+ .ws-content-field textarea {
221
+ resize: vertical;
222
+ min-height: 4.5rem;
223
+ }
224
+
225
+ .ws-content-modal-error {
226
+ padding: 0 1rem 0.5rem;
227
+ color: #b91c1c;
228
+ font-size: 0.8125rem;
229
+ flex-shrink: 0;
230
+ }
231
+
232
+ .ws-content-modal-footer {
233
+ display: flex;
234
+ justify-content: flex-end;
235
+ gap: 0.5rem;
236
+ padding: 0.625rem 1rem 0.875rem;
237
+ border-top: 1px solid #f1f5f9;
238
+ flex-shrink: 0;
239
+ }
240
+
241
+ .ws-content-btn {
242
+ border-radius: 0.5rem;
243
+ padding: 0.45rem 0.875rem;
244
+ font-size: 0.8125rem;
245
+ font-weight: 600;
246
+ cursor: pointer;
247
+ border: 1px solid transparent;
248
+ font-family: inherit;
249
+ }
250
+
251
+ .ws-content-btn-muted {
252
+ background: #f8fafc;
253
+ border-color: #e2e8f0;
254
+ color: #475569;
255
+ }
256
+
257
+ .ws-content-btn-muted:hover {
258
+ background: #f1f5f9;
259
+ }
260
+
261
+ .ws-content-btn-primary {
262
+ background: #2563eb;
263
+ color: #fff;
264
+ }
265
+
266
+ .ws-content-btn-primary:hover:not(:disabled) {
267
+ background: #1d4ed8;
268
+ }
269
+
270
+ .ws-content-btn-primary:disabled {
271
+ opacity: 0.6;
272
+ cursor: not-allowed;
273
+ }
274
+
275
+ /* Mobile: bottom sheet */
276
+ @media (max-width: 639px) {
277
+ .ws-content-modal-panel.is-sheet {
278
+ left: 0 !important;
279
+ right: 0 !important;
280
+ bottom: 0 !important;
281
+ top: auto !important;
282
+ width: 100% !important;
283
+ max-width: none;
284
+ max-height: min(88vh, 32rem);
285
+ border-radius: 1rem 1rem 0 0;
286
+ border-bottom: none;
287
+ }
288
+
289
+ .ws-content-modal-panel.is-sheet .ws-content-popover-arrow {
290
+ display: none;
291
+ }
292
+
293
+ .ws-content-modal-backdrop {
294
+ background: rgba(15, 23, 42, 0.35);
295
+ }
296
+ }