@scalio-cloud/ui 0.5.1 → 0.5.2

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.2
4
+ - Navigation items take `match` (further paths that mark the item active), for one menu item over several pages.
5
+ - New `SubNav` (tabs as links on top of a page) and `WorkspaceSwitcher` (Identity / HR in Nexus). No change for
6
+ apps that do not use them.
7
+
3
8
  ## 0.5.1
4
9
  - Phones: the context select (tenant, customer) is hidden in the header below 640px, there is no room next to
5
10
  the header buttons. `AppShell` takes `drawerTop` and shows it above the navigation in the phone drawer.
package/README.md CHANGED
@@ -117,6 +117,12 @@ sidebar renders plain links and marks the item for `activePath`. Item test ids d
117
117
  `EmptyState` (children = next step), `Loading`, `Notice` (`info`, `ok`, `warn`, `error`), `StatusBadge`
118
118
  (tones `neutral`, `running`, `ok`, `warn`, `error`) and `PageHeader` with `back` for detail pages.
119
119
 
120
+ Pages that belong together under one menu item keep their own addresses: the item lists them in `match`
121
+ (`{ to: "/groups", label: "Gruppen", match: ["/teams", "/distribution-lists"] }`, pass `activePath` with
122
+ `link`) and each page shows `<SubNav items={[{ to: "/groups", label: "Sicherheit", end: true }, ...]} link={NavLink} />`
123
+ on top. An app with two work areas (Nexus Identity and HR) puts `<WorkspaceSwitcher options value onSelect />`
124
+ into the sidebar's `top`.
125
+
120
126
  ## Sign-in page and profile (React)
121
127
 
122
128
  All sign-in pages use the same frame and order: Microsoft first, then "oder mit E-Mail", the password field with
package/nav.css CHANGED
@@ -32,6 +32,22 @@
32
32
  .scalio-nav-item.is-active { border-left-color: hsl(var(--primary)); background: hsl(var(--background)); color: hsl(var(--foreground)); font-weight: 600; }
33
33
  .scalio-nav-item.is-bold { font-weight: 700; }
34
34
  .scalio-nav-item:focus-visible, .scalio-nav-cta:focus-visible, .scalio-nav-search:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: -2px; }
35
+ .scalio-workspace { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 0.25rem; padding: 0.25rem; border-radius: 0.375rem; background: hsl(var(--border) / 0.6); }
36
+ .scalio-workspace-item {
37
+ display: flex; align-items: center; justify-content: center; gap: 0.375rem; padding: 0.375rem 0.5rem; border: 0; border-radius: 0.25rem;
38
+ background: transparent; font: inherit; font-size: 0.75rem; font-weight: 600; color: hsl(var(--muted-foreground)); cursor: pointer;
39
+ }
40
+ .scalio-workspace-item.is-active { background: hsl(var(--background)); color: hsl(var(--primary)); box-shadow: 0 1px 2px rgb(0 0 0 / 0.08); }
41
+ .scalio-workspace-item:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 1px; }
42
+ .scalio-workspace-item .scalio-icon { width: 0.875rem; height: 0.875rem; }
43
+ .scalio-subnav { display: flex; gap: 0.25rem; margin-bottom: 1.5rem; border-bottom: 1px solid hsl(var(--border)); overflow-x: auto; }
44
+ .scalio-subnav-item {
45
+ display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; margin-bottom: -1px; white-space: nowrap;
46
+ border-bottom: 2px solid transparent; font-size: 0.875rem; color: hsl(var(--muted-foreground)); text-decoration: none;
47
+ }
48
+ .scalio-subnav-item:hover { color: hsl(var(--foreground)); }
49
+ .scalio-subnav-item.is-active { border-bottom-color: hsl(var(--primary)); color: hsl(var(--foreground)); font-weight: 600; }
50
+ .scalio-subnav-item:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: -2px; }
35
51
  .scalio-nav-icon { width: 1rem; height: 1rem; flex-shrink: 0; }
36
52
  .scalio-nav-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
