@keemakr/ui-sdk 1.6.0-dev-20261029104555 → 1.6.0-dev-20261029112042

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.
@@ -73,6 +73,10 @@ import type { ChatPress, ScreenRootNode, ScreenTree } from './schema.js';
73
73
  * published release; core's own renderer draws it). An older parse strips it without a sign,
74
74
  * so without it the bar's legend ("Red 8 · Amber 13 · Green 29") is written into the tile's
75
75
  * `hint`, after any hint it had, and the bar is dropped.
76
+ * - `stat.sparkline`: a stat tile's `sparkline`, a small trend line in the tile (newer than any
77
+ * published release; core's own renderer draws it). An older parse strips it without a sign,
78
+ * so without it the line is dropped on purpose and the fallback list says so. The tile keeps
79
+ * its number; the line carried no exact value.
76
80
  * - `section.action`: a section's `action`, a press drawn at the right end of its title line
77
81
  * (newer than any published release; core's own renderer draws it). An older parse strips it,
78
82
  * so without it the action becomes a `button` with the same label and press, last in the
@@ -94,7 +98,7 @@ import type { ChatPress, ScreenRootNode, ScreenTree } from './schema.js';
94
98
  * button included (never left as muted text, which would read as a label), and a row, card,
95
99
  * entry or image stays but is no longer pressable.
96
100
  */
97
- export type UiFeature = 'breadcrumbs' | 'button.disabled' | 'section.row' | 'form.field_kinds' | 'form.confirm' | 'press.chat' | 'form.radio' | 'table.cells' | 'header.meta' | 'tabs.many' | 'choice.action' | 'markdown.links' | 'form.disabled' | 'press.seek' | 'edit.inline' | 'timeline.time_tag' | 'stat.bar' | 'section.action' | 'facts.meter' | 'form.show_when' | 'press.focus';
101
+ export type UiFeature = 'breadcrumbs' | 'button.disabled' | 'section.row' | 'form.field_kinds' | 'form.confirm' | 'press.chat' | 'form.radio' | 'table.cells' | 'header.meta' | 'tabs.many' | 'choice.action' | 'markdown.links' | 'form.disabled' | 'press.seek' | 'edit.inline' | 'timeline.time_tag' | 'stat.bar' | 'stat.sparkline' | 'section.action' | 'facts.meter' | 'form.show_when' | 'press.focus';
98
102
  /** The node types and features one ui-sdk release, or one client, draws. */
99
103
  export interface NodeSet {
100
104
  /** For messages: "ui-sdk 1.4.0", "mobile 0.2.5". */
@@ -131,7 +135,7 @@ export declare const UI_SDK_SNAPSHOTS: {
131
135
  readonly current: {
132
136
  readonly name: "ui-sdk (this package)";
133
137
  readonly nodes: readonly ["heading", "text", "markdown", "stat", "button", "form", "table", "timeline", "pending_request", "pending_requests", "choice_cards", "progress", "chart", "section", "tabs", "pagination", "select", "alert", "details", "timestamp", "divider", "board", "link", "tags", "toggle", "segmented", "progress_bar", "conversation", "composer", "calendar", "image", "video", "stacked_bar", "heatmap", "audio", "document"];
134
- readonly features: readonly ["breadcrumbs", "button.disabled", "section.row", "form.field_kinds", "form.confirm", "press.chat", "form.radio", "table.cells", "header.meta", "tabs.many", "choice.action", "markdown.links", "form.disabled", "press.seek", "edit.inline", "timeline.time_tag", "stat.bar", "section.action", "facts.meter", "form.show_when", "press.focus"];
138
+ readonly features: readonly ["breadcrumbs", "button.disabled", "section.row", "form.field_kinds", "form.confirm", "press.chat", "form.radio", "table.cells", "header.meta", "tabs.many", "choice.action", "markdown.links", "form.disabled", "press.seek", "edit.inline", "timeline.time_tag", "stat.bar", "stat.sparkline", "section.action", "facts.meter", "form.show_when", "press.focus"];
135
139
  };
136
140
  };
137
141
  export type UiSdkSnapshot = keyof typeof UI_SDK_SNAPSHOTS;
package/dist/negotiate.js CHANGED
@@ -52,6 +52,7 @@ const ALL_FEATURES = [
52
52
  'edit.inline',
53
53
  'timeline.time_tag',
54
54
  'stat.bar',
55
+ 'stat.sparkline',
55
56
  'section.action',
56
57
  'facts.meter',
57
58
  'form.show_when',
@@ -407,10 +408,21 @@ export function negotiateTree(tree, set) {
407
408
  return changed ? { ...form, fields } : form;
408
409
  }
409
410
  case 'stat': {
410
- if (!node.bar || features.has('stat.bar'))
411
- return node;
411
+ let stat = node;
412
+ if (stat.sparkline && !features.has('stat.sparkline')) {
413
+ fallbacks.push({
414
+ path: `${path}.sparkline`,
415
+ type: 'stat',
416
+ reason: 'stat.sparkline',
417
+ as: 'dropped',
418
+ });
419
+ const { sparkline: _sparkline, ...rest } = stat;
420
+ stat = rest;
421
+ }
422
+ if (!stat.bar || features.has('stat.bar'))
423
+ return stat;
412
424
  fallbacks.push({ path, type: 'stat', reason: 'stat.bar', as: 'folded' });
413
- const { bar, ...tile } = node;
425
+ const { bar, ...tile } = stat;
414
426
  const { legend } = stackedBarParts({ label: node.label, ...bar });
415
427
  return {
416
428
  ...tile,
package/dist/schema.d.ts CHANGED
@@ -318,7 +318,6 @@ export declare const tableCellSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
318
318
  locale: z.ZodOptional<z.ZodString>;
319
319
  }, z.core.$strip>>;
320
320
  }, z.core.$strip>, z.ZodObject<{
321
- kind: z.ZodLiteral<"sparkline">;
322
321
  values: z.ZodArray<z.ZodNumber>;
323
322
  tone: z.ZodDefault<z.ZodEnum<{
324
323
  default: "default";
@@ -329,6 +328,7 @@ export declare const tableCellSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
329
328
  accent: "accent";
330
329
  }>>;
331
330
  label: z.ZodString;
331
+ kind: z.ZodLiteral<"sparkline">;
332
332
  }, z.core.$strip>, z.ZodObject<{
333
333
  kind: z.ZodLiteral<"meter">;
334
334
  value: z.ZodNumber;
@@ -525,6 +525,18 @@ declare const leafNodeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
525
525
  }, z.core.$strip>>;
526
526
  max: z.ZodOptional<z.ZodNumber>;
527
527
  }, z.core.$strip>>;
528
+ sparkline: z.ZodOptional<z.ZodObject<{
529
+ values: z.ZodArray<z.ZodNumber>;
530
+ tone: z.ZodDefault<z.ZodEnum<{
531
+ default: "default";
532
+ success: "success";
533
+ warning: "warning";
534
+ destructive: "destructive";
535
+ info: "info";
536
+ accent: "accent";
537
+ }>>;
538
+ label: z.ZodString;
539
+ }, z.core.$strip>>;
528
540
  }, z.core.$strip>, z.ZodObject<{
529
541
  type: z.ZodLiteral<"button">;
530
542
  label: z.ZodString;
@@ -757,7 +769,6 @@ declare const leafNodeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
757
769
  locale: z.ZodOptional<z.ZodString>;
758
770
  }, z.core.$strip>>;
759
771
  }, z.core.$strip>, z.ZodObject<{
760
- kind: z.ZodLiteral<"sparkline">;
761
772
  values: z.ZodArray<z.ZodNumber>;
762
773
  tone: z.ZodDefault<z.ZodEnum<{
763
774
  default: "default";
@@ -768,6 +779,7 @@ declare const leafNodeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
768
779
  accent: "accent";
769
780
  }>>;
770
781
  label: z.ZodString;
782
+ kind: z.ZodLiteral<"sparkline">;
771
783
  }, z.core.$strip>, z.ZodObject<{
772
784
  kind: z.ZodLiteral<"meter">;
773
785
  value: z.ZodNumber;
@@ -931,6 +943,18 @@ declare const leafNodeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
931
943
  }, z.core.$strip>>;
