@scalio-cloud/ui 0.13.0 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.0
4
+ New app Scalio Beacon (service desk, scalio-docs ADR 0043).
5
+ - Product switcher: entry "Beacon" (icon `RadioTower`) between Citadel and Pulse, available like the others.
6
+ - `manualUrl("beacon")`: docs.scaliobeacon.com / devdocs.scaliobeacon.com.
7
+ - **Visible change:** every app shows "Beacon" in the switcher after its upgrade (greyed out until the instance
8
+ has a link in `SCALIO_APP_LINKS`). No token changes.
9
+
10
+ ## 0.14.0
11
+ UI alignment round 1, follow-up (scalio-docs ADR 0042, Markus 2026-10-30).
12
+ - `accountGroups({ lang, base, show })` and `accountArea(part)`: Konten und Rollen in the structure of the settings
13
+ page (groups Personen, Rechte, Sicherheit; one address per area, `/accounts/<area>`).
14
+ - `SettingsLayout` takes `root` (e.g. "Konten und Rollen") for the crumb, title fallback and nav label.
15
+ - `SubNav` with `value` and `onSelect` renders in-page tabs (buttons, `role="tablist"`).
16
+ - New CSS classes `.scalio-tabs` / `.scalio-tab` in `nav.css`: in-page tabs (shadcn Tabs) look like the sub
17
+ navigation (underline, like Nexus "Gruppen").
18
+ - **Visible change:** none until an app uses the new parts. No token changes.
19
+
3
20
  ## 0.13.0
4
21
  UI alignment round 1 (scalio-docs ADR 0041).
5
22
  - `editionBadges(license, { admin, demo, lang })` and `EditionBadges`: edition badge next to the product name from the
package/nav.css CHANGED
@@ -48,6 +48,17 @@
48
48
  .scalio-subnav-item:hover { color: hsl(var(--foreground)); }
49
49
  .scalio-subnav-item.is-active { border-bottom-color: hsl(var(--primary)); color: hsl(var(--foreground)); font-weight: 600; }
50
50
  .scalio-subnav-item:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: -2px; }
51
+ /* In-page tabs (shadcn/Radix Tabs in the apps: TabsList = .scalio-tabs, TabsTrigger = .scalio-tab) look like the sub navigation. */
52
+ .scalio-tabs { display: flex; align-items: flex-end; gap: 0.25rem; border-bottom: 1px solid hsl(var(--border)); overflow-x: auto; background: transparent; }
53
+ .scalio-tab {
54
+ display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; margin-bottom: -1px; white-space: nowrap; flex-shrink: 0;
55
+ border: 0; border-bottom: 2px solid transparent; border-radius: 0; background: transparent; cursor: pointer;
56
+ font: inherit; font-size: 0.875rem; color: hsl(var(--muted-foreground));
57
+ }
58
+ .scalio-tab:hover { color: hsl(var(--foreground)); }
59
+ .scalio-tab[data-state="active"], .scalio-tab.is-active { border-bottom-color: hsl(var(--primary)); color: hsl(var(--foreground)); font-weight: 600; }
60
+ .scalio-tab:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: -2px; }
61
+ .scalio-tab:disabled { opacity: 0.5; pointer-events: none; }
51
62
  .scalio-nav-icon { width: 1rem; height: 1rem; flex-shrink: 0; }
52
63
  .scalio-nav-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
53
64
  .scalio-nav-badge {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalio-cloud/ui",
3
- "version": "0.13.0",
3
+ "version": "0.15.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/icons.js CHANGED
@@ -113,8 +113,18 @@ const ScrollText = icon([
113
113
  ["path", { d: "M15 12h-5M15 8h-5M19 17V5a2 2 0 0 0-2-2H4" }],
114
114
  ["path", { d: "M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3" }],
115
115
  ]);
116
+ // lucide "radio-tower": Scalio Beacon
117
+ const RadioTower = icon([
118
+ ["path", { d: "M4.9 16.1C1 12.2 1 5.8 4.9 1.9" }],
119
+ ["path", { d: "M7.8 4.7a6.14 6.14 0 0 0-.8 7.5" }],
120
+ ["circle", { cx: 12, cy: 9, r: 2 }],
121
+ ["path", { d: "M16.2 4.8c2 2 2.26 5.11.8 7.47" }],
122
+ ["path", { d: "M19.1 1.9a9.96 9.96 0 0 1 0 14.1" }],
123
+ ["path", { d: "M9.5 18h5" }],
124
+ ["path", { d: "m8 22 4-11 4 11" }],
125
+ ]);
116
126
  const Info = icon([["circle", { cx: 12, cy: 12, r: 10 }], ["path", { d: "M12 16v-4M12 8h.01" }]]);
117
127
 
118
128
  module.exports = { Sun, Moon, Monitor, LifeBuoy, BookOpen, LogOut, Menu, Check, X, Search, ChevronDown, Fingerprint, FileStack, ShieldCheck, Hammer, Activity,
119
129
  Smartphone, KeyRound, Trash, Copy, Puzzle, Plus, Alert, History, CornerDownLeft,
120
- Users, Settings, ScrollText, Info };
130
+ Users, Settings, ScrollText, Info, RadioTower };
package/react/index.js CHANGED
@@ -241,6 +241,7 @@ const DOCS_DOMAINS = {
241
241
  citadel: "scaliocitadel.com",
242
242
  foundry: "scaliofoundry.com",
243
243
  rosetta: "scaliorosetta.com",
244
+ beacon: "scaliobeacon.com",
244
245
  };
