@scalio-cloud/ui 0.13.0 → 0.14.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 +10 -0
- package/nav.css +11 -0
- package/package.json +1 -1
- package/react/labels.js +2 -0
- package/react/nav.js +10 -3
- package/react/settings.js +6 -4
- package/react/unify.js +26 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
UI alignment round 1, follow-up (scalio-docs ADR 0042, Markus 2026-10-30).
|
|
5
|
+
- `accountGroups({ lang, base, show })` and `accountArea(part)`: Konten und Rollen in the structure of the settings
|
|
6
|
+
page (groups Personen, Rechte, Sicherheit; one address per area, `/accounts/<area>`).
|
|
7
|
+
- `SettingsLayout` takes `root` (e.g. "Konten und Rollen") for the crumb, title fallback and nav label.
|
|
8
|
+
- `SubNav` with `value` and `onSelect` renders in-page tabs (buttons, `role="tablist"`).
|
|
9
|
+
- New CSS classes `.scalio-tabs` / `.scalio-tab` in `nav.css`: in-page tabs (shadcn Tabs) look like the sub
|
|
10
|
+
navigation (underline, like Nexus "Gruppen").
|
|
11
|
+
- **Visible change:** none until an app uses the new parts. No token changes.
|
|
12
|
+
|
|
3
13
|
## 0.13.0
|
|
4
14
|
UI alignment round 1 (scalio-docs ADR 0041).
|
|
5
15
|
- `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.14.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
|
@@ -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
|
}
|
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,
|