932
944
  max: z.ZodOptional<z.ZodNumber>;
933
945
  }, z.core.$strip>>;
946
+ sparkline: z.ZodOptional<z.ZodObject<{
947
+ values: z.ZodArray<z.ZodNumber>;
948
+ tone: z.ZodDefault<z.ZodEnum<{
949
+ default: "default";
950
+ success: "success";
951
+ warning: "warning";
952
+ destructive: "destructive";
953
+ info: "info";
954
+ accent: "accent";
955
+ }>>;
956
+ label: z.ZodString;
957
+ }, z.core.$strip>>;
934
958
  }, z.core.$strip>, z.ZodObject<{
935
959
  type: z.ZodLiteral<"button">;
936
960
  label: z.ZodString;
package/dist/schema.js CHANGED
@@ -337,6 +337,15 @@ const meterCellSchema = z.object({
337
337
  * reason under a health badge.
338
338
  */
339
339
  const cellSecondary = z.string().min(1).max(200).optional();
340
+ /** A trend line with no axis: a table's `sparkline` cell, or a stat tile's `sparkline`. */
341
+ const sparklineFields = {
342
+ /** The points, oldest first: 30 days of usage. */
343
+ values: z.array(z.number()).min(2).max(60),
344
+ /** The line's colour. */
345
+ tone: tone.default('default'),
346
+ /** What the line says, in words: "Usage down 18% in 30 days". Required. */
347
+ label: z.string().min(1).max(120),
348
+ };
340
349
  /**
341
350
  * A table cell: every value cell kind, plus `sparkline` and `meter`, and a
342
351
  * `secondary` line on `text` and `badge`. Table cells only: a `details` row and a board card
@@ -355,15 +364,7 @@ export const tableCellSchema = z.discriminatedUnion('kind', [
355
364
  textCellSchema.extend({ secondary: cellSecondary }),
356
365
  badgeCellSchema.extend({ secondary: cellSecondary }),
357
366
  dateCellSchema,
358
- z.object({
359
- kind: z.literal('sparkline'),
360
- /** The points, oldest first: 30 days of usage. */
361
- values: z.array(z.number()).min(2).max(60),
362
- /** The line's colour. */
363
- tone: tone.default('default'),
364
- /** What the line says, in words: "Usage down 18% in 30 days". Required. */
365
- label: z.string().min(1).max(120),
366
- }),
367
+ z.object({ kind: z.literal('sparkline'), ...sparklineFields }),
367
368
  meterCellSchema,
368
369
  ]);
