@scalio-cloud/ui 0.10.0 → 0.12.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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0
4
+ - New manual button in `AppHeader` (prop `docsUrl`): a book icon between theme and support that opens the product
5
+ manual in a new tab; `DocsButton` on its own, `manualUrl(product, { stage, lang })` builds the address
6
+ (`devdocs.<domain>` on dev and test, `docs.<domain>` on production, demo and self-hosted, German under `/de/`).
7
+ **Visible change** only in apps that pass `docsUrl`; no token changes.
8
+
9
+ ## 0.11.0
10
+ - Self-hosted support (ADR 0022, T11; needs scalio-connect 0.7.0 in the backend): `supportApi` gets `status()` and
11
+ `setButtonHidden(hidden)`. The support drawer asks `status()` first; a self-hosted installation without a
12
+ connection shows no form, only a note and the button "Zum Scalio-Portal" (`<portal>/support`). The card
13
+ "Support-Verbindung" gets the switch "Support-Knopf in der Kopfzeile ausblenden". **Visible change** only where
14
+ the backend reports `link_only` or the client has `setButtonHidden`; no token changes.
15
+
3
16
  ## 0.10.0
4
17
  - `LicensePanel` takes more from an app's own `map`: a `hint` per usage bar (e.g. base + add-on seats), add-ons with
5
18
  `badges`, `detail`, `tone` (`info`/`warn`) and `removable: false`, `addonErrors` (invalid add-ons, removable),
package/README.md CHANGED
@@ -45,7 +45,7 @@ support, avatar.
45
45
  A part only appears when the app passes its handler (Compass has no dark mode, so no `onThemeChange`).
46
46
 
