@crombolo/backoffice-host 0.0.0-stage → 0.9.1
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/LICENSE +21 -0
- package/NOTICE +69 -0
- package/assets/login.js +352 -0
- package/package.json +35 -4
- package/plugin/assistant/client.js +53 -0
- package/plugin/assistant/drawer.js +865 -0
- package/plugin/assistant/umbraco-package.json +16 -0
- package/plugin/branding/backdrop.svg +1 -0
- package/plugin/branding/branding.js +100 -0
- package/plugin/branding/bundle-builder-labels.js +63 -0
- package/plugin/branding/client-credentials.js +49 -0
- package/plugin/branding/components-tree.js +40 -0
- package/plugin/branding/content-type-labels.js +27 -0
- package/plugin/branding/favicon.svg +1 -0
- package/plugin/branding/help-menu.js +46 -0
- package/plugin/branding/icon-mark.js +15 -0
- package/plugin/branding/icon-registry.js +40 -0
- package/plugin/branding/localization-en.js +115 -0
- package/plugin/branding/log-viewer-menu.js +70 -0
- package/plugin/branding/logo-header.svg +1 -0
- package/plugin/branding/logo-surface.svg +1 -0
- package/plugin/branding/query-builder.js +28 -0
- package/plugin/branding/sign-in.js +80 -0
- package/plugin/branding/sysinfo.js +89 -0
- package/plugin/branding/theme.css +102 -0
- package/plugin/bundles-client.js +34 -0
- package/plugin/bundles-created-overview.js +150 -0
- package/plugin/bundles-created.js +70 -0
- package/plugin/bundles-installed.js +147 -0
- package/plugin/bundles-marketplace.js +199 -0
- package/plugin/changes-dashboard.js +132 -0
- package/plugin/element-type-hint.js +58 -0
- package/plugin/form-picker.js +116 -0
- package/plugin/forms-client.js +52 -0
- package/plugin/forms-entries.js +244 -0
- package/plugin/forms-overview.js +380 -0
- package/plugin/localizations/ar.js +97 -0
- package/plugin/localizations/bs.js +86 -0
- package/plugin/localizations/cs.js +73 -0
- package/plugin/localizations/cy.js +92 -0
- package/plugin/localizations/da.js +101 -0
- package/plugin/localizations/de.js +79 -0
- package/plugin/localizations/en.js +71 -0
- package/plugin/localizations/es.js +52 -0
- package/plugin/localizations/fr.js +78 -0
- package/plugin/localizations/he.js +24 -0
- package/plugin/localizations/hr.js +89 -0
- package/plugin/localizations/it.js +86 -0
- package/plugin/localizations/ja.js +43 -0
- package/plugin/localizations/ko.js +24 -0
- package/plugin/localizations/nb.js +44 -0
- package/plugin/localizations/nl.js +85 -0
- package/plugin/localizations/pl.js +44 -0
- package/plugin/localizations/pt-br.js +25 -0
- package/plugin/localizations/pt.js +100 -0
- package/plugin/localizations/ro.js +9 -0
- package/plugin/localizations/ru.js +46 -0
- package/plugin/localizations/sv.js +46 -0
- package/plugin/localizations/tr.js +80 -0
- package/plugin/localizations/uk.js +46 -0
- package/plugin/localizations/umbraco-package.json +279 -0
- package/plugin/localizations/vi.js +104 -0
- package/plugin/localizations/zh-tw.js +38 -0
- package/plugin/localizations/zh.js +38 -0
- package/plugin/read-only-banner.js +48 -0
- package/plugin/report-client.js +13 -0
- package/plugin/seo/client.js +35 -0
- package/plugin/seo/dashboard.js +242 -0
- package/plugin/seo/document-view.js +161 -0
- package/plugin/seo/type-hint.js +45 -0
- package/plugin/seo/umbraco-package.json +61 -0
- package/plugin/settings-dashboard.js +123 -0
- package/plugin/tsx-editors.js +337 -0
- package/plugin/tsx-snippets.js +63 -0
- package/plugin/umbraco-package.json +298 -0
- package/plugin/welcome-dashboard.js +106 -0
- package/src/branding.ts +59 -0
- package/src/extensions.ts +228 -0
- package/src/graphics.ts +87 -0
- package/src/index.ts +7 -0
- package/src/login-assets.ts +18 -0
- package/src/manifests.ts +154 -0
- package/src/paths.ts +117 -0
- package/src/shell.ts +139 -0
- package/src/static.ts +107 -0
- package/src/text-imports.d.ts +23 -0
- package/README.md +0 -3
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../backoffice-dist/dist/umbraco-package-schema.json",
|
|
3
|
+
"name": "Crombolo",
|
|
4
|
+
"id": "Crombolo.Core",
|
|
5
|
+
"version": "0.9.1",
|
|
6
|
+
"allowPublicAccess": false,
|
|
7
|
+
"extensions": [
|
|
8
|
+
{
|
|
9
|
+
"type": "sectionView",
|
|
10
|
+
"alias": "Crombolo.SectionView.Bundles.Marketplace",
|
|
11
|
+
"name": "Crombolo Packages Marketplace",
|
|
12
|
+
"element": "/crombolo-plugin/bundles-marketplace.js",
|
|
13
|
+
"weight": 300,
|
|
14
|
+
"overwrites": ["Umb.SectionView.Packages.Marketplace"],
|
|
15
|
+
"meta": {
|
|
16
|
+
"label": "Marketplace",
|
|
17
|
+
"pathname": "packages",
|
|
18
|
+
"icon": "icon-cloud"
|
|
19
|
+
},
|
|
20
|
+
"conditions": [
|
|
21
|
+
{
|
|
22
|
+
"alias": "Umb.Condition.SectionAlias",
|
|
23
|
+
"match": "Umb.Section.Packages"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "sectionView",
|
|
29
|
+
"alias": "Crombolo.SectionView.Bundles.Created",
|
|
30
|
+
"name": "Crombolo Created Bundles",
|
|
31
|
+
"element": "/crombolo-plugin/bundles-created.js",
|
|
32
|
+
"weight": 100,
|
|
33
|
+
"overwrites": ["Umb.SectionView.Packages.Builder"],
|
|
34
|
+
"meta": {
|
|
35
|
+
"label": "Created",
|
|
36
|
+
"pathname": "created",
|
|
37
|
+
"icon": "icon-files"
|
|
38
|
+
},
|
|
39
|
+
"conditions": [
|
|
40
|
+
{
|
|
41
|
+
"alias": "Umb.Condition.SectionAlias",
|
|
42
|
+
"match": "Umb.Section.Packages"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"type": "sectionView",
|
|
48
|
+
"alias": "Crombolo.SectionView.Bundles.Installed",
|
|
49
|
+
"name": "Crombolo Installed Packages",
|
|
50
|
+
"element": "/crombolo-plugin/bundles-installed.js",
|
|
51
|
+
"weight": 200,
|
|
52
|
+
"overwrites": ["Umb.SectionView.Packages.Installed"],
|
|
53
|
+
"meta": {
|
|
54
|
+
"label": "Installed",
|
|
55
|
+
"pathname": "installed",
|
|
56
|
+
"icon": "icon-box"
|
|
57
|
+
},
|
|
58
|
+
"conditions": [
|
|
59
|
+
{
|
|
60
|
+
"alias": "Umb.Condition.SectionAlias",
|
|
61
|
+
"match": "Umb.Section.Packages"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"type": "dashboard",
|
|
67
|
+
"alias": "Crombolo.Dashboard.Changes",
|
|
68
|
+
"name": "Crombolo Changes Dashboard",
|
|
69
|
+
"element": "/crombolo-plugin/changes-dashboard.js",
|
|
70
|
+
"weight": 5,
|
|
71
|
+
"meta": {
|
|
72
|
+
"label": "Changes",
|
|
73
|
+
"pathname": "changes"
|
|
74
|
+
},
|
|
75
|
+
"conditions": [
|
|
76
|
+
{
|
|
77
|
+
"alias": "Umb.Condition.SectionAlias",
|
|
78
|
+
"match": "Umb.Section.Settings"
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"type": "dashboard",
|
|
84
|
+
"alias": "Crombolo.Dashboard.Welcome",
|
|
85
|
+
"name": "Crombolo Welcome Dashboard",
|
|
86
|
+
"element": "/crombolo-plugin/welcome-dashboard.js",
|
|
87
|
+
"weight": 20,
|
|
88
|
+
"overwrites": ["Umb.Dashboard.UmbracoNews"],
|
|
89
|
+
"meta": {
|
|
90
|
+
"label": "Welcome",
|
|
91
|
+
"pathname": "welcome"
|
|
92
|
+
},
|
|
93
|
+
"conditions": [
|
|
94
|
+
{
|
|
95
|
+
"alias": "Umb.Condition.SectionAlias",
|
|
96
|
+
"match": "Umb.Section.Content"
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"type": "dashboard",
|
|
102
|
+
"alias": "Crombolo.Dashboard.Settings",
|
|
103
|
+
"name": "Crombolo Settings Dashboard",
|
|
104
|
+
"element": "/crombolo-plugin/settings-dashboard.js",
|
|
105
|
+
"weight": 500,
|
|
106
|
+
"overwrites": ["Umb.Dashboard.SettingsWelcome"],
|
|
107
|
+
"meta": {
|
|
108
|
+
"label": "Welcome",
|
|
109
|
+
"pathname": "welcome"
|
|
110
|
+
},
|
|
111
|
+
"conditions": [
|
|
112
|
+
{
|
|
113
|
+
"alias": "Umb.Condition.SectionAlias",
|
|
114
|
+
"match": "Umb.Section.Settings"
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"type": "backofficeEntryPoint",
|
|
120
|
+
"alias": "Crombolo.EntryPoint.TsxEditors",
|
|
121
|
+
"name": "Crombolo TSX Template and Partial View Editors",
|
|
122
|
+
"js": "/crombolo-plugin/tsx-editors.js"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"type": "headerApp",
|
|
126
|
+
"alias": "Crombolo.HeaderApp.ReadOnly",
|
|
127
|
+
"name": "Crombolo Read-only Banner",
|
|
128
|
+
"element": "/crombolo-plugin/read-only-banner.js",
|
|
129
|
+
"weight": 1000
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"type": "workspaceFooterApp",
|
|
133
|
+
"alias": "Crombolo.WorkspaceFooterApp.ElementTypeHint",
|
|
134
|
+
"name": "Crombolo Element Type Library Hint",
|
|
135
|
+
"element": "/crombolo-plugin/element-type-hint.js",
|
|
136
|
+
"weight": 900,
|
|
137
|
+
"conditions": [
|
|
138
|
+
{
|
|
139
|
+
"alias": "Umb.Condition.WorkspaceAlias",
|
|
140
|
+
"match": "Umb.Workspace.DocumentType"
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"type": "localization",
|
|
146
|
+
"alias": "Crombolo.Localization.EN",
|
|
147
|
+
"name": "Crombolo English Overrides",
|
|
148
|
+
"weight": 0,
|
|
149
|
+
"meta": {
|
|
150
|
+
"culture": "en"
|
|
151
|
+
},
|
|
152
|
+
"js": "/crombolo-plugin/branding/localization-en.js"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"type": "localization",
|
|
156
|
+
"alias": "Crombolo.Localization.EN_US",
|
|
157
|
+
"name": "Crombolo English (United States) Overrides",
|
|
158
|
+
"weight": 0,
|
|
159
|
+
"meta": {
|
|
160
|
+
"culture": "en-us"
|
|
161
|
+
},
|
|
162
|
+
"js": "/crombolo-plugin/branding/localization-en.js"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"type": "backofficeEntryPoint",
|
|
166
|
+
"alias": "Crombolo.EntryPoint.Branding",
|
|
167
|
+
"name": "Crombolo Branding",
|
|
168
|
+
"js": "/crombolo-plugin/branding/branding.js"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"type": "backofficeEntryPoint",
|
|
172
|
+
"alias": "Crombolo.EntryPoint.HelpMenu",
|
|
173
|
+
"name": "Crombolo Help Menu",
|
|
174
|
+
"js": "/crombolo-plugin/branding/help-menu.js"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"type": "backofficeEntryPoint",
|
|
178
|
+
"alias": "Crombolo.EntryPoint.Sysinfo",
|
|
179
|
+
"name": "Crombolo System Information Branding",
|
|
180
|
+
"js": "/crombolo-plugin/branding/sysinfo.js"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"type": "section",
|
|
184
|
+
"alias": "Crombolo.Section.Forms",
|
|
185
|
+
"name": "Crombolo Forms Section",
|
|
186
|
+
"weight": 50,
|
|
187
|
+
"meta": {
|
|
188
|
+
"label": "Forms",
|
|
189
|
+
"pathname": "forms"
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"type": "sectionView",
|
|
194
|
+
"alias": "Crombolo.SectionView.Forms.Overview",
|
|
195
|
+
"name": "Crombolo Forms Overview",
|
|
196
|
+
"element": "/crombolo-plugin/forms-overview.js",
|
|
197
|
+
"weight": 200,
|
|
198
|
+
"meta": {
|
|
199
|
+
"label": "Forms",
|
|
200
|
+
"pathname": "forms",
|
|
201
|
+
"icon": "icon-notepad"
|
|
202
|
+
},
|
|
203
|
+
"conditions": [
|
|
204
|
+
{
|
|
205
|
+
"alias": "Umb.Condition.SectionAlias",
|
|
206
|
+
"match": "Crombolo.Section.Forms"
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"type": "sectionView",
|
|
212
|
+
"alias": "Crombolo.SectionView.Forms.Entries",
|
|
213
|
+
"name": "Crombolo Forms Entries",
|
|
214
|
+
"element": "/crombolo-plugin/forms-entries.js",
|
|
215
|
+
"weight": 100,
|
|
216
|
+
"meta": {
|
|
217
|
+
"label": "Entries",
|
|
218
|
+
"pathname": "entries",
|
|
219
|
+
"icon": "icon-inbox"
|
|
220
|
+
},
|
|
221
|
+
"conditions": [
|
|
222
|
+
{
|
|
223
|
+
"alias": "Umb.Condition.SectionAlias",
|
|
224
|
+
"match": "Crombolo.Section.Forms"
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"type": "propertyEditorSchema",
|
|
230
|
+
"name": "Form Picker",
|
|
231
|
+
"alias": "Crombolo.FormPicker",
|
|
232
|
+
"meta": {
|
|
233
|
+
"defaultPropertyEditorUiAlias": "Crombolo.PropertyEditorUi.FormPicker"
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"type": "propertyEditorUi",
|
|
238
|
+
"alias": "Crombolo.PropertyEditorUi.FormPicker",
|
|
239
|
+
"name": "Form Picker",
|
|
240
|
+
"element": "/crombolo-plugin/form-picker.js",
|
|
241
|
+
"meta": {
|
|
242
|
+
"label": "Form Picker",
|
|
243
|
+
"icon": "icon-notepad",
|
|
244
|
+
"group": "pickers",
|
|
245
|
+
"propertyEditorSchemaAlias": "Crombolo.FormPicker"
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"type": "backofficeEntryPoint",
|
|
250
|
+
"alias": "Crombolo.EntryPoint.ClientCredentials",
|
|
251
|
+
"name": "Crombolo Client Credential Prefix",
|
|
252
|
+
"js": "/crombolo-plugin/branding/client-credentials.js"
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"type": "backofficeEntryPoint",
|
|
256
|
+
"alias": "Crombolo.EntryPoint.BundleBuilderLabels",
|
|
257
|
+
"name": "Crombolo Bundle Builder Labels",
|
|
258
|
+
"js": "/crombolo-plugin/branding/bundle-builder-labels.js"
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"type": "backofficeEntryPoint",
|
|
262
|
+
"alias": "Crombolo.EntryPoint.ComponentsTree",
|
|
263
|
+
"name": "Crombolo Components Tree",
|
|
264
|
+
"js": "/crombolo-plugin/branding/components-tree.js"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"type": "backofficeEntryPoint",
|
|
268
|
+
"alias": "Crombolo.EntryPoint.ContentTypeLabels",
|
|
269
|
+
"name": "Crombolo Content Type Labels",
|
|
270
|
+
"js": "/crombolo-plugin/branding/content-type-labels.js"
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"type": "backofficeEntryPoint",
|
|
274
|
+
"alias": "Crombolo.EntryPoint.QueryBuilder",
|
|
275
|
+
"name": "Crombolo Query Builder Language",
|
|
276
|
+
"js": "/crombolo-plugin/branding/query-builder.js"
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"type": "backofficeEntryPoint",
|
|
280
|
+
"alias": "Crombolo.EntryPoint.SignIn",
|
|
281
|
+
"name": "Crombolo Sign-in Button",
|
|
282
|
+
"js": "/crombolo-plugin/branding/sign-in.js"
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"type": "backofficeEntryPoint",
|
|
286
|
+
"alias": "Crombolo.EntryPoint.LogViewerMenu",
|
|
287
|
+
"name": "Crombolo Log Viewer Search Menu",
|
|
288
|
+
"js": "/crombolo-plugin/branding/log-viewer-menu.js"
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"type": "globalContext",
|
|
292
|
+
"alias": "Crombolo.GlobalContext.Icons",
|
|
293
|
+
"name": "Crombolo Icons Context",
|
|
294
|
+
"overwrites": ["Umb.GlobalContext.Icons"],
|
|
295
|
+
"api": "/crombolo-plugin/branding/icon-registry.js"
|
|
296
|
+
}
|
|
297
|
+
]
|
|
298
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Welcome dashboard in the Content section. Its manifest declares
|
|
3
|
+
* `overwrites: ['Umb.Dashboard.UmbracoNews']`, which takes the place of
|
|
4
|
+
* Umbraco's news dashboard without mutating the registry while the backoffice
|
|
5
|
+
* is rendering — the hazard the TSX entry point documents.
|
|
6
|
+
*/
|
|
7
|
+
// UmbLitElement, not plain LitElement: a dashboard is used as a controller host,
|
|
8
|
+
// and a plain element fails with `controllerHost.provideContext is not a function`.
|
|
9
|
+
import { css, html } from '@umbraco-cms/backoffice/external/lit'
|
|
10
|
+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
11
|
+
|
|
12
|
+
const STEPS = [
|
|
13
|
+
{
|
|
14
|
+
title: 'Model the content',
|
|
15
|
+
body: html`Open <strong>Settings → Content Types</strong> and describe a page: its
|
|
16
|
+
properties, its tabs, and which types may sit beneath it. This is the same
|
|
17
|
+
editor Umbraco ships, so anything you know about Umbraco document types applies.`,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
title: 'Write a template',
|
|
21
|
+
body: html`Templates are <strong>TSX</strong>, not Razor. A template is a file in
|
|
22
|
+
<code>Views/</code> that default-exports a component and receives the page as
|
|
23
|
+
<code>model</code>; <code>export const layout = 'alias'</code> names its master.
|
|
24
|
+
The editor under Settings → Templates writes that shape for you.`,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
title: 'Create and publish',
|
|
28
|
+
body: html`Back in <strong>Content</strong>, create a page from your content type, fill it
|
|
29
|
+
in and publish. Publishing is per culture, and the published version is what the
|
|
30
|
+
site renders — drafts stay invisible until you say otherwise.`,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
title: 'Keep the schema in the repository',
|
|
34
|
+
body: html`Content types, data types and templates are also files:
|
|
35
|
+
<code>schema/*.toml</code>. <code>crombolo schema sync</code> writes changes made in
|
|
36
|
+
the backoffice back out, and <code>crombolo schema check</code> tells you whether a
|
|
37
|
+
database matches. Schema travels through Git, not a database export.`,
|
|
38
|
+
},
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
export default class CromboloWelcomeDashboardElement extends UmbLitElement {
|
|
42
|
+
render() {
|
|
43
|
+
return html`
|
|
44
|
+
<uui-box headline="Welcome to Crombolo">
|
|
45
|
+
<p class="lede">
|
|
46
|
+
Crombolo is a content management system that runs on
|
|
47
|
+
<strong>Bun and TypeScript</strong>. It serves the real Umbraco backoffice you
|
|
48
|
+
are looking at now and follows Umbraco's API contract, so the editing
|
|
49
|
+
experience is Umbraco's — while the server, the templates and the schema are
|
|
50
|
+
TypeScript, and the database is SQLite or Postgres.
|
|
51
|
+
</p>
|
|
52
|
+
<p class="lede">
|
|
53
|
+
There is no .NET anywhere in it: templates are TSX rather than Razor, and a
|
|
54
|
+
site's schema lives in TOML files beside its code.
|
|
55
|
+
</p>
|
|
56
|
+
</uui-box>
|
|
57
|
+
|
|
58
|
+
<uui-box headline="Getting started">
|
|
59
|
+
<ol>
|
|
60
|
+
${STEPS.map(
|
|
61
|
+
(step) => html`<li>
|
|
62
|
+
<h4>${step.title}</h4>
|
|
63
|
+
<p>${step.body}</p>
|
|
64
|
+
</li>`,
|
|
65
|
+
)}
|
|
66
|
+
</ol>
|
|
67
|
+
</uui-box>
|
|
68
|
+
|
|
69
|
+
<uui-box headline="Worth knowing">
|
|
70
|
+
<ul>
|
|
71
|
+
<li>
|
|
72
|
+
The <strong>Settings</strong> section holds templates, partial views,
|
|
73
|
+
stylesheets and scripts — all served from your site's folders.
|
|
74
|
+
</li>
|
|
75
|
+
<li>
|
|
76
|
+
<strong>Upgrade</strong>, also under Settings, reports anything a version
|
|
77
|
+
change needs from you before it will run.
|
|
78
|
+
</li>
|
|
79
|
+
<li>
|
|
80
|
+
The log viewer under Settings reads the server's own structured logs, so a
|
|
81
|
+
failed publish or a broken template is searchable rather than guessed at.
|
|
82
|
+
</li>
|
|
83
|
+
</ul>
|
|
84
|
+
</uui-box>
|
|
85
|
+
`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static styles = css`
|
|
89
|
+
:host { display: block; padding: var(--uui-size-layout-1); }
|
|
90
|
+
uui-box { margin-bottom: var(--uui-size-layout-1); }
|
|
91
|
+
.lede { max-width: 70ch; line-height: 1.6; }
|
|
92
|
+
ol { margin: 0; padding-left: var(--uui-size-layout-1); }
|
|
93
|
+
ol li { margin-bottom: var(--uui-size-space-5); max-width: 70ch; }
|
|
94
|
+
ol h4 { margin: 0 0 var(--uui-size-space-2); }
|
|
95
|
+
ol p { margin: 0; line-height: 1.6; }
|
|
96
|
+
ul { margin: 0; padding-left: var(--uui-size-layout-1); }
|
|
97
|
+
ul li { margin-bottom: var(--uui-size-space-3); max-width: 70ch; line-height: 1.6; }
|
|
98
|
+
code {
|
|
99
|
+
background: var(--uui-color-surface-alt);
|
|
100
|
+
padding: 0 var(--uui-size-space-1);
|
|
101
|
+
border-radius: var(--uui-border-radius);
|
|
102
|
+
}
|
|
103
|
+
`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
customElements.define('crombolo-welcome-dashboard', CromboloWelcomeDashboardElement)
|
package/src/branding.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The brand as data: the marks, and the palette behind them.
|
|
3
|
+
*
|
|
4
|
+
* The back office serves these as files, by path (`graphics.ts`). This exports
|
|
5
|
+
* the same bytes to anything that has to *draw* the brand rather than serve it
|
|
6
|
+
* — the enterprise console, which is a separate product surface in another
|
|
7
|
+
* repository and reaches the CMS only through the `@crombolo/crombolo` umbrella.
|
|
8
|
+
*
|
|
9
|
+
* Imported as text rather than duplicated as string literals, so the `.svg`
|
|
10
|
+
* and `.css` files stay the one source of truth and a new logo is one file to
|
|
11
|
+
* change. Bun inlines a text import at bundle time, so this works in a browser
|
|
12
|
+
* bundle as well as on the server — which matters, because the console that
|
|
13
|
+
* consumes it is a client-side app and could not read a file at runtime.
|
|
14
|
+
*/
|
|
15
|
+
/// <reference path="./text-imports.d.ts" />
|
|
16
|
+
import faviconText from '../plugin/branding/favicon.svg' with { type: 'text' }
|
|
17
|
+
import logoText from '../plugin/branding/logo-header.svg' with { type: 'text' }
|
|
18
|
+
import themeText from '../plugin/branding/theme.css' with { type: 'text' }
|
|
19
|
+
|
|
20
|
+
/** The mark and wordmark, for a header or a sign-in card. */
|
|
21
|
+
export const logoSvg: string = logoText.trim()
|
|
22
|
+
|
|
23
|
+
/** The mark alone, square, for a tab icon. */
|
|
24
|
+
export const faviconSvg: string = faviconText.trim()
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The palette, read out of `theme.css` rather than restated here.
|
|
28
|
+
*
|
|
29
|
+
* The theme is what the back office is actually painted with, so parsing it is
|
|
30
|
+
* what makes a consumer's colours the same colours rather than a copy that was
|
|
31
|
+
* the same once. Only the `--crombolo-*` tokens are ours; everything else in
|
|
32
|
+
* that file is an Umbraco variable being pointed at one of them.
|
|
33
|
+
*/
|
|
34
|
+
export interface BrandPalette {
|
|
35
|
+
orange: string
|
|
36
|
+
orangeLight: string
|
|
37
|
+
orangeDeep: string
|
|
38
|
+
orangeText: string
|
|
39
|
+
cocoa: string
|
|
40
|
+
cream: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `--crombolo-orange-deep` → `orangeDeep`. */
|
|
44
|
+
const camel = (name: string) =>
|
|
45
|
+
name.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase())
|
|
46
|
+
|
|
47
|
+
export function paletteFrom(css: string): BrandPalette {
|
|
48
|
+
const found: Record<string, string> = {}
|
|
49
|
+
for (const [, name, value] of css.matchAll(/--crombolo-([a-z-]+)\s*:\s*([^;]+);/g))
|
|
50
|
+
found[camel(name as string)] = (value as string).trim()
|
|
51
|
+
const wanted = ['orange', 'orangeLight', 'orangeDeep', 'orangeText', 'cocoa', 'cream']
|
|
52
|
+
const missing = wanted.filter((key) => found[key] === undefined)
|
|
53
|
+
// A theme that stopped declaring one of these would otherwise hand a
|
|
54
|
+
// consumer `undefined` and paint something invisible.
|
|
55
|
+
if (missing.length > 0) throw new Error(`theme.css declares no ${missing.join(', ')}.`)
|
|
56
|
+
return Object.fromEntries(wanted.map((key) => [key, found[key]])) as unknown as BrandPalette
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const brandPalette: BrandPalette = paletteFrom(themeText)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backoffice extensions as npm dependencies. `docs/17-bundles.md`.
|
|
3
|
+
*
|
|
4
|
+
* A site's extensions are its dependencies: discovery reads the site's own
|
|
5
|
+
* `package.json`, resolves each dependency through `node_modules`, and takes the
|
|
6
|
+
* `crombolo` field from the dependency's `package.json`. There is no directory
|
|
7
|
+
* convention to keep in step, and `bun add` is the install.
|
|
8
|
+
*
|
|
9
|
+
* Only declared dependencies are read, never a walk of `node_modules`, so a
|
|
10
|
+
* transitive dependency cannot put an extension into a backoffice that never
|
|
11
|
+
* asked for it.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
14
|
+
import { dirname, join, normalize, sep } from 'node:path'
|
|
15
|
+
import type { PackageManifest, PackageManifestImportmap } from './manifests.ts'
|
|
16
|
+
|
|
17
|
+
/** Where an installed bundle's assets are served from. */
|
|
18
|
+
export const EXTENSION_PATH = '/bundles'
|
|
19
|
+
|
|
20
|
+
/** The npm keyword a bundle publishes so the marketplace can find it. */
|
|
21
|
+
export const EXTENSION_KEYWORD = 'crombolo-bundle'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* What a dependency declares to become a backoffice extension. The shape is
|
|
25
|
+
* Umbraco's manifest minus the parts the server owns — the name and version
|
|
26
|
+
* come from the package itself.
|
|
27
|
+
*/
|
|
28
|
+
export interface CromboloField {
|
|
29
|
+
id?: string
|
|
30
|
+
name?: string
|
|
31
|
+
allowPublicAccess?: boolean
|
|
32
|
+
allowTelemetry?: boolean
|
|
33
|
+
extensions?: unknown[]
|
|
34
|
+
importmap?: PackageManifestImportmap
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface InstalledExtension {
|
|
38
|
+
/** The npm package name, which is also its segment in the asset path. */
|
|
39
|
+
packageName: string
|
|
40
|
+
version: string
|
|
41
|
+
directory: string
|
|
42
|
+
manifest: PackageManifest
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Asset references inside an extension. Only these carry a path; rewriting
|
|
47
|
+
* every string would corrupt labels and aliases that happen to look like one.
|
|
48
|
+
*/
|
|
49
|
+
const ASSET_KEYS = new Set(['element', 'api', 'js', 'loader'])
|
|
50
|
+
|
|
51
|
+
interface PackageJson {
|
|
52
|
+
name?: string
|
|
53
|
+
version?: string
|
|
54
|
+
dependencies?: Record<string, string>
|
|
55
|
+
crombolo?: CromboloField
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readJson(file: string): PackageJson | undefined {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8')) as unknown
|
|
61
|
+
return parsed && typeof parsed === 'object' ? (parsed as PackageJson) : undefined
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The directory a dependency resolves to, walking up as Node does, so a
|
|
69
|
+
* workspace root's hoisted `node_modules` is found from a site inside it.
|
|
70
|
+
*/
|
|
71
|
+
export function resolvePackageDir(from: string, name: string): string | undefined {
|
|
72
|
+
// A name may not climb out of node_modules, whatever a package.json says.
|
|
73
|
+
if (name.includes('..') || name.startsWith('/') || name.startsWith('.')) return undefined
|
|
74
|
+
let dir = normalize(from)
|
|
75
|
+
for (;;) {
|
|
76
|
+
const candidate = join(dir, 'node_modules', name)
|
|
77
|
+
if (existsSync(join(candidate, 'package.json'))) return candidate
|
|
78
|
+
const parent = dirname(dir)
|
|
79
|
+
if (parent === dir) return undefined
|
|
80
|
+
dir = parent
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `dist/seo.js` inside `@acme/seo` becomes `/bundles/@acme/seo/dist/seo.js`. */
|
|
85
|
+
const assetPath = (packageName: string, target: string): string =>
|
|
86
|
+
target.startsWith('/') || /^[a-z][a-z0-9+.-]*:/i.test(target)
|
|
87
|
+
? target
|
|
88
|
+
: `${EXTENSION_PATH}/${packageName}/${target.replace(/^\.?\//, '')}`
|
|
89
|
+
|
|
90
|
+
function rewriteAssets(value: unknown, packageName: string): unknown {
|
|
91
|
+
if (Array.isArray(value)) return value.map((item) => rewriteAssets(item, packageName))
|
|
92
|
+
if (!value || typeof value !== 'object') return value
|
|
93
|
+
const out: Record<string, unknown> = {}
|
|
94
|
+
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
|
95
|
+
out[key] =
|
|
96
|
+
ASSET_KEYS.has(key) && typeof item === 'string'
|
|
97
|
+
? assetPath(packageName, item)
|
|
98
|
+
: rewriteAssets(item, packageName)
|
|
99
|
+
}
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function rewriteImportmap(
|
|
104
|
+
importmap: PackageManifestImportmap | undefined,
|
|
105
|
+
packageName: string,
|
|
106
|
+
): PackageManifestImportmap | undefined {
|
|
107
|
+
if (!importmap) return undefined
|
|
108
|
+
const imports: Record<string, string> = {}
|
|
109
|
+
for (const [specifier, target] of Object.entries(importmap.imports ?? {}))
|
|
110
|
+
imports[specifier] = assetPath(packageName, target)
|
|
111
|
+
const scopes: Record<string, Record<string, string>> = {}
|
|
112
|
+
for (const [scope, scopeImports] of Object.entries(importmap.scopes ?? {})) {
|
|
113
|
+
const rewritten: Record<string, string> = {}
|
|
114
|
+
for (const [specifier, target] of Object.entries(scopeImports))
|
|
115
|
+
rewritten[specifier] = assetPath(packageName, target)
|
|
116
|
+
scopes[assetPath(packageName, scope)] = rewritten
|
|
117
|
+
}
|
|
118
|
+
return Object.keys(scopes).length > 0 ? { imports, scopes } : { imports }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** One dependency's manifest, or undefined when it declares no extensions. */
|
|
122
|
+
export function readExtension(
|
|
123
|
+
directory: string,
|
|
124
|
+
packageName: string,
|
|
125
|
+
): InstalledExtension | undefined {
|
|
126
|
+
const json = readJson(join(directory, 'package.json'))
|
|
127
|
+
const field = json?.crombolo
|
|
128
|
+
if (!field || !Array.isArray(field.extensions) || field.extensions.length === 0) return undefined
|
|
129
|
+
return {
|
|
130
|
+
packageName,
|
|
131
|
+
version: json?.version ?? '0.0.0',
|
|
132
|
+
directory,
|
|
133
|
+
manifest: {
|
|
134
|
+
name: field.name ?? packageName,
|
|
135
|
+
id: field.id ?? packageName,
|
|
136
|
+
version: json?.version ?? '0.0.0',
|
|
137
|
+
allowPublicAccess: field.allowPublicAccess ?? false,
|
|
138
|
+
allowTelemetry: field.allowTelemetry ?? false,
|
|
139
|
+
extensions: field.extensions.map((extension) => rewriteAssets(extension, packageName)),
|
|
140
|
+
importmap: rewriteImportmap(field.importmap, packageName),
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Every declared dependency of the site that is a backoffice extension. */
|
|
146
|
+
export function discoverExtensions(siteDir: string): InstalledExtension[] {
|
|
147
|
+
const json = readJson(join(siteDir, 'package.json'))
|
|
148
|
+
if (!json?.dependencies) return []
|
|
149
|
+
const found: InstalledExtension[] = []
|
|
150
|
+
for (const name of Object.keys(json.dependencies).sort()) {
|
|
151
|
+
const directory = resolvePackageDir(siteDir, name)
|
|
152
|
+
if (!directory) continue
|
|
153
|
+
const extension = readExtension(directory, name)
|
|
154
|
+
if (extension) found.push(extension)
|
|
155
|
+
}
|
|
156
|
+
return found
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The package a `/packages/…` request names, and the file inside it.
|
|
161
|
+
*
|
|
162
|
+
* The name is matched against the site's own extensions rather than trusted, so
|
|
163
|
+
* this cannot be used to read an arbitrary file out of `node_modules`; a scoped
|
|
164
|
+
* name takes two segments, as npm writes it.
|
|
165
|
+
*/
|
|
166
|
+
export function resolveExtensionFile(
|
|
167
|
+
extensions: readonly InstalledExtension[],
|
|
168
|
+
pathname: string,
|
|
169
|
+
): string | undefined {
|
|
170
|
+
const prefix = `${EXTENSION_PATH}/`
|
|
171
|
+
if (!pathname.startsWith(prefix)) return undefined
|
|
172
|
+
const rest = pathname.slice(prefix.length)
|
|
173
|
+
const segments = rest.split('/')
|
|
174
|
+
const take = rest.startsWith('@') || rest.toLowerCase().startsWith('%40') ? 2 : 1
|
|
175
|
+
if (segments.length <= take) return undefined
|
|
176
|
+
// Decoded before matching, so `%40acme/seo` finds the same package the
|
|
177
|
+
// manifest names. Safe because the name is only compared against the known
|
|
178
|
+
// extensions, never joined into a path — the file part is what gets joined.
|
|
179
|
+
let name: string
|
|
180
|
+
try {
|
|
181
|
+
name = decodeURIComponent(segments.slice(0, take).join('/'))
|
|
182
|
+
} catch {
|
|
183
|
+
return undefined
|
|
184
|
+
}
|
|
185
|
+
const relative = segments.slice(take).join('/')
|
|
186
|
+
const extension = extensions.find((candidate) => candidate.packageName === name)
|
|
187
|
+
if (!extension || relative.length === 0) return undefined
|
|
188
|
+
|
|
189
|
+
const decoded = decodeURIComponent(relative)
|
|
190
|
+
if (decoded.includes('\0')) return undefined
|
|
191
|
+
const root = extension.directory.endsWith(sep)
|
|
192
|
+
? extension.directory
|
|
193
|
+
: `${extension.directory}${sep}`
|
|
194
|
+
const target = normalize(join(extension.directory, decoded))
|
|
195
|
+
return target.startsWith(root) ? target : undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface ExtensionRegistry {
|
|
199
|
+
list(): readonly InstalledExtension[]
|
|
200
|
+
/** Called after an install or an uninstall, so the next read sees it. */
|
|
201
|
+
invalidate(): void
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Discovery, memoized.
|
|
206
|
+
*
|
|
207
|
+
* Reading a handful of `package.json` files on every manifest request would be
|
|
208
|
+
* wasteful, but the uncached behaviour is load-bearing in development: an author
|
|
209
|
+
* editing a local extension expects a reload to show it, which is why `cache`
|
|
210
|
+
* is off there rather than everywhere.
|
|
211
|
+
*/
|
|
212
|
+
export function createExtensionRegistry(
|
|
213
|
+
siteDir: string,
|
|
214
|
+
options: { cache?: boolean } = {},
|
|
215
|
+
): ExtensionRegistry {
|
|
216
|
+
let cached: InstalledExtension[] | undefined
|
|
217
|
+
const cache = options.cache ?? true
|
|
218
|
+
return {
|
|
219
|
+
list() {
|
|
220
|
+
if (!cache) return discoverExtensions(siteDir)
|
|
221
|
+
cached ??= discoverExtensions(siteDir)
|
|
222
|
+
return cached
|
|
223
|
+
},
|
|
224
|
+
invalidate() {
|
|
225
|
+
cached = undefined
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
}
|