369
370
  /**
@@ -1585,22 +1586,27 @@ const markdownSchema = z.object({
1585
1586
  });
1586
1587
  /**
1587
1588
  * A number with its label: a KPI tile. All of `tone`, `trend`, `onPress`,
1588
- * `selected` and `bar` are optional; a renderer that does not know them draws `label`,
1589
+ * `selected`, `bar` and `sparkline` are optional; a renderer that does not know them draws `label`,
1589
1590
  * `value` and `hint` as before.
1590
1591
  *
1591
- * - `tone` colours the tile for a red, amber or green count. The label must
1592
- * still say what the number is: colour is never the only signal.
1592
+ * - `tone` colours the tile for a red, amber or green count: a coloured edge, and
1593
+ * the label drawn as a tinted pill with a dot. The label must still say what
1594
+ * the number is: colour is never the only signal.
1593
1595
  * - `trend` is the change since the last period, drawn as an arrow and its
1594
1596
  * text ("↑ +12% vs last week"). `good` says which way is good, so a falling
1595
- * complaint count reads green; without it the trend is neutral.
1597
+ * complaint count reads green; without it the trend is neutral. A value that
1598
+ * starts with its own arrow ("▲ 2 pts") is drawn as written, with no second one.
1596
1599
  * - `onPress` makes the tile a button: open the list behind the number, or
1597
1600
  * filter the list below it.
1598
1601
  * - `selected` is for a tile that filters: set it on every tile of the group,
1599
- * true on the active one. It is drawn as pressed and said as pressed.
1602
+ * true on the active one. It is drawn as pressed, ringed in the tile's tone,
1603
+ * and said as pressed.
1600
1604
  * - `bar` is a small bar split into coloured parts under the value, the shape
1601
1605
  * of a `stacked_bar`'s segments: Book health 50, Red 8 · Amber 13 · Green 29.
1602
1606
  * The stat's `label` names it. A segment takes no press: the tile is one
1603
1607
  * control, and `onPress` is its press.
1608
+ * - `sparkline` is a small trend line in the tile, the shape of a table's
1609
+ * sparkline cell: 30 days of usage. Its `label` says what the line says.
1604
1610
  */
