@startsimpli/ui 0.4.105 → 0.4.107
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startsimpli/ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.107",
|
|
4
4
|
"description": "Shared UI components package for StartSimpli applications",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
46
46
|
"react": "^18.0.0 || ^19.0.0",
|
|
47
47
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
48
|
+
"@startsimpli/api": "^0.5.57",
|
|
48
49
|
"@startsimpli/auth": "^0.4.35",
|
|
49
|
-
"@startsimpli/i18n": "^0.1.3"
|
|
50
|
-
"@startsimpli/api": "^0.5.56"
|
|
50
|
+
"@startsimpli/i18n": "^0.1.3"
|
|
51
51
|
},
|
|
52
52
|
"peerDependenciesMeta": {
|
|
53
53
|
"@startsimpli/api": {
|
|
@@ -121,8 +121,8 @@
|
|
|
121
121
|
"tailwindcss": "^3.4.19",
|
|
122
122
|
"tsup": "^8.5.1",
|
|
123
123
|
"typescript": "^6.0.3",
|
|
124
|
-
"@startsimpli/
|
|
125
|
-
"@startsimpli/
|
|
124
|
+
"@startsimpli/realtime": "0.0.2",
|
|
125
|
+
"@startsimpli/i18n": "0.1.3"
|
|
126
126
|
},
|
|
127
127
|
"keywords": [
|
|
128
128
|
"ui",
|
|
@@ -94,6 +94,51 @@ describe('renderDashboardConfig', () => {
|
|
|
94
94
|
expect(renderDashboardConfig([]).ok).toBe(false);
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
// startsim-y5dnx. The builder wrote exactly this entry into the tandem fork:
|
|
98
|
+
//
|
|
99
|
+
// { kind: 'source-freshness', title: 'How current is it' }
|
|
100
|
+
//
|
|
101
|
+
// and the home rendered "How current is it — 152 stale" with every one of the
|
|
102
|
+
// 152 commitments listed as "never", over a corpus whose newest record had
|
|
103
|
+
// arrived 30 minutes earlier. The widget had nothing to go on, so it guessed a
|
|
104
|
+
// type and a date field, and the field it landed on was a business date that is
|
|
105
|
+
// null until somebody resolves a commitment.
|
|
106
|
+
//
|
|
107
|
+
// "Never writes a config the app cannot render" has to cover this too: a kind
|
|
108
|
+
// the app knows, configured so incompletely that all it can do is guess.
|
|
109
|
+
it('rejects a source-freshness widget that names no timestamp field', () => {
|
|
110
|
+
const r = renderDashboardConfig([
|
|
111
|
+
{ kind: 'metrics' },
|
|
112
|
+
{ kind: 'source-freshness', title: 'How current is it' },
|
|
113
|
+
]);
|
|
114
|
+
expect(r.ok).toBe(false);
|
|
115
|
+
expect(r.file).toBeUndefined();
|
|
116
|
+
expect(r.errors.join(' ')).toContain('timestampField');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('rejects a source-freshness widget that names a type but no field', () => {
|
|
120
|
+
const r = renderDashboardConfig([{ kind: 'source-freshness', type: 'commitment' }]);
|
|
121
|
+
expect(r.ok).toBe(false);
|
|
122
|
+
expect(r.errors.join(' ')).toContain('timestampField');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('renders a source-freshness widget once it is told what to read', () => {
|
|
126
|
+
const r = renderDashboardConfig([
|
|
127
|
+
{ kind: 'source-freshness', type: 'document', timestampField: 'fetched_at', title: 'Freshness' },
|
|
128
|
+
]);
|
|
129
|
+
expect(r.ok).toBe(true);
|
|
130
|
+
expect(r.errors).toEqual([]);
|
|
131
|
+
expect(r.file?.content).toContain('"timestampField":"fetched_at"');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('tells the AI, in the catalog, that both keys are required', () => {
|
|
135
|
+
const entry = getCatalogEntry('source-freshness');
|
|
136
|
+
expect(entry?.configExample).toMatchObject({ kind: 'source-freshness' });
|
|
137
|
+
expect(Object.keys(entry?.configExample ?? {})).toContain('timestampField');
|
|
138
|
+
// The prompt line is all the builder sees; the requirement has to be in it.
|
|
139
|
+
expect(renderCatalogForPrompt()).toContain('timestampField');
|
|
140
|
+
});
|
|
141
|
+
|
|
97
142
|
it('flags chosen widgets that are not built into the template', () => {
|
|
98
143
|
expect(widgetsNeedingScaffold([{ kind: 'metrics' }, { kind: 'kanban-board' }])).toEqual(['kanban-board']);
|
|
99
144
|
expect(widgetsNeedingScaffold([{ kind: 'metrics' }, { kind: 'recent' }])).toEqual([]);
|
package/src/catalog/catalog.ts
CHANGED
|
@@ -89,10 +89,17 @@ export const UI_CATALOG: CatalogEntry[] = [
|
|
|
89
89
|
category: 'widget',
|
|
90
90
|
title: 'Source freshness',
|
|
91
91
|
description:
|
|
92
|
-
'A health card listing upstream sources as fresh vs stale by their last-updated time — any stale feed pulls the card to a warning. Use to prove ongoing ingestion is actually flowing (a stale source is a bug, not coverage).',
|
|
93
|
-
dataNeeds:
|
|
92
|
+
'A health card listing upstream sources as fresh vs stale by their last-updated time — any stale feed pulls the card to a warning. Use to prove ongoing ingestion is actually flowing (a stale source is a bug, not coverage). One row per record, so point it at a type whose records ARE feeds, not at a high-volume record type.',
|
|
93
|
+
dataNeeds:
|
|
94
|
+
'A type whose records carry a date field meaning "this arrived" — a fetched-at / last-synced-at / last-updated stamp, never a business date like due-at or resolved-at. Name the type AND the field: it does not guess, and it renders nothing until it is told.',
|
|
94
95
|
source: { export: 'SourceFreshness', from: '@startsimpli/ui' },
|
|
95
|
-
|
|
96
|
+
requiredParams: ['type', 'timestampField'],
|
|
97
|
+
configExample: {
|
|
98
|
+
kind: 'source-freshness',
|
|
99
|
+
type: '<typeKey>',
|
|
100
|
+
timestampField: '<arrivalDateAttr>',
|
|
101
|
+
title: 'Source freshness',
|
|
102
|
+
},
|
|
96
103
|
},
|
|
97
104
|
{
|
|
98
105
|
id: 'delivery-health',
|
package/src/catalog/index.ts
CHANGED
|
@@ -42,7 +42,14 @@ export function renderCatalogForPrompt(): string {
|
|
|
42
42
|
const flags = [e.builtIn ? 'built-in' : 'scaffolded', e.heavy ? '⚠ heavy' : null]
|
|
43
43
|
.filter(Boolean)
|
|
44
44
|
.join(', ');
|
|
45
|
-
|
|
45
|
+
// `requiredParams` is the difference between a block the AI can offer and one
|
|
46
|
+
// it can offer WRONG — an under-configured widget that guesses is worse on the
|
|
47
|
+
// page than one that was never offered (bd startsim-y5dnx). The renderer
|
|
48
|
+
// rejects a config that omits these, so the prompt has to name them.
|
|
49
|
+
const required = e.requiredParams?.length
|
|
50
|
+
? ` Requires config: ${e.requiredParams.join(', ')}.`
|
|
51
|
+
: '';
|
|
52
|
+
return `- ${e.id} (${e.category}${flags ? `; ${flags}` : ''}) — ${e.title}: ${e.description} Needs: ${e.dataNeeds}${required}`;
|
|
46
53
|
};
|
|
47
54
|
return UI_CATALOG.map(line).join('\n');
|
|
48
55
|
}
|
package/src/catalog/scaffold.ts
CHANGED
|
@@ -19,6 +19,19 @@ export interface DashboardWidgetConfig {
|
|
|
19
19
|
|
|
20
20
|
const WIDGET_IDS = new Set(UI_CATALOG.filter((e) => e.category === 'widget').map((e) => e.id));
|
|
21
21
|
|
|
22
|
+
/** The config keys each widget cannot work without (see CatalogEntry.requiredParams). */
|
|
23
|
+
const REQUIRED_PARAMS = new Map(
|
|
24
|
+
UI_CATALOG.filter((e) => e.requiredParams?.length).map((e) => [e.id, e.requiredParams!]),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
/** Params a widget must be given, and was not — `undefined`/`null`/'' all count as absent. */
|
|
28
|
+
function missingParams(widget: DashboardWidgetConfig): string[] {
|
|
29
|
+
return (REQUIRED_PARAMS.get(widget.kind) ?? []).filter((p) => {
|
|
30
|
+
const v = widget[p];
|
|
31
|
+
return v === undefined || v === null || v === '';
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
/** Which of the chosen widgets are NOT built into the base template (need scaffolding). */
|
|
23
36
|
export function widgetsNeedingScaffold(widgets: DashboardWidgetConfig[]): string[] {
|
|
24
37
|
const builtIn = new Set(UI_CATALOG.filter((e) => e.builtIn).map((e) => e.id));
|
|
@@ -37,6 +50,11 @@ export interface RenderResult {
|
|
|
37
50
|
* widget `kind` against the catalog's widget blocks; an unknown kind fails the
|
|
38
51
|
* whole render (so we never write a config referencing a widget the app can't
|
|
39
52
|
* render). Serializes params as a plain literal — valid TS the owner can hand-edit.
|
|
53
|
+
*
|
|
54
|
+
* A KNOWN kind fails the same way when it is missing a param it cannot work
|
|
55
|
+
* without (`requiredParams`). "A widget the app can't render" and "a widget with
|
|
56
|
+
* nothing to read" end the same way on the page — the second one worse, because
|
|
57
|
+
* it renders a number instead of nothing (bd startsim-y5dnx).
|
|
40
58
|
*/
|
|
41
59
|
export function renderDashboardConfig(widgets: DashboardWidgetConfig[]): RenderResult {
|
|
42
60
|
const errors: string[] = [];
|
|
@@ -44,6 +62,15 @@ export function renderDashboardConfig(widgets: DashboardWidgetConfig[]): RenderR
|
|
|
44
62
|
for (const w of widgets) {
|
|
45
63
|
if (!w.kind) errors.push('a widget is missing its "kind"');
|
|
46
64
|
else if (!WIDGET_IDS.has(w.kind)) errors.push(`unknown widget kind "${w.kind}"`);
|
|
65
|
+
else {
|
|
66
|
+
const missing = missingParams(w);
|
|
67
|
+
if (missing.length > 0) {
|
|
68
|
+
errors.push(
|
|
69
|
+
`widget "${w.kind}" is missing required config: ${missing.join(', ')} — ` +
|
|
70
|
+
'it will not guess, so it would render nothing (or worse, a number nobody can act on)',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
47
74
|
}
|
|
48
75
|
if (errors.length > 0) return { ok: false, errors };
|
|
49
76
|
|
package/src/catalog/types.ts
CHANGED
|
@@ -54,6 +54,20 @@ export interface CatalogEntry {
|
|
|
54
54
|
* config. `kind` matches `id` for widgets.
|
|
55
55
|
*/
|
|
56
56
|
configExample?: Record<string, unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* Config keys this block cannot work without, beyond `kind`. A widget that can
|
|
59
|
+
* still render an honest empty state with no params declares none; one that
|
|
60
|
+
* would have to GUESS what to read declares what it must be told, and
|
|
61
|
+
* `renderDashboardConfig` refuses to write a config that omits them.
|
|
62
|
+
*
|
|
63
|
+
* `source-freshness` is the reason this exists: it used to auto-pick a type and
|
|
64
|
+
* a date attribute, and on the tandem tenant that guess landed on a business
|
|
65
|
+
* date that is null until a commitment is resolved — 152 records reported as
|
|
66
|
+
* "never" and counted stale, half an hour after the newest one arrived
|
|
67
|
+
* (bd startsim-y5dnx). A schema does not record what a date MEANS, so the fork
|
|
68
|
+
* has to say.
|
|
69
|
+
*/
|
|
70
|
+
requiredParams?: string[];
|
|
57
71
|
}
|
|
58
72
|
|
|
59
73
|
/** A block plus the concrete files to add to a fork to enable it (scaffolding). */
|