feat(ui): AttrCombobox declare vs. consume semantics
Attr-string fields in the Custom Modifier Editor now differentiate between declare-sites (seed-attribute.attr) and consume-sites (add-to-attribute.attr, multiply-attribute.attr, add-aura.targetAttr, absorb-damage-with-attribute.attr): - Consume-sites hide the illustrative 'User-defined examples' group (ArmorPlates/BloodStacks/ManaPool) because those names are fiction unless something seeds them. - Consume-sites surface a new 'Seeded in this descriptor' group populated from seed-attribute primitives elsewhere in the current tree (including inside trigger children via childPrimitives). - Badge states: emerald 'seeded' when the typed value matches an in-tree seed; red 'not seeded' when it's a non-schema name with no backing seed; amber 'user-defined' only in declare-mode for off-catalog names. Declare-mode behaviour is unchanged — inventing ShieldCharges there still works without warnings.
This commit is contained in:
parent
934db775f9
commit
8605a22530
5 changed files with 349 additions and 25 deletions
|
|
@ -55,9 +55,26 @@ suggestions:
|
|||
ManaPool. Illustrative names matching the recipes; invent your own.
|
||||
|
||||
The combobox is **free-form** — type any name and hit Enter to accept it,
|
||||
even if it isn't in the list. Typed values not in the catalog get a small
|
||||
`user-defined` badge so you know you're outside the schema. Numeric-context
|
||||
primitives (add, multiply, aura, absorb) rank numeric suggestions first.
|
||||
even if it isn't in the list. Numeric-context primitives (add, multiply,
|
||||
aura, absorb) rank numeric suggestions first.
|
||||
|
||||
**Declare vs. consume sites.** The combobox distinguishes between
|
||||
primitives that *declare* a new attribute and primitives that *consume* an
|
||||
existing one:
|
||||
|
||||
- **`seed-attribute.attr`** is a **declare-site** — any name is fine,
|
||||
including inventing brand new counters. The illustrative "User-defined
|
||||
examples" group is surfaced for inspiration. Typed values outside both
|
||||
the schema and the examples catalog carry an amber `user-defined` badge.
|
||||
- **`add-to-attribute.attr`**, **`multiply-attribute.attr`**,
|
||||
**`absorb-damage-with-attribute.attr`**, and **`add-aura.targetAttr`**
|
||||
are **consume-sites** — they read an attribute that must already exist.
|
||||
The illustrative examples group is **hidden** because those names are
|
||||
fiction until something seeds them. Instead, a "Seeded in this
|
||||
descriptor" group surfaces attrs that `seed-attribute` primitives
|
||||
elsewhere in the current tree actually declare. Typed names that
|
||||
aren't built-in and aren't seeded carry a red `not seeded` warning;
|
||||
names that are seeded carry an emerald `seeded` confirmation.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1368,11 +1368,94 @@ test.describe('T29 — Custom modifier DSL e2e', () => {
|
|||
await page.getByTestId('attr-combobox-option-Hp').first().click();
|
||||
await expect(input).toHaveValue('Hp');
|
||||
|
||||
// seed-attribute is a declare-site, so the illustrative examples
|
||||
// (ArmorPlates, BloodStacks...) SHOULD be present.
|
||||
await input.click();
|
||||
await expect(
|
||||
page.getByTestId('attr-combobox-option-ArmorPlates'),
|
||||
).toBeVisible();
|
||||
|
||||
// Type a user-defined attr name — badge appears, dropdown filters down.
|
||||
await input.fill('MyStacks');
|
||||
await expect(combobox).toContainText('user-defined');
|
||||
});
|
||||
|
||||
test('AttrCombobox consume-mode (add-aura.targetAttr) hides user-defined examples and warns on unseeded names', async ({
|
||||
page,
|
||||
}) => {
|
||||
await freshLobby(page);
|
||||
await openProfileEditor(page);
|
||||
await openCustomModifierEditor(page);
|
||||
|
||||
// Add an add-aura primitive — its targetAttr field is a consume-site.
|
||||
await page.getByTestId('custom-primitive-palette-add-aura').click();
|
||||
await page.getByTestId('custom-primitive-node-0').click();
|
||||
|
||||
const input = page.getByTestId('primitive-add-aura-targetAttr-input');
|
||||
await input.click();
|
||||
const dropdown = page.getByTestId('primitive-add-aura-targetAttr-dropdown');
|
||||
await expect(dropdown).toBeVisible();
|
||||
|
||||
// Illustrative "User-defined examples" group should NOT appear in
|
||||
// consume-mode — those names (ArmorPlates, BloodStacks, ManaPool)
|
||||
// are fiction unless a seed-attribute declares them.
|
||||
await expect(dropdown).not.toContainText('User-defined examples');
|
||||
await expect(
|
||||
page.getByTestId('attr-combobox-option-ArmorPlates'),
|
||||
).toHaveCount(0);
|
||||
|
||||
// Typing an unseeded attr raises the red "not seeded" badge.
|
||||
await input.fill('MyGhostAttr');
|
||||
const badge = page.getByTestId('primitive-add-aura-targetAttr-badge');
|
||||
await expect(badge).toHaveText('not seeded');
|
||||
});
|
||||
|
||||
test('AttrCombobox consume-mode surfaces attrs seeded by other primitives in the same descriptor', async ({
|
||||
page,
|
||||
}) => {
|
||||
await freshLobby(page);
|
||||
await openProfileEditor(page);
|
||||
await openCustomModifierEditor(page);
|
||||
|
||||
// 1. Add a seed-attribute primitive that declares ShieldCharges=3.
|
||||
await page.getByTestId('custom-primitive-palette-seed-attribute').click();
|
||||
await page.getByTestId('custom-primitive-node-0').click();
|
||||
const seedAttrInput = page.getByTestId('primitive-seed-attribute-attr-input');
|
||||
await seedAttrInput.fill('ShieldCharges');
|
||||
// Blur the input so the dropdown closes — the open dropdown is
|
||||
// absolutely positioned and would intercept the next palette click.
|
||||
await seedAttrInput.blur();
|
||||
|
||||
// 2. Add an absorb-damage-with-attribute primitive. Its attr field
|
||||
// is a consume-site and should now offer ShieldCharges under
|
||||
// a "Seeded in this descriptor" group.
|
||||
await page
|
||||
.getByTestId('custom-primitive-palette-absorb-damage-with-attribute')
|
||||
.click();
|
||||
await page.getByTestId('custom-primitive-node-1').click();
|
||||
|
||||
const absorbInput = page.getByTestId(
|
||||
'primitive-absorb-damage-with-attribute-attr-input',
|
||||
);
|
||||
await absorbInput.click();
|
||||
const dropdown = page.getByTestId(
|
||||
'primitive-absorb-damage-with-attribute-attr-dropdown',
|
||||
);
|
||||
await expect(dropdown).toBeVisible();
|
||||
await expect(dropdown).toContainText('Seeded in this descriptor');
|
||||
await expect(
|
||||
page.getByTestId('attr-combobox-option-ShieldCharges'),
|
||||
).toBeVisible();
|
||||
|
||||
// Picking it commits the value and badge shows emerald "seeded".
|
||||
await page.getByTestId('attr-combobox-option-ShieldCharges').click();
|
||||
await expect(absorbInput).toHaveValue('ShieldCharges');
|
||||
const absorbBadge = page.getByTestId(
|
||||
'primitive-absorb-damage-with-attribute-attr-badge',
|
||||
);
|
||||
await expect(absorbBadge).toHaveText('seeded');
|
||||
});
|
||||
|
||||
test('Templates picker loads a recipe into the editor', async ({ page }) => {
|
||||
await freshLobby(page);
|
||||
await openProfileEditor(page);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,29 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|||
import {
|
||||
ATTR_SUGGESTION_GROUPS,
|
||||
ALL_SUGGESTION_NAMES,
|
||||
USER_DEFINED_EXAMPLES_GROUP_LABEL,
|
||||
CORE_NUMERIC_GROUP_LABEL,
|
||||
CORE_OTHER_GROUP_LABEL,
|
||||
type AttrSuggestion,
|
||||
type AttrTypeHint,
|
||||
} from "./attr-suggestions.js";
|
||||
|
||||
/**
|
||||
* Names that live in one of the "Core" groups (numeric or non-numeric).
|
||||
* Used by consume-mode to filter the prepended "Seeded in this
|
||||
* descriptor" list so we don't surface Hp twice when something seeds
|
||||
* it in-tree. Illustrative user-defined example names (ShieldCharges,
|
||||
* ArmorPlates, …) are intentionally EXCLUDED here so the "Seeded"
|
||||
* group still surfaces them when they're actually declared.
|
||||
*/
|
||||
const CORE_SUGGESTION_NAMES: ReadonlySet<string> = new Set(
|
||||
ATTR_SUGGESTION_GROUPS.filter(
|
||||
(g) =>
|
||||
g.label === CORE_NUMERIC_GROUP_LABEL ||
|
||||
g.label === CORE_OTHER_GROUP_LABEL,
|
||||
).flatMap((g) => g.suggestions.map((s) => s.name)),
|
||||
);
|
||||
|
||||
/**
|
||||
* Attribute-name combobox for the Custom Modifier editor.
|
||||
*
|
||||
|
|
@ -34,11 +53,42 @@ import {
|
|||
* are NOT filtered out — users occasionally need non-numeric attrs
|
||||
* even in numeric-context primitives.
|
||||
*/
|
||||
/**
|
||||
* Semantics of the attr-string field the combobox is editing.
|
||||
*
|
||||
* - `declare` — the primitive is SEEDING this attr (seed-attribute).
|
||||
* Any name is fine, including user-invented ones; the illustrative
|
||||
* examples group ("ShieldCharges", "ArmorPlates"…) is surfaced to
|
||||
* advertise the "invent your own counter" pattern.
|
||||
* - `consume` — the primitive is READING an attr that must already
|
||||
* exist on the piece (add-to-attribute, multiply-attribute,
|
||||
* absorb-damage-with-attribute, add-aura.targetAttr). Illustrative
|
||||
* examples are hidden because they'd be no-ops without a matching
|
||||
* seed. Instead, a "Seeded in this descriptor" group is surfaced
|
||||
* showing attrs that OTHER primitives in the current tree declare
|
||||
* via their params. Free-form typing is still allowed so advanced
|
||||
* users can reference engine attrs not enumerated here.
|
||||
*/
|
||||
export type AttrComboboxMode = "declare" | "consume";
|
||||
|
||||
export interface AttrComboboxProps {
|
||||
readonly value: string;
|
||||
readonly onChange: (next: string) => void;
|
||||
/** If provided, numeric suggestions rank first. */
|
||||
readonly preferredType?: AttrTypeHint;
|
||||
/**
|
||||
* Field semantics — see {@link AttrComboboxMode}. Defaults to
|
||||
* "declare" so a freshly-added primitive (whose use context isn't
|
||||
* known to the combobox) gets the richer, more forgiving catalog.
|
||||
*/
|
||||
readonly mode?: AttrComboboxMode;
|
||||
/**
|
||||
* Attr names the rest of the descriptor is SEEDING. Only meaningful
|
||||
* when `mode === "consume"`; supplies the dynamic "Seeded in this
|
||||
* descriptor" group so users can target counters they've already
|
||||
* declared (ShieldCharges, ArmorPlates, …).
|
||||
*/
|
||||
readonly seededAttrs?: readonly string[];
|
||||
/** data-testid passthrough — lets e2e tests target specific fields. */
|
||||
readonly testId?: string;
|
||||
/** Placeholder shown in the input. */
|
||||
|
|
@ -49,6 +99,8 @@ export function AttrCombobox({
|
|||
value,
|
||||
onChange,
|
||||
preferredType,
|
||||
mode = "declare",
|
||||
seededAttrs = [],
|
||||
testId,
|
||||
placeholder = "Attribute name…",
|
||||
}: AttrComboboxProps) {
|
||||
|
|
@ -61,28 +113,68 @@ export function AttrCombobox({
|
|||
* Flat list of visible suggestions after applying the free-form
|
||||
* substring filter and the preferred-type ranking. Kept memoized
|
||||
* so arrow-key nav stays cheap while typing.
|
||||
*
|
||||
* Consume-mode additions:
|
||||
* - The illustrative "User-defined examples" group is hidden — the
|
||||
* names are fiction unless something actually seeds them, and
|
||||
* surfacing them invites users to target attrs that don't exist.
|
||||
* - A prepended "Seeded in this descriptor" group surfaces attrs
|
||||
* the current primitive tree actually seeds (minus the core
|
||||
* engine attrs we already show in "Core numeric/non-numeric").
|
||||
*/
|
||||
const filteredGroups = useMemo(() => {
|
||||
const q = value.trim().toLowerCase();
|
||||
const shouldFilter = q.length > 0 && !ALL_SUGGESTION_NAMES.has(value);
|
||||
return ATTR_SUGGESTION_GROUPS.map((group) => {
|
||||
const matched = shouldFilter
|
||||
? group.suggestions.filter(
|
||||
(s) =>
|
||||
s.name.toLowerCase().includes(q) ||
|
||||
s.description.toLowerCase().includes(q),
|
||||
)
|
||||
: [...group.suggestions];
|
||||
if (preferredType !== undefined) {
|
||||
matched.sort((a, b) => {
|
||||
const aPref = a.type === preferredType ? 0 : 1;
|
||||
const bPref = b.type === preferredType ? 0 : 1;
|
||||
return aPref - bPref;
|
||||
|
||||
const baseGroups = ATTR_SUGGESTION_GROUPS.filter((g) =>
|
||||
mode === "consume"
|
||||
? g.label !== USER_DEFINED_EXAMPLES_GROUP_LABEL
|
||||
: true,
|
||||
);
|
||||
|
||||
const extra = [];
|
||||
if (mode === "consume" && seededAttrs.length > 0) {
|
||||
// Exclude only names that already appear in a core group —
|
||||
// illustrative examples (ShieldCharges, ArmorPlates, …) are
|
||||
// fair game here because the whole point of this group is to
|
||||
// surface actually-seeded user-defined attrs.
|
||||
const extras: AttrSuggestion[] = seededAttrs
|
||||
.filter((name) => !CORE_SUGGESTION_NAMES.has(name))
|
||||
.map((name) => ({
|
||||
name,
|
||||
description:
|
||||
"Declared by a seed-attribute primitive elsewhere in this descriptor.",
|
||||
type: "numeric",
|
||||
builtin: false,
|
||||
}));
|
||||
if (extras.length > 0) {
|
||||
extra.push({
|
||||
label: "Seeded in this descriptor",
|
||||
suggestions: extras,
|
||||
});
|
||||
}
|
||||
return { ...group, suggestions: matched };
|
||||
}).filter((g) => g.suggestions.length > 0);
|
||||
}, [value, preferredType]);
|
||||
}
|
||||
|
||||
return [...extra, ...baseGroups]
|
||||
.map((group) => {
|
||||
const matched = shouldFilter
|
||||
? group.suggestions.filter(
|
||||
(s) =>
|
||||
s.name.toLowerCase().includes(q) ||
|
||||
s.description.toLowerCase().includes(q),
|
||||
)
|
||||
: [...group.suggestions];
|
||||
if (preferredType !== undefined) {
|
||||
matched.sort((a, b) => {
|
||||
const aPref = a.type === preferredType ? 0 : 1;
|
||||
const bPref = b.type === preferredType ? 0 : 1;
|
||||
return aPref - bPref;
|
||||
});
|
||||
}
|
||||
return { ...group, suggestions: matched };
|
||||
})
|
||||
.filter((g) => g.suggestions.length > 0);
|
||||
}, [value, preferredType, mode, seededAttrs]);
|
||||
|
||||
/**
|
||||
* Flat ordering mirrors what the user sees so arrow-key indices
|
||||
|
|
@ -149,14 +241,68 @@ export function AttrCombobox({
|
|||
}
|
||||
};
|
||||
|
||||
const isKnown = ALL_SUGGESTION_NAMES.has(value);
|
||||
const showUserDefinedBadge = !isKnown && value.trim().length > 0;
|
||||
const trimmed = value.trim();
|
||||
// "Core" = engine-schema known (Hp, HpBonus, …). Illustrative
|
||||
// user-defined examples are NOT core — they're only safe to use in
|
||||
// consume-mode when something actually seeds them.
|
||||
const isCoreKnown = CORE_SUGGESTION_NAMES.has(value);
|
||||
const isCatalogKnown = ALL_SUGGESTION_NAMES.has(value);
|
||||
const isSeededInTree = seededAttrs.includes(value);
|
||||
const isRecognized = isCoreKnown || isSeededInTree;
|
||||
/**
|
||||
* Badge policy:
|
||||
* - declare-mode: any non-core typed value → amber "user-defined"
|
||||
* (same behaviour as before — seeding a new counter is legit).
|
||||
* - consume-mode: non-core AND not seeded by another primitive in
|
||||
* this descriptor → red "not seeded" warning. Non-core BUT
|
||||
* seeded-here → emerald "seeded" confirmation.
|
||||
*/
|
||||
let badge: { tone: "amber" | "red" | "emerald"; label: string; title: string } | null =
|
||||
null;
|
||||
if (trimmed.length > 0 && !isCoreKnown) {
|
||||
if (mode === "consume") {
|
||||
if (isSeededInTree) {
|
||||
badge = {
|
||||
tone: "emerald",
|
||||
label: "seeded",
|
||||
title:
|
||||
"This attribute is declared by a seed-attribute primitive in this descriptor. Safe to target.",
|
||||
};
|
||||
} else {
|
||||
badge = {
|
||||
tone: "red",
|
||||
label: "not seeded",
|
||||
title:
|
||||
"This attribute isn't a built-in and isn't seeded by any primitive in this descriptor. Reading it will be a no-op unless seeded elsewhere.",
|
||||
};
|
||||
}
|
||||
} else if (!isCatalogKnown) {
|
||||
// declare-mode only flags truly off-catalog names. Illustrative
|
||||
// catalog entries (ShieldCharges, ArmorPlates) are "known" here
|
||||
// so we skip the badge — they're valid seed targets.
|
||||
badge = {
|
||||
tone: "amber",
|
||||
label: "user-defined",
|
||||
title:
|
||||
"Not a schema-known attribute. This is fine for user-defined counters, but typos will silently land here.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const badgeClass =
|
||||
badge?.tone === "red"
|
||||
? "text-red-800 bg-red-50 border border-red-200"
|
||||
: badge?.tone === "emerald"
|
||||
? "text-emerald-800 bg-emerald-50 border border-emerald-200"
|
||||
: "text-amber-800 bg-amber-100 border border-amber-200";
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
className="relative"
|
||||
data-testid={testId ?? "attr-combobox"}
|
||||
data-recognized={isRecognized ? "true" : "false"}
|
||||
data-mode={mode}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
|
|
@ -170,18 +316,20 @@ export function AttrCombobox({
|
|||
setHighlight(-1);
|
||||
}}
|
||||
onFocus={() => setOpen(true)}
|
||||
onClick={() => setOpen(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="flex-1 px-3 py-2 text-sm border border-neutral-300 rounded focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
data-testid={testId ? `${testId}-input` : "attr-combobox-input"}
|
||||
aria-autocomplete="list"
|
||||
aria-expanded={open}
|
||||
/>
|
||||
{showUserDefinedBadge && (
|
||||
{badge !== null && (
|
||||
<span
|
||||
className="text-[10px] font-semibold text-amber-800 bg-amber-100 border border-amber-200 px-1.5 py-0.5 rounded"
|
||||
title="Not a schema-known attribute. This is fine for user-defined counters like ShieldCharges, but typos will silently land here."
|
||||
className={`text-[10px] font-semibold px-1.5 py-0.5 rounded ${badgeClass}`}
|
||||
title={badge.title}
|
||||
data-testid={testId ? `${testId}-badge` : "attr-combobox-badge"}
|
||||
>
|
||||
user-defined
|
||||
{badge.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -406,6 +406,7 @@ export function CustomModifierEditor({ isOpen, onClose, onShareWithRoom }: Props
|
|||
<PrimitiveInspector
|
||||
node={descriptor.primitives[selectedIndex]}
|
||||
primitive={PRIMITIVE_REGISTRY.get(descriptor.primitives[selectedIndex].kind)!}
|
||||
allPrimitives={descriptor.primitives}
|
||||
onChange={(newParams) => {
|
||||
updateDescriptor((p) => {
|
||||
const next = [...p.primitives];
|
||||
|
|
@ -565,6 +566,57 @@ function attrFieldPreferredType(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this primitive+field DECLARE a new attr (seed-attribute.attr)
|
||||
* or CONSUME an existing one (add/multiply/absorb/aura)? Drives the
|
||||
* AttrCombobox's mode so consume-mode can hide the illustrative
|
||||
* user-defined-examples group and warn on unseeded names.
|
||||
*/
|
||||
function attrFieldMode(
|
||||
primitiveKind: string,
|
||||
_fieldKey: string,
|
||||
): 'declare' | 'consume' {
|
||||
if (primitiveKind === 'seed-attribute') return 'declare';
|
||||
return 'consume';
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the current descriptor's primitive tree and collect every
|
||||
* user-defined attr name a seed-attribute primitive declares. Used
|
||||
* by the combobox to surface a "Seeded in this descriptor" group
|
||||
* when the active field is a consume-site (add-to-attribute.attr,
|
||||
* add-aura.targetAttr, etc.). Built-in attrs (ChessAttrMap keys)
|
||||
* are omitted — they already appear in the core groups.
|
||||
*/
|
||||
function collectSeededAttrs(
|
||||
nodes: readonly EffectPrimitiveNode[],
|
||||
): string[] {
|
||||
const seen = new Set<string>();
|
||||
const walk = (ns: readonly EffectPrimitiveNode[]): void => {
|
||||
for (const node of ns) {
|
||||
if (node.kind === 'seed-attribute') {
|
||||
const params = node.params as { attr?: unknown } | undefined;
|
||||
if (typeof params?.attr === 'string' && params.attr.length > 0) {
|
||||
seen.add(params.attr);
|
||||
}
|
||||
}
|
||||
// Recurse through trigger primitives' nested children so a
|
||||
// seed-attribute inside on-turn-start / on-capture / conditional
|
||||
// still contributes to the "seeded" set the inspector offers.
|
||||
const primitive = PRIMITIVE_REGISTRY.get(node.kind);
|
||||
if (primitive?.childPrimitives !== undefined) {
|
||||
try {
|
||||
walk(primitive.childPrimitives(node.params));
|
||||
} catch {
|
||||
/* malformed params → skip its children */
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(nodes);
|
||||
return [...seen];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-component for rendering the parameter form based on Zod schema introspection.
|
||||
* Since fully parsing arbitrary Zod schemas into UI is complex, we use a hybrid approach:
|
||||
|
|
@ -573,12 +625,23 @@ function attrFieldPreferredType(
|
|||
function PrimitiveInspector({
|
||||
node,
|
||||
primitive,
|
||||
allPrimitives,
|
||||
onChange
|
||||
}: {
|
||||
node: EffectPrimitiveNode;
|
||||
primitive?: EffectPrimitive;
|
||||
/** Full descriptor tree — used to compute seededAttrs for consume-mode fields. */
|
||||
allPrimitives: readonly EffectPrimitiveNode[];
|
||||
onChange: (params: unknown) => void;
|
||||
}) {
|
||||
// Precompute the set of attr names declared by seed-attribute
|
||||
// primitives anywhere in the current descriptor tree. The result
|
||||
// feeds the AttrCombobox's "Seeded in this descriptor" group when
|
||||
// the field is a consume-site.
|
||||
const seededAttrs = useMemo(
|
||||
() => collectSeededAttrs(allPrimitives),
|
||||
[allPrimitives],
|
||||
);
|
||||
// Local expansion state for the docs panel. Default collapsed so the
|
||||
// form fields stay the focal point; the user toggles "Show examples"
|
||||
// when they want reference material.
|
||||
|
|
@ -734,6 +797,7 @@ function PrimitiveInspector({
|
|||
{type === 'string' && isAttrFieldName(key) ? (
|
||||
(() => {
|
||||
const preferred = attrFieldPreferredType(primitive.kind, key);
|
||||
const mode = attrFieldMode(primitive.kind, key);
|
||||
// exactOptionalPropertyTypes: spread preferredType only
|
||||
// when defined so we don't pass `undefined` explicitly.
|
||||
return (
|
||||
|
|
@ -742,6 +806,8 @@ function PrimitiveInspector({
|
|||
onChange={(next) => onChange({ ...params, [key]: next })}
|
||||
testId={`primitive-${primitive.kind}-${key}`}
|
||||
placeholder="Attribute name…"
|
||||
mode={mode}
|
||||
seededAttrs={seededAttrs}
|
||||
{...(preferred !== undefined ? { preferredType: preferred } : {})}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -42,6 +42,16 @@ export interface AttrSuggestionGroup {
|
|||
readonly suggestions: readonly AttrSuggestion[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the three named groups so the combobox's `mode` option
|
||||
* can suppress the illustrative examples without having to match a
|
||||
* string label.
|
||||
*/
|
||||
export const CORE_NUMERIC_GROUP_LABEL = "Core numeric attributes" as const;
|
||||
export const CORE_OTHER_GROUP_LABEL = "Core non-numeric attributes" as const;
|
||||
export const USER_DEFINED_EXAMPLES_GROUP_LABEL =
|
||||
"User-defined examples (invent your own)" as const;
|
||||
|
||||
/**
|
||||
* Pre-baked groups. Order here IS the display order in the combobox
|
||||
* (numeric first because it's the most-used when targeting additive
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue