@scalio-cloud/ui 0.16.0 → 0.18.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 +19 -0
- package/README.md +13 -0
- package/nav.css +11 -0
- package/package.json +6 -2
- package/react/actions.js +77 -0
- package/react/index.js +2 -1
- package/react/nav.js +18 -14
- package/shell.css +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.0
|
|
4
|
+
Page actions in the sidebar (Markus 2026-10-31, reference Scalio Kontor; scalio-docs ADR 0042 Nachtrag).
|
|
5
|
+
- New `react/actions.js`: `PageActions` puts a page's actions (Neu …, Import, Zurück) into a fixed slot in the
|
|
6
|
+
sidebar right below Übersicht from 768px; on phones they stay in the page header. `ActionSlot` for apps with their
|
|
7
|
+
own sidebar, `useDesktop()` (768px, same as the sidebar).
|
|
8
|
+
- `Sidebar` gets `pageActions`: Übersicht (first section without heading) is pinned on top, then the slot, then the
|
|
9
|
+
other sections. The slot keeps room for two buttons when empty, so the menu never moves. Not in the phone drawer.
|
|
10
|
+
- CSS (`nav.css`): `.scalio-sidebar-pinned`, `.scalio-action-slot`, `.scalio-side-actions` (buttons stacked, full
|
|
11
|
+
width). `shell.css`: an empty `.scalio-page-actions` is hidden.
|
|
12
|
+
- `react-dom` is an optional peer dependency (for the portal; every app has it).
|
|
13
|
+
- **Visible change:** none until an app sets `pageActions` and wraps its actions in `PageActions`. No token changes.
|
|
14
|
+
|
|
15
|
+
## 0.17.0
|
|
16
|
+
Scalio Pulse is built (scalio-docs ADR 0043, P1 started 2026-10-31).
|
|
17
|
+
- Product switcher: "Pulse" is no longer marked "bald"; it behaves like the other apps (link from `SCALIO_APP_LINKS`).
|
|
18
|
+
- `manualUrl("pulse")`: docs.scaliopulse.com / devdocs.scaliopulse.com.
|
|
19
|
+
- **Visible change:** after the upgrade, Pulse in the switcher loses the "bald" badge and is greyed out until the
|
|
20
|
+
instance has a link for it. No token changes.
|
|
21
|
+
|
|
3
22
|
## 0.16.0
|
|
4
23
|
Same profile page in every app, reference Scalio Nexus (Markus 2026-10-31, scalio-docs ADR 0042 Nachtrag).
|
|
5
24
|
- New `react/profile.js`: `ProfileIdentity` (letters, name, email · role), `ThemeChoice` (Hell, Dunkel, System as
|
package/README.md
CHANGED
|
@@ -129,6 +129,19 @@ Pages that belong together under one menu item keep their own addresses: the ite
|
|
|
129
129
|
on top. An app with two work areas (Nexus Identity and HR) puts `<WorkspaceSwitcher options value onSelect />`
|
|
130
130
|
into the sidebar's `top`.
|
|
131
131
|
|
|
132
|
+
**Page actions in the sidebar** (from 0.18.0, UISPEC section 2): `<Sidebar pageActions ... />` keeps Übersicht (the
|
|
133
|
+
first section without a heading) on top and adds a fixed slot right below it. A page wraps its actions in
|
|
134
|
+
`<PageActions>`; from 768px they move into the slot, on phones they stay in the page header. The slot keeps room for
|
|
135
|
+
two buttons even when a page has none, so the menu never moves.
|
|
136
|
+
|
|
137
|
+
```jsx
|
|
138
|
+
<PageHeader title="Kontakte" actions={<PageActions><Button onClick={add}><Plus /> Neuer Kontakt</Button></PageActions>} />
|
|
139
|
+
<PageHeader title={doc.number} actions={<PageActions><Button variant="outline" asChild><Link to="/invoices"><ArrowLeft /> Zurück</Link></Button></PageActions>} />
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Only actions that start something or lead away go there (Neu …, Import, Scannen, Zurück), at most two per page.
|
|
143
|
+
Filters, search, tabs and Speichern stay on the page. Apps with their own sidebar place `<ActionSlot />` themselves.
|
|
144
|
+
|
|
132
145
|
## Settings page (React)
|
|
133
146
|
|
|
134
147
|
Every area of the settings has its own address (`/settings/<area>`); the app's router picks the area, the
|
package/nav.css
CHANGED
|
@@ -17,6 +17,17 @@
|
|
|
17
17
|
.scalio-sidebar-top { padding: 0.75rem 0.75rem 0; }
|
|
18
18
|
.scalio-sidebar-scroll { flex: 1 1 auto; overflow-y: auto; padding: 0.5rem 0 0.75rem; }
|
|
19
19
|
.scalio-nav-section + .scalio-nav-section { margin-top: 0.25rem; }
|
|
20
|
+
/* Page actions (react/actions.js): Übersicht stays on top, then a fixed slot for the page's actions. The slot
|
|
21
|
+
keeps room for two buttons even when empty, so the menu below never moves. */
|
|
22
|
+
.scalio-sidebar-pinned { flex-shrink: 0; padding-top: 0.5rem; }
|
|
23
|
+
.scalio-action-slot {
|
|
24
|
+
flex-shrink: 0; box-sizing: border-box; min-height: 6.25rem; margin: 0 0.75rem; padding: 0.5rem 0 0.75rem;
|
|
25
|
+
border-bottom: 1px solid hsl(var(--border));
|
|
26
|
+
}
|
|
27
|
+
.scalio-sidebar-scroll.has-action-slot { padding-top: 0; }
|
|
28
|
+
.scalio-side-actions, .scalio-side-actions > div { display: flex; flex-direction: column; align-items: stretch; gap: 0.5rem; }
|
|
29
|
+
.scalio-side-actions > :not(div), .scalio-side-actions > div > * { width: 100%; justify-content: center; }
|
|
30
|
+
.scalio-side-actions input[type="file"], .scalio-side-actions [hidden] { display: none; }
|
|
20
31
|
.scalio-nav-heading {
|
|
21
32
|
padding: 1.25rem 1.25rem 0.5rem; font-size: 11px; font-weight: 700; text-transform: uppercase;
|
|
22
33
|
letter-spacing: 0.12em; color: hsl(var(--muted-foreground));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalio-cloud/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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,
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"react": ">=18",
|
|
49
|
-
"tailwindcss": ">=3.3 <4"
|
|
49
|
+
"tailwindcss": ">=3.3 <4",
|
|
50
|
+
"react-dom": ">=18"
|
|
50
51
|
},
|
|
51
52
|
"peerDependenciesMeta": {
|
|
52
53
|
"tailwindcss": {
|
|
@@ -54,6 +55,9 @@
|
|
|
54
55
|
},
|
|
55
56
|
"react": {
|
|
56
57
|
"optional": true
|
|
58
|
+
},
|
|
59
|
+
"react-dom": {
|
|
60
|
+
"optional": true
|
|
57
61
|
}
|
|
58
62
|
},
|
|
59
63
|
"scripts": {
|
package/react/actions.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Page actions in the sidebar (UISPEC section 2, "Seitenaktionen", from 0.18.0). Buttons such as "Neu ...",
|
|
3
|
+
// "Import" or "Zurück" sit in a fixed slot right below Übersicht, close to the navigation, instead of far
|
|
4
|
+
// right in the page header. The slot keeps its height when a page has no actions, so the menu never moves.
|
|
5
|
+
// Phones have no sidebar: there the actions stay in the page header.
|
|
6
|
+
//
|
|
7
|
+
// The app turns the slot on with <Sidebar pageActions />; a page wraps its actions in <PageActions>.
|
|
8
|
+
// No provider: the slot registers itself in a small store, so pages need no wiring.
|
|
9
|
+
const React = require("react");
|
|
10
|
+
const h = React.createElement;
|
|
11
|
+
|
|
12
|
+
const DESKTOP_QUERY = "(min-width: 768px)";
|
|
13
|
+
|
|
14
|
+
let slotEl = null;
|
|
15
|
+
const listeners = new Set();
|
|
16
|
+
|
|
17
|
+
function setActionSlot(el) {
|
|
18
|
+
if (slotEl === el) return;
|
|
19
|
+
slotEl = el;
|
|
20
|
+
listeners.forEach((fn) => fn());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function subscribe(fn) {
|
|
24
|
+
listeners.add(fn);
|
|
25
|
+
return () => listeners.delete(fn);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const getSlot = () => slotEl;
|
|
29
|
+
const getServerSlot = () => null;
|
|
30
|
+
|
|
31
|
+
function useActionSlot() {
|
|
32
|
+
return React.useSyncExternalStore(subscribe, getSlot, getServerSlot);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function matchesDesktop() {
|
|
36
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia(DESKTOP_QUERY).matches;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// True from 768px, the width from which the sidebar is shown (nav.css).
|
|
40
|
+
function useDesktop() {
|
|
41
|
+
const [desktop, setDesktop] = React.useState(matchesDesktop);
|
|
42
|
+
React.useEffect(() => {
|
|
43
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return undefined;
|
|
44
|
+
const mq = window.matchMedia(DESKTOP_QUERY);
|
|
45
|
+
const on = () => setDesktop(mq.matches);
|
|
46
|
+
on();
|
|
47
|
+
if (mq.addEventListener) {
|
|
48
|
+
mq.addEventListener("change", on);
|
|
49
|
+
return () => mq.removeEventListener("change", on);
|
|
50
|
+
}
|
|
51
|
+
mq.addListener(on);
|
|
52
|
+
return () => mq.removeListener(on);
|
|
53
|
+
}, []);
|
|
54
|
+
return desktop;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The empty slot in the sidebar. Sidebar renders it when pageActions is set; apps with their own sidebar
|
|
58
|
+
// can place it themselves.
|
|
59
|
+
function ActionSlot({ testId }) {
|
|
60
|
+
return h("div", { ref: setActionSlot, className: "scalio-action-slot", "data-testid": testId || "action-slot" });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Wrap a page's actions: in the sidebar slot from 768px, otherwise where it stands (page header).
|
|
64
|
+
// Only for actions that start something or lead away (Neu, Import, Scannen, Zurück); filters, search and
|
|
65
|
+
// Speichern stay on the page.
|
|
66
|
+
function PageActions({ children, testId }) {
|
|
67
|
+
const slot = useActionSlot();
|
|
68
|
+
const desktop = useDesktop();
|
|
69
|
+
if (children == null || children === false) return null;
|
|
70
|
+
if (slot && desktop) {
|
|
71
|
+
const ReactDOM = require("react-dom");
|
|
72
|
+
return ReactDOM.createPortal(h("div", { className: "scalio-side-actions", "data-testid": testId || "page-actions" }, children), slot);
|
|
73
|
+
}
|
|
74
|
+
return h(React.Fragment, null, children);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { PageActions, ActionSlot, useActionSlot, useDesktop, setActionSlot };
|
package/react/index.js
CHANGED
|
@@ -242,6 +242,7 @@ const DOCS_DOMAINS = {
|
|
|
242
242
|
foundry: "scaliofoundry.com",
|
|
243
243
|
rosetta: "scaliorosetta.com",
|
|
244
244
|
beacon: "scaliobeacon.com",
|
|
245
|
+
pulse: "scaliopulse.com",
|
|
245
246
|
};
|
|
246
247
|
|
|
247
248
|
function manualUrl(product, opts) {
|
|
@@ -377,6 +378,6 @@ module.exports = {
|
|
|
377
378
|
TEST_IDS,
|
|
378
379
|
initials,
|
|
379
380
|
};
|
|
380
|
-
Object.assign(module.exports, Nav, Blocks, require("./settings"), require("./support"), require("./security"), require("./license"), require("./palette"), require("./unify"), require("./profile"));
|
|
381
|
+
Object.assign(module.exports, Nav, Blocks, require("./settings"), require("./support"), require("./security"), require("./license"), require("./palette"), require("./unify"), require("./profile"), require("./actions"));
|
|
381
382
|
// After the exports above: auth.js uses LanguageToggle and ThemeToggle from this module.
|
|
382
383
|
Object.assign(module.exports, require("./auth"));
|
package/react/nav.js
CHANGED
|
@@ -6,6 +6,7 @@ const React = require("react");
|
|
|
6
6
|
const h = React.createElement;
|
|
7
7
|
const Icons = require("./icons");
|
|
8
8
|
const { labelsFor } = require("./labels");
|
|
9
|
+
const { ActionSlot } = require("./actions");
|
|
9
10
|
|
|
10
11
|
function cx() {
|
|
11
12
|
return Array.prototype.slice.call(arguments).filter(Boolean).join(" ");
|
|
@@ -101,14 +102,26 @@ function SearchButton({ search, lang, labels, testIdPrefix, onNavigate }) {
|
|
|
101
102
|
|
|
102
103
|
// Sidebar. variant "desktop" shows from 768px, "drawer" is the same content inside NavDrawer.
|
|
103
104
|
// link: the router's NavLink (gets to, end, className(fn), onClick); without it plain <a> + activePath.
|
|
105
|
+
// pageActions: a fixed slot for the page's actions (react/actions.js) right below Übersicht, desktop only.
|
|
106
|
+
// Übersicht (the first section when it has no heading) then stays on top and does not scroll.
|
|
104
107
|
function Sidebar(props) {
|
|
105
108
|
const {
|
|
106
109
|
nav, link, activePath, top, search, primaryAction, footer, version, commit, lang, labels,
|
|
107
|
-
variant, onNavigate, testId, testIdPrefix,
|
|
110
|
+
variant, onNavigate, testId, testIdPrefix, pageActions,
|
|
108
111
|
} = props;
|
|
109
112
|
const prefix = testIdPrefix != null ? testIdPrefix : variant === "drawer" ? "m-" : "";
|
|
110
113
|
const L = labelsFor(lang, labels);
|
|
111
114
|
const sections = visibleSections(nav);
|
|
115
|
+
const slot = !!pageActions && variant !== "drawer";
|
|
116
|
+
const pinned = slot && sections.length && !sections[0].label ? sections[0] : null;
|
|
117
|
+
const rest = pinned ? sections.slice(1) : sections;
|
|
118
|
+
const section = (sec, i) =>
|
|
119
|
+
h(
|
|
120
|
+
"div",
|
|
121
|
+
{ key: sec.id || i, className: "scalio-nav-section", "data-section": sec.id },
|
|
122
|
+
sec.label ? h("div", { className: cx("scalio-nav-heading", sec.tone && `is-${sec.tone}`) }, sec.label) : null,
|
|
123
|
+
sec.items.map((item) => h(NavItem, { key: item.to, item, link, activePath, onNavigate, testIdPrefix: prefix }))
|
|
124
|
+
);
|
|
112
125
|
return h(
|
|
113
126
|
"nav",
|
|
114
127
|
{
|
|
@@ -119,18 +132,9 @@ function Sidebar(props) {
|
|
|
119
132
|
top ? h("div", { className: "scalio-sidebar-top" }, top) : null,
|
|
120
133
|
h(SearchButton, { search, lang, labels, testIdPrefix: prefix, onNavigate }),
|
|
121
134
|
h(PrimaryAction, { action: primaryAction, link, onNavigate, testIdPrefix: prefix }),
|
|
122
|
-
h(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
sections.map((sec, i) =>
|
|
126
|
-
h(
|
|
127
|
-
"div",
|
|
128
|
-
{ key: sec.id || i, className: "scalio-nav-section", "data-section": sec.id },
|
|
129
|
-
sec.label ? h("div", { className: cx("scalio-nav-heading", sec.tone && `is-${sec.tone}`) }, sec.label) : null,
|
|
130
|
-
sec.items.map((item) => h(NavItem, { key: item.to, item, link, activePath, onNavigate, testIdPrefix: prefix }))
|
|
131
|
-
)
|
|
132
|
-
)
|
|
133
|
-
),
|
|
135
|
+
pinned ? h("div", { className: "scalio-sidebar-pinned" }, section(pinned, 0)) : null,
|
|
136
|
+
slot ? h(ActionSlot, { testId: `${prefix}action-slot` }) : null,
|
|
137
|
+
h("div", { className: cx("scalio-sidebar-scroll", slot && "has-action-slot") }, rest.map(section)),
|
|
134
138
|
footer || version
|
|
135
139
|
? h(
|
|
136
140
|
"div",
|
|
@@ -307,7 +311,7 @@ const PRODUCTS = [
|
|
|
307
311
|
{ key: "foundry", name: "Foundry", icon: Icons.Hammer },
|
|
308
312
|
{ key: "citadel", name: "Citadel", icon: Icons.ShieldCheck },
|
|
309
313
|
{ key: "beacon", name: "Beacon", icon: Icons.RadioTower },
|
|
310
|
-
{ key: "pulse", name: "Pulse", icon: Icons.Activity
|
|
314
|
+
{ key: "pulse", name: "Pulse", icon: Icons.Activity },
|
|
311
315
|
];
|
|
312
316
|
|
|
313
317
|
function productStatus(product, current) {
|
package/shell.css
CHANGED
|
@@ -102,6 +102,7 @@ a.scalio-btn { text-decoration: none; }
|
|
|
102
102
|
@media (min-width: 640px) { .scalio-page-title { font-size: 1.875rem; } }
|
|
103
103
|
.scalio-page-subtitle { margin: 0.25rem 0 0; font-size: 0.875rem; color: hsl(var(--muted-foreground)); }
|
|
104
104
|
.scalio-page-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
|
|
105
|
+
.scalio-page-actions:empty { display: none; }
|
|
105
106
|
|
|
106
107
|
.scalio-version {
|
|
107
108
|
padding: 0.5rem 0.75rem; font-family: 'JetBrains Mono', monospace; font-size: 10px;
|