245
246
 
246
247
  function manualUrl(product, opts) {
package/react/labels.js CHANGED
@@ -50,6 +50,7 @@ const LABELS = {
50
50
  graceDays: "Kulanz {n} T",
51
51
  graceTip: "Diese Version liegt nach Ihrem Lizenzdatum und wechselt nach der Kulanzzeit in den Nur-Lesen-Modus.",
52
52
  },
53
+ accountGroups: { people: "Personen", access: "Rechte" },
53
54
  accountTabs: { users: "Benutzer", roles: "Rollen", entra: "Microsoft Entra", signIn: "Anmeldung", organizations: "Organisationen" },
54
55
  passwordPolicy: {
55
56
  label: "Anmeldung mit Passwort",
@@ -278,6 +279,7 @@ const LABELS = {
278
279
  graceDays: "Grace {n} d",
279
280
  graceTip: "This version is past your license date and turns read-only after the grace period.",
280
281
  },
282
+ accountGroups: { people: "People", access: "Permissions" },
281
283
  accountTabs: { users: "Users", roles: "Roles", entra: "Microsoft Entra", signIn: "Sign-in", organizations: "Organizations" },
282
284
  passwordPolicy: {
283
285
  label: "Password sign-in",
package/react/nav.js CHANGED
@@ -144,16 +144,23 @@ function Sidebar(props) {
144
144
 
145
145
  // Tabs as links on top of a page, for pages that belong together under one menu item
146
146
  // (e.g. Gruppen: Sicherheit, Verteiler, Microsoft 365, Teams). Each tab keeps its own address.
147
- function SubNav({ items, link, activePath, label, testId }) {
147
+ // Tabs: as links (items with `to`, the router decides what is shown) or, with `onSelect`, as in-page tabs
148
+ // (items with `id`, `value` is the active one). Both look the same (UISPEC: tabs like Nexus "Gruppen").
149
+ function SubNav({ items, link, activePath, label, testId, value, onSelect }) {
148
150
  const visible = (items || []).filter((i) => i && !i.hidden);
149
151
  if (visible.length < 2) return null;
152
+ const buttons = typeof onSelect === "function";
150
153
  return h(
151
154
  "nav",
152
- { className: "scalio-subnav", "aria-label": label || undefined, "data-testid": testId || "subnav" },
155
+ { className: "scalio-subnav", "aria-label": label || undefined, role: buttons ? "tablist" : undefined, "data-testid": testId || "subnav" },
153
156
  visible.map((item) => {
154
- const tid = item.testId || `subnav-${slug(item.to)}`;
157
+ const tid = item.testId || `subnav-${buttons ? item.id : slug(item.to)}`;
155
158
  const cls = (active) => cx("scalio-subnav-item", active && "is-active");
156
159
  const content = [item.label, item.badge != null && item.badge !== false ? h("span", { key: "b", className: "scalio-nav-badge" }, item.badge) : null];
160
+ if (buttons) {
161
+ const active = item.id === value;
162
+ return h("button", { key: item.id, type: "button", role: "tab", "aria-selected": active, className: cls(active), onClick: () => onSelect(item.id), "data-testid": tid }, content);
163
+ }
157
164
  if (link) {
158
165
  return h(link, { key: item.to, to: item.to, end: item.end, className: ({ isActive }) => cls(isActive), "data-testid": tid }, content);
159
166
  }
@@ -299,6 +306,7 @@ const PRODUCTS = [
299
306
  { key: "rosetta", name: "Rosetta", icon: Icons.FileStack },
300
307
  { key: "foundry", name: "Foundry", icon: Icons.Hammer },
301
308
  { key: "citadel", name: "Citadel", icon: Icons.ShieldCheck },
309
+ { key: "beacon", name: "Beacon", icon: Icons.RadioTower },
302
310
  { key: "pulse", name: "Pulse", icon: Icons.Activity, soon: true },
303
311
  ];
304
312
 
package/react/settings.js CHANGED
@@ -57,9 +57,11 @@ function findSetting(groups, id) {
57
57
  // link: the router's NavLink for the list (optional, without it plain links + onSelect).
58
58
  // minItems: below this many areas the sub-navigation is left out (Citadel, Foundry), default 3.
59
59
  // search: true (default, filters the list), false, or the app's own search element.
60
+ // root: label of the page when it is not the settings page (e.g. "Konten und Rollen"): crumb and nav label.
60
61
  function SettingsLayout(props) {
61
- const { groups, active, onSelect, link, lang, labels, title, hint, actions, children, minItems, search, testId } = props;
62
+ const { groups, active, onSelect, link, lang, labels, title, hint, actions, children, minItems, search, testId, root } = props;
62
63
  const L = labelsFor(lang, labels);
64
+ const rootLabel = root || L.settings;
63
65
  const visible = settingsGroups(groups, L);
64
66
  const [query, setQuery] = React.useState("");
65
67
  const current = findSetting(visible, active);
@@ -126,7 +128,7 @@ function SettingsLayout(props) {
126
128
  const nav = showNav
127
129
  ? h(
128
130
  "nav",
129
- { className: "scalio-settings-nav", "aria-label": L.settings, "data-testid": "settings-nav" },
131
+ { className: "scalio-settings-nav", "aria-label": rootLabel, "data-testid": "settings-nav" },
130
132
  searchBox,
131
133
  // Phones: one select instead of the list.
132
134
  h(
@@ -162,7 +164,7 @@ function SettingsLayout(props) {
162
164
  )
163
165
  : null;
164
166
 
165
- const heading = title || (current && current.item.label) || L.settings;
167
+ const heading = title || (current && current.item.label) || rootLabel;
166
168
  const sub = hint !== undefined ? hint : current && current.item.hint;
167
169
  return h(
168
170
  "div",
@@ -180,7 +182,7 @@ function SettingsLayout(props) {
180
182
  h(
181
183
  "div",
182
184
  { className: "scalio-eyebrow", "data-testid": "settings-crumb" },
183
- current && showNav ? `${L.settings} › ${current.group.label}` : L.settings
185
+ current && showNav ? `${rootLabel} › ${current.group.label}` : rootLabel
184
186
  ),
185
187
  h("h1", { className: "scalio-page-title", "data-testid": "settings-title" }, heading),
186
188
  sub ? h("p", { className: "scalio-page-subtitle" }, sub) : null
package/react/unify.js CHANGED
@@ -139,6 +139,30 @@ function accountTabs(opts) {
139
139
  return ["users", "roles", "entra", "signIn", "organizations"].filter((id) => show[id]).map((id) => ({ id, label: L[id] }));
140
140
  }
141
141
 
142
+ // Konten und Rollen in the structure of the settings page (SettingsLayout, UISPEC): areas on the left in groups,
143
+ // one address per area (<base>/users, /organizations, /roles, /entra, /sign-in).
144
+ const ACCOUNT_AREAS = { users: "users", organizations: "organizations", roles: "roles", entra: "entra", signIn: "sign-in" };
145
+
146
+ function accountGroups(opts) {
147
+ const o = opts || {};
148
+ const all = labelsFor(o.lang, o.labels);
149
+ const L = all.accountTabs;
150
+ const base = (o.base || "/accounts").replace(/\/$/, "");
151
+ const show = Object.assign({ users: true, roles: true, entra: true, signIn: true, organizations: false }, o.show);
152
+ const item = (id) => ({ id, to: `${base}/${ACCOUNT_AREAS[id]}`, label: L[id], hidden: !show[id] });
153
+ return [
154
+ { id: "people", label: all.accountGroups.people, items: [item("users"), item("organizations")] },
155
+ { id: "access", label: all.accountGroups.access, items: [item("roles"), item("entra")] },
156
+ { id: "security", label: all.settingsGroups.security, items: [item("signIn")] },
157
+ ].map((g) => Object.assign({}, g, { items: g.items.filter((i) => !i.hidden) })).filter((g) => g.items.length);
158
+ }
159
+
160
+ // Area id from the address part (/accounts/sign-in -> signIn); unknown -> users.
161
+ function accountArea(part) {
162
+ for (const id of Object.keys(ACCOUNT_AREAS)) if (part === id || part === ACCOUNT_AREAS[id]) return id;
163
+ return "users";
164
+ }
165
+
142
166
  // Password sign-in: "local" (everybody), "emergency" (only emergency administrators), "off" (Microsoft only).
143
167
  // "emergency" and "off" need at least one emergency administrator; without one they stay disabled.
144
168
  const PASSWORD_POLICIES = ["local", "emergency", "off"];
@@ -189,6 +213,8 @@ module.exports = {
189
213
  useApiStatus,
190
214
  ApiStatus,
191
215
  accountTabs,
216
+ accountGroups,
217
+ accountArea,
192
218
  PASSWORD_POLICIES,
193
219
  policyAllowed,
194
220
  PasswordPolicySelect,