47
47
  ```jsx
48
- import { AppHeader, PageHeader, VersionTag } from "@scalio-cloud/ui/react";
48
+ import { AppHeader, PageHeader, VersionTag, manualUrl } from "@scalio-cloud/ui/react";
49
49
 
50
50
  <AppHeader
51
51
  brand={{ mark: "N", name: "Scalio Nexus", sub: "Dev" }} // or your own element
@@ -55,6 +55,7 @@ import { AppHeader, PageHeader, VersionTag } from "@scalio-cloud/ui/react";
55
55
  status={<ApiStatus />} // hidden below 768px
56
56
  lang={lang} onLangChange={setLang} // one click, shows the other language
57
57
  theme={theme} themeModes={["light", "dark", "system"]} onThemeChange={setTheme}
58
+ docsUrl={manualUrl("nexus", { stage, lang })} // book icon, opens the manual in a new tab
58
59
  onSupport={() => setSupportOpen(true)}
59
60
  user={{ name, email, role }} onLogout={logout}
60
61
  menuItems={[{ label: "Profil", icon: User, onSelect: () => nav("/profile"), testId: "profile-link" }]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalio-cloud/ui",
3
- "version": "0.10.0",
3
+ "version": "0.12.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
@@ -32,6 +32,7 @@ const LifeBuoy = icon([
32
32
  ["circle", { cx: 12, cy: 12, r: 4 }],
33
33
  ["path", { d: "m4.93 4.93 4.24 4.24M14.83 9.17l4.24-4.24M14.83 14.83l4.24 4.24M9.17 14.83l-4.24 4.24" }],
34
34
  ]);
35
+ const BookOpen = icon([["path", { d: "M12 7v14" }], ["path", { d: "M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" }]]);
35
36
  const LogOut = icon([["path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9" }]]);
36
37
  const Menu = icon([["path", { d: "M4 6h16M4 12h16M4 18h16" }]]);
37
38
  const Check = icon([["path", { d: "M20 6 9 17l-5-5" }]]);
@@ -99,5 +100,5 @@ const History = icon([
99
100
  ]);
100
101
  const CornerDownLeft = icon([["path", { d: "M20 4v7a4 4 0 0 1-4 4H4" }], ["path", { d: "m9 10-5 5 5 5" }]]);
101
102
 
102
- module.exports = { Sun, Moon, Monitor, LifeBuoy, LogOut, Menu, Check, X, Search, ChevronDown, Fingerprint, FileStack, ShieldCheck, Hammer, Activity,
103
+ module.exports = { Sun, Moon, Monitor, LifeBuoy, BookOpen, LogOut, Menu, Check, X, Search, ChevronDown, Fingerprint, FileStack, ShieldCheck, Hammer, Activity,
103
104
  Smartphone, KeyRound, Trash, Copy, Puzzle, Plus, Alert, History, CornerDownLeft };
package/react/index.js CHANGED
@@ -10,6 +10,7 @@ const TEST_IDS = {
10
10
  menu: "mobile-menu-btn",
11
11
  language: "language-toggle-btn",
12
12
  theme: "theme-toggle-btn",
13
+ docs: "docs-link-btn",
13
14
  support: "support-ticket-trigger",
14
15
  avatar: "user-avatar-btn",
15
16
  userName: "user-name",
@@ -233,6 +234,43 @@ function SupportButton({ onClick, lang, labels, testId }) {
233
234
  );
234
235
  }
235
236
 
237
+ // The customer manual of a product: devdocs.<domain> on our dev and test instances, docs.<domain> everywhere
238
+ // else (production, demo, self-hosted). German readers get the German version under /de/.
239
+ const DOCS_DOMAINS = {
240
+ nexus: "scalionexus.com",
241
+ citadel: "scaliocitadel.com",
242
+ foundry: "scaliofoundry.com",
243
+ rosetta: "scaliorosetta.com",
244
+ };
245
+
246
+ function manualUrl(product, opts) {
247
+ const o = opts || {};
248
+ const domain = DOCS_DOMAINS[product] || product;
249
+ if (!domain || domain.indexOf(".") < 0) return null;
250
+ const stage = String(o.stage || "").toLowerCase();
251
+ const host = (stage === "dev" || stage === "test" ? "devdocs." : "docs.") + domain;
252
+ return `https://${host}/${o.lang === "de" ? "de/" : ""}`;
253
+ }
254
+
255
+ // Opens the manual in a new tab.
256
+ function DocsButton({ href, lang, labels, testId }) {
257
+ if (!href) return null;
258
+ const L = labelsFor(lang, labels);
259
+ return h(
260
+ "a",
261
+ {
262
+ className: "scalio-btn scalio-btn-icon",
263
+ href,
264
+ target: "_blank",
265
+ rel: "noopener noreferrer",
266
+ title: L.docs,
267
+ "aria-label": L.docs,
268
+ "data-testid": testId || TEST_IDS.docs,
269
+ },
270
+ h(Icons.BookOpen, { className: "scalio-icon" })
271
+ );
272
+ }
273
+
236
274
  function Brand({ brand }) {
237
275
  if (!brand) return null;
238
276
  if (React.isValidElement(brand)) return brand;
@@ -249,7 +287,7 @@ function Brand({ brand }) {
249
287
  return h("div", { className: "scalio-brand" }, inner);
250
288
  }
251
289
 
252
- // Header: [menu] brand, left slot ... app actions, status, language, theme, support, avatar.
290
+ // Header: [menu] brand, left slot ... app actions, status, language, theme, manual, support, avatar.
253
291
  // The part from status to avatar has the same order in every app.
254
292
  function AppHeader(props) {
255
293
  const L = labelsFor(props.lang, props.labels);
@@ -284,6 +322,7 @@ function AppHeader(props) {
284
322
  labels: props.labels,
285
323
  testId: T.theme,
286
324
  }),
325
+ h(DocsButton, { href: props.docsUrl, lang: props.lang, labels: props.labels, testId: T.docs }),
287
326
  h(SupportButton, { onClick: props.onSupport, lang: props.lang, labels: props.labels, testId: T.support }),
288
327
  props.user
289
328
  ? h(UserMenu, {
@@ -326,6 +365,9 @@ module.exports = {
326
365
  LanguageToggle,
327
366
  ThemeToggle,
328
367
  SupportButton,
368
+ DocsButton,
369
+ manualUrl,
370
+ DOCS_DOMAINS,
329
371
  PageHeader,
330
372
  VersionTag,
331
373
  Icons,
package/react/labels.js CHANGED
@@ -8,6 +8,7 @@ const LABELS = {
8
8
  dark: "Dunkel",
9
9
  system: "System",
10
10
  support: "Support",
11
+ docs: "Handbuch (öffnet in neuem Tab)",
11
12
  logout: "Abmelden",
12
13
  userMenu: "Benutzermenü",
13
14
  menu: "Menü",
@@ -88,6 +89,12 @@ const LABELS = {
88
89
  supportConnStatus: { unpaired: "Nicht verbunden", pending: "Wartet auf Bestätigung", active: "Verbunden", blocked: "Gesperrt" },
89
90
  supportConnHelp: "Tickets aus dieser Installation gehen an den Scalio Support. Den einmaligen Code erhalten Sie im Scalio-Portal unter Support -> Verbundene Apps.",
90
91
  supportConnHelpConnected: "Tickets aus dieser Installation gehen an den Scalio Support.",
92
+ supportLinkOnly: "Diese Installation ist nicht mit dem Scalio Support verbunden. Eröffnen Sie Ihre Anfrage bitte im Scalio-Portal.",
93
+ supportLinkOnlyAdmin: "Ein Admin kann die Verbindung unter Einstellungen -> Support-Verbindung einrichten, dann gehen Anfragen direkt aus der App.",
94
+ supportOpenPortal: "Zum Scalio-Portal",
95
+ supportButtonHidden: "Support-Knopf in der Kopfzeile ausblenden",
96
+ supportButtonHiddenNote: "Der Support-Knopf ist ausgeblendet.",
97
+ supportButtonShownNote: "Der Support-Knopf ist wieder sichtbar.",
91
98
  supportConnPending: "Ein Administrator Ihrer Organisation muss die Verbindung im Scalio-Portal bestätigen.",
92
99
  supportConnBlocked: "Diese Installation ist für den Support gesperrt. Bitte wenden Sie sich an Scalio.",
93
100
  supportConnKeyLost: "Der Schlüssel dieser Verbindung kann nicht mehr gelesen werden. Bitte verbinden Sie neu.",
@@ -202,6 +209,7 @@ const LABELS = {
202
209
  dark: "Dark",
203
210
  system: "System",
204
211
  support: "Support",
212
+ docs: "Manual (opens in a new tab)",
205
213
  logout: "Sign out",
206
214
  userMenu: "User menu",
207
215
  menu: "Menu",
@@ -277,6 +285,12 @@ const LABELS = {
277
285
  supportConnStatus: { unpaired: "Not connected", pending: "Waiting for confirmation", active: "Connected", blocked: "Blocked" },
278
286
  supportConnHelp: "Tickets from this installation go to Scalio Support. You get the one-time code in the Scalio portal under Support -> Connected apps.",
279
287
  supportConnHelpConnected: "Tickets from this installation go to Scalio Support.",
288
+ supportLinkOnly: "This installation is not connected to Scalio Support. Please open your request in the Scalio portal.",
289
+ supportLinkOnlyAdmin: "An admin can set up the connection under Settings -> Support connection; then requests go straight from the app.",
290
+ supportOpenPortal: "Open the Scalio portal",
291
+ supportButtonHidden: "Hide the support button in the header",
292
+ supportButtonHiddenNote: "The support button is hidden.",
293
+ supportButtonShownNote: "The support button is visible again.",
280
294
  supportConnPending: "An administrator of your organisation must confirm the connection in the Scalio portal.",
281
295
  supportConnBlocked: "This installation is blocked for support. Please contact Scalio.",
282
296
  supportConnKeyLost: "The key of this connection can no longer be read. Please connect again.",
package/react/support.js CHANGED
@@ -29,6 +29,8 @@ function supportApi(api) {
29
29
  connect: (portal_url, code) => data(api.post("/support/connection", { portal_url, code })),
30
30
  check: () => data(api.post("/support/connection/check")),
31
31
  disconnect: () => data(api.delete("/support/connection")),
32
+ status: () => data(api.get("/support/status")),
33
+ setButtonHidden: (hidden) => data(api.put("/support/connection/button", { hidden })),
32
34
  };
33
35
  }
34
36
 
@@ -223,8 +225,27 @@ function TicketDetail({ client, id, L, lang, onBack }) {
223
225
  );
224
226
  }
225
227
 
228
+ // Self-hosted without a connection (ADR 0022, T11): no form, only the way to the portal's support page.
229
+ function PortalLink({ status, L }) {
230
+ return h(
231
+ "div",
232
+ { className: "scalio-support-form", "data-testid": "support-link-only" },
233
+ h("p", { className: "scalio-support-meta" }, L.supportLinkOnly),
234
+ h("a", { className: "scalio-btn-primary", href: status.portal_page, target: "_blank", rel: "noopener noreferrer",
235
+ "data-testid": "support-portal-link" }, L.supportOpenPortal),
236
+ h("p", { className: "scalio-support-meta" }, L.supportLinkOnlyAdmin)
237
+ );
238
+ }
239
+
226
240
  function SupportDesk({ client, page, lang, labels }) {
227
241
  const L = labelsFor(lang, labels);
242
+ const [status, setStatus] = React.useState(client.status ? null : {});
243
+ React.useEffect(() => {
244
+ if (!client.status) return undefined;
245
+ let alive = true;
246
+ client.status().then((d) => alive && setStatus(d || {})).catch(() => alive && setStatus({}));
247
+ return () => { alive = false; };
248
+ }, [client]);
228
249
  const [tab, setTab] = React.useState("new");
229
250
  const [ticket, setTicket] = React.useState(null);
230
251
  const [note, setNote] = React.useState("");
@@ -233,6 +254,8 @@ function SupportDesk({ client, page, lang, labels }) {
233
254
  type: "button", role: "tab", "aria-selected": tab === key, className: cx("scalio-support-tab", tab === key && "is-active"),
234
255
  onClick: () => { setTab(key); setTicket(null); setNote(""); }, "data-testid": `support-tab-${key}`,
235
256
  }, label);
257
+ if (status === null) return h("div", { className: "scalio-support-meta" }, L.loading);
258
+ if (status.link_only && status.portal_page) return h(PortalLink, { status, L });
236
259
  return h(
237
260
  React.Fragment,
238
261
  null,
@@ -313,6 +336,15 @@ function SupportConnection({ client, lang, labels, onChange }) {
313
336
  : null,
314
337
  note ? h("div", { className: "scalio-notice is-ok", "data-testid": "support-note" }, note) : null,
315
338
  error ? h("div", { className: "scalio-notice is-error", role: "alert", "data-testid": "support-error" }, error) : null,
339
+ client.setButtonHidden
340
+ ? h(
341
+ "label",
342
+ { className: "scalio-support-check", "data-testid": "support-button-hidden" },
343
+ h("input", { type: "checkbox", checked: !!st.button_hidden, disabled: busy,
344
+ onChange: (e) => { const hide = e.target.checked; run(() => client.setButtonHidden(hide), hide ? L.supportButtonHiddenNote : L.supportButtonShownNote); } }),
345
+ h("span", null, L.supportButtonHidden)
346
+ )
347
+ : null,
316
348
  connected
317
349
  ? h(
318
350
  "div",
package/shell.css CHANGED
@@ -45,6 +45,7 @@
45
45
  }
46
46
  @media (min-width: 640px) { .scalio-btn { padding: 0 1rem; } }
47
47
  .scalio-btn:hover { background: hsl(var(--secondary)); }
48
+ a.scalio-btn { text-decoration: none; }
48
49
  .scalio-btn:focus-visible, .scalio-avatar:focus-visible, .scalio-menu-item:focus-visible {
49
50
  outline: 2px solid hsl(var(--ring)); outline-offset: 1px;
50
51
  }