@scalio-cloud/ui 0.25.0 → 0.26.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 CHANGED
@@ -335,13 +335,14 @@ see `fonts/licenses/`. Icons in `react/icons.js`: Lucide, ISC license, see `lice
335
335
 
336
336
  ## Release
337
337
 
338
- 1. Change the files, add a file `changelog.d/<name>.md` (never edit `CHANGELOG.md` directly), bump `version` in `package.json` (semver; tag without `v`).
338
+ 1. Change the files and bump `version` in `package.json` (semver; tag without `v`).
339
339
  2. Merge to `main`, then release it: in the Scalio Dev cockpit (row Scalio UI, "Freigeben" with the version) or
340
340
  in GitHub (Actions -> publish -> Run workflow on main, or create the tag/release yourself). The `publish`
341
341
  workflow tests and publishes that version to npmjs.com via trusted publishing (no token; npm org
342
342
  `scalio-cloud`, trusted publisher Scalio-Cloud / scalio-ui / publish.yml) and, when started by hand or from
343
- the cockpit, creates the tag and GitHub release with the CHANGELOG lines. Before merging the version bump, run
344
- `python3 tools/changelog.py release <version>` to move `changelog.d/` into `CHANGELOG.md`.
343
+ the cockpit, creates the tag and GitHub release with the CHANGELOG lines. The changelog lives in scalio-docs
344
+ `changelog/scalio-ui/` (ADR 0066): every change adds a file `changelog.d/<name>.md` there, and before the release a
345
+ docs PR runs `python3 tools/changelog.py --repo scalio-ui release <version>`.
345
346
  3. Apps update with `yarn upgrade @scalio-cloud/ui@<version>` in their own PR.
346
347
 
347
348
  `npm test` checks: light/dark variables match, the preset only uses defined variables, all font files exist,
package/nav.css CHANGED
@@ -484,6 +484,19 @@
484
484
  .dark .scalio-roles-yes { color: #4ade80; }
485
485
  .scalio-roles-no { color: hsl(var(--muted-foreground)); }
486
486
  .scalio-roles-lic { margin-left: 0.375rem; }
487
+ .scalio-roles-cell > svg, .scalio-roles-cell > input, .scalio-roles-cell > .scalio-roles-no { display: block; margin-left: auto; margin-right: auto; }
488
+ .scalio-roles-table th.scalio-roles-col.is-mine, .scalio-roles-cell.is-mine { background: hsl(var(--primary) / 0.08); }
489
+ .scalio-roles-table th.scalio-roles-col.is-mine .scalio-roles-name { color: hsl(var(--primary)); }
490
+ .scalio-roles-mine { margin-bottom: 0.25rem; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.15em; color: hsl(var(--primary)); }
491
+ .scalio-tenant-list { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; border: 1px solid hsl(var(--border)); border-radius: 2px; }
492
+ .scalio-tenant-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.625rem 0.875rem; border-bottom: 1px solid hsl(var(--border)); }
493
+ .scalio-tenant-row:last-child { border-bottom: 0; }
494
+ .scalio-tenant-who { min-width: 0; }
495
+ .scalio-tenant-domain { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; font-weight: 600; overflow-wrap: anywhere; }
496
+ .scalio-tenant-id { margin-top: 0.125rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; color: hsl(var(--muted-foreground)); overflow-wrap: anywhere; }
497
+ .scalio-tenant-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
498
+ .scalio-tenant-manual { display: flex; flex-direction: column; gap: 0.5rem; max-width: 28rem; }
499
+ .scalio-btn.is-disabled { pointer-events: none; opacity: 0.5; }
487
500
  .scalio-roles-source { display: flex; flex-direction: column; gap: 0.875rem; padding: 1rem; border: 1px solid hsl(var(--border)); border-radius: 2px; background: hsl(var(--card)); font-size: 0.875rem; }
488
501
 
489
502
  /* Setup wizard (SetupWizard, ADR 0060): design of the Nexus wizard, first built in Kontor */
@@ -493,7 +506,7 @@
493
506
  padding: 1.5rem; border: 1px solid hsl(var(--border)); border-radius: 2px; background: hsl(var(--background)); color: hsl(var(--foreground));
494
507
  box-shadow: 0 16px 40px rgb(0 0 0 / 0.25); font-size: 0.875rem;
495
508
  }
496
- .scalio-wizard.is-wide { max-width: 56rem; }
509
+ .scalio-wizard.is-wide { max-width: 72rem; }
497
510
  .scalio-wizard-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
498
511
  .scalio-wizard-title { margin: 0; font-family: 'Chivo', sans-serif; font-size: 1.5rem; font-weight: 900; line-height: 1.2; text-transform: uppercase; letter-spacing: -0.025em; }
499
512
  .scalio-wizard-subtitle { margin: 0.25rem 0 0; color: hsl(var(--muted-foreground)); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalio-cloud/ui",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Scalio Control Room design tokens, Tailwind preset, self-hosted fonts, the shared app header, sign-in layout, sidebar and page blocks",
5
5
  "license": "SEE LICENSE IN README.md",
6
6
  "private": false,
package/react/labels.js CHANGED
@@ -64,7 +64,7 @@ const LABELS = {
64
64
  graceTip: "Diese Version liegt nach Ihrem Lizenzdatum und wechselt nach der Kulanzzeit in den Nur-Lesen-Modus.",
65
65
  },
66
66
  accountGroups: { people: "Personen", access: "Rechte" },
67
- accountTabs: { users: "Benutzer", roles: "Rollen", entra: "Microsoft Entra", signIn: "Anmeldung", organizations: "Organisationen" },
67
+ accountTabs: { users: "Benutzer", roles: "Rollen", entra: "Rollen aus Entra ID", signIn: "Anmeldung", organizations: "Organisationen" },
68
68
  passwordPolicy: {
69
69
  label: "Anmeldung mit Passwort",
70
70
  local: "Erlaubt",
@@ -460,7 +460,7 @@ const LABELS = {
460
460
  graceTip: "This version is past your license date and turns read-only after the grace period.",
461
461
  },
462
462
  accountGroups: { people: "People", access: "Permissions" },
463
- accountTabs: { users: "Users", roles: "Roles", entra: "Microsoft Entra", signIn: "Sign-in", organizations: "Organizations" },
463
+ accountTabs: { users: "Users", roles: "Roles", entra: "Roles from Entra ID", signIn: "Sign-in", organizations: "Organizations" },
464
464
  passwordPolicy: {
465
465
  label: "Password sign-in",
466
466
  local: "Allowed",
package/react/roles.js CHANGED
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
- // Konten und Rollen -> Rollen and -> Microsoft Entra, the same in every Scalio app (scalio-docs concepts/app-roles.md,
2
+ // Konten und Rollen -> Rollen and -> Rollen aus Entra ID, the same in every Scalio app (scalio-docs concepts/app-roles.md,
3
3
  // roles/R1-R6). rolesApi(api, opts) maps the shared role API of scalio-platform `roles_router` (GET /roles/permissions,
4
4
  // GET/POST /roles, PATCH/DELETE /roles/{id}); RoleAdmin shows "welche Rolle darf was" (permissions as rows, roles as
5
- // columns), built-in roles read-only, own roles with checkboxes, add / copy / rename / delete. RoleSourceSettings is
6
- // the role source (Scalio app or Entra app roles) with the Entra values and, where the app has them, the tenant IDs.
5
+ // columns), built-in roles read-only, own roles with checkboxes, add / copy / rename / delete; the column of the signed-in
6
+ // person's role is marked. RoleSourceSettings is the role source (Scalio app or Entra app roles) with the Entra values
7
+ // and the list of tenants whose roles count, with «Tenant hinzufügen» by admin consent (ADR 0068).
7
8
  const React = require("react");
8
9
  const h = React.createElement;
9
10
  const Icons = require("./icons");
@@ -47,8 +48,26 @@ const TEXT = {
47
48
  sourceEntra: "Microsoft Entra ID (App-Rollen)",
48
49
  hintLocal: "Sie vergeben die Rollen unter Benutzer. Werte für Entra ID, falls Sie später umstellen:",
49
50
  hintEntra: "Ab der nächsten Anmeldung entscheiden die App-Rollen der Unternehmensanwendung «Scalio» für jedes Microsoft-Konto. Wer keine Rolle hat, kann sich nicht mehr anmelden. Eigene Rollen und lokale Konten vergeben Sie weiter hier. Werte:",
50
- tenants: "Microsoft-Tenant-IDs",
51
- tenantsHint: "Nur App-Rollen aus diesen Tenants zählen, durch Komma getrennt.",
51
+ yourRole: "Ihre Rolle",
52
+ tenants: "Vertraute Tenants",
53
+ tenantsHint: "Nur App-Rollen aus diesen Tenants zählen, und Personen aus diesen Tenants können sich anmelden.",
54
+ tenantsEmpty: "Noch kein Tenant. Mit «Tenant hinzufügen» gibt ein Admin des Tenants die Zustimmung für die App «Scalio».",
55
+ tenantAdd: "Tenant hinzufügen",
56
+ tenantAddHint: "Sie melden sich als Admin des Tenants bei Microsoft an und stimmen der App «Scalio» zu.",
57
+ tenantManual: "Von Hand eintragen",
58
+ tenantManualHint: "Nur wenn ein Admin des Tenants der App «Scalio» schon zugestimmt hat.",
59
+ tenantId: "Tenant-ID",
60
+ tenantAddManual: "Eintragen",
61
+ tenantInvalid: "Das ist keine Tenant-ID (GUID).",
62
+ tenantRemove: "Entfernen",
63
+ tenantRemoveTitle: "Tenant «{name}» entfernen?",
64
+ tenantRemoveText: "App-Rollen aus diesem Tenant zählen dann nicht mehr. Die Unternehmensanwendung im Tenant bleibt; entfernen Sie sie dort bei Bedarf selbst.",
65
+ tenantFixed: "Fest",
66
+ tenantAdded: "Tenant hinzugefügt.",
67
+ tenantRemoved: "Tenant entfernt.",
68
+ consentCancelled: "Die Zustimmung wurde abgebrochen.",
69
+ consentFailed: "Die Zustimmung hat nicht geklappt. Melden Sie sich mit einem Konto an, das in diesem Tenant Admin ist.",
70
+ consentForbidden: "Nur Admins dieser App können einen Tenant hinzufügen.",
52
71
  entraConfirm: "Rollen aus Entra ID übernehmen?",
53
72
  entraConfirmText: "Ab der nächsten Anmeldung entscheiden die App-Rollen in Entra ID für jedes Microsoft-Konto. Personen ohne Rolle können sich nicht mehr anmelden.",
54
73
  entraAction: "Entra ID verwenden",
@@ -90,8 +109,26 @@ const TEXT = {
90
109
  sourceEntra: "Microsoft Entra ID (app roles)",
91
110
  hintLocal: "You give roles under Users. Values for Entra ID, in case you switch later:",
92
111
  hintEntra: "From the next sign-in, the app roles of the enterprise application “Scalio” decide for every Microsoft account. People without a role can no longer sign in. Own roles and local accounts are still given here. Values:",
93
- tenants: "Microsoft tenant IDs",
94
- tenantsHint: "Only app roles from these tenants count, separated by commas.",
112
+ yourRole: "Your role",
113
+ tenants: "Trusted tenants",
114
+ tenantsHint: "Only app roles from these tenants count, and people from these tenants can sign in.",
115
+ tenantsEmpty: "No tenant yet. With “Add tenant” an admin of the tenant consents to the app “Scalio”.",
116
+ tenantAdd: "Add tenant",
117
+ tenantAddHint: "You sign in to Microsoft as an admin of the tenant and consent to the app “Scalio”.",
118
+ tenantManual: "Enter by hand",
119
+ tenantManualHint: "Only if an admin of the tenant already consented to the app “Scalio”.",
120
+ tenantId: "Tenant ID",
121
+ tenantAddManual: "Add",
122
+ tenantInvalid: "This is not a tenant ID (GUID).",
123
+ tenantRemove: "Remove",
124
+ tenantRemoveTitle: "Remove the tenant “{name}”?",
125
+ tenantRemoveText: "App roles from this tenant no longer count. The enterprise application stays in the tenant; remove it there yourself if needed.",
126
+ tenantFixed: "Fixed",
127
+ tenantAdded: "Tenant added.",
128
+ tenantRemoved: "Tenant removed.",
129
+ consentCancelled: "The consent was cancelled.",
130
+ consentFailed: "The consent did not work. Sign in with an account that is an admin in this tenant.",
131
+ consentForbidden: "Only admins of this app can add a tenant.",
95
132
  entraConfirm: "Take roles from Entra ID?",
96
133
  entraConfirmText: "From the next sign-in, the app roles in Entra ID decide for every Microsoft account. People without a role can no longer sign in.",
97
134
  entraAction: "Use Entra ID",
@@ -265,9 +302,11 @@ function RoleAdmin(props) {
265
302
  const byGroup = (g) => catalog.permissions.filter((p) => (p.group || "") === g || (!groups.includes(p.group || "") && g === groups[0]));
266
303
  const usersText = (r) => (r.users === null ? "" : r.users === 1 ? L.oneUser : fill(L.users, { n: r.users }));
267
304
 
305
+ const mine = (r) => props.currentRole !== undefined && props.currentRole !== null && String(props.currentRole) === r.id;
268
306
  const head = h("tr", null,
269
307
  h("th", { className: "scalio-roles-perm" }, L.permission),
270
- roles.map((r) => h("th", { key: r.id, className: cx("scalio-roles-col", r.builtin ? "is-builtin" : "is-own"), "data-testid": `role-col-${r.id}` },
308
+ roles.map((r) => h("th", { key: r.id, className: cx("scalio-roles-col", r.builtin ? "is-builtin" : "is-own", mine(r) && "is-mine"), "data-testid": `role-col-${r.id}` },
309
+ mine(r) ? h("div", { className: "scalio-roles-mine", "data-testid": "role-yours" }, L.yourRole) : null,
271
310
  h("div", { className: "scalio-roles-name" }, tr(r.name, lang)),
272
311
  h("div", { className: "scalio-roles-meta" },
273
312
  h("span", null, r.builtin ? L.builtin : L.own),
@@ -282,10 +321,10 @@ function RoleAdmin(props) {
282
321
  const cell = (r, p) => {
283
322
  const has = r.admin || r.permissions.includes(p.key);
284
323
  if (r.builtin || !canEdit) {
285
- return h("td", { key: r.id, className: "scalio-roles-cell", "data-testid": `role-cell-${r.id}-${p.key}` },
324
+ return h("td", { key: r.id, className: cx("scalio-roles-cell", mine(r) && "is-mine"), "data-testid": `role-cell-${r.id}-${p.key}` },
286
325
  has ? h(Icons.Check, { className: "scalio-icon scalio-roles-yes", "aria-label": "✓" }) : h("span", { className: "scalio-roles-no", "aria-label": "–" }, "–"));
287
326
  }
288
- return h("td", { key: r.id, className: "scalio-roles-cell" },
327
+ return h("td", { key: r.id, className: cx("scalio-roles-cell", mine(r) && "is-mine") },
289
328
  h("input", { type: "checkbox", checked: has, disabled: busy, onChange: () => toggle(r, p.key),
290
329
  "aria-label": `${tr(r.name, lang)}: ${tr(p.label, lang)}`, "data-testid": `role-check-${r.id}-${p.key}` }));
291
330
  };
@@ -303,7 +342,7 @@ function RoleAdmin(props) {
303
342
  body.push(h("tr", { key: "admin-only", className: "scalio-roles-group" }, h("td", { colSpan: roles.length + 1 }, L.adminOnly)));
304
343
  body.push(h("tr", { key: "admin-row" },
305
344
  h("td", { className: "scalio-roles-perm" }, L.adminAll),
306
- roles.map((r) => h("td", { key: r.id, className: "scalio-roles-cell" },
345
+ roles.map((r) => h("td", { key: r.id, className: cx("scalio-roles-cell", mine(r) && "is-mine") },
307
346
  r.admin ? h(Icons.Check, { className: "scalio-icon scalio-roles-yes", "aria-label": "✓" }) : h("span", { className: "scalio-roles-no" }, "–")))));
308
347
 
309
348
  const dialogEl = dialog ? h(Dialog, { title: dialog.mode === "add" ? L.addTitle : L.renameTitle, onClose: close, testId: "role-dialog" },
@@ -339,46 +378,128 @@ function RoleAdmin(props) {
339
378
  dialogEl);
340
379
  }
341
380
 
342
- // Props: value {source: "local"|"entra", values: [...], tenants?: [...]}, onSave({source, tenants}) -> Promise,
343
- // tenants (show the tenant field), lang, labels, confirm, notify, formatError, disabled, testId.
381
+ const GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
382
+
383
+ // A tenant entry: "tid" (old lists) or {tid, domain, fixed, label, href}.
384
+ function tenantOf(x) {
385
+ if (typeof x === "string") return { tid: x.trim().toLowerCase(), domain: "" };
386
+ return Object.assign({}, x, { tid: String(x.tid || x.id || "").trim().toLowerCase(), domain: x.domain || "" });
387
+ }
388
+
389
+ // Back from «Tenant hinzufügen»: ?entra_tenant=added|cancelled|failed|forbidden (scalio-identity 0.5.0).
390
+ function consentResult() {
391
+ if (typeof window === "undefined" || !window.location) return "";
392
+ try {
393
+ const url = new URL(window.location.href);
394
+ const res = url.searchParams.get("entra_tenant") || "";
395
+ if (res && window.history && window.history.replaceState) {
396
+ url.searchParams.delete("entra_tenant");
397
+ window.history.replaceState(window.history.state, "", url.pathname + url.search + url.hash);
398
+ }
399
+ return res;
400
+ } catch (e) {
401
+ return "";
402
+ }
403
+ }
404
+
405
+ // Props: value {source: "local"|"entra", values: [...], tenants?: [tid | {tid, domain, fixed}]},
406
+ // onSave({source, tenants: [{tid, domain}]}) -> Promise, tenants (show the tenant list), consentUrl (button «Tenant
407
+ // hinzufügen», e.g. "/api/auth/scalio/consent"), manual (default true: enter a tenant ID by hand), fixed (one fixed
408
+ // tenant, no add or remove: Nexus), extraTenants [{tid, domain, label, href}] and extraTitle (view only: Rosetta's
409
+ // customer tenants), lang, labels, confirm, notify, formatError, disabled, testId.
344
410
  function RoleSourceSettings(props) {
345
411
  const lang = props.lang === "en" ? "en" : "de";
346
412
  const L = rolesText(lang, props.labels);
347
413
  const v = props.value || { source: "local", values: [] };
414
+ const list = (v.tenants || []).map(tenantOf).filter((t) => t.tid);
348
415
  const [source, setSource] = React.useState(v.source || "local");
349
- const [tenants, setTenants] = React.useState((v.tenants || []).join(", "));
416
+ const [manual, setManual] = React.useState(null); // null = closed, else the typed id
350
417
  const [busy, setBusy] = React.useState(false);
351
- React.useEffect(() => { setSource(v.source || "local"); setTenants((v.tenants || []).join(", ")); }, [v.source, (v.tenants || []).join(",")]);
352
- const dirty = source !== (v.source || "local") || (props.tenants && tenants !== (v.tenants || []).join(", "));
353
- const save = async () => {
354
- if (source === "entra" && v.source !== "entra" && !(await ask(props.confirm, L.entraConfirm, L.entraConfirmText, L.entraAction, false))) return;
418
+ const [error, setError] = React.useState("");
419
+ const say = (text, tone) => props.notify && props.notify(text, tone || "success");
420
+ const fmt = props.formatError || errorText;
421
+ React.useEffect(() => { setSource(v.source || "local"); }, [v.source]);
422
+ React.useEffect(() => {
423
+ const res = consentResult();
424
+ if (res === "added") say(L.tenantAdded);
425
+ else if (res === "cancelled") say(L.consentCancelled, "error");
426
+ else if (res === "forbidden") say(L.consentForbidden, "error");
427
+ else if (res) say(L.consentFailed, "error");
428
+ }, []);
429
+ const dirty = source !== (v.source || "local");
430
+ const store = async (src, tenants, okText) => {
355
431
  setBusy(true);
432
+ setError("");
356
433
  try {
357
- await props.onSave({ source, tenants: tenants.split(/[\s,;]+/).filter(Boolean) });
358
- if (props.notify) props.notify(L.saved, "success");
434
+ await props.onSave({ source: src, tenants: tenants.map((t) => ({ tid: t.tid, domain: t.domain || "" })) });
435
+ say(okText || L.saved);
436
+ return true;
359
437
  } catch (e) {
360
- if (props.notify) props.notify((props.formatError || errorText)(e), "error");
438
+ say(fmt(e), "error");
439
+ return false;
361
440
  } finally {
362
441
  setBusy(false);
363
442
  }
364
443
  };
444
+ const save = async () => {
445
+ if (source === "entra" && v.source !== "entra" && !(await ask(props.confirm, L.entraConfirm, L.entraConfirmText, L.entraAction, false))) return;
446
+ store(source, list.filter((t) => !t.fixed));
447
+ };
448
+ const remove = async (t) => {
449
+ if (!(await ask(props.confirm, fill(L.tenantRemoveTitle, { name: t.domain || t.tid }), L.tenantRemoveText, L.tenantRemove))) return;
450
+ store(v.source || "local", list.filter((x) => !x.fixed && x.tid !== t.tid), L.tenantRemoved);
451
+ };
452
+ const addManual = async (e) => {
453
+ e.preventDefault();
454
+ const tid = (manual || "").trim().toLowerCase();
455
+ if (!GUID.test(tid)) { setError(L.tenantInvalid); return; }
456
+ if (list.some((t) => t.tid === tid)) { setManual(null); return; }
457
+ if (await store(v.source || "local", [...list.filter((t) => !t.fixed), { tid, domain: "" }], L.tenantAdded)) setManual(null);
458
+ };
365
459
  const values = (v.values || []).map((x) => (typeof x === "string" ? x : x.value || x.name)).filter(Boolean);
460
+ const off = props.disabled || busy;
461
+ const row = (t, extra) => h("li", { key: t.tid, className: "scalio-tenant-row", "data-testid": `tenant-${t.tid}` },
462
+ h("div", { className: "scalio-tenant-who" },
463
+ h("div", { className: "scalio-tenant-domain" }, t.href ? h("a", { href: t.href }, t.domain || t.label || t.tid) : (t.domain || t.label || t.tid),
464
+ t.label && t.domain ? h("span", { className: "scalio-badge" }, t.label) : null),
465
+ t.domain || t.label ? h("div", { className: "scalio-tenant-id" }, t.tid) : null),
466
+ extra ? null : t.fixed || props.fixed
467
+ ? h("span", { className: "scalio-badge" }, L.tenantFixed)
468
+ : h("button", { type: "button", className: "scalio-btn is-ghost scalio-roles-tool is-danger", onClick: () => remove(t), disabled: off, "data-testid": `tenant-remove-${t.tid}` }, L.tenantRemove));
469
+ const tenantBlock = props.tenants ? h("div", { className: "scalio-users-field", "data-testid": "role-source-tenants" },
470
+ h("div", { className: "scalio-field-label" }, L.tenants),
471
+ list.length
472
+ ? h("ul", { className: "scalio-tenant-list" }, list.map((t) => row(t, false)))
473
+ : h("p", { className: "scalio-users-hint", "data-testid": "tenants-empty" }, L.tenantsEmpty),
474
+ h("p", { className: "scalio-users-hint" }, L.tenantsHint),
475
+ props.fixed ? null : h("div", { className: "scalio-tenant-actions" },
476
+ props.consentUrl ? h("a", { className: cx("scalio-btn is-primary", off && "is-disabled"), href: props.consentUrl, title: L.tenantAddHint, "data-testid": "tenant-add" },
477
+ h(Icons.Plus, { className: "scalio-icon" }), L.tenantAdd) : null,
478
+ props.manual !== false && manual === null ? h("button", { type: "button", className: "scalio-btn", onClick: () => { setError(""); setManual(""); }, disabled: off, "data-testid": "tenant-manual" }, L.tenantManual) : null),
479
+ manual !== null ? h("form", { className: "scalio-tenant-manual", onSubmit: addManual },
480
+ h("label", { className: "scalio-field-label", htmlFor: "tenant-manual-id" }, L.tenantId),
481
+ h("input", { id: "tenant-manual-id", className: "scalio-input", value: manual, autoFocus: true, placeholder: "00000000-0000-0000-0000-000000000000",
482
+ onChange: (e) => setManual(e.target.value), disabled: off, "data-testid": "tenant-manual-id" }),
483
+ h("p", { className: "scalio-users-hint" }, L.tenantManualHint),
484
+ error ? h("p", { className: "scalio-notice is-error", role: "alert", "data-testid": "tenant-manual-error" }, error) : null,
485
+ h("div", { className: "scalio-tenant-actions" },
486
+ h("button", { type: "button", className: "scalio-btn is-ghost", onClick: () => setManual(null) }, L.cancel),
487
+ h("button", { type: "submit", className: "scalio-btn is-primary", disabled: off || !manual.trim(), "data-testid": "tenant-manual-save" }, L.tenantAddManual))) : null,
488
+ props.extraTenants && props.extraTenants.length ? h("div", { className: "scalio-users-field", "data-testid": "role-source-extra" },
489
+ props.extraTitle ? h("div", { className: "scalio-field-label" }, props.extraTitle) : null,
490
+ h("ul", { className: "scalio-tenant-list" }, props.extraTenants.map(tenantOf).map((t) => row(t, true)))) : null) : null;
366
491
  return h("div", { className: "scalio-roles-source", "data-testid": props.testId || "role-source" },
367
492
  h("div", { className: "scalio-users-field" },
368
493
  h("label", { className: "scalio-field-label", htmlFor: "role-source-select" }, L.source),
369
- h("select", { id: "role-source-select", className: "scalio-input scalio-users-select", value: source, disabled: props.disabled || busy,
370
- onChange: (e) => setSource(e.target.value), "data-testid": "role-source-select" },
371
- h("option", { value: "local" }, L.sourceLocal),
372
- h("option", { value: "entra" }, L.sourceEntra))),
373
- props.tenants ? h("div", { className: "scalio-users-field" },
374
- h("label", { className: "scalio-field-label", htmlFor: "role-source-tenants" }, L.tenants),
375
- h("input", { id: "role-source-tenants", className: "scalio-input", value: tenants, disabled: props.disabled || busy,
376
- onChange: (e) => setTenants(e.target.value), "data-testid": "role-source-tenants" }),
377
- h("p", { className: "scalio-users-hint" }, L.tenantsHint)) : null,
494
+ h("div", { className: "scalio-tenant-actions" },
495
+ h("select", { id: "role-source-select", className: "scalio-input scalio-users-select", value: source, disabled: off,
496
+ onChange: (e) => setSource(e.target.value), "data-testid": "role-source-select" },
497
+ h("option", { value: "local" }, L.sourceLocal),
498
+ h("option", { value: "entra" }, L.sourceEntra)),
499
+ h("button", { type: "button", className: "scalio-btn is-primary", onClick: save, disabled: off || !dirty, "data-testid": "role-source-save" }, L.save))),
378
500
  h("p", { className: "scalio-users-hint", "data-testid": "role-source-hint" }, source === "entra" ? L.hintEntra : L.hintLocal, " ",
379
501
  values.map((x, i) => h(React.Fragment, { key: x }, i ? ", " : "", h("code", null, x)))),
380
- h("div", null,
381
- h("button", { type: "button", className: "scalio-btn is-primary", onClick: save, disabled: props.disabled || busy || !dirty, "data-testid": "role-source-save" }, L.save)));
502
+ tenantBlock);
382
503
  }
383
504
 
384
505
  module.exports = { RoleAdmin, RoleSourceSettings, rolesApi, rolesText };
package/CHANGELOG.md DELETED
@@ -1,266 +0,0 @@
1
- # Changelog
2
-
3
- ## 0.25.0
4
- Rollen, Microsoft Entra und eigene Rollen in allen Apps angleichen (scalio-docs `concepts/app-roles.md`, `roles/R1`–`R6`).
5
- - New `RoleAdmin` for Konten und Rollen → Rollen: table «welche Rolle darf was» (permissions as rows, grouped; roles as
6
- columns with number of accounts and a license mark), built-in roles read-only, own roles with checkboxes, «Rolle
7
- anlegen» (empty or with the permissions of another role), copy, rename, delete (the app refuses roles that are still
8
- given). A row «Nur für Administratoren» shows what only the administrator may do.
9
- - `rolesApi(api, {base, permissions})` for the shared role API of scalio-platform 0.3.0 `roles_router`
10
- (`GET /roles/permissions`, `GET/POST /roles`, `PATCH/DELETE /roles/{id}`; Nexus passes `permissions: "/permissions"`).
11
- Texts of permissions and roles may be strings or `{de, en}`.
12
- - New `RoleSourceSettings` for Konten und Rollen → Microsoft Entra: role source (this app or Entra app roles), the
13
- Entra values, optional tenant IDs, confirmation before switching to Entra.
14
- - Styles `.scalio-roles*` in nav.css. No token changes, nothing changes until an app uses the new components.
15
- - `UserAdmin` (follow-up to 0.23.0, built then but not released): badge «Eingeladen» for open invitations (`invited`
16
- on an account, `invite_only` for a row that is only an invitation; such a row offers only «Einladung zurückziehen»,
17
- which calls `DELETE /users/{id}`); when the invitation mail could not be sent, the app returns `invite_link` and
18
- `UserAdmin` shows the link once; badges below the address, actions wrap, last sign-in only on wide screens,
19
- disabled fields greyed.
20
-
21
- ## 0.24.0
22
- Einrichtungsassistent als Standard für alle Apps (scalio-docs ADR 0060, contract `interfaces/setup-wizard.md`).
23
- - New `SetupWizard`: the first-step dialog of Nexus and Kontor as a shared shell. Steps come as configuration
24
- (`id`, `label`, `title`, `description`, `content`, `onNext`, `canNext`, `optional`); stepper on top, «Schritt n von
25
- m», Zurück/Weiter, «Überspringen» for optional steps, errors of `onNext` keep the step open, short success message
26
- at the end. Without `onClose` it cannot be closed; `wide` for steps with tables. Texts DE/EN in `labels.wizard`.
27
- - `setupWizardApi(api, {base})` for `GET/PUT /api/setup/wizard` (reads snake_case and camelCase), `useSetupWizard()`
28
- (loads the state, opens once by itself at the saved step while the setup is needed, saves each step, completes),
29
- `openSetupWizard()` and `SetupWizardButton` for Einstellungen.
30
- - Styles `.scalio-wizard*` in nav.css. No token changes, nothing changes until an app uses the wizard.
31
-
32
- ## 0.23.0
33
- Benutzerverwaltung in allen Apps angleichen (scalio-docs ADR 0059, `users/B1`–`B6`, contract `interfaces/app-users.md`).
34
- - New `UserAdmin` for Konten und Rollen → Benutzer, the same in every app: list with search and badges (Microsoft,
35
- Passwort, Rolle aus Entra, Notfall-Administrator, Gesperrt, Wartet auf Freigabe), «Benutzer hinzufügen» (Microsoft,
36
- Passwort with a start password shown once, or invitation by e-mail), «Bearbeiten» (name and e-mail only for accounts
37
- without Microsoft, role, access, lock, emergency administrator) with the actions new start password, remove password,
38
- reset second factor, unlink Microsoft and delete; waiting accounts: role, access, «Freischalten», «Zusammenführen».
39
- The app passes roles and an optional access column/field (customers, organisation).
40
- - `usersApi(api, {base})` for the shared routes (`/users`, Nexus `/admins`), reads snake_case and camelCase.
41
- - `MicrosoftConnection` for the profile section «Verknüpfungen» (link or unlink one's own Microsoft account).
42
- - New icons `Pencil`, `Link2`; modal dialog styles (`.scalio-dialog*`). No token changes, nothing changes until an app
43
- uses the new components.
44
-
45
- ## 0.22.0
46
- Login, Navigation und Banner vereinheitlichen (scalio-docs UISPEC section 3, ADR 0042 Nachtrag 2026-11-01).
47
- - **Notice in the look of the test mode banner:** thin frame in the tone, 3px bar on the left, tinted background,
48
- icon in the tone, text in the normal text colour (also for errors). Colours: red = `error` (Fehler), orange = `warn`
49
- (wichtig), green = `ok` (gute Meldung), blue = `info` (Aktion). New aliases `important`, `success`, `action`
50
- (`noticeKind`). Banners keep the full frame; stacked banners share one line. Dark mode has its own tones.
51
- - `SelfTestBanner` is now a `Notice` banner (kind warn) and takes an optional `action` (e.g. link to «Tests»).
52
- - **Behaviour change:** `normalizeTheme()` returns `system` when nothing (or something unknown) is stored, so every
53
- app starts like the device on the first visit and on the login page (before: `light`).
54
- - New `initialLang(stored, navLang)` / `browserLang()`: stored choice wins, otherwise German for a German browser and
55
- English for everything else, the same in every app.
56
- - **Visible change:** every `Notice` gets the new frame and colours (info notices turn from Klein blue to a lighter
57
- blue, error text is no longer red, only bar and icon). No token changes.
58
-
59
- ## 0.21.0
60
- Testmodus (scalio-docs ADR 0053, package scalio-selftest).
61
- - `SelfTestPanel` for the page «Tests»: last runs with verdict, scope, source, version and counts; details per
62
- section; «Neu fehlgeschlagen» and «Wieder in Ordnung» since the run before; «Jetzt ausführen» and «Abbrechen» with
63
- live progress; schedule (scope and hour of the nightly run). `selftestApi(api)` maps the routes `/e2e/*`.
64
- - `SelfTestBanner`: hint «Testmodus (SAFE)» while the test mode is on.
65
- - CSS in `nav.css` (`.scalio-selftest*`).
66
- - **Visible change:** only in apps that show the new page. No token changes.
67
-
68
- ## 0.20.0
69
- Scalio Ideen (ADR 0050): feature requests from the apps, votes and comments, kept centrally in Compass.
70
- - `supportApi(api, { ideas: true })` adds the routes of scalio-connect 0.8.0 (`ideas_router`). The support drawer then
71
- gets a third tab «Ideen»: ideas of this app (most votes, newest, done), vote and take back, «Meine», idea page with
72
- the answer from Scalio and comments, and «Idee vorschlagen» with similar ideas shown while typing.
73
- - With ideas on, the ticket category «Wunsch» leaves the ticket form. A self-hosted installation without a connection
74
- shows a link to the public page (`<portal>/ideen`), also in the link-only support view.
75
- - New export `IdeasPanel`; CSS in `nav.css` (`.scalio-idea-*`, `.scalio-ideas*`).
76
- - **Visible change:** only in apps that pass `{ ideas: true }`. No token changes.
77
-
78
- ## 0.19.0
79
- Settings card "Scalio Beacon" for the apps (Beacon phase 3, scalio-docs `concepts/beacon-suite.md`).
80
- - `BeaconConnection` with `peersApi(api)`: pair the installation with a Beacon instance (address + one-time code from
81
- Beacon), show the pairing (address, version, last contact, queue), test it and disconnect. Routes from
82
- scalio-connect 0.8.0 (`scalio_connect.peers.web.admin_router`).
83
- - Labels `beacon*` in German and English.
84
- - **Visible change:** none until an app shows the card. No token changes.
85
-
86
- ## 0.18.1
87
- Page actions after Markus' feedback (2026-10-31, UISPEC section 2 "Page actions"). 0.18.0 was released before these
88
- changes were merged; apps use 0.18.1.
89
- - The slot is shown only when a page has actions and pushes the menu down; no reserved room, no separator line.
90
- - 0.25rem above the buttons, 0.875rem from the last button to the next section heading (half the usual gap).
91
- - Only Übersicht (first item of a first section without heading) is pinned; further items of that section stay in
92
- the menu (Pulse: Geräte).
93
- - Buttons in the slot: same height (2.25rem), one line, long labels end with "…". Labels without "Neu" (README).
94
- - **Visible change:** only in apps that set `pageActions`. No token changes.
95
-
96
- ## 0.18.0
97
- Page actions in the sidebar, first version (Markus 2026-10-31, reference Scalio Kontor; scalio-docs ADR 0042 Nachtrag).
98
- - New `react/actions.js`: `PageActions` puts a page's actions (Neu …, Import, Zurück) into a fixed slot in the
99
- sidebar right below Übersicht from 768px; on phones they stay in the page header. `ActionSlot`, `useDesktop()`.
100
- - `Sidebar` gets `pageActions`: Übersicht pinned on top, then the slot (room for two buttons, line below), then the
101
- sections. `shell.css`: an empty `.scalio-page-actions` is hidden. `react-dom` is an optional peer dependency.
102
- - **Visible change:** none until an app sets `pageActions`. No token changes.
103
-
104
- ## 0.17.0
105
- Scalio Pulse is built (scalio-docs ADR 0043, P1 started 2026-10-31).
106
- - Product switcher: "Pulse" is no longer marked "bald"; it behaves like the other apps (link from `SCALIO_APP_LINKS`).
107
- - `manualUrl("pulse")`: docs.scaliopulse.com / devdocs.scaliopulse.com.
108
- - **Visible change:** after the upgrade, Pulse in the switcher loses the "bald" badge and is greyed out until the
109
- instance has a link for it. No token changes.
110
-
111
- ## 0.16.0
112
- Same profile page in every app, reference Scalio Nexus (Markus 2026-10-31, scalio-docs ADR 0042 Nachtrag).
113
- - New `react/profile.js`: `ProfileIdentity` (letters, name, email · role), `ThemeChoice` (Hell, Dunkel, System as
114
- option cards), `LanguageChoice` (flags, `fixed` for an instance-wide language), `TimezoneChoice` (searchable
115
- list, "Automatisch" = browser), `PasswordChange` (current, new, confirm; mismatch check, error from the API).
116
- - Helpers: `setUserTimezone`, `currentTz`, `browserTz`, `listTimezones`, `tzOffset` (apps format dates with
117
- `currentTz()`), `applyTheme(mode)` / `resolveTheme` / `normalizeTheme` for light, dark and system.
118
- - Icons `Globe`, `Clock`; labels `profile_` (DE/EN). Default security hint now names authenticator app and passkeys.
119
- - **Visible change:** none until an app uses the new parts. No token changes.
120
-
121
- ## 0.15.0
122
- New app Scalio Beacon (service desk, scalio-docs ADR 0044).
123
- - Product switcher: entry "Beacon" (icon `RadioTower`) between Citadel and Pulse, available like the others.
124
- - `manualUrl("beacon")`: docs.scaliobeacon.com / devdocs.scaliobeacon.com.
125
- - **Visible change:** every app shows "Beacon" in the switcher after its upgrade (greyed out until the instance
126
- has a link in `SCALIO_APP_LINKS`). No token changes.
127
-
128
- ## 0.14.0
129
- UI alignment round 1, follow-up (scalio-docs ADR 0042, Markus 2026-10-30).
130
- - `accountGroups({ lang, base, show })` and `accountArea(part)`: Konten und Rollen in the structure of the settings
131
- page (groups Personen, Rechte, Sicherheit; one address per area, `/accounts/<area>`).
132
- - `SettingsLayout` takes `root` (e.g. "Konten und Rollen") for the crumb, title fallback and nav label.
133
- - `SubNav` with `value` and `onSelect` renders in-page tabs (buttons, `role="tablist"`).
134
- - New CSS classes `.scalio-tabs` / `.scalio-tab` in `nav.css`: in-page tabs (shadcn Tabs) look like the sub
135
- navigation (underline, like Nexus "Gruppen").
136
- - **Visible change:** none until an app uses the new parts. No token changes.
137
-
138
- ## 0.13.0
139
- UI alignment round 1 (scalio-docs ADR 0041).
140
- - `editionBadges(license, { admin, demo, lang })` and `EditionBadges`: edition badge next to the product name from the
141
- scalio-license state (Free, plan, trial with days left, demo; read-only or grace for administrators). `Brand`
142
- takes `brand.badges` and shows them on the name line.
143
- - `adminSection({ audit, accounts, settings })`: the sidebar section "Verwaltung" with fixed order, words and icons
144
- (new inline icons `ScrollText`, `Users`, `Settings`, `Info`).
145
- - `ApiStatus` / `useApiStatus(check)`: nothing while the API answers, a red badge "API nicht erreichbar" when not.
146
- - `Notice` gets an icon, `action`, `onDismiss` ("Später") and `banner` (full width above the page).
147
- - `accountTabs()` (Benutzer, Rollen, Microsoft Entra, Anmeldung, Organisationen), `PasswordPolicySelect` and
148
- `policyAllowed()`: "nur Notfall-Administrator" and "aus" stay disabled until an emergency administrator exists.
149
- - **Visible change:** existing `Notice` boxes get an icon in front and lay out as a row. Everything else only where
150
- an app uses the new parts. No token changes.
151
-
152
- ## 0.12.0
153
- - New manual button in `AppHeader` (prop `docsUrl`): a book icon between theme and support that opens the product
154
- manual in a new tab; `DocsButton` on its own, `manualUrl(product, { stage, lang })` builds the address
155
- (`devdocs.<domain>` on dev and test, `docs.<domain>` on production, demo and self-hosted, German under `/de/`).
156
- **Visible change** only in apps that pass `docsUrl`; no token changes.
157
-
158
- ## 0.11.0
159
- - Self-hosted support (ADR 0022, T11; needs scalio-connect 0.7.0 in the backend): `supportApi` gets `status()` and
160
- `setButtonHidden(hidden)`. The support drawer asks `status()` first; a self-hosted installation without a
161
- connection shows no form, only a note and the button "Zum Scalio-Portal" (`<portal>/support`). The card
162
- "Support-Verbindung" gets the switch "Support-Knopf in der Kopfzeile ausblenden". **Visible change** only where
163
- the backend reports `link_only` or the client has `setButtonHidden`; no token changes.
164
-
165
- ## 0.10.0
166
- - `LicensePanel` takes more from an app's own `map`: a `hint` per usage bar (e.g. base + add-on seats), add-ons with
167
- `badges`, `detail`, `tone` (`info`/`warn`) and `removable: false`, `addonErrors` (invalid add-ons, removable),
168
- `addonsHidden`, `addonsReadOnly` and `keyReadOnly` (a note instead of the form) and a `copied` text per row.
169
- Lets Nexus keep its trials, demo mode and badges on the shared panel. **No visible change** for apps that use
170
- `mapScalioLicense`.
171
-
172
- ## 0.9.0
173
- - New `CommandPalette` (⌘K / Strg+K): finds every page of the sidebar data (`nav`) plus extra `pages`, remembers the
174
- last visited pages, and takes app commands as `groups` (Nexus: AI and guarded actions). Arrow keys, Enter, Esc,
175
- no dialog library. Helpers `openCommandPalette()` (for the sidebar search button), `paletteShortcut(lang)`,
176
- `paletteGroups`, `palettePages`. `LABELS` get the palette texts, `Icons` get History and CornerDownLeft.
177
- **No visible change** until an app uses it; Nexus keeps its test ids (`command-palette-input`, `cmd-nav-*`).
178
-
179
- ## 0.8.1
180
- - `SupportConnection` fills in the portal address of the instance's stage (`default_portal` from scalio-connect
181
- 0.5.0: dev -> portaldev, test -> portaltest, others -> portal.scaliocloud.com). **Visible change:** the address
182
- field is no longer empty on the settings card. No change with older backends.
183
-
184
- ## 0.8.0
185
- - New `SecurityFactors` (profile: authenticator app, backup codes, passkeys) with `securityApi(api, webauthn)`, and
186
- `LicensePanel` (status, usage, add-ons, key) with `licenseApi(api)` and `mapScalioLicense`. They replace the copies
187
- in Citadel, Foundry, Rosetta and Nexus. `LABELS` get `mfa` and `license` texts, `Icons` get Smartphone, KeyRound,
188
- Trash, Copy, Puzzle, Plus, Alert, and `.scalio-btn` gets the variants `is-primary`, `is-ghost`, `is-danger`.
189
- **No visible change** until an app uses the new blocks; then they look the same in every app.
190
-
191
- ## 0.7.0
192
- - `SupportDrawer` takes `client` (from the new `supportApi(api)`) and `page`: tabs "Neue Anfrage" (category,
193
- subject, description, technical details) and "Meine Tickets" (list, answers from support, reply, rating) for the
194
- central support desk (ADR 0022). New settings card `SupportConnection` (portal address and one-time code, status,
195
- check, disconnect). `LABELS` get the support texts. **Visible change only for apps that pass `client`**; without
196
- it the drawer keeps its single form.
197
-
198
- ## 0.6.2
199
- - **Visible change for apps using `SettingsLayout`:** the settings area uses the full width of the window (was at most
200
- 64rem), so wide screens show more cards side by side. No change for other apps.
201
-
202
- ## 0.6.1
203
- - **Visible change for apps using `SettingsLayout`:** the settings page has no outer padding of its own any more;
204
- the app's content area brings it (0.6.0 had both, so the page got a double margin). No change for other apps.
205
-
206
- ## 0.6.0
207
- - New `SettingsLayout` and `SettingsSection` for the settings page (UISPEC "Einstellungen"): sub-navigation on
208
- the left in fixed groups (Allgemein, app groups, Verbindungen, Benachrichtigungen, Sicherheit, System) with a
209
- search, a select on phones, crumb and title of the area; areas look like the profile page. Below three areas
210
- the sub-navigation is left out. `LABELS` gets the group names. No change for apps that do not use them.
211
-
212
- ## 0.5.2
213
- - Navigation items take `match` (further paths that mark the item active), for one menu item over several pages.
214
- - New `SubNav` (tabs as links on top of a page) and `WorkspaceSwitcher` (Identity / HR in Nexus). No change for
215
- apps that do not use them.
216
-
217
- ## 0.5.1
218
- - Phones: the context select (tenant, customer) is hidden in the header below 640px, there is no room next to
219
- the header buttons. `AppShell` takes `drawerTop` and shows it above the navigation in the phone drawer.
220
-
221
- ## 0.5.0
222
- - **Visible change:** `rounded-sm` is now 2px as UISPEC says (was 0px from the preset). Buttons, fields, cards and
223
- badges in Nexus, Citadel and Foundry get barely visible round corners; Rosetta already had 2px and can drop its
224
- own `borderRadius` override.
225
- - New app frame in `@scalio-cloud/ui/react`: `AppShell`, `Sidebar` (navigation as data: sections, hidden items,
226
- badges, search button, one main action, version), `NavDrawer` for phones, `ContextSelect` for tenant, customer or
227
- organisation in the header.
228
- - Moved here from the apps: `ProductSwitcher` with `PRODUCTS`, `switcherHref`, `stageBadge`, `scalioFeatures`,
229
- `useScalioApps`, and `SupportDrawer` (the app passes the send function). Same test ids as before.
230
- - Page blocks: `KpiTile`, `EmptyState`, `Loading`, `Notice`, `StatusBadge`; `PageHeader` takes `back`.
231
- - `LABELS` gets the shared navigation words (Übersicht, Verwaltung, Protokoll, Konten und Rollen, Einstellungen, ...).
232
- - New `nav.css` (imported by `index.css`). No other visual change for apps that do not use the new parts.
233
-
234
- ## 0.4.1
235
- - No change to tokens, styles or components. CI and publish workflow on current action versions (past Node 20).
236
-
237
- ## 0.4.0
238
- - New sign-in building blocks in `@scalio-cloud/ui/react`: `AuthLayout` (app brand on a dark grid on the left, form
239
- on the right, language and optional theme top right, small brand above the form on phones), `AuthHeader`,
240
- `MicrosoftButton`, `AuthDivider` ("oder mit E-Mail"), `AuthMessage`.
241
- - New `ProfileSection` and `PROFILE_SECTIONS` for the profile page (Konto, Darstellung, Sprache und Region,
242
- Sicherheit, Verknüpfungen), title and hint on the left, content in a card on the right.
243
- - More shared words in `LABELS` (Anmelden, Passwort vergessen?, Passwort ändern, Zwei-Faktor-Anmeldung, ...).
244
- - New `auth.css` (imported by `index.css`). No visual change for apps that do not use the new components.
245
-
246
- ## 0.3.0
247
- - New `@scalio-cloud/ui/react`: shared app header (`AppHeader` with status, language, theme, support and user menu
248
- in a fixed order), `UserMenu`, `LanguageToggle`, `ThemeToggle`, `SupportButton`, `PageHeader`, `VersionTag`.
249
- Only peer dependency is React (optional for apps that use just the tokens).
250
- - New `shell.css` (imported by `index.css`): styles for these parts with `scalio-*` classes. No visual change for
251
- apps that do not use the components.
252
-
253
- ## 0.2.0
254
- - Dark mode colours from Scalio Rosetta: sidebar and other `bg-muted` areas are now darker than the page
255
- (before: lighter grey), primary blue in dark mode `223 100% 45%` (before: brighter `223 85% 62%`), card,
256
- border and input slightly adjusted. Light mode unchanged.
257
-
258
- ## 0.1.2
259
- - Published via npm trusted publishing (GitHub Actions OIDC), no npm token. Content unchanged.
260
-
261
- ## 0.1.1
262
- - Published publicly on npmjs.com instead of GitHub Packages (no token needed to install). Content unchanged.
263
-
264
- ## 0.1.0
265
- - First version: Control Room tokens (light/dark, taken 1:1 from Scalio Nexus), Tailwind preset, self-hosted
266
- fonts Chivo / IBM Plex Sans / JetBrains Mono. Compiled against Nexus it gives the same CSS as Nexus's own config.