@scalio-cloud/ui 0.9.0 → 0.10.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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0
4
+ - `LicensePanel` takes more from an app's own `map`: a `hint` per usage bar (e.g. base + add-on seats), add-ons with
5
+ `badges`, `detail`, `tone` (`info`/`warn`) and `removable: false`, `addonErrors` (invalid add-ons, removable),
6
+ `addonsHidden`, `addonsReadOnly` and `keyReadOnly` (a note instead of the form) and a `copied` text per row.
7
+ Lets Nexus keep its trials, demo mode and badges on the shared panel. **No visible change** for apps that use
8
+ `mapScalioLicense`.
9
+
3
10
  ## 0.9.0
4
11
  - New `CommandPalette` (⌘K / Strg+K): finds every page of the sidebar data (`nav`) plus extra `pages`, remembers the
5
12
  last visited pages, and takes app commands as `groups` (Nexus: AI and guarded actions). Arrow keys, Enter, Esc,
package/README.md CHANGED
@@ -214,7 +214,8 @@ const license = licenseApi(api);
214
214
  `securityApi` reads snake_case and camelCase answers. `notify(text, tone)` gets `"success"` or `"error"`; without it
215
215
  the blocks show the message themselves. `confirm` is the app's dialog (`{title, action, danger}` -> Promise<boolean>),
216
216
  `window.confirm` otherwise. `formatError(err)` lets the app phrase API errors (for example license limit codes). A license backend with other routes passes its own `client` and a `map(license, {L})`
217
- that returns the view model described in `react/license.js`.
217
+ that returns the view model described in `react/license.js`. Nexus does this: its `map` adds trial badges, seat hints, invalid
218
+ add-ons and read-only notes (demo mode), and passes its trial notices as `statusExtra` and the reminder settings as `children`.
218
219
 
219
220
  ## Command palette (React)
220
221
 
package/nav.css CHANGED
@@ -400,3 +400,11 @@
400
400
  .scalio-palette { max-height: 85vh; }
401
401
  .scalio-palette-foot, .scalio-palette-enter { display: none; }
402
402
  }
