@scalio-cloud/ui 0.18.0 → 0.19.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 +24 -10
- package/README.md +9 -4
- package/nav.css +15 -8
- package/package.json +1 -1
- package/react/actions.js +2 -2
- package/react/beacon.js +112 -0
- package/react/index.js +1 -1
- package/react/labels.js +26 -0
- package/react/nav.js +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.19.0
|
|
4
|
+
Settings card "Scalio Beacon" for the apps (Beacon phase 3, scalio-docs `concepts/beacon-suite.md`).
|
|
5
|
+
- `BeaconConnection` with `peersApi(api)`: pair the installation with a Beacon instance (address + one-time code from
|
|
6
|
+
Beacon), show the pairing (address, version, last contact, queue), test it and disconnect. Routes from
|
|
7
|
+
scalio-connect 0.8.0 (`scalio_connect.peers.web.admin_router`).
|
|
8
|
+
- Labels `beacon*` in German and English.
|
|
9
|
+
- **Visible change:** none until an app shows the card. No token changes.
|
|
10
|
+
|
|
11
|
+
## 0.18.1
|
|
12
|
+
Page actions after Markus' feedback (2026-10-31, UISPEC section 2 "Page actions"). 0.18.0 was released before these
|
|
13
|
+
changes were merged; apps use 0.18.1.
|
|
14
|
+
- The slot is shown only when a page has actions and pushes the menu down; no reserved room, no separator line.
|
|
15
|
+
- 0.25rem above the buttons, 0.875rem from the last button to the next section heading (half the usual gap).
|
|
16
|
+
- Only Übersicht (first item of a first section without heading) is pinned; further items of that section stay in
|
|
17
|
+
the menu (Pulse: Geräte).
|
|
18
|
+
- Buttons in the slot: same height (2.25rem), one line, long labels end with "…". Labels without "Neu" (README).
|
|
19
|
+
- **Visible change:** only in apps that set `pageActions`. No token changes.
|
|
20
|
+
|
|
3
21
|
## 0.18.0
|
|
4
|
-
Page actions in the sidebar (Markus 2026-10-31, reference Scalio Kontor; scalio-docs ADR 0042 Nachtrag).
|
|
22
|
+
Page actions in the sidebar, first version (Markus 2026-10-31, reference Scalio Kontor; scalio-docs ADR 0042 Nachtrag).
|
|
5
23
|
- 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`
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
24
|
+
sidebar right below Übersicht from 768px; on phones they stay in the page header. `ActionSlot`, `useDesktop()`.
|
|
25
|
+
- `Sidebar` gets `pageActions`: Übersicht pinned on top, then the slot (room for two buttons, line below), then the
|
|
26
|
+
sections. `shell.css`: an empty `.scalio-page-actions` is hidden. `react-dom` is an optional peer dependency.
|
|
27
|
+
- **Visible change:** none until an app sets `pageActions`. No token changes.
|
|
14
28
|
|
|
15
29
|
## 0.17.0
|
|
16
30
|
Scalio Pulse is built (scalio-docs ADR 0043, P1 started 2026-10-31).
|
|
@@ -30,7 +44,7 @@ Same profile page in every app, reference Scalio Nexus (Markus 2026-10-31, scali
|
|
|
30
44
|
- **Visible change:** none until an app uses the new parts. No token changes.
|
|
31
45
|
|
|
32
46
|
## 0.15.0
|
|
33
|
-
New app Scalio Beacon (service desk, scalio-docs ADR
|
|
47
|
+
New app Scalio Beacon (service desk, scalio-docs ADR 0044).
|
|
34
48
|
- Product switcher: entry "Beacon" (icon `RadioTower`) between Citadel and Pulse, available like the others.
|
|
35
49
|
- `manualUrl("beacon")`: docs.scaliobeacon.com / devdocs.scaliobeacon.com.
|
|
36
50
|
- **Visible change:** every app shows "Beacon" in the switcher after its upgrade (greyed out until the instance
|
package/README.md
CHANGED
|
@@ -117,6 +117,10 @@ With `client` the drawer has the tabs "Neue Anfrage" (category, subject, descrip
|
|
|
117
117
|
axios instance. The settings card `<SupportConnection client={supportApi(api)} lang={lang} />` connects the
|
|
118
118
|
installation with a one-time code (admins). Without `client` the drawer keeps the old single form with `onSubmit`.
|
|
119
119
|
|
|
120
|
+
The settings card `<BeaconConnection client={peersApi(api)} lang={lang} />` pairs the app with Scalio Beacon
|
|
121
|
+
(address and one-time code from Beacon, Einstellungen -> Verbundene Apps); findings then go to Beacon. Backend:
|
|
122
|
+
scalio-connect 0.8.0 `peers.web.admin_router`.
|
|
123
|
+
|
|
120
124
|
`link` is the router's `NavLink` (it gets `to`, `end`, a `className` function and `onClick`); without it the
|
|
121
125
|
sidebar renders plain links and marks the item for `activePath`. Item test ids default to `nav-<path>`
|
|
122
126
|
(`nav-overview` for `/`), or set `testId`. Page blocks: `KpiTile` (tone `error` = red number),
|
|
@@ -130,16 +134,17 @@ on top. An app with two work areas (Nexus Identity and HR) puts `<WorkspaceSwitc
|
|
|
130
134
|
into the sidebar's `top`.
|
|
131
135
|
|
|
132
136
|
**Page actions in the sidebar** (from 0.18.0, UISPEC section 2): `<Sidebar pageActions ... />` keeps Übersicht (the
|
|
133
|
-
first section without
|
|
134
|
-
`<PageActions>`; from 768px they move into the slot, on phones they stay in the page header. The slot
|
|
135
|
-
|
|
137
|
+
first item of a first section without heading) on top and adds a slot right below it. A page wraps its actions in
|
|
138
|
+
`<PageActions>`; from 768px they move into the slot, on phones they stay in the page header. The slot takes no
|
|
139
|
+
room on pages without actions; with actions it pushes the menu down.
|
|
136
140
|
|
|
137
141
|
```jsx
|
|
138
|
-
<PageHeader title="Kontakte" actions={<PageActions><Button onClick={add}><Plus />
|
|
142
|
+
<PageHeader title="Kontakte" actions={<PageActions><Button onClick={add}><Plus /> Kontakt</Button></PageActions>} />
|
|
139
143
|
<PageHeader title={doc.number} actions={<PageActions><Button variant="outline" asChild><Link to="/invoices"><ArrowLeft /> Zurück</Link></Button></PageActions>} />
|
|
140
144
|
```
|
|
141
145
|
|
|
142
146
|
Only actions that start something or lead away go there (Neu …, Import, Scannen, Zurück), at most two per page.
|
|
147
|
+
Labels without "Neu"/"New": the + icon says it ("+ Kontakt", not "+ Neuer Kontakt").
|
|
143
148
|
Filters, search, tabs and Speichern stay on the page. Apps with their own sidebar place `<ActionSlot />` themselves.
|
|
144
149
|
|
|
145
150
|
## Settings page (React)
|
package/nav.css
CHANGED
|
@@ -17,16 +17,23 @@
|
|
|
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
|
|
21
|
-
|
|
20
|
+
/* Page actions (react/actions.js): Übersicht stays on top, then the slot for the page's actions. The slot takes no
|
|
21
|
+
room when a page has no actions; with actions it shows them and pushes the menu down. No separator line. */
|
|
22
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
|
-
}
|
|
23
|
+
.scalio-action-slot { flex-shrink: 0; margin: 0 0.75rem; }
|
|
27
24
|
.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.
|
|
29
|
-
.scalio-side-actions
|
|
25
|
+
.scalio-side-actions, .scalio-side-actions > div { display: flex; flex-direction: column; align-items: stretch; gap: 0.375rem; }
|
|
26
|
+
.scalio-side-actions { padding-top: 0.25rem; }
|
|
27
|
+
/* Below the buttons: half the usual space before the next heading (0.875rem instead of 1.75rem), a little room
|
|
28
|
+
before a plain menu item. */
|
|
29
|
+
.scalio-action-slot:has(.scalio-side-actions:not(:empty)) + .scalio-sidebar-scroll > .scalio-nav-section:first-child > .scalio-nav-heading { padding-top: 0.875rem; }
|
|
30
|
+
.scalio-action-slot:has(.scalio-side-actions:not(:empty)) + .scalio-sidebar-scroll > .scalio-nav-section:first-child:not(:has(> .scalio-nav-heading)) { padding-top: 0.375rem; }
|
|
31
|
+
.scalio-side-actions:empty { display: none; }
|
|
32
|
+
/* Same height for every button (2.25rem), one line; a long label is cut with "…". */
|
|
33
|
+
.scalio-side-actions > :not(div), .scalio-side-actions > div > * {
|
|
34
|
+
box-sizing: border-box; width: 100%; height: 2.25rem; min-height: 0; margin: 0; justify-content: center;
|
|
35
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
36
|
+
}
|
|
30
37
|
.scalio-side-actions input[type="file"], .scalio-side-actions [hidden] { display: none; }
|
|
31
38
|
.scalio-nav-heading {
|
|
32
39
|
padding: 1.25rem 1.25rem 0.5rem; font-size: 11px; font-weight: 700; text-transform: uppercase;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalio-cloud/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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/actions.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// Page actions in the sidebar (UISPEC section 2, "Seitenaktionen", from 0.18.0). Buttons such as "Neu ...",
|
|
3
|
-
// "Import" or "Zurück" sit
|
|
4
|
-
//
|
|
3
|
+
// "Import" or "Zurück" sit right below Übersicht, close to the navigation, instead of far right in the page
|
|
4
|
+
// header. The slot takes no room when a page has no actions; with actions it pushes the menu down.
|
|
5
5
|
// Phones have no sidebar: there the actions stay in the page header.
|
|
6
6
|
//
|
|
7
7
|
// The app turns the slot on with <Sidebar pageActions />; a page wraps its actions in <PageActions>.
|
package/react/beacon.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Settings card "Scalio Beacon" (Beacon phase 3, scalio-docs concepts/beacon-suite.md): an app (Citadel, Rosetta,
|
|
3
|
+
// Foundry, Nexus) pairs with a Beacon instance using the address and one-time code from Beacon (Einstellungen ->
|
|
4
|
+
// Verbundene Apps); findings then go to Beacon and become tickets. Backend routes from scalio-connect
|
|
5
|
+
// (`scalio_connect.peers.web.admin_router`); peersApi(api) maps them for an axios-like client.
|
|
6
|
+
const React = require("react");
|
|
7
|
+
const h = React.createElement;
|
|
8
|
+
const { labelsFor } = require("./labels");
|
|
9
|
+
|
|
10
|
+
function errorText(err) {
|
|
11
|
+
const d = err && err.response && err.response.data && err.response.data.detail;
|
|
12
|
+
if (typeof d === "string") return d;
|
|
13
|
+
if (d && typeof d.message === "string") return d.message;
|
|
14
|
+
return (err && err.message) || String(err);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fmtDate(value, lang) {
|
|
18
|
+
if (!value) return "";
|
|
19
|
+
const d = new Date(value);
|
|
20
|
+
if (isNaN(d.getTime())) return String(value);
|
|
21
|
+
return d.toLocaleString(lang === "en" ? "en-GB" : "de-CH", { dateStyle: "medium", timeStyle: "short" });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// api: axios instance with the app's base URL (/api). Every method resolves to the response body.
|
|
25
|
+
function peersApi(api) {
|
|
26
|
+
const data = (p) => p.then((r) => r.data);
|
|
27
|
+
const enc = encodeURIComponent;
|
|
28
|
+
return {
|
|
29
|
+
list: () => data(api.get("/peers")),
|
|
30
|
+
connect: (url, code) => data(api.post("/peers", { url, code })),
|
|
31
|
+
check: (id) => data(api.post(`/peers/${enc(id)}/check`)),
|
|
32
|
+
remove: (id) => data(api.delete(`/peers/${enc(id)}`)),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function BeaconConnection({ client, lang, labels, onChange }) {
|
|
37
|
+
const L = labelsFor(lang, labels);
|
|
38
|
+
const [st, setSt] = React.useState(null);
|
|
39
|
+
const [url, setUrl] = React.useState("");
|
|
40
|
+
const [code, setCode] = React.useState("");
|
|
41
|
+
const [busy, setBusy] = React.useState(false);
|
|
42
|
+
const [error, setError] = React.useState("");
|
|
43
|
+
const [note, setNote] = React.useState("");
|
|
44
|
+
const load = React.useCallback(() => client.list().then((d) => setSt(d)).catch((err) => (setError(errorText(err)), setSt(false))), [client]);
|
|
45
|
+
React.useEffect(() => { load(); }, [load]);
|
|
46
|
+
const run = async (fn, ok) => {
|
|
47
|
+
setBusy(true);
|
|
48
|
+
setError("");
|
|
49
|
+
setNote("");
|
|
50
|
+
try {
|
|
51
|
+
const d = await fn();
|
|
52
|
+
if (d && d.ok === false) throw new Error(L.beaconCheckFailed.replace("{detail}", d.detail || d.status));
|
|
53
|
+
setCode("");
|
|
54
|
+
setNote(typeof ok === "function" ? ok(d) : ok);
|
|
55
|
+
await load();
|
|
56
|
+
if (onChange) onChange(d);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
setError(errorText(err));
|
|
59
|
+
} finally {
|
|
60
|
+
setBusy(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
if (st === null) return h("div", { className: "scalio-support-meta" }, L.loading);
|
|
64
|
+
if (st === false) return h("div", { className: "scalio-notice is-error", role: "alert" }, error);
|
|
65
|
+
const beacons = (st.peers || []).filter((p) => p.app === "beacon");
|
|
66
|
+
return h(
|
|
67
|
+
"div",
|
|
68
|
+
{ className: "scalio-support-conn", "data-testid": "beacon-connection" },
|
|
69
|
+
h("p", { className: "scalio-support-meta" }, beacons.length ? L.beaconHelpConnected : L.beaconHelp),
|
|
70
|
+
beacons.map((p) => {
|
|
71
|
+
const q = p.queue || {};
|
|
72
|
+
return h(
|
|
73
|
+
"div",
|
|
74
|
+
{ key: p.id, className: "scalio-support-conn", "data-testid": "beacon-peer" },
|
|
75
|
+
h("div", { className: "scalio-support-conn-head" },
|
|
76
|
+
h("span", { className: "scalio-badge " + (p.usable ? "is-ok" : "is-error") }, p.usable ? L.beaconConnected : L.beaconKeyLost)),
|
|
77
|
+
h("dl", { className: "scalio-support-conn-facts" },
|
|
78
|
+
h("dt", null, L.beaconAddress), h("dd", null, p.url),
|
|
79
|
+
p.version ? h(React.Fragment, null, h("dt", null, L.version), h("dd", null, p.version)) : null,
|
|
80
|
+
h("dt", null, L.supportSince), h("dd", null, fmtDate(p.paired_at, lang)),
|
|
81
|
+
h("dt", null, L.beaconLastContact), h("dd", null, fmtDate(p.last_seen, lang))),
|
|
82
|
+
q.pending || q.failed
|
|
83
|
+
? h("div", { className: "scalio-support-meta", "data-testid": "beacon-queue" },
|
|
84
|
+
L.supportOutbox.replace("{pending}", q.pending || 0).replace("{failed}", q.failed || 0) + (q.last_error ? ` (${q.last_error})` : ""))
|
|
85
|
+
: null,
|
|
86
|
+
h("div", { className: "scalio-support-conn-actions" },
|
|
87
|
+
h("button", { type: "button", className: "scalio-btn", disabled: busy, "data-testid": "beacon-check",
|
|
88
|
+
onClick: () => run(() => client.check(p.id), (d) => L.beaconChecked.replace("{version}", (d && d.version) || "")) }, L.supportCheck),
|
|
89
|
+
h("button", { type: "button", className: "scalio-btn scalio-btn-danger", disabled: busy, "data-testid": "beacon-remove",
|
|
90
|
+
onClick: () => (typeof window === "undefined" || window.confirm(L.beaconRemoveConfirm)) && run(() => client.remove(p.id), L.supportRemoved) },
|
|
91
|
+
L.supportRemove))
|
|
92
|
+
);
|
|
93
|
+
}),
|
|
94
|
+
note ? h("div", { className: "scalio-notice is-ok", "data-testid": "beacon-note" }, note) : null,
|
|
95
|
+
error ? h("div", { className: "scalio-notice is-error", role: "alert", "data-testid": "beacon-error" }, error) : null,
|
|
96
|
+
beacons.length
|
|
97
|
+
? null
|
|
98
|
+
: h(
|
|
99
|
+
"form",
|
|
100
|
+
{ className: "scalio-support-form", onSubmit: (e) => { e.preventDefault(); run(() => client.connect(url.trim(), code.trim()), L.beaconPaired); } },
|
|
101
|
+
h("label", { className: "scalio-field-label", htmlFor: "beacon-url" }, L.beaconAddress),
|
|
102
|
+
h("input", { id: "beacon-url", className: "scalio-input", placeholder: "https://beacon.example.com", value: url, required: true,
|
|
103
|
+
onChange: (e) => setUrl(e.target.value), "data-testid": "beacon-url-input" }),
|
|
104
|
+
h("label", { className: "scalio-field-label", htmlFor: "beacon-code" }, L.beaconCode),
|
|
105
|
+
h("input", { id: "beacon-code", className: "scalio-input", placeholder: "bpc_…", value: code, required: true,
|
|
106
|
+
onChange: (e) => setCode(e.target.value), "data-testid": "beacon-code-input" }),
|
|
107
|
+
h("button", { type: "submit", className: "scalio-btn-primary", disabled: busy, "data-testid": "beacon-connect-btn" }, L.supportConnect)
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { peersApi, BeaconConnection };
|
package/react/index.js
CHANGED
|
@@ -378,6 +378,6 @@ module.exports = {
|
|
|
378
378
|
TEST_IDS,
|
|
379
379
|
initials,
|
|
380
380
|
};
|
|
381
|
-
Object.assign(module.exports, Nav, Blocks, require("./settings"), require("./support"), require("./security"), require("./license"), require("./palette"), require("./unify"), require("./profile"), require("./actions"));
|
|
381
|
+
Object.assign(module.exports, Nav, Blocks, require("./settings"), require("./support"), require("./beacon"), require("./security"), require("./license"), require("./palette"), require("./unify"), require("./profile"), require("./actions"));
|
|
382
382
|
// After the exports above: auth.js uses LanguageToggle and ThemeToggle from this module.
|
|
383
383
|
Object.assign(module.exports, require("./auth"));
|
package/react/labels.js
CHANGED
|
@@ -140,6 +140,19 @@ const LABELS = {
|
|
|
140
140
|
supportRemove: "Trennen",
|
|
141
141
|
supportRemoveConfirm: "Verbindung zum Support trennen? Neue Tickets warten dann, bis wieder verbunden ist.",
|
|
142
142
|
supportRemoved: "Verbindung getrennt.",
|
|
143
|
+
// Scalio Beacon (connected apps, Beacon phase 3)
|
|
144
|
+
beaconTitle: "Scalio Beacon",
|
|
145
|
+
beaconHelp: "Befunde dieser Installation gehen an Scalio Beacon und werden dort Tickets. Adresse und Kopplungscode erhalten Sie in Beacon unter Einstellungen -> Verbundene Apps.",
|
|
146
|
+
beaconHelpConnected: "Befunde dieser Installation gehen an Scalio Beacon und werden dort Tickets.",
|
|
147
|
+
beaconAddress: "Beacon-Adresse",
|
|
148
|
+
beaconCode: "Kopplungscode",
|
|
149
|
+
beaconConnected: "Verbunden",
|
|
150
|
+
beaconKeyLost: "Schlüssel nicht lesbar, bitte neu verbinden",
|
|
151
|
+
beaconLastContact: "Letzter Kontakt",
|
|
152
|
+
beaconPaired: "Mit Beacon verbunden.",
|
|
153
|
+
beaconChecked: "Verbindung funktioniert (Beacon {version}).",
|
|
154
|
+
beaconCheckFailed: "Keine Verbindung: {detail}",
|
|
155
|
+
beaconRemoveConfirm: "Verbindung zu Beacon trennen? Befunde gehen dann nicht mehr an Beacon; die Tickets dort bleiben.",
|
|
143
156
|
// Two-factor card (SecurityFactors) and license panel (LicensePanel)
|
|
144
157
|
mfa: {
|
|
145
158
|
backupTitle: "Ihre Backup-Codes",
|
|
@@ -384,6 +397,19 @@ const LABELS = {
|
|
|
384
397
|
supportRemove: "Disconnect",
|
|
385
398
|
supportRemoveConfirm: "Disconnect from support? New tickets then wait until the instance is connected again.",
|
|
386
399
|
supportRemoved: "Disconnected.",
|
|
400
|
+
// Scalio Beacon (connected apps, Beacon phase 3)
|
|
401
|
+
beaconTitle: "Scalio Beacon",
|
|
402
|
+
beaconHelp: "Findings of this installation go to Scalio Beacon and become tickets there. You get the address and the pairing code in Beacon under Settings -> Connected apps.",
|
|
403
|
+
beaconHelpConnected: "Findings of this installation go to Scalio Beacon and become tickets there.",
|
|
404
|
+
beaconAddress: "Beacon address",
|
|
405
|
+
beaconCode: "Pairing code",
|
|
406
|
+
beaconConnected: "Connected",
|
|
407
|
+
beaconKeyLost: "Key cannot be read, please connect again",
|
|
408
|
+
beaconLastContact: "Last contact",
|
|
409
|
+
beaconPaired: "Connected with Beacon.",
|
|
410
|
+
beaconChecked: "Connection works (Beacon {version}).",
|
|
411
|
+
beaconCheckFailed: "No connection: {detail}",
|
|
412
|
+
beaconRemoveConfirm: "Disconnect from Beacon? Findings no longer go to Beacon; the tickets there stay.",
|
|
387
413
|
mfa: {
|
|
388
414
|
backupTitle: "Your backup codes",
|
|
389
415
|
backupHint: "Each code works once, if you do not have your phone. Keep them in a safe place, for example your password manager. They are shown only now.",
|
package/react/nav.js
CHANGED
|
@@ -102,8 +102,8 @@ function SearchButton({ search, lang, labels, testIdPrefix, onNavigate }) {
|
|
|
102
102
|
|
|
103
103
|
// Sidebar. variant "desktop" shows from 768px, "drawer" is the same content inside NavDrawer.
|
|
104
104
|
// link: the router's NavLink (gets to, end, className(fn), onClick); without it plain <a> + activePath.
|
|
105
|
-
// pageActions: a
|
|
106
|
-
// Übersicht (the first section when it has no heading) then stays on top and does not scroll.
|
|
105
|
+
// pageActions: a slot for the page's actions (react/actions.js) right below Übersicht, desktop only.
|
|
106
|
+
// Übersicht (first item of the first section when it has no heading) then stays on top and does not scroll.
|
|
107
107
|
function Sidebar(props) {
|
|
108
108
|
const {
|
|
109
109
|
nav, link, activePath, top, search, primaryAction, footer, version, commit, lang, labels,
|
|
@@ -113,8 +113,10 @@ function Sidebar(props) {
|
|
|
113
113
|
const L = labelsFor(lang, labels);
|
|
114
114
|
const sections = visibleSections(nav);
|
|
115
115
|
const slot = !!pageActions && variant !== "drawer";
|
|
116
|
-
|
|
117
|
-
const
|
|
116
|
+
// Only Übersicht (first item of a first section without heading) is pinned; the rest of that section scrolls.
|
|
117
|
+
const first = sections[0];
|
|
118
|
+
const pinned = slot && first && !first.label ? Object.assign({}, first, { items: first.items.slice(0, 1) }) : null;
|
|
119
|
+
const rest = !pinned ? sections : first.items.length > 1 ? [Object.assign({}, first, { items: first.items.slice(1) })].concat(sections.slice(1)) : sections.slice(1);
|
|
118
120
|
const section = (sec, i) =>
|
|
119
121
|
h(
|
|
120
122
|
"div",
|