37
53
  .scalio-nav-badge {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalio-cloud/ui",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
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/nav.js CHANGED
@@ -47,10 +47,12 @@ function NavItem({ item, link, activePath, onNavigate, testIdPrefix }) {
47
47
  : null,
48
48
  ];
49
49
  const testId = itemTestId(item, testIdPrefix);
50
+ // match: further paths that belong to this item (e.g. "Gruppen" also for /teams); needs activePath.
51
+ const matched = (item.match || []).some((m) => isActivePath(activePath, m));
50
52
  if (link) {
51
- return h(link, { to: item.to, end: item.end, className: ({ isActive }) => cls(isActive), onClick: onNavigate, "data-testid": testId }, content);
53
+ return h(link, { to: item.to, end: item.end, className: ({ isActive }) => cls(isActive || matched), onClick: onNavigate, "data-testid": testId }, content);
52
54
  }
53
- const active = isActivePath(activePath, item.to, item.end);
55
+ const active = isActivePath(activePath, item.to, item.end) || matched;
54
56
  return h("a", { href: item.to, className: cls(active), "aria-current": active ? "page" : undefined, onClick: onNavigate, "data-testid": testId }, content);
55
57
  }
56
58
 
@@ -140,6 +142,53 @@ function Sidebar(props) {
140
142
  );
141
143
  }
142
144
 
145
+ // Tabs as links on top of a page, for pages that belong together under one menu item
146
+ // (e.g. Gruppen: Sicherheit, Verteiler, Microsoft 365, Teams). Each tab keeps its own address.
147
+ function SubNav({ items, link, activePath, label, testId }) {
148
+ const visible = (items || []).filter((i) => i && !i.hidden);
149
+ if (visible.length < 2) return null;
150
+ return h(
151
+ "nav",
152
+ { className: "scalio-subnav", "aria-label": label || undefined, "data-testid": testId || "subnav" },
153
+ visible.map((item) => {
154
+ const tid = item.testId || `subnav-${slug(item.to)}`;
155
+ const cls = (active) => cx("scalio-subnav-item", active && "is-active");
156
+ const content = [item.label, item.badge != null && item.badge !== false ? h("span", { key: "b", className: "scalio-nav-badge" }, item.badge) : null];
157
+ if (link) {
158
+ return h(link, { key: item.to, to: item.to, end: item.end, className: ({ isActive }) => cls(isActive), "data-testid": tid }, content);
159
+ }
160
+ const active = isActivePath(activePath, item.to, item.end);
161
+ return h("a", { key: item.to, href: item.to, className: cls(active), "aria-current": active ? "page" : undefined, "data-testid": tid }, content);
162
+ })
163
+ );
164
+ }
165
+
166
+ // Two (or more) work areas of one app, e.g. Nexus Identity and HR. Sits in the sidebar's top slot.
167
+ // options: [{ id, label, icon }]; onSelect(id) switches (the app navigates).
168
+ function WorkspaceSwitcher({ options, value, onSelect, testIdPrefix, testId }) {
169
+ const prefix = testIdPrefix || "";
170
+ if (!options || options.length < 2) return null;
171
+ return h(
172
+ "div",
173
+ { className: "scalio-workspace", role: "group", "data-testid": prefix + (testId || "workspace-switcher") },
174
+ options.map((o) =>
175
+ h(
176
+ "button",
177
+ {
178
+ key: o.id,
179
+ type: "button",
180
+ className: cx("scalio-workspace-item", o.id === value && "is-active"),
181
+ "aria-pressed": o.id === value,
182
+ onClick: () => onSelect && onSelect(o.id),
183
+ "data-testid": `${prefix}ws-${o.id}`,
184
+ },
185
+ o.icon ? h(o.icon, { className: "scalio-icon", "aria-hidden": "true" }) : null,
186
+ o.label
187
+ )
188
+ )
189
+ );
190
+ }
191
+
143
192
  // "Version 1.2.4 · 3f2a1bc" at the bottom of the sidebar.
144
193
  function VersionTag({ version, commit, lang, labels, testId }) {
145
194
  const L = labelsFor(lang, labels);
@@ -443,6 +492,8 @@ module.exports = {
443
492
  Sidebar,
444
493
  NavItem,
445
494
  NavDrawer,
495
+ SubNav,
496
+ WorkspaceSwitcher,
446
497
  AppShell,
447
498
  ContextSelect,
448
499
  ProductSwitcher,