@crombolo/backoffice-host 0.14.4 → 0.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crombolo/backoffice-host",
3
- "version": "0.14.4",
3
+ "version": "0.15.0",
4
4
  "description": "Serves the Umbraco backoffice SPA: shell HTML, import map and package manifests.",
5
5
  "license": "MIT",
6
6
  "author": "Crombolo",
@@ -31,7 +31,7 @@
31
31
  "access": "public"
32
32
  },
33
33
  "dependencies": {
34
- "@crombolo/core": "0.14.4",
34
+ "@crombolo/core": "0.15.0",
35
35
  "@crombolo/backoffice-dist": "18.2.0"
36
36
  }
37
37
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../../backoffice-dist/dist/umbraco-package-schema.json",
3
3
  "name": "Crombolo Assistant",
4
4
  "id": "Crombolo.Assistant",
5
- "version": "0.14.4",
5
+ "version": "0.15.0",
6
6
  "allowPublicAccess": false,
7
7
  "extensions": [
8
8
  {
@@ -0,0 +1,61 @@
1
+ /*
2
+ * Crombolo's dark mode (story 049): the vendored `dark.css` for every token,
3
+ * warmed to the brand — cocoa surfaces, cream text, terracotta accents.
4
+ *
5
+ * The theme context appends this after `theme.css` when Dark is chosen, so it
6
+ * overrides the light palette. `/umbraco/backoffice/…` is served wherever the
7
+ * back office is mounted, which is why the import can name it outright.
8
+ * `tests/theme.test.ts` checks the contrast of every pair below.
9
+ */
10
+ @import url("/umbraco/backoffice/css/dark.css");
11
+
12
+ :root {
13
+ --crombolo-orange: #da987c;
14
+ --crombolo-orange-light: #e2ae98;
15
+ --crombolo-ink: #f6ede4;
16
+ --crombolo-ink-dimmed: #c9b5a5;
17
+ --crombolo-night: #1f1813;
18
+ --crombolo-night-surface: #2a211b;
19
+ --crombolo-night-alt: #342922;
20
+ --crombolo-night-header: #2b1d15;
21
+ --crombolo-cocoa-deep: #25160e;
22
+
23
+ --uui-color-background: var(--crombolo-night);
24
+ --uui-color-surface: var(--crombolo-night-surface);
25
+ --uui-color-surface-alt: var(--crombolo-night-alt);
26
+ --uui-color-surface-emphasis: #30261f;
27
+ --uui-color-text: var(--crombolo-ink);
28
+ --uui-color-text-alt: var(--crombolo-ink-dimmed);
29
+ --uui-color-interactive: var(--crombolo-ink);
30
+ --uui-color-interactive-emphasis: var(--crombolo-orange-light);
31
+
32
+ /* The accent is light, so what sits on it is dark, as in the light mode. */
33
+ --uui-color-default: var(--crombolo-orange);
34
+ --uui-color-default-emphasis: var(--crombolo-orange-light);
35
+ --uui-color-default-standalone: var(--crombolo-orange);
36
+ --uui-color-default-contrast: var(--crombolo-cocoa-deep);
37
+ --uui-color-selected: var(--crombolo-orange);
38
+ --uui-color-selected-emphasis: var(--crombolo-orange-light);
39
+ --uui-color-selected-standalone: var(--crombolo-orange);
40
+ --uui-color-selected-contrast: var(--crombolo-cocoa-deep);
41
+ --uui-color-current: var(--crombolo-orange);
42
+ --uui-color-current-emphasis: var(--crombolo-orange-light);
43
+ --uui-color-current-contrast: var(--crombolo-cocoa-deep);
44
+ --uui-color-focus: var(--crombolo-orange);
45
+
46
+ /* A dark header with cream text; the active section's marker is the accent. */
47
+ --uui-color-header-surface: var(--crombolo-night-header);
48
+ --uui-color-header-contrast: rgb(246 237 228 / 80%);
49
+ --uui-color-header-contrast-emphasis: var(--crombolo-ink);
50
+ --crombolo-header-current: var(--crombolo-orange);
51
+
52
+ /* The SEO view's search-result preview, in Google's dark-mode link blue. */
53
+ --crombolo-snippet-title: #8ab4f8;
54
+
55
+ --uui-color-border: #4a3b31;
56
+ --uui-color-border-standalone: #8b7564;
57
+ --uui-color-border-emphasis: #a38c7a;
58
+ --uui-color-divider: #3a2e26;
59
+ --uui-color-divider-standalone: #4a3b31;
60
+ --uui-color-divider-emphasis: #5a4638;
61
+ }
@@ -0,0 +1,6 @@
1
+ /*
2
+ * "Match my system" (story 049): the dark mode while the computer asks for
3
+ * dark, and the light one otherwise — including a change made while the back
4
+ * office is open, which the media query follows by itself.
5
+ */
6
+ @import url("./theme-dark.css") (prefers-color-scheme: dark);
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../../backoffice-dist/dist/umbraco-package-schema.json",
3
3
  "name": "Crombolo Localizations",
4
4
  "id": "Crombolo.Localizations",
5
- "version": "0.14.4",
5
+ "version": "0.15.0",
6
6
  "allowPublicAccess": true,
7
7
  "extensions": [
8
8
  {
@@ -122,7 +122,8 @@ export default class CromboloSeoDocumentViewElement extends UmbLitElement {
122
122
  overflow-wrap: anywhere;
123
123
  }
124
124
  .snippet .title {
125
- color: #1a0dab;
125
+ /* A search result's link blue: Google's own, light and dark. */
126
+ color: var(--crombolo-snippet-title, #1a0dab);
126
127
  font-size: 20px;
127
128
  margin: 4px 0;
128
129
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../../backoffice-dist/dist/umbraco-package-schema.json",
3
3
  "name": "Crombolo Search & sharing",
4
4
  "id": "Crombolo.Seo",
5
- "version": "0.14.4",
5
+ "version": "0.15.0",
6
6
  "allowPublicAccess": false,
7
7
  "extensions": [
8
8
  {
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../../backoffice-dist/dist/umbraco-package-schema.json",
3
3
  "name": "Crombolo Sign-in Dialog",
4
4
  "id": "Crombolo.SignInDialog",
5
- "version": "0.14.4",
5
+ "version": "0.15.0",
6
6
  "allowPublicAccess": true,
7
7
  "extensions": [
8
8
  {
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Light, dark or the computer's choice, from the header (story 049).
3
+ *
4
+ * The choice is Umbraco's theme context's — it links the stylesheet and keeps
5
+ * the alias in `localStorage` for the shell's early script to apply before the
6
+ * next paint. This adds the menu, and keeps the choice for the person in user
7
+ * data, so it follows them to another browser.
8
+ */
9
+ import { css, html } from '@umbraco-cms/backoffice/external/lit'
10
+ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
11
+ import { UMB_THEME_CONTEXT } from '@umbraco-cms/backoffice/themes'
12
+
13
+ const USER_DATA = '/umbraco/management/api/v1/user-data'
14
+ const GROUP = 'crombolo'
15
+ const IDENTIFIER = 'theme'
16
+ const EARLY = 'crombolo-early-theme'
17
+
18
+ const CHOICES = [
19
+ { alias: 'umb-light-theme', label: 'Light', icon: 'icon-sunny' },
20
+ { alias: 'Crombolo.Theme.Dark', label: 'Dark', icon: 'icon-contrast' },
21
+ { alias: 'Crombolo.Theme.System', label: 'Match my system', icon: 'icon-lightbulb' },
22
+ ]
23
+
24
+ export default class CromboloThemeSwitchElement extends UmbLitElement {
25
+ static properties = { _alias: { state: true } }
26
+
27
+ #context
28
+ #savedKey
29
+
30
+ constructor() {
31
+ super()
32
+ this._alias = 'umb-light-theme'
33
+ this.consumeContext(UMB_THEME_CONTEXT, (context) => {
34
+ this.#context = context
35
+ this.observe(context.theme, (alias) => {
36
+ this._alias = alias || 'umb-light-theme'
37
+ // The shell's early stylesheet stood in until the context took over;
38
+ // once the choice differs from it, it would only fight the new one.
39
+ const early = document.getElementById(EARLY)
40
+ if (early && early.dataset.theme !== this._alias) early.remove()
41
+ })
42
+ this.#restore()
43
+ })
44
+ }
45
+
46
+ /** The person's saved choice, when this browser's differs from it. */
47
+ async #restore() {
48
+ try {
49
+ const query = new URLSearchParams({ groups: GROUP, identifiers: IDENTIFIER, take: '1' })
50
+ const response = await fetch(`${USER_DATA}?${query}`, { credentials: 'include' })
51
+ if (!response.ok) return
52
+ const found = (await response.json()).items?.[0]
53
+ if (!found) return
54
+ this.#savedKey = found.key
55
+ if (found.value && found.value !== this._alias) this.#context?.setThemeByAlias(found.value)
56
+ } catch {
57
+ // This browser's choice stands.
58
+ }
59
+ }
60
+
61
+ async #save(alias) {
62
+ const body = { group: GROUP, identifier: IDENTIFIER, value: alias }
63
+ try {
64
+ if (this.#savedKey) {
65
+ await fetch(USER_DATA, {
66
+ method: 'PUT',
67
+ credentials: 'include',
68
+ headers: { 'content-type': 'application/json' },
69
+ body: JSON.stringify({ ...body, key: this.#savedKey }),
70
+ })
71
+ return
72
+ }
73
+ const response = await fetch(USER_DATA, {
74
+ method: 'POST',
75
+ credentials: 'include',
76
+ headers: { 'content-type': 'application/json' },
77
+ body: JSON.stringify(body),
78
+ })
79
+ this.#savedKey = response.headers.get('umb-generated-resource') ?? undefined
80
+ } catch {
81
+ // Kept in this browser regardless; another will start from its own.
82
+ }
83
+ }
84
+
85
+ #choose(alias) {
86
+ this.#context?.setThemeByAlias(alias)
87
+ this.#save(alias)
88
+ this.renderRoot.querySelector('#crombolo-theme-menu')?.hidePopover?.()
89
+ }
90
+
91
+ render() {
92
+ const current = CHOICES.find((choice) => choice.alias === this._alias) ?? CHOICES[0]
93
+ return html`
94
+ <uui-button
95
+ compact
96
+ look="primary"
97
+ label="Light or dark: ${current.label}"
98
+ popovertarget="crombolo-theme-menu">
99
+ <umb-icon name=${current.icon}></umb-icon>
100
+ </uui-button>
101
+ <uui-popover-container id="crombolo-theme-menu" placement="bottom-end">
102
+ <umb-popover-layout>
103
+ ${CHOICES.map(
104
+ (choice) => html`<uui-menu-item
105
+ label=${choice.label}
106
+ ?active=${choice.alias === this._alias}
107
+ @click-label=${() => this.#choose(choice.alias)}>
108
+ <umb-icon slot="icon" name=${choice.icon}></umb-icon>
109
+ </uui-menu-item>`,
110
+ )}
111
+ </umb-popover-layout>
112
+ </uui-popover-container>
113
+ `
114
+ }
115
+
116
+ static styles = css`
117
+ uui-button {
118
+ font-size: 18px;
119
+ --uui-button-background-color: var(--umb-header-app-button-background-color, transparent);
120
+ --uui-button-background-color-hover: var(
121
+ --umb-header-app-button-background-color-hover,
122
+ var(--uui-color-emphasis)
123
+ );
124
+ }
125
+ :host {
126
+ --uui-menu-item-flat-structure: 1;
127
+ }
128
+ umb-popover-layout {
129
+ min-width: 12rem;
130
+ }
131
+ `
132
+ }
133
+
134
+ customElements.define('crombolo-theme-switch', CromboloThemeSwitchElement)
@@ -124,6 +124,7 @@ export default class CromboloTwoFactorSetupElement extends UmbLitElement {
124
124
  width: 200px;
125
125
  height: 200px;
126
126
  margin: var(--uui-size-space-4) auto;
127
+ /* White in every mode: a scanner reads dark modules on a light ground. */
127
128
  background: #fff;
128
129
  }
129
130
  .secret code,
@@ -2,9 +2,31 @@
2
2
  "$schema": "../../backoffice-dist/dist/umbraco-package-schema.json",
3
3
  "name": "Crombolo",
4
4
  "id": "Crombolo.Core",
5
- "version": "0.14.4",
5
+ "version": "0.15.0",
6
6
  "allowPublicAccess": false,
7
7
  "extensions": [
8
+ {
9
+ "type": "theme",
10
+ "alias": "Crombolo.Theme.Dark",
11
+ "name": "Dark",
12
+ "css": "/crombolo-plugin/branding/theme-dark.css",
13
+ "weight": 200,
14
+ "overwrites": "umb-dark-theme"
15
+ },
16
+ {
17
+ "type": "theme",
18
+ "alias": "Crombolo.Theme.System",
19
+ "name": "Match my system",
20
+ "css": "/crombolo-plugin/branding/theme-system.css",
21
+ "weight": 250
22
+ },
23
+ {
24
+ "type": "headerApp",
25
+ "alias": "Crombolo.HeaderApp.Theme",
26
+ "name": "Light or dark",
27
+ "element": "/crombolo-plugin/theme-switch.js",
28
+ "weight": 850
29
+ },
8
30
  {
9
31
  "type": "entityUserPermission",
10
32
  "alias": "Crombolo.UserPermission.Remote.Pull",
@@ -55,7 +77,10 @@
55
77
  "name": "Crombolo Signed-in Devices",
56
78
  "element": "/crombolo-plugin/signed-in-devices.js",
57
79
  "weight": 100,
58
- "meta": { "label": "Signed-in devices", "pathname": "devices" }
80
+ "meta": {
81
+ "label": "Signed-in devices",
82
+ "pathname": "devices"
83
+ }
59
84
  },
60
85
  {
61
86
  "type": "mfaLoginProvider",
@@ -63,7 +88,9 @@
63
88
  "name": "Crombolo Authenticator App",
64
89
  "forProviderName": "UmbracoUserAppAuthenticator",
65
90
  "element": "/crombolo-plugin/two-factor-setup.js",
66
- "meta": { "label": "Authenticator app" }
91
+ "meta": {
92
+ "label": "Authenticator app"
93
+ }
67
94
  },
68
95
  {
69
96
  "type": "sectionView",
@@ -31,8 +31,8 @@ const STEPS = [
31
31
  {
32
32
  title: 'Keep the schema in the repository',
33
33
  body: html`Content types, data types and templates are also files:
34
- <code>schema/*.toml</code>. <code>crombolo schema sync</code> writes changes made in
35
- the backoffice back out, and <code>crombolo schema check</code> tells you whether a
34
+ <code>schema/*.toml</code>. <code>crombolo types sync</code> writes changes made in
35
+ the backoffice back out, and <code>crombolo types check</code> tells you whether a
36
36
  database matches. Schema travels through Git, not a database export.`,
37
37
  },
38
38
  ]
package/src/index.ts CHANGED
@@ -6,3 +6,4 @@ export * from './manifests.ts'
6
6
  export * from './paths.ts'
7
7
  export * from './shell.ts'
8
8
  export * from './static.ts'
9
+ export * from './theme-early.ts'
package/src/shell.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  import { MANAGEMENT_API_PATH } from '@crombolo/core'
10
10
  import type { PackageManifestImportmap } from './manifests.ts'
11
11
  import type { BackOfficePaths } from './paths.ts'
12
+ import { earlyThemeScript } from './theme-early.ts'
12
13
 
13
14
  export interface ShellSettings {
14
15
  /** GlobalSettings.DefaultUILanguage — the initial UI language. */
@@ -81,6 +82,7 @@ export function renderBackOfficeShell(
81
82
  <link rel="stylesheet" href="${assets}/css/umb-css.css" />
82
83
  <link rel="stylesheet" href="${assets}/css/light.css" />
83
84
  <link rel="stylesheet" href="${paths.pluginPath}/branding/theme.css" />
85
+ <script>${earlyThemeScript(paths)}</script>
84
86
  <script type="importmap">
85
87
  ${JSON.stringify(importmap, null, 2)}
86
88
  </script>
@@ -117,7 +119,8 @@ export function renderLoginShell(
117
119
  <title>${escapeHtml(settings.title)}</title>
118
120
  <link rel="stylesheet" href="${assets}/css/light.css" />
119
121
  <link rel="stylesheet" href="${paths.pluginPath}/branding/theme.css" />
120
- <style>body { margin: 0; padding: 0; background-color: #fff7ea; }</style>
122
+ <script>${earlyThemeScript(paths)}</script>
123
+ <style>body { margin: 0; padding: 0; background-color: var(--uui-color-background, #fff7ea); }</style>
121
124
  <script type="importmap">
122
125
  ${JSON.stringify(importmap, null, 2)}
123
126
  </script>
@@ -163,7 +166,8 @@ export function renderStandaloneShell(
163
166
  <title>${escapeHtml(settings.title)}</title>
164
167
  <link rel="stylesheet" href="${assets}/css/light.css" />
165
168
  <link rel="stylesheet" href="${paths.pluginPath}/branding/theme.css" />
166
- <style>body { margin: 0; padding: 0; background-color: #fff7ea; }</style>
169
+ <script>${earlyThemeScript(paths)}</script>
170
+ <style>body { margin: 0; padding: 0; background-color: var(--uui-color-background, #fff7ea); }</style>
167
171
  <script type="importmap">
168
172
  ${JSON.stringify(importmap, null, 2)}
169
173
  </script>
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The back office's light or dark mode, applied before the first paint
3
+ * (story 049). Umbraco's theme context keeps the choice in `localStorage` and
4
+ * applies it once the client has booted, so a reload painted light first; and
5
+ * the login shell boots no theme context at all. This inline script reads the
6
+ * same key and links the same stylesheet at once.
7
+ */
8
+ import type { BackOfficePaths } from './paths.ts'
9
+
10
+ /** The key Umbraco's theme context keeps the choice under. */
11
+ export const THEME_STORAGE_KEY = 'umb-theme-alias'
12
+
13
+ export const THEME_DARK = 'Crombolo.Theme.Dark'
14
+ export const THEME_SYSTEM = 'Crombolo.Theme.System'
15
+
16
+ /** Umbraco's own dark, which crombolo's replaces: a browser that chose it gets the new one. */
17
+ const RENAMED: Record<string, string> = { 'umb-dark-theme': THEME_DARK }
18
+
19
+ export const EARLY_THEME_ID = 'crombolo-early-theme'
20
+
21
+ /** Each choice's stylesheet; light, or nothing chosen, needs none beyond the shell's own. */
22
+ export function themeStylesheets(paths: BackOfficePaths): Record<string, string> {
23
+ return {
24
+ [THEME_DARK]: `${paths.pluginPath}/branding/theme-dark.css`,
25
+ [THEME_SYSTEM]: `${paths.pluginPath}/branding/theme-system.css`,
26
+ 'umb-high-contrast-theme': `${paths.assetsPath}/css/high-contrast.css`,
27
+ }
28
+ }
29
+
30
+ /** The inline script, as the shells embed it. It fails quietly: light is a fine fallback. */
31
+ export function earlyThemeScript(paths: BackOfficePaths): string {
32
+ const sheets = JSON.stringify(themeStylesheets(paths))
33
+ const renamed = JSON.stringify(RENAMED)
34
+ return `(function () {
35
+ try {
36
+ var key = ${JSON.stringify(THEME_STORAGE_KEY)};
37
+ var alias = localStorage.getItem(key);
38
+ var renamed = ${renamed};
39
+ if (alias && renamed[alias]) { alias = renamed[alias]; localStorage.setItem(key, alias); }
40
+ var href = alias ? ${sheets}[alias] : undefined;
41
+ if (!href) return;
42
+ var link = document.createElement('link');
43
+ link.id = ${JSON.stringify(EARLY_THEME_ID)};
44
+ link.rel = 'stylesheet';
45
+ link.href = href;
46
+ link.setAttribute('data-theme', alias);
47
+ document.head.appendChild(link);
48
+ } catch (error) {}
49
+ })();`
50
+ }