@cosmicdrift/kumiko-renderer-web 0.351.0 → 0.352.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.
|
@@ -225,12 +225,17 @@ function readDelta(panel, record) {
|
|
|
225
225
|
const tone = readTone(panel.deltaToneField !== undefined ? record[panel.deltaToneField] : undefined);
|
|
226
226
|
return { value: String(value), direction, ...(tone !== undefined && { tone }) };
|
|
227
227
|
}
|
|
228
|
+
function labeledGroupColumns(valueCount) {
|
|
229
|
+
if (valueCount <= 1)
|
|
230
|
+
return "";
|
|
231
|
+
return valueCount === 2 ? "sm:grid-cols-2" : "sm:grid-cols-3";
|
|
232
|
+
}
|
|
228
233
|
function StatGroupPanelBody({ panel, label, screenId, screenParams, translate, }) {
|
|
229
234
|
const testId = `dashboard-panel-${panel.id}`;
|
|
230
235
|
if (label === undefined) {
|
|
231
236
|
return (_jsx("div", { "data-testid": testId, className: "grid grid-cols-2 gap-y-2 rounded-lg border border-border bg-card py-1 lg:auto-cols-fr lg:grid-flow-col lg:divide-x lg:divide-border-row", children: panel.stats.map((stat) => (_jsx(StatPanelBody, { panel: stat, label: translate(stat.label), screenId: screenId, screenParams: screenParams, translate: translate, variant: "strip" }, stat.id))) }));
|
|
232
237
|
}
|
|
233
|
-
return (_jsx(SectionCard, { title: label, ...(panel.subtitle !== undefined && { subtitle: translate(panel.subtitle) }), testId: testId, children: _jsx("section", { className:
|
|
238
|
+
return (_jsx(SectionCard, { title: label, ...(panel.subtitle !== undefined && { subtitle: translate(panel.subtitle) }), testId: testId, children: _jsx("section", { className: `grid grid-cols-1 gap-3 ${labeledGroupColumns(panel.stats.length)}`, children: panel.stats.map((stat) => (_jsx(StatPanelBody, { panel: stat, label: translate(stat.label), screenId: screenId, screenParams: screenParams, translate: translate, variant: "card" }, stat.id))) }) }));
|
|
234
239
|
}
|
|
235
240
|
function markerKindColor(kind) {
|
|
236
241
|
if (kind === undefined)
|
package/dist/layout/nav-tree.js
CHANGED
|
@@ -468,11 +468,12 @@ export function buildNavRegistrySliceForApp(app, allowedNavQns) {
|
|
|
468
468
|
});
|
|
469
469
|
}
|
|
470
470
|
}
|
|
471
|
+
const existing = dropNavsWithNonexistentParent(qualified);
|
|
471
472
|
// Workspace filter: drop nav entries whose qualified id isn't in the
|
|
472
473
|
// allow-set. A child whose parent gets dropped surfaces as a top-level
|
|
473
474
|
// entry — the workspace owner should list parents explicitly if they
|
|
474
475
|
// want the grouping preserved.
|
|
475
|
-
const filtered = allowedNavQns !== undefined ?
|
|
476
|
+
const filtered = allowedNavQns !== undefined ? existing.filter((n) => allowedNavQns.has(n.id)) : existing;
|
|
476
477
|
const allowedQnSet = new Set(filtered.map((n) => n.id));
|
|
477
478
|
const topLevel = [];
|
|
478
479
|
const byParentMap = new Map();
|
|
@@ -495,6 +496,18 @@ export function buildNavRegistrySliceForApp(app, allowedNavQns) {
|
|
|
495
496
|
byParent: (parent) => byParentMap.get(parent) ?? [],
|
|
496
497
|
};
|
|
497
498
|
}
|
|
499
|
+
// A parent absent from the (role-projected) schema is not a workspace
|
|
500
|
+
// filter result: the child must not leak to the top level. Transitive.
|
|
501
|
+
function dropNavsWithNonexistentParent(navs) {
|
|
502
|
+
let current = [...navs];
|
|
503
|
+
for (;;) {
|
|
504
|
+
const ids = new Set(current.map((n) => n.id));
|
|
505
|
+
const kept = current.filter((n) => n.parent === undefined || ids.has(n.parent));
|
|
506
|
+
if (kept.length === current.length)
|
|
507
|
+
return current;
|
|
508
|
+
current = kept;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
498
511
|
// Exported (not part of the package's public index.ts surface) so other
|
|
499
512
|
// modules under layout/ — e.g. filter-app-schema-navs.ts — qualify nav ids
|
|
500
513
|
// the same way instead of keeping a second copy in sync.
|
|
@@ -6,6 +6,8 @@ export type ComboboxOption = {
|
|
|
6
6
|
readonly description?: string;
|
|
7
7
|
/** Group heading; see `groupOptions` for the ordering rule. */
|
|
8
8
|
readonly group?: string;
|
|
9
|
+
/** Visible but not choosable. */
|
|
10
|
+
readonly disabled?: boolean;
|
|
9
11
|
};
|
|
10
12
|
type ComboboxBaseProps = {
|
|
11
13
|
readonly id: string;
|
|
@@ -105,7 +105,7 @@ export function ComboboxInput(props) {
|
|
|
105
105
|
const isSelected = multiple
|
|
106
106
|
? selectedValues.has(opt.value)
|
|
107
107
|
: opt.value === singleValue;
|
|
108
|
-
return (_jsxs(Command.Item, { "data-testid": `combobox-${id}-option-${opt.value}`, value: opt.label, keywords: opt.description !== undefined ? [opt.description] : undefined, onSelect: () => {
|
|
108
|
+
return (_jsxs(Command.Item, { "data-testid": `combobox-${id}-option-${opt.value}`, value: opt.label, keywords: opt.description !== undefined ? [opt.description] : undefined, disabled: opt.disabled, onSelect: () => {
|
|
109
109
|
if (props.multiple === true) {
|
|
110
110
|
toggleMulti(opt.value);
|
|
111
111
|
}
|
|
@@ -117,7 +117,7 @@ export function ComboboxInput(props) {
|
|
|
117
117
|
// No pointer-events toggles on the item: with Tailwind active,
|
|
118
118
|
// `data-[disabled]:pointer-events-none` silently swallowed mouse
|
|
119
119
|
// clicks (keyboard select still worked, jsdom tests stayed green).
|
|
120
|
-
className: "relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground", children: [isSelected && (_jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(Check, { className: "h-4 w-4" }) })), _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { children: opt.label }), opt.description !== undefined && (_jsx("span", { className: "text-xs text-muted-foreground", children: opt.description }))] })] }, opt.value));
|
|
120
|
+
className: cn("relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground", opt.disabled === true && "cursor-not-allowed opacity-50"), children: [isSelected && (_jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(Check, { className: "h-4 w-4" }) })), _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { children: opt.label }), opt.description !== undefined && (_jsx("span", { className: "text-xs text-muted-foreground", children: opt.description }))] })] }, opt.value));
|
|
121
121
|
});
|
|
122
122
|
return section.heading === undefined ? (items) : (_jsx(Command.Group, { heading: section.heading, className: "[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", children: items }, section.heading));
|
|
123
123
|
})] }), onCreate !== undefined && (_jsxs("button", { type: "button", "data-testid": `combobox-${id}-create`, onClick: () => {
|
package/dist/primitives/index.js
CHANGED
|
@@ -257,21 +257,30 @@ function SegmentedSelect({ id, name, value, onChange, options, disabled, require
|
|
|
257
257
|
onChange(target.value);
|
|
258
258
|
buttonRefs.current[index]?.focus();
|
|
259
259
|
};
|
|
260
|
+
// Arrow keys skip disabled segments; with none enabled they stay put.
|
|
261
|
+
const nextEnabledIndex = (from, step) => {
|
|
262
|
+
for (let hop = 1; hop <= options.length; hop++) {
|
|
263
|
+
const candidate = (from + step * hop + options.length * hop) % options.length;
|
|
264
|
+
if (options[candidate]?.disabled !== true)
|
|
265
|
+
return candidate;
|
|
266
|
+
}
|
|
267
|
+
return from;
|
|
268
|
+
};
|
|
260
269
|
const handleKeyDown = (e, index) => {
|
|
261
270
|
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
|
262
271
|
e.preventDefault();
|
|
263
|
-
selectAt((index
|
|
272
|
+
selectAt(nextEnabledIndex(index, 1));
|
|
264
273
|
}
|
|
265
274
|
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
|
266
275
|
e.preventDefault();
|
|
267
|
-
selectAt((index -
|
|
276
|
+
selectAt(nextEnabledIndex(index, -1));
|
|
268
277
|
}
|
|
269
278
|
};
|
|
270
279
|
return (_jsxs("div", { role: "radiogroup", "aria-labelledby": fieldLabelId(id), "aria-required": required, "aria-invalid": hasError === true ? true : undefined, "data-testid": `segmented-${id}`, className: cn("inline-flex min-h-9 w-fit flex-wrap overflow-hidden rounded-md border", hasError === true ? "border-destructive" : "border-input"), children: [_jsx("input", { type: "hidden", name: name, value: value }), options.map((opt, index) => {
|
|
271
280
|
const checked = opt.value === value;
|
|
272
281
|
return (_jsx("button", { ref: (node) => {
|
|
273
282
|
buttonRefs.current[index] = node;
|
|
274
|
-
}, type: "button", role: "radio", "aria-checked": checked, tabIndex: index === tabStopIndex ? 0 : -1, disabled: disabled, "data-testid": `segmented-${id}-${opt.value}`, onClick: () => onChange(opt.value), onKeyDown: (e) => handleKeyDown(e, index), className: cn(
|
|
283
|
+
}, type: "button", role: "radio", "aria-checked": checked, tabIndex: index === tabStopIndex ? 0 : -1, disabled: disabled === true || opt.disabled === true, "data-testid": `segmented-${id}-${opt.value}`, onClick: () => onChange(opt.value), onKeyDown: (e) => handleKeyDown(e, index), className: cn(
|
|
275
284
|
// Per-segment top/left border instead of the container's
|
|
276
285
|
// `divide-x`: divide-x only draws verticals between siblings
|
|
277
286
|
// in source order, which misplaces borders the moment a
|
|
@@ -302,7 +311,7 @@ function RadioListSelect({ id, name, value, onChange, options, variant = "list",
|
|
|
302
311
|
const descriptionIdOf = (opt) => `${id}-option-${options.findIndex((o) => o.value === opt.value)}-description`;
|
|
303
312
|
return (_jsx("div", { role: "radiogroup", "aria-labelledby": fieldLabelId(id), "aria-required": required, "aria-invalid": hasError === true ? true : undefined, "data-testid": `radio-list-${id}`, "data-radio-list": "", className: "flex flex-col gap-2", children: groupOptions(options).map((section) => (_jsxs(Fragment, { children: [section.heading !== undefined && (_jsx("div", { "data-testid": `radio-list-${id}-group-${section.heading}`, className: "mt-1 text-xs font-medium text-muted-foreground", children: section.heading })), section.options.map((opt) => (_jsxs("label", { className: card
|
|
304
313
|
? RADIO_CARD_CLASS
|
|
305
|
-
: "flex items-start gap-2 text-sm font-normal text-foreground", children: [_jsx("input", { type: "radio", name: name, value: opt.value, checked: opt.value === value, disabled: disabled, "aria-describedby": opt.description !== undefined ? descriptionIdOf(opt) : undefined, "data-testid": `radio-list-${id}-${opt.value}`, onChange: () => onChange(opt.value), className: cn("size-4 accent-primary", card ? "mt-0.5 shrink-0" : "mt-0.5") }), _jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: cn(card && "font-semibold"), children: opt.label }), opt.description !== undefined && (_jsx("span", { id: descriptionIdOf(opt), className: "text-[13px] text-foreground-secondary", children: opt.description }))] })] }, opt.value)))] }, section.heading ?? ""))) }));
|
|
314
|
+
: "flex items-start gap-2 text-sm font-normal text-foreground has-[:disabled]:opacity-50", children: [_jsx("input", { type: "radio", name: name, value: opt.value, checked: opt.value === value, disabled: disabled === true || opt.disabled === true, "aria-describedby": opt.description !== undefined ? descriptionIdOf(opt) : undefined, "data-testid": `radio-list-${id}-${opt.value}`, onChange: () => onChange(opt.value), className: cn("size-4 accent-primary", card ? "mt-0.5 shrink-0" : "mt-0.5") }), _jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: cn(card && "font-semibold"), children: opt.label }), opt.description !== undefined && (_jsx("span", { id: descriptionIdOf(opt), className: "text-[13px] text-foreground-secondary", children: opt.description }))] })] }, opt.value)))] }, section.heading ?? ""))) }));
|
|
306
315
|
}
|
|
307
316
|
// The row count reaches the textarea through an untyped view-model hint, so a
|
|
308
317
|
// sloppy schema can hand over a fraction or a zero — neither renders a sane
|
|
@@ -318,6 +327,16 @@ function normalizedTextareaRows(rows) {
|
|
|
318
327
|
function textareaMinHeight(rows) {
|
|
319
328
|
return { minHeight: `calc(${rows} * 1lh + 1rem + 2px)` };
|
|
320
329
|
}
|
|
330
|
+
function withInlineDisabledHint(option) {
|
|
331
|
+
if (option.disabled !== true || option.description === undefined)
|
|
332
|
+
return option;
|
|
333
|
+
return {
|
|
334
|
+
value: option.value,
|
|
335
|
+
label: `${option.label} (${option.description})`,
|
|
336
|
+
disabled: true,
|
|
337
|
+
...(option.group !== undefined && { group: option.group }),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
321
340
|
function DefaultInput(props) {
|
|
322
341
|
// Vendored ui/input + ui/checkbox stylen Fehler über `aria-invalid`
|
|
323
342
|
// selbst — kein manuelles border-destructive mehr nötig.
|
|
@@ -365,6 +384,8 @@ function DefaultInput(props) {
|
|
|
365
384
|
// 4-Item-Status-Selects vorhanden, das ist eine bewusst akzeptierte
|
|
366
385
|
// UX-Konsequenz für Style-Konsistenz.
|
|
367
386
|
const comboOptions = props.options.map((o) => typeof o === "string" ? { value: o, label: o } : o);
|
|
387
|
+
// The dropdown shows a disabled option's hint inline, so it stays readable in the trigger and search.
|
|
388
|
+
const dropdownOptions = comboOptions.map(withInlineDisabledHint);
|
|
368
389
|
// The radio group can't render the unselected placeholder as its own
|
|
369
390
|
// segment, so it counts and renders on the real options only — the
|
|
370
391
|
// dropdown below keeps comboOptions (placeholder included) since it
|
|
@@ -392,7 +413,7 @@ function DefaultInput(props) {
|
|
|
392
413
|
if (presentation === "segmented" && radioGroupOptions.length > 0) {
|
|
393
414
|
return (_jsx(SegmentedSelect, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, options: radioGroupOptions, ...(props.disabled !== undefined && { disabled: props.disabled }), ...(props.required !== undefined && { required: props.required }), ...(props.hasError !== undefined && { hasError: props.hasError }) }));
|
|
394
415
|
}
|
|
395
|
-
return (_jsx(ComboboxInput, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, options:
|
|
416
|
+
return (_jsx(ComboboxInput, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, options: dropdownOptions, ...(props.disabled !== undefined && { disabled: props.disabled }), ...(props.required !== undefined && { required: props.required }), ...(props.hasError !== undefined && { hasError: props.hasError }) }));
|
|
396
417
|
}
|
|
397
418
|
case "combobox": {
|
|
398
419
|
// Tier 2.1c + Tier 2.7e: Discriminated-Union per `multiple` —
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.352.0",
|
|
4
4
|
"description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
24
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
25
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
26
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
27
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
23
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.352.0",
|
|
24
|
+
"@cosmicdrift/kumiko-framework": "0.352.0",
|
|
25
|
+
"@cosmicdrift/kumiko-headless": "0.352.0",
|
|
26
|
+
"@cosmicdrift/kumiko-renderer": "0.352.0",
|
|
27
|
+
"@cosmicdrift/kumiko-types": "0.352.0",
|
|
28
28
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
29
29
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
30
30
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"@types/react-dom": "^19.2.3",
|
|
73
73
|
"jsdom": "^29.1.1",
|
|
74
74
|
"tailwindcss": "^4.3.0",
|
|
75
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
75
|
+
"@cosmicdrift/kumiko-locale-de": "0.352.0"
|
|
76
76
|
},
|
|
77
77
|
"repository": {
|
|
78
78
|
"type": "git",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.352.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Stat group span and column count",
|
|
6
|
+
"detail": "Set span: \"half\" on a stat-group to place two groups side by side."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.352.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "The sidebar drops navs whose parent is missing from the role-projected schema instead of promoting them to the top level",
|
|
12
|
+
"detail": "Navs whose parent does not exist in the app schema are dropped, transitively, instead of being promoted to the top level. A role-projected schema no longer shows an adopted child nav (e.g. AI providers for a TenantAdmin) when its parent section is system-admin only. Children whose parent exists but is filtered out by a workspace allowlist still surface at the top level."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.352.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "Disabled select options are muted and not choosable in dropdown, radio list, radio cards and segmented control"
|
|
18
|
+
},
|
|
2
19
|
{
|
|
3
20
|
"version": "0.351.0",
|
|
4
21
|
"type": "improvement",
|