@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 +17 -0
- package/nav.css +11 -0
- package/package.json +1 -1
- package/react/icons.js +11 -1
- package/react/index.js +1 -0
- package/react/labels.js +2 -0
- package/react/nav.js +11 -3
- package/react/settings.js +6 -4
- package/react/unify.js +26 -0
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.
|
|
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
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
|
-
|
|
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":
|
|
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) ||
|
|
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 ? `${
|
|
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,
|