@ggui-ai/ui-gen 0.17.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/dist/adapters/generation-dispatch.d.ts +1 -1
- package/dist/adapters/generation-dispatch.js +449 -194
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +4 -6
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +2 -2
- package/dist/advanced/index.js +391 -134
- package/dist/advanced/index.js.map +1 -1
- package/dist/boilerplate.js +29 -5
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js +70 -0
- package/dist/check/index.js.map +1 -1
- package/dist/{create-ui-generator-C8Ki9Wz1.d.ts → create-ui-generator-CxPvudbs.d.ts} +1 -1
- package/dist/{dispatch-D2htzi9h.d.ts → dispatch-N8wdNOX-.d.ts} +4 -2
- package/dist/evaluation/axis-checks/index.d.ts +3 -3
- package/dist/evaluation/axis-checks/index.js +147 -3
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +1 -1
- package/dist/evaluation/axis-checks/registry.js +142 -1
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +3 -3
- package/dist/evaluation/index.js +217 -84
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +1 -1
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.js +91 -64
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.js +4 -0
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +1 -1
- package/dist/harness/runtime.d.ts +1 -1
- package/dist/harness/runtime.js +31 -7
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +8 -1
- package/dist/harness/types-public.js +91 -64
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +391 -134
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +1 -1
- package/dist/{types-public-DVSssdc-.d.ts → types-public-DKR_h70H.d.ts} +9 -0
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/workflows.d.ts +1 -1
- package/package.json +12 -12
- package/src/adapters/google/sdk.ts +11 -7
- package/src/boilerplate/free-design-prompt.ts +2 -0
- package/src/boilerplate/generate.ts +8 -1
- package/src/boilerplate/hard-sections.ts +32 -3
- package/src/boilerplate/system-prompt.ts +3 -1
- package/src/boilerplate/templates/layouts/fullscreen-desktop.tsx.tmpl +5 -3
- package/src/boilerplate/templates/layouts/fullscreen-mobile.tsx.tmpl +3 -3
- package/src/boilerplate/templates/layouts/fullscreen-universal.tsx.tmpl +5 -3
- package/src/check/run-tier0.ts +152 -0
- package/src/coding-agent/tools.ts +58 -6
- package/src/contract-context.ts +13 -0
- package/src/create-harness.ts +1 -0
- package/src/evaluation/axis-checks/checks/universal.ts +179 -1
- package/src/evaluation/axis-checks/dispatch.ts +8 -1
- package/src/evaluation/types-public.ts +9 -0
- package/src/harness/coding/generate-task-runner.ts +1 -0
- package/src/harness/coding/run-coding-turn.ts +14 -0
- package/src/harness/types-public.ts +7 -0
- package/src/run-check.ts +2 -0
- package/src/tools/get-wire.ts +1 -1
- package/src/validation/primitives.ts +1 -1
|
@@ -2,7 +2,7 @@ import * as _ggui_ai_protocol from '@ggui-ai/protocol';
|
|
|
2
2
|
import { JsonValue, DataContract, JsonObject, GadgetDescriptor } from '@ggui-ai/protocol';
|
|
3
3
|
import { G as GenerationProfileInput } from '../styling-profile-CxfvV48g.js';
|
|
4
4
|
import { C as Classification } from '../axes-CzLEMDeB.js';
|
|
5
|
-
import { E as EvalIssue, R as RuntimeProbeStatus, A as AxisCheck, c as EvalTier } from '../types-public-
|
|
5
|
+
import { E as EvalIssue, R as RuntimeProbeStatus, A as AxisCheck, c as EvalTier } from '../types-public-DKR_h70H.js';
|
|
6
6
|
import { LLMToolDef } from '../llm.js';
|
|
7
7
|
import { H as HarnessFragment, a as CacheTier } from '../types-BOvHNG7K.js';
|
|
8
8
|
import { ProcessMode, HarnessPolicy } from '../policy.js';
|
|
@@ -365,6 +365,13 @@ interface Harness {
|
|
|
365
365
|
* into `free`.
|
|
366
366
|
*/
|
|
367
367
|
readonly designMode: DesignMode;
|
|
368
|
+
/**
|
|
369
|
+
* The rendering canvas this harness was built for (ggui#1117) — threaded
|
|
370
|
+
* from `CreateHarnessInput.canvas` into the HOW leg (the `free` prompt
|
|
371
|
+
* states it) and, via `run-check.ts`, onto every axis check's input.
|
|
372
|
+
* ABSENT when the caller had no rendering context; never defaulted.
|
|
373
|
+
*/
|
|
374
|
+
readonly canvas?: CanvasClass;
|
|
368
375
|
/** The app's generation profile in force for this harness (#991), if any. */
|
|
369
376
|
readonly profile?: GenerationProfileInput;
|
|
370
377
|
readonly how: HowLeg;
|
|
@@ -65,6 +65,61 @@ function truncateSourceForTrace(source) {
|
|
|
65
65
|
function newValidatorTraceId() {
|
|
66
66
|
return Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
// src/design-mode.ts
|
|
70
|
+
var DEFAULT_DESIGN_MODE = "constrained";
|
|
71
|
+
function canvasDisplayModeForShell(shell) {
|
|
72
|
+
return shell === "chat" || shell === "inline" ? "inline" : "fullscreen";
|
|
73
|
+
}
|
|
74
|
+
function canvasForRendering(shellType, screen) {
|
|
75
|
+
const shell = shellType ?? "fullscreen";
|
|
76
|
+
if (canvasDisplayModeForShell(shell) === "inline") return "xs-chat-card";
|
|
77
|
+
if (shell === "spatial") return "md";
|
|
78
|
+
switch (screen ?? "universal") {
|
|
79
|
+
case "mobile":
|
|
80
|
+
return "mobile-fullscreen-small";
|
|
81
|
+
case "tablet":
|
|
82
|
+
return "md";
|
|
83
|
+
default:
|
|
84
|
+
return "lg";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
var CANVAS_DESCRIPTORS = {
|
|
88
|
+
"xs-chat-card": {
|
|
89
|
+
label: "extra-small chat card",
|
|
90
|
+
minWidthPx: 320,
|
|
91
|
+
maxWidthPx: 480,
|
|
92
|
+
host: "an inline card inside a chat message bubble \u2014 the host draws the card border, shadow and rounded corners; the component fills the bubble width and takes its natural height"
|
|
93
|
+
},
|
|
94
|
+
"mobile-fullscreen-small": {
|
|
95
|
+
label: "small mobile fullscreen",
|
|
96
|
+
minWidthPx: 320,
|
|
97
|
+
maxWidthPx: 599,
|
|
98
|
+
host: "a phone-sized viewport the component owns edge to edge \u2014 single column, touch targets at least 44px, the component manages its own scroll"
|
|
99
|
+
},
|
|
100
|
+
md: {
|
|
101
|
+
label: "medium panel",
|
|
102
|
+
minWidthPx: 600,
|
|
103
|
+
maxWidthPx: 1023,
|
|
104
|
+
host: "a tablet viewport or a floating panel (spatial / picture-in-picture) \u2014 one or two columns, medium density"
|
|
105
|
+
},
|
|
106
|
+
lg: {
|
|
107
|
+
label: "large fullscreen",
|
|
108
|
+
minWidthPx: 1024,
|
|
109
|
+
maxWidthPx: 1439,
|
|
110
|
+
host: "a desktop viewport the component owns edge to edge \u2014 multi-column layouts, hover states and pointer interactions are available"
|
|
111
|
+
},
|
|
112
|
+
xl: {
|
|
113
|
+
label: "extra-large fullscreen",
|
|
114
|
+
minWidthPx: 1440,
|
|
115
|
+
maxWidthPx: 1920,
|
|
116
|
+
host: "a wide desktop viewport the component owns edge to edge \u2014 cap content width or use the extra room deliberately; never stretch a single column across the whole width"
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
function describeCanvas(canvas) {
|
|
120
|
+
const d = CANVAS_DESCRIPTORS[canvas];
|
|
121
|
+
return `This component renders on the \`${canvas}\` canvas (${d.label}): ${d.minWidthPx}\u2013${d.maxWidthPx}px wide, ${d.host}. Design for the whole range \u2014 the host may size the iframe anywhere inside it, and the host owns the height: size to your content, never to the viewport.`;
|
|
122
|
+
}
|
|
68
123
|
function axisCheckTraceEnabled() {
|
|
69
124
|
return process.env["GGUI_AXIS_CHECK_TRACE"] === "1";
|
|
70
125
|
}
|
|
@@ -330,7 +385,9 @@ function traceFacts(input) {
|
|
|
330
385
|
designMode: input.designMode ?? "constrained",
|
|
331
386
|
classification: input.classification.vector,
|
|
332
387
|
propsSpecKeys: Object.keys(propsSpec ?? {}),
|
|
333
|
-
propsSpecPropertyKeys: Object.keys(propsSpec?.properties ?? {})
|
|
388
|
+
propsSpecPropertyKeys: Object.keys(propsSpec?.properties ?? {}),
|
|
389
|
+
// No default here, unlike designMode: an absent canvas is a fact about the caller, not a guess to paper over.
|
|
390
|
+
...input.canvas !== void 0 ? { canvas: input.canvas } : {}
|
|
334
391
|
};
|
|
335
392
|
}
|
|
336
393
|
|
|
@@ -368,7 +425,9 @@ async function runCheck(input) {
|
|
|
368
425
|
contract,
|
|
369
426
|
originalPrompt: prompt,
|
|
370
427
|
classification: harness.classification,
|
|
371
|
-
designMode: harness.designMode
|
|
428
|
+
designMode: harness.designMode,
|
|
429
|
+
// ggui#1117: the canvas the harness was built for, when it has one — never a default.
|
|
430
|
+
...harness.canvas !== void 0 ? { canvas: harness.canvas } : {}
|
|
372
431
|
};
|
|
373
432
|
const axisRun = runGatedAxisChecks(check.axisChecks, axisInput);
|
|
374
433
|
issues.push(...axisRun.issues);
|
|
@@ -984,63 +1043,6 @@ function compose(classification) {
|
|
|
984
1043
|
fragments: matched
|
|
985
1044
|
};
|
|
986
1045
|
}
|
|
987
|
-
|
|
988
|
-
// src/design-mode.ts
|
|
989
|
-
var DEFAULT_DESIGN_MODE = "constrained";
|
|
990
|
-
function canvasDisplayModeForShell(shell) {
|
|
991
|
-
return shell === "chat" || shell === "inline" ? "inline" : "fullscreen";
|
|
992
|
-
}
|
|
993
|
-
function canvasForRendering(shellType, screen) {
|
|
994
|
-
const shell = shellType ?? "fullscreen";
|
|
995
|
-
if (canvasDisplayModeForShell(shell) === "inline") return "xs-chat-card";
|
|
996
|
-
if (shell === "spatial") return "md";
|
|
997
|
-
switch (screen ?? "universal") {
|
|
998
|
-
case "mobile":
|
|
999
|
-
return "mobile-fullscreen-small";
|
|
1000
|
-
case "tablet":
|
|
1001
|
-
return "md";
|
|
1002
|
-
default:
|
|
1003
|
-
return "lg";
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
var CANVAS_DESCRIPTORS = {
|
|
1007
|
-
"xs-chat-card": {
|
|
1008
|
-
label: "extra-small chat card",
|
|
1009
|
-
minWidthPx: 320,
|
|
1010
|
-
maxWidthPx: 480,
|
|
1011
|
-
host: "an inline card inside a chat message bubble \u2014 the host draws the card border, shadow and rounded corners; the component fills the bubble width and takes its natural height"
|
|
1012
|
-
},
|
|
1013
|
-
"mobile-fullscreen-small": {
|
|
1014
|
-
label: "small mobile fullscreen",
|
|
1015
|
-
minWidthPx: 320,
|
|
1016
|
-
maxWidthPx: 599,
|
|
1017
|
-
host: "a phone-sized viewport the component owns edge to edge \u2014 single column, touch targets at least 44px, the component manages its own scroll"
|
|
1018
|
-
},
|
|
1019
|
-
md: {
|
|
1020
|
-
label: "medium panel",
|
|
1021
|
-
minWidthPx: 600,
|
|
1022
|
-
maxWidthPx: 1023,
|
|
1023
|
-
host: "a tablet viewport or a floating panel (spatial / picture-in-picture) \u2014 one or two columns, medium density"
|
|
1024
|
-
},
|
|
1025
|
-
lg: {
|
|
1026
|
-
label: "large fullscreen",
|
|
1027
|
-
minWidthPx: 1024,
|
|
1028
|
-
maxWidthPx: 1439,
|
|
1029
|
-
host: "a desktop viewport the component owns edge to edge \u2014 multi-column layouts, hover states and pointer interactions are available"
|
|
1030
|
-
},
|
|
1031
|
-
xl: {
|
|
1032
|
-
label: "extra-large fullscreen",
|
|
1033
|
-
minWidthPx: 1440,
|
|
1034
|
-
maxWidthPx: 1920,
|
|
1035
|
-
host: "a wide desktop viewport the component owns edge to edge \u2014 cap content width or use the extra room deliberately; never stretch a single column across the whole width"
|
|
1036
|
-
}
|
|
1037
|
-
};
|
|
1038
|
-
function describeCanvas(canvas) {
|
|
1039
|
-
const d = CANVAS_DESCRIPTORS[canvas];
|
|
1040
|
-
return `This component renders on the \`${canvas}\` canvas (${d.label}): ${d.minWidthPx}\u2013${d.maxWidthPx}px wide, ${d.host}. Design for the whole range \u2014 the host may size the iframe anywhere inside it, and the host owns the height: size to your content, never to the viewport.`;
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
// src/boilerplate/render.ts
|
|
1044
1046
|
var __dirname$1 = dirname(fileURLToPath(import.meta.url));
|
|
1045
1047
|
var TEMPLATE_DIRS = [
|
|
1046
1048
|
resolve(__dirname$1, "templates"),
|
|
@@ -1312,7 +1314,8 @@ type ${typeName} = ${tsType};`
|
|
|
1312
1314
|
);
|
|
1313
1315
|
const callSig = tsType === "void" ? "() => void \u2014 fire and forget" : `(data: ${tsType}) => void`;
|
|
1314
1316
|
const toolHint = tool ? ` \u2192 nextStep: ${tool}` : "";
|
|
1315
|
-
|
|
1317
|
+
const onceHint = " \u2014 if this is meant ONCE, disable its control after it fires";
|
|
1318
|
+
actionHookCalls.push(` const ${key} = useAction<${typeName}>('${key}'); // ${callSig}${toolHint}${onceHint}`);
|
|
1316
1319
|
}
|
|
1317
1320
|
const streamChannels = contract?.streamSpec ?? {};
|
|
1318
1321
|
const streamChannelEntries = Object.entries(streamChannels);
|
|
@@ -1645,14 +1648,35 @@ Rules:
|
|
|
1645
1648
|
1. All request-specific data \u2192 props with defaults. City names, tickers, user names, dates, counts.
|
|
1646
1649
|
2. Layout and styling are universal. Colors, spacing, structure \u2014 these are the reusable part.
|
|
1647
1650
|
3. Default values come from the current request \u2014 so the component renders correctly standalone.
|
|
1648
|
-
4. Copy comes from the props. NEVER invent display text the contract does not carry \u2014 no eyebrow / kicker / overline (a \`<Text caps>\` label), no helper line, no status text, no tagline.
|
|
1651
|
+
4. Copy comes from the props. NEVER invent display text the contract does not carry \u2014 no eyebrow / kicker / overline (a \`<Text caps>\` label), no helper line, no status text, no tagline. **Do NOT add a caps label above the heading** \u2014 not \`<Text caps>LET'S BEGIN</Text>\`, not \`WELCOME\`, not a section kicker like \`GETTING STARTED\` or \`OVERVIEW\` \u2014 UNLESS a prop supplies that exact text (\`<Text caps>{props.section}</Text>\`). When the request asks for a heading and a line, render the heading and the line and NOTHING above the heading. A heading's fallback is the request's own words, never a slogan of your own. Text the agent cannot change is chatter on every render, and a caps eyebrow you wrote is the most common form of it.
|
|
1649
1652
|
5. Props interface must be typed and exported.`;
|
|
1650
|
-
var
|
|
1653
|
+
var TERMINAL_ACTIONS = `## Actions the user means once
|
|
1654
|
+
|
|
1655
|
+
A second press is a mistake only when it would make a SECOND THING HAPPEN IN THE WORLD \u2014 a second booking, a second charge, a second order, a second application sent. If pressing again would only write the SAME STATE AGAIN \u2014 saving, updating, renaming, setting a preference, toggling \u2014 nothing is duplicated and the control stays armed.
|
|
1656
|
+
|
|
1657
|
+
Guard a control only where you have evidence, not a feeling about the verb: the request itself names a one-time commitment \u2014 "submit the application", "place the order", "confirm the booking".
|
|
1658
|
+
|
|
1659
|
+
**When in doubt, leave it armed.** A control the user cannot press a second time has lost them something that worked \u2014 save, edit, save again is ordinary. A control left armed is only what every interface does by default.
|
|
1660
|
+
|
|
1661
|
+
When you do guard one:
|
|
1662
|
+
- Hold a small piece of state for it and set it in the handler: \`const [submitted, setSubmitted] = useState(false)\`.
|
|
1663
|
+
- \`disabled={submitted}\` on the control that fires it, and say so in the label or beside it ("Submitted", "Request sent") \u2014 a control that is disabled without explanation reads as broken.
|
|
1664
|
+
- A card re-rendered with new props starts fresh; the state above lives with the component, not with the request.
|
|
1665
|
+
|
|
1666
|
+
Nothing in the wire DECLARES one-shot-ness yet, which is why the test is what a second press would DO \u2014 not how final the word sounds.`;
|
|
1667
|
+
var FRAME_SIZING = `## Sizing \u2014 the frame owns the height and the width
|
|
1651
1668
|
|
|
1652
1669
|
The host sizes the iframe; the component never sizes itself to the viewport. NEVER write \`100vh\` / \`100dvh\` / \`100svh\` (or an \`h-screen\` / \`min-h-screen\` class) as a height on any element:
|
|
1653
1670
|
- An inline card takes its natural height. A content-sized frame is measured FROM the content, so a \`100vh\` root can never shrink and grows the frame on every re-measure.
|
|
1654
1671
|
- Under fullscreen the frame already stretches your root to its full height. Centre inside it with flex (\`display: flex; flex-direction: column; justify-content: center\`), never with a viewport height.
|
|
1655
|
-
- To fill a parent that has a height use \`height: 100%\` or \`flex: 1\`; a viewport unit is only ever a \`max-height\` cap on a scroll region
|
|
1672
|
+
- To fill a parent that has a height use \`height: 100%\` or \`flex: 1\`; a viewport unit is only ever a \`max-height\` cap on a scroll region.
|
|
1673
|
+
|
|
1674
|
+
The width works the same way, and the mistake is the mirror image: **your outermost element FILLS the frame; a width cap belongs INSIDE it, on the column that holds text.**
|
|
1675
|
+
|
|
1676
|
+
- Never put \`maxWidth\` (or a \`max-w-*\` class) on the element you return at the top. On a wide canvas that turns your card into a narrow strip with the frame's ground on either side of it, and on a narrow one it does nothing \u2014 the same composition reading differently at two sizes.
|
|
1677
|
+
- DO cap the reading measure inside: a column of prose is easiest to read at 60\u201370 characters, so wrap the TEXT in a \`<Container maxWidth="sm">\` (or your own \`max-width\`) and leave the root filling. A hero band, a toolbar, a table or an image then still uses the whole width, which is what makes a wide card look composed rather than padded.
|
|
1678
|
+
- Centre that inner column with the container's own centring, not by shrinking the root.
|
|
1679
|
+
- **They are two different elements.** Even when the whole card is one column of text, the root still fills and the column sits INSIDE it \u2014 \`<Box padding="lg"><Container maxWidth="sm">\u2026</Container></Box>\`, never a root that is itself the container. A centred card is centring inside a filled frame, not a shrunken frame.`;
|
|
1656
1680
|
var COMPONENT_STRUCTURE = `## Component Structure
|
|
1657
1681
|
|
|
1658
1682
|
Keep JSX nesting depth to 3\u20135 levels. When deeper, extract repeated/complex sections into helper components \u2014 named functions defined above the main Component in the same file. Helpers take data + callbacks via props; they don't own state.
|
|
@@ -1864,6 +1888,7 @@ ${axisSection}
|
|
|
1864
1888
|
CONTRACT_SURFACE,
|
|
1865
1889
|
DEFENSIVE_CODING,
|
|
1866
1890
|
GESTURE_ROUTING,
|
|
1891
|
+
TERMINAL_ACTIONS,
|
|
1867
1892
|
FREE_GESTURE_SURFACE,
|
|
1868
1893
|
ANTI_PATTERNS,
|
|
1869
1894
|
CROSS_REFERENCE_RULES,
|
|
@@ -2262,6 +2287,7 @@ ${axisSection}
|
|
|
2262
2287
|
CONTRACT_SURFACE,
|
|
2263
2288
|
DEFENSIVE_CODING,
|
|
2264
2289
|
GESTURE_ROUTING,
|
|
2290
|
+
TERMINAL_ACTIONS,
|
|
2265
2291
|
INTERACTIVE_TRAITS,
|
|
2266
2292
|
ANTI_PATTERNS,
|
|
2267
2293
|
CROSS_REFERENCE_RULES,
|
|
@@ -2469,7 +2495,7 @@ Don't render text-only metrics. A weather widget without a sun/cloud, a stock ca
|
|
|
2469
2495
|
</Row>
|
|
2470
2496
|
\`\`\`
|
|
2471
2497
|
|
|
2472
|
-
Use \`<Icon name="..." />\` for line icons \u2014 ONLY the curated
|
|
2498
|
+
Use \`<Icon name="..." />\` for line icons \u2014 ONLY the design system's curated set renders (~200 kebab-case Lucide names); a name OUTSIDE it renders an empty box and fails the check. On a single-pass render you cannot look the set up, so use ONLY a name you are sure is in it. Safe common names: \`check\` \`x\` \`plus\` \`minus\` \`search\` \`settings\` \`user\` \`users\` \`calendar\` \`clock\` \`arrow-right\` \`arrow-left\` \`chevron-right\` \`chevron-down\` \`trash-2\` \`edit\` \`info\` \`alert-circle\` \`bell\` \`home\` \`file\` \`file-text\` \`star\` \`heart\` \`mail\` \`send\` \`download\` \`upload\` \`external-link\` \`filter\` \`check-circle\`. The numbered variants (\`edit-2\`, \`edit-3\`) and synonyms (\`pencil\`, \`pen\`, \`trash\`, \`x-circle\`) are NOT in the set \u2014 use \`edit\` and \`trash-2\`. If the icon you want is not clearly one of these, OMIT it: a card with no icon beats one with an empty box. Emoji render directly for status/weather/mood. For per-stat tiny accents, use a small icon next to the label.
|
|
2473
2499
|
|
|
2474
2500
|
### Spacing \u2014 generosity beats compactness
|
|
2475
2501
|
|
|
@@ -2856,6 +2882,7 @@ function createHarness(input) {
|
|
|
2856
2882
|
name,
|
|
2857
2883
|
classification,
|
|
2858
2884
|
designMode,
|
|
2885
|
+
...input.canvas !== void 0 ? { canvas: input.canvas } : {},
|
|
2859
2886
|
...input.profile !== void 0 ? { profile: input.profile } : {},
|
|
2860
2887
|
how,
|
|
2861
2888
|
what,
|