1605
1611
  const statSchema = z
1606
1612
  .object({
@@ -1629,6 +1635,7 @@ const statSchema = z
1629
1635
  max: z.number().finite().positive().optional(),
1630
1636
  })
1631
1637
  .optional(),
1638
+ sparkline: z.object(sparklineFields).optional(),
1632
1639
  })
1633
1640
  .superRefine((node, ctx) => {
1634
1641
  if (node.bar)
package/dist/stat.d.ts CHANGED
@@ -7,8 +7,11 @@ export interface StatTrendLike {
7
7
  /** Whether a trend is good news, bad news, or neither. */
8
8
  export type StatTrendFeel = 'good' | 'bad' | 'neutral';
9
9
  export interface StatTrend {
10
- /** The arrow drawn before the text: ↑, ↓ or →. Never the only signal. */
11
- arrow: '↑' | '↓' | '→';
10
+ /**
11
+ * The arrow drawn before the text: ↑, ↓ or →. Never the only signal. Empty when the value
12
+ * starts with an arrow of its own ("▲ 2 pts"): the value is drawn as written.
13
+ */
14
+ arrow: '↑' | '↓' | '→' | '';
12
15
  /** good: drawn in the success colour; bad: in the destructive colour; neutral: muted. */
13
16
  feel: StatTrendFeel;
14
17
  /** What is drawn after the arrow: "+12% vs last week". */
@@ -19,6 +22,7 @@ export interface StatTrend {
19
22
  /**
20
23
  * How to draw a tile's trend. A flat trend, or one without `good`, is neutral.
21
24
  * The spoken text drops a leading sign from the value ("+12%" is said "up 12%"),
22
- * because the direction already says it.
25
+ * because the direction already says it. A value that starts with its own arrow
26
+ * ("▲ 2 pts") gets no second one, and its arrow is not said either.
23
27
  */
24
28
  export declare function statTrend(trend: StatTrendLike): StatTrend;
package/dist/stat.js CHANGED
@@ -4,10 +4,13 @@
4
4
  // reads green on web and red on the phone.
5
5
  const ARROW = { up: '↑', down: '↓', flat: '→' };
6
6
  const WORD = { up: 'up', down: 'down', flat: 'no change' };
7
+ /** An arrow the author wrote at the start of the value ("▲ 2 pts"), and the space after it. */
8
+ const OWN_ARROW = /^[↑↓→←↗↘▲▼△▽▴▾⬆⬇]\s*/u;
7
9
  /**
8
10
  * How to draw a tile's trend. A flat trend, or one without `good`, is neutral.
9
11
  * The spoken text drops a leading sign from the value ("+12%" is said "up 12%"),
10
- * because the direction already says it.
12
+ * because the direction already says it. A value that starts with its own arrow
13
+ * ("▲ 2 pts") gets no second one, and its arrow is not said either.
11
14
  */
12
15
  export function statTrend(trend) {
13
16
  const feel = !trend.good || trend.direction === 'flat'
@@ -16,10 +19,11 @@ export function statTrend(trend) {
16
19
  ? 'good'
17
20
  : 'bad';
18
21
  const text = trend.label ? `${trend.value} ${trend.label}` : trend.value;
19
- const amount = trend.value.replace(/^[+\-−]\s*/, '');
22
+ const ownArrow = OWN_ARROW.test(trend.value);
23
+ const amount = trend.value.replace(OWN_ARROW, '').replace(/^[+\-−]\s*/, '');
20
24
  const said = [WORD[trend.direction], amount, trend.label].filter(Boolean).join(' ');
21
25
  return {
22
- arrow: ARROW[trend.direction],
26
+ arrow: ownArrow ? '' : ARROW[trend.direction],
23
27
  feel,
24
28
  text,
25
29
  spoken: feel === 'neutral' ? said : `${said}, ${feel}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keemakr/ui-sdk",
3
- "version": "1.6.0-dev-20261029104555",
3
+ "version": "1.6.0-dev-20261029112042",
4
4
  "description": "Product screens for keemakr marketplace modules: a versioned, server-driven UI tree schema plus defineScreen() handlers. Module code never ships to the browser — keemakr-core validates the tree and renders it with platform components.",
5
5
  "license": "MIT",
6
6
  "type": "module",