403
+ .scalio-license-bar-hint { margin: 0.25rem 0 0; font-size: 0.6875rem; color: hsl(var(--primary)); }
404
+ .scalio-license-addon-name { display: flex; flex-wrap: wrap; align-items: center; gap: 0.375rem; }
405
+ .scalio-license-addon-name .scalio-badge { font-size: 10px; }
406
+ .scalio-license-addon.tone-info { border-color: hsl(var(--primary) / 0.35); background: hsl(var(--primary) / 0.05); }
407
+ .scalio-license-addon.tone-warn { border-color: #fcd34d; background: rgb(254 243 199 / 0.5); }
408
+ .dark .scalio-license-addon.tone-warn { border-color: rgb(251 191 36 / 0.4); background: rgb(251 191 36 / 0.08); }
409
+ .scalio-license-addon-error { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
410
+ .scalio-license-link { padding: 0; border: 0; background: none; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalio-cloud/ui",
3
- "version": "0.9.0",
3
+ "version": "0.10.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/license.js CHANGED
@@ -36,8 +36,11 @@ function licenseApi(api) {
36
36
  }
37
37
 
38
38
  // View model:
39
- // { edition, licensed, customer, managed, messages[], rows[{label, value, tone, mono, copy, testId}],
40
- // features[{key, label}] | null, usage[{key, label, used, max}], addons[{id, name, detail, included}], addonErrors[{key, error}], keyPresent }
39
+ // { edition, licensed, customer, managed, messages[], rows[{label, value, tone, mono, copy, copied, testId}],
40
+ // features[{key, label}] | null, usage[{key, label, used, max, hint}],
41
+ // addons[{id, name, seats, exp, detail, badges[{label, tone}], tone, removable}], addonErrors[{key, error, addon}], keyPresent,
42
+ // addonsHidden, addonsReadOnly (note instead of the add-on form), keyReadOnly (note instead of the key form) }
43
+ // Optional fields let an app with its own license model (Nexus: trials, demo) show more without its own panel.
41
44
  function mapScalioLicense(lic, opts) {
42
45
  const o = opts || {};
43
46
  const L = o.L || labelsFor(o.lang).license;
@@ -94,7 +97,8 @@ function UsageBar({ item }) {
94
97
  h("span", { className: "scalio-license-bar-label" }, item.label),
95
98
  h("span", { className: cx("scalio-license-bar-value", full && "is-error") }, used === null ? "-" : used, " / ", unlimited ? "∞" : item.max)
96
99
  ),
97
- h("div", { className: "scalio-license-bar" }, h("div", { className: cx(full && "is-full", unlimited && "is-unlimited"), style: { width: `${pct}%` } }))
100
+ h("div", { className: "scalio-license-bar" }, h("div", { className: cx(full && "is-full", unlimited && "is-unlimited"), style: { width: `${pct}%` } })),
101
+ item.hint ? h("p", { className: "scalio-license-bar-hint", "data-testid": `license-limit-${item.key}-hint` }, item.hint) : null
98
102
  );
99
103
  }
100
104
 
@@ -155,10 +159,10 @@ function LicensePanel(props) {
155
159
  const addAddon = async () => {
156
160
  if (await run(() => client.addAddon(addon.trim()), L.addonActivated)) setAddon("");
157
161
  };
158
- const copy = async (value) => {
162
+ const copy = async (value, copied) => {
159
163
  try {
160
164
  await navigator.clipboard.writeText(value);
161
- say(L.idCopied);
165
+ say(copied || L.idCopied);
162
166
  } catch (e) {
163
167
  say(L.copyFailed, "error");
164
168
  }
@@ -170,7 +174,7 @@ function LicensePanel(props) {
170
174
  { key: r.label + i },
171
175
  h("dt", null, r.label),
172
176
  r.copy
173
- ? h("dd", null, h("button", { type: "button", className: "scalio-license-copy", title: L.copy, onClick: () => copy(r.value), "data-testid": r.testId },
177
+ ? h("dd", null, h("button", { type: "button", className: "scalio-license-copy", title: L.copy, onClick: () => copy(r.value, r.copied), "data-testid": r.testId },
174
178
  r.value, h(Icons.Copy, { className: "scalio-icon", width: 12, height: 12 })))
175
179
  : h("dd", { className: cx(r.mono && "is-mono"), "data-testid": r.testId }, r.tone ? h("span", { className: `is-${r.tone}` }, r.value) : r.value)
176
180
  );
@@ -219,7 +223,7 @@ function LicensePanel(props) {
219
223
  ? h("div", { className: "scalio-license-usage" }, h("div", { className: "scalio-license-sub" }, L.usage), m.usage.map((u) => h(UsageBar, { key: u.key, item: u })))
220
224
  : null
221
225
  ),
222
- h(
226
+ m.addonsHidden ? null : h(
223
227
  "div",
224
228
  { className: "scalio-license-part", "data-testid": "license-addons" },
225
229
  h("div", { className: "scalio-license-sub" }, h(Icons.Puzzle, { className: "scalio-icon", width: 14, height: 14 }), L.addons),
@@ -230,16 +234,23 @@ function LicensePanel(props) {
230
234
  m.addons.map((a) =>
231
235
  h(
232
236
  "div",
233
- { key: a.id, className: "scalio-license-addon", "data-testid": `addon-card-${a.id}` },
237
+ { key: a.id, className: cx("scalio-license-addon", a.tone && `tone-${a.tone}`), "data-testid": `addon-card-${a.id}` },
234
238
  h(
235
239
  "div",
236
240
  { style: { minWidth: 0 } },
237
- h("div", { className: "scalio-license-addon-name" }, a.name),
241
+ h(
242
+ "div",
243
+ { className: "scalio-license-addon-name" },
244
+ a.name,
245
+ (a.badges || []).map((b) =>
246
+ h("span", { key: b.label, className: cx("scalio-badge", b.tone && `is-${b.tone}`), "data-testid": b.testId }, b.label))
247
+ ),
238
248
  h("p", { className: "scalio-license-addon-detail" },
239
- a.seats ? `+${a.seats}` : h("span", { className: "is-ok" }, L.included),
240
- a.exp ? ` · ${L.maintenanceUntil} ${a.exp}` : "")
249
+ a.detail !== undefined
250
+ ? a.detail
251
+ : [a.seats ? `+${a.seats}` : h("span", { key: "i", className: "is-ok" }, L.included), a.exp ? ` · ${L.maintenanceUntil} ${a.exp}` : ""])
241
252
  ),
242
- m.managed
253
+ m.managed || m.addonsReadOnly || a.removable === false
243
254
  ? null
244
255
  : h("button", { type: "button", className: "scalio-btn is-ghost is-danger", disabled: busy, title: L.remove, "aria-label": L.remove,
245
256
  "data-testid": `addon-remove-${a.id}`, onClick: () => run(() => client.removeAddon(a), L.addonRemoved) },
@@ -248,8 +259,20 @@ function LicensePanel(props) {
248
259
  )
249
260
  )
250
261
  : h("p", { className: "scalio-license-empty" }, L.noAddons),
251
- m.addonErrors.map((e) => h("div", { key: e.key, className: "scalio-license-addon-error" }, e.error)),
252
- m.managed
262
+ m.addonErrors.map((e) =>
263
+ h(
264
+ "div",
265
+ { key: e.key, className: "scalio-license-addon-error", "data-testid": e.testId },
266
+ h("span", null, e.error),
267
+ e.addon && !m.managed && !m.addonsReadOnly
268
+ ? h("button", { type: "button", className: "scalio-license-link", disabled: busy, "data-testid": `addon-remove-${e.addon.id}`,
269
+ onClick: () => run(() => client.removeAddon(e.addon), L.addonRemoved) }, L.remove)
270
+ : null
271
+ )
272
+ ),
273
+ m.addonsReadOnly
274
+ ? h("div", { className: "scalio-notice is-info", "data-testid": "license-addon-readonly" }, m.addonsReadOnly)
275
+ : m.managed
253
276
  ? null
254
277
  : h(
255
278
  "div",
@@ -261,7 +284,9 @@ function LicensePanel(props) {
261
284
  h(Icons.Plus, { className: "scalio-icon" }), L.add))
262
285
  )
263
286
  ),
264
- m.managed
287
+ m.keyReadOnly && !m.managed
288
+ ? h("div", { className: "scalio-license-part" }, h("div", { className: "scalio-notice is-info", "data-testid": "license-key-readonly" }, m.keyReadOnly))
289
+ : m.managed
265
290
  ? null
266
291
  : h(
267
292
  "div",