@nanisoft/prism-ui 0.16.1 → 1.1.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.
Files changed (170) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/blocks/activity-feed-01/activity-feed-01.d.ts +120 -190
  3. package/dist/blocks/activity-feed-01/activity-feed-01.d.ts.map +1 -1
  4. package/dist/blocks/activity-feed-01/activity-feed-01.js +107 -97
  5. package/dist/blocks/activity-feed-01/index.d.ts +1 -1
  6. package/dist/blocks/activity-feed-01/index.d.ts.map +1 -1
  7. package/dist/blocks/auth-form-01/auth-form.d.ts +86 -53
  8. package/dist/blocks/auth-form-01/auth-form.d.ts.map +1 -1
  9. package/dist/blocks/auth-form-01/auth-form.js +95 -31
  10. package/dist/blocks/auth-form-01/index.d.ts +1 -1
  11. package/dist/blocks/auth-form-01/index.d.ts.map +1 -1
  12. package/dist/blocks/calendar-01/calendar.d.ts +28 -0
  13. package/dist/blocks/calendar-01/calendar.d.ts.map +1 -1
  14. package/dist/blocks/calendar-01/calendar.js +15 -1
  15. package/dist/blocks/card-index-01/card-index.d.ts +225 -0
  16. package/dist/blocks/card-index-01/card-index.d.ts.map +1 -0
  17. package/dist/blocks/card-index-01/card-index.js +154 -0
  18. package/dist/blocks/card-index-01/index.d.ts +3 -0
  19. package/dist/blocks/card-index-01/index.d.ts.map +1 -0
  20. package/dist/blocks/card-index-01/index.js +1 -0
  21. package/dist/blocks/chart-card-01/chart-card-01.d.ts +23 -21
  22. package/dist/blocks/chart-card-01/chart-card-01.d.ts.map +1 -1
  23. package/dist/blocks/chart-card-01/chart-card-01.js +46 -1
  24. package/dist/blocks/chart-card-01/index.d.ts +1 -1
  25. package/dist/blocks/chart-card-01/index.d.ts.map +1 -1
  26. package/dist/blocks/chart-group-01/chart-group-01.d.ts +3 -3
  27. package/dist/blocks/chart-group-01/chart-group-01.d.ts.map +1 -1
  28. package/dist/blocks/contact-01/contact.d.ts +30 -82
  29. package/dist/blocks/contact-01/contact.d.ts.map +1 -1
  30. package/dist/blocks/contact-01/contact.js +59 -30
  31. package/dist/blocks/contact-01/index.d.ts +1 -1
  32. package/dist/blocks/contact-01/index.d.ts.map +1 -1
  33. package/dist/blocks/dashboard-01/dashboard-01.d.ts +26 -71
  34. package/dist/blocks/dashboard-01/dashboard-01.d.ts.map +1 -1
  35. package/dist/blocks/dashboard-01/dashboard-01.js +34 -23
  36. package/dist/blocks/dashboard-01/index.d.ts +1 -1
  37. package/dist/blocks/dashboard-01/index.d.ts.map +1 -1
  38. package/dist/blocks/data-table-01/data-table.d.ts +276 -59
  39. package/dist/blocks/data-table-01/data-table.d.ts.map +1 -1
  40. package/dist/blocks/data-table-01/data-table.js +217 -35
  41. package/dist/blocks/data-table-01/index.d.ts +1 -1
  42. package/dist/blocks/data-table-01/index.d.ts.map +1 -1
  43. package/dist/blocks/directory-01/directory-01.d.ts +23 -2
  44. package/dist/blocks/directory-01/directory-01.d.ts.map +1 -1
  45. package/dist/blocks/directory-01/directory-01.js +3 -3
  46. package/dist/blocks/empty-state-01/empty-state.d.ts +60 -21
  47. package/dist/blocks/empty-state-01/empty-state.d.ts.map +1 -1
  48. package/dist/blocks/empty-state-01/empty-state.js +52 -17
  49. package/dist/blocks/index-detail-01/index-detail.d.ts +85 -0
  50. package/dist/blocks/index-detail-01/index-detail.d.ts.map +1 -0
  51. package/dist/blocks/index-detail-01/index-detail.js +58 -0
  52. package/dist/blocks/index-detail-01/index.d.ts +3 -0
  53. package/dist/blocks/index-detail-01/index.d.ts.map +1 -0
  54. package/dist/blocks/index-detail-01/index.js +1 -0
  55. package/dist/blocks/index.d.ts +20 -8
  56. package/dist/blocks/index.d.ts.map +1 -1
  57. package/dist/blocks/index.js +7 -0
  58. package/dist/blocks/issue-detail-01/issue-detail.d.ts +18 -0
  59. package/dist/blocks/issue-detail-01/issue-detail.d.ts.map +1 -1
  60. package/dist/blocks/issue-detail-01/issue-detail.js +18 -0
  61. package/dist/blocks/nothing-chosen-01/index.d.ts +3 -0
  62. package/dist/blocks/nothing-chosen-01/index.d.ts.map +1 -0
  63. package/dist/blocks/nothing-chosen-01/index.js +1 -0
  64. package/dist/blocks/nothing-chosen-01/nothing-chosen.d.ts +119 -0
  65. package/dist/blocks/nothing-chosen-01/nothing-chosen.d.ts.map +1 -0
  66. package/dist/blocks/nothing-chosen-01/nothing-chosen.js +79 -0
  67. package/dist/blocks/project-dashboard-01/index.d.ts +1 -1
  68. package/dist/blocks/project-dashboard-01/index.d.ts.map +1 -1
  69. package/dist/blocks/project-dashboard-01/project-dashboard.d.ts +21 -27
  70. package/dist/blocks/project-dashboard-01/project-dashboard.d.ts.map +1 -1
  71. package/dist/blocks/project-dashboard-01/project-dashboard.js +45 -5
  72. package/dist/blocks/provisioning-01/index.d.ts +1 -1
  73. package/dist/blocks/provisioning-01/index.d.ts.map +1 -1
  74. package/dist/blocks/provisioning-01/provisioning.d.ts +9 -97
  75. package/dist/blocks/provisioning-01/provisioning.d.ts.map +1 -1
  76. package/dist/blocks/provisioning-01/provisioning.js +26 -13
  77. package/dist/blocks/record-detail-01/index.d.ts +3 -0
  78. package/dist/blocks/record-detail-01/index.d.ts.map +1 -0
  79. package/dist/blocks/record-detail-01/index.js +1 -0
  80. package/dist/blocks/record-detail-01/record-detail.d.ts +239 -0
  81. package/dist/blocks/record-detail-01/record-detail.d.ts.map +1 -0
  82. package/dist/blocks/record-detail-01/record-detail.js +150 -0
  83. package/dist/blocks/record-form-01/index.d.ts +3 -0
  84. package/dist/blocks/record-form-01/index.d.ts.map +1 -0
  85. package/dist/blocks/record-form-01/index.js +1 -0
  86. package/dist/blocks/record-form-01/record-form.d.ts +148 -0
  87. package/dist/blocks/record-form-01/record-form.d.ts.map +1 -0
  88. package/dist/blocks/record-form-01/record-form.js +107 -0
  89. package/dist/blocks/record-grid-01/index.d.ts +3 -0
  90. package/dist/blocks/record-grid-01/index.d.ts.map +1 -0
  91. package/dist/blocks/record-grid-01/index.js +1 -0
  92. package/dist/blocks/record-grid-01/record-grid.d.ts +145 -0
  93. package/dist/blocks/record-grid-01/record-grid.d.ts.map +1 -0
  94. package/dist/blocks/record-grid-01/record-grid.js +114 -0
  95. package/dist/blocks/record-wizard-01/index.d.ts +3 -0
  96. package/dist/blocks/record-wizard-01/index.d.ts.map +1 -0
  97. package/dist/blocks/record-wizard-01/index.js +1 -0
  98. package/dist/blocks/record-wizard-01/record-wizard.d.ts +152 -0
  99. package/dist/blocks/record-wizard-01/record-wizard.d.ts.map +1 -0
  100. package/dist/blocks/record-wizard-01/record-wizard.js +144 -0
  101. package/dist/blocks/settings-members-01/settings-members.d.ts +2 -1
  102. package/dist/blocks/settings-members-01/settings-members.d.ts.map +1 -1
  103. package/dist/blocks/settings-panel-01/index.d.ts +1 -1
  104. package/dist/blocks/settings-panel-01/index.d.ts.map +1 -1
  105. package/dist/blocks/settings-panel-01/settings-panel.d.ts +165 -67
  106. package/dist/blocks/settings-panel-01/settings-panel.d.ts.map +1 -1
  107. package/dist/blocks/settings-panel-01/settings-panel.js +92 -33
  108. package/dist/blocks/signup-01/index.d.ts +1 -1
  109. package/dist/blocks/signup-01/index.d.ts.map +1 -1
  110. package/dist/blocks/signup-01/signup-01.d.ts +10 -84
  111. package/dist/blocks/signup-01/signup-01.d.ts.map +1 -1
  112. package/dist/blocks/signup-01/signup-01.js +41 -23
  113. package/dist/blocks/stats-01/index.d.ts +5 -5
  114. package/dist/blocks/stats-01/index.d.ts.map +1 -1
  115. package/dist/blocks/stats-01/index.js +4 -4
  116. package/dist/blocks/stats-01/stats.d.ts +49 -14
  117. package/dist/blocks/stats-01/stats.d.ts.map +1 -1
  118. package/dist/blocks/stats-01/stats.js +65 -15
  119. package/dist/blocks/trend-01/index.d.ts +1 -1
  120. package/dist/blocks/trend-01/index.d.ts.map +1 -1
  121. package/dist/blocks/trend-01/trend.d.ts +26 -73
  122. package/dist/blocks/trend-01/trend.d.ts.map +1 -1
  123. package/dist/blocks/trend-01/trend.js +47 -30
  124. package/dist/catalog.d.ts.map +1 -1
  125. package/dist/catalog.js +93 -3
  126. package/dist/components/ui/metric.d.ts +4 -2
  127. package/dist/components/ui/metric.d.ts.map +1 -1
  128. package/dist/index.d.ts +10 -0
  129. package/dist/index.d.ts.map +1 -1
  130. package/dist/index.js +6 -0
  131. package/dist/lib/field-render.d.ts +107 -0
  132. package/dist/lib/field-render.d.ts.map +1 -0
  133. package/dist/lib/field-render.js +251 -0
  134. package/dist/lib/spec.d.ts +609 -0
  135. package/dist/lib/spec.d.ts.map +1 -0
  136. package/dist/lib/spec.js +1 -0
  137. package/dist/live/index.d.ts +2 -0
  138. package/dist/live/index.d.ts.map +1 -1
  139. package/dist/live/index.js +6 -0
  140. package/dist/live/message-thread-01/index.d.ts +3 -0
  141. package/dist/live/message-thread-01/index.d.ts.map +1 -0
  142. package/dist/live/message-thread-01/index.js +1 -0
  143. package/dist/live/message-thread-01/message-thread.d.ts +282 -0
  144. package/dist/live/message-thread-01/message-thread.d.ts.map +1 -0
  145. package/dist/live/message-thread-01/message-thread.js +221 -0
  146. package/dist/live/run-stream-01/run-stream.d.ts +2 -1
  147. package/dist/live/run-stream-01/run-stream.d.ts.map +1 -1
  148. package/dist/live/run-stream-01/run-stream.js +1 -7
  149. package/dist/live/status.d.ts +27 -0
  150. package/dist/live/status.d.ts.map +1 -0
  151. package/dist/live/status.js +15 -0
  152. package/dist/live/tool-call-row.d.ts +26 -0
  153. package/dist/live/tool-call-row.d.ts.map +1 -0
  154. package/dist/live/tool-call-row.js +60 -0
  155. package/dist/live/tool-ledger-01/tool-ledger.d.ts.map +1 -1
  156. package/dist/live/tool-ledger-01/tool-ledger.js +2 -42
  157. package/dist/pages/contact-page/index.d.ts +7 -7
  158. package/dist/pages/contact-page/index.d.ts.map +1 -1
  159. package/dist/pages/contact-page/index.js +17 -5
  160. package/dist/pages/document-page/document-page.d.ts +146 -0
  161. package/dist/pages/document-page/document-page.d.ts.map +1 -0
  162. package/dist/pages/document-page/document-page.js +80 -0
  163. package/dist/pages/document-page/index.d.ts +3 -0
  164. package/dist/pages/document-page/index.d.ts.map +1 -0
  165. package/dist/pages/document-page/index.js +1 -0
  166. package/dist/pages/index.d.ts +2 -0
  167. package/dist/pages/index.d.ts.map +1 -1
  168. package/dist/pages/index.js +6 -0
  169. package/dist/styles.css +35 -3
  170. package/package.json +7 -2
@@ -1,81 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { type HeadingLevel } from '../../components/ui/section.js';
3
- /**
4
- * One choice in a `select` field, and nothing else.
5
- *
6
- * A native `<option>` holds text and nothing more, which is the whole reason an
7
- * account form's select is a `NativeSelect` rather than the Base UI one. The two
8
- * values are a string and its label, so a caller who needs a description beside an
9
- * option, a count, or anything nested is reaching for a control the platform cannot
10
- * render, and `Signup01` does not pretend otherwise: it names `NativeSelect`'s own
11
- * argument in its JSDoc and stops there.
12
- */
13
- export type SignupOption = {
14
- /** What the form submits for this choice, which is a key and not a sentence. */
15
- value: string;
16
- /** The words the reader sees, in the product's own language. */
17
- label: string;
18
- };
19
- /**
20
- * What every account field carries, whichever control it draws.
21
- *
22
- * `required` is a boolean rather than an optional prop because there is no third
23
- * state worth having: a field is one the reader must fill or one they may leave, and
24
- * a `required?: boolean` leaves a caller who passed nothing unable to say which of
25
- * the two they meant. A field is a data declaration and nothing else, and the two
26
- * arms below differ only in which control is drawn and in the one property that
27
- * control needs and the other has no use for.
28
- */
29
- type SignupFieldBase = {
30
- /** The caller's key for this field, and the key the value arrives under. */
31
- id: string;
32
- /** The field's visible name, and the accessible name its control announces. */
33
- label: string;
34
- /** Whether the reader may submit the form without filling it. */
35
- required: boolean;
36
- /**
37
- * A line under the control, announced with it rather than printed under the row.
38
- *
39
- * A node, because the honest line in this position is often not one sentence: a
40
- * caller's hint, a link to the document that says why a field is being asked for,
41
- * a piece of the page's own copy. It is wired to the control through
42
- * `aria-describedby`, so a screen reader hears what this field is for as the reader
43
- * reaches it rather than as a footnote they have to go back for.
44
- */
45
- description?: ReactNode;
46
- /** What the control shows while it is empty. A hint, not a name. */
47
- placeholder?: string;
48
- /** The platform's own autofill hint for this field. */
49
- autoComplete?: string;
50
- };
51
- /**
52
- * One field of the caller's account form, and the control it draws.
53
- *
54
- * **A union and not one type with an optional `options`, and that is the shape of
55
- * the whole argument in this Block.** An account form's fields are the caller's
56
- * declaration, not this package's opinion about what a person may be asked, and the
57
- * one place that shows is the select: a select with no options is a control with
58
- * nothing in it, which is a question no reader can answer, so `options` is required
59
- * inside the select arm and forbidden beside the other three. A `options?: string[]`
60
- * on one type would typecheck the defect and only the diagnostic at run time would
61
- * catch it, which is one gate too late. The union is the same mechanism
62
- * `ContactField` uses for the same reason: the shape that requires something declares
63
- * it, and the shape that forbids it says so in the type rather than in prose.
64
- *
65
- * The cost of the union is that a caller who wants to build a field list
66
- * generically, from a schema, has to narrow before it can type the result, and the
67
- * answer to that is a narrowing function in the caller's own code rather than a
68
- * looser type here.
69
- */
70
- export type SignupField = (SignupFieldBase & {
71
- type: 'select';
72
- /** The choices, in the order the reader should meet them. */
73
- options: readonly SignupOption[];
74
- }) | (SignupFieldBase & {
75
- type: 'text' | 'email' | 'tel';
76
- /** A select with no options is a question nobody can answer, so it is not one here. */
77
- options?: never;
78
- });
3
+ import type { FieldSpecGroup } from '../../lib/spec.js';
79
4
  /**
80
5
  * One reading of the secret the reader is typing, in the caller's own words and on
81
6
  * the caller's own scale.
@@ -237,7 +162,7 @@ export type Signup01Props = {
237
162
  */
238
163
  onSubmit: (value: SignupValue) => void;
239
164
  /**
240
- * The fields, in the order the reader should meet them.
165
+ * The fields, grouped and ordered, as the shared field specification.
241
166
  *
242
167
  * **Required, and this list is the whole design rather than a convenience.** An
243
168
  * account form with a fixed set of fields is a form that decides what a person may
@@ -247,13 +172,14 @@ export type Signup01Props = {
247
172
  * where every entity is scoped to one, a form that cannot ask for a job title
248
173
  * guarantees a sales team three weeks of discovery calls, and a form that cannot
249
174
  * ask which estate somebody is observing sends a half a dozen accounts onto a
250
- * queue that belongs to one. So `fields` is the caller's declaration, Prism names
251
- * the four controls it can draw and supplies no field at all. The cost is stated
252
- * rather than hidden: one column in the order the list gives, so a consumer that
253
- * wants a two-column form for a subset composes two of these or reaches for a
254
- * `Page`.
175
+ * queue that belongs to one. The fields arrive as `FieldSpecGroup`, the same type
176
+ * a record write form takes, so the vocabulary a consumer learns on one screen is
177
+ * the vocabulary here and a control this package does not ship is the `slot` arm.
178
+ * The cost is stated rather than hidden: one column in the order the list gives,
179
+ * so a consumer that wants a two-column form for a subset composes two of these
180
+ * or reaches for a `Page`.
255
181
  */
256
- fields: readonly SignupField[];
182
+ groups: readonly FieldSpecGroup[];
257
183
  /** The secret, and the caller's own reading of it. See `Signup01Password`. */
258
184
  password: Signup01Password;
259
185
  /**
@@ -400,6 +326,6 @@ export type Signup01Props = {
400
326
  * function to a prop, so a caller rendering this from a server component gets a form
401
327
  * that submits and calls nothing and a reading that never appears.
402
328
  */
403
- export declare function Signup01({ eyebrow, title, description, onSubmit, fields, password, terms, submitLabel, submitting, status, signIn, headingLevel, className, }: Signup01Props): import("react").JSX.Element;
329
+ export declare function Signup01({ eyebrow, title, description, onSubmit, groups, password, terms, submitLabel, submitting, status, signIn, headingLevel, className, }: Signup01Props): import("react").JSX.Element;
404
330
  export default Signup01;
405
331
  //# sourceMappingURL=signup-01.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"signup-01.d.ts","sourceRoot":"","sources":["../../../src/blocks/signup-01/signup-01.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"signup-01.d.ts","sourceRoot":"","sources":["../../../src/blocks/signup-01/signup-01.tsx"],"names":[],"mappings":"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"}
@@ -4,13 +4,13 @@ import { useId, useState } from 'react';
4
4
  import { Button } from '../../components/ui/button.js';
5
5
  import { Card } from '../../components/ui/card.js';
6
6
  import { CtaLink } from '../../components/ui/cta-link.js';
7
- import { Field, FieldDescription, FieldGroup, FieldLabel } from '../../components/ui/field.js';
8
- import { Input } from '../../components/ui/input.js';
7
+ import { Field, FieldDescription, FieldGroup } from '../../components/ui/field.js';
8
+ import { Label } from '../../components/ui/label.js';
9
9
  import { LiveRegion } from '../../components/ui/live-region.js';
10
- import { NativeSelect } from '../../components/ui/native-select.js';
11
10
  import { PasswordField } from '../../components/ui/password-field.js';
12
11
  import { Progress } from '../../components/ui/progress.js';
13
12
  import { Section, SectionHeading } from '../../components/ui/section.js';
13
+ import { CHOICE_KINDS, fieldText, renderControl, SELF_LABELLED, } from '../../lib/field-render.js';
14
14
  import { cn } from '../../lib/utils.js';
15
15
  /**
16
16
  * The empty form, which is an empty record and an empty secret.
@@ -47,18 +47,21 @@ function blank(value) {
47
47
  * reader is watching while they choose it.
48
48
  */
49
49
  function assertSignup(input) {
50
- const { fields, password, signIn } = input;
51
- for (const field of fields) {
52
- if (blank(field.label)) {
53
- throw new Error(`Signup01: the field "${field.id}" declares no label, so its control would be announced with no name ` +
54
- 'and two fields on this page would be announced identically. Every control this Block draws is named ' +
55
- 'by its label, so there is no fallback to fall back to.');
56
- }
57
- if (field.type === 'select' && (field.options === undefined || field.options.length === 0)) {
58
- throw new Error(`Signup01: the field "${field.id}" is a select and passes no options, so it would render a control ` +
59
- 'with nothing in it, which is a question no reader can answer and looks answered only because the ' +
60
- 'control is there and focusable. Pass the choices, or declare the field as a text field and check the ' +
61
- 'answer in your own handler.');
50
+ const { groups, password, signIn } = input;
51
+ for (const group of groups) {
52
+ for (const field of group.fields) {
53
+ if (blank(typeof field.label === 'string' ? field.label : undefined)) {
54
+ throw new Error(`Signup01: the field "${field.key}" declares no label, so its control would be announced with no name ` +
55
+ 'and two fields on this page would be announced identically. Every control this Block draws is named ' +
56
+ 'by its label, so there is no fallback to fall back to.');
57
+ }
58
+ if (CHOICE_KINDS.has(field.kind) &&
59
+ (field.options?.length ?? 0) === 0) {
60
+ throw new Error(`Signup01: the field "${field.key}" is a choice and passes no options, so it would render a control ` +
61
+ 'with nothing in it, which is a question no reader can answer and looks answered only because the ' +
62
+ 'control is there and focusable. Pass the choices, or declare the field as a text field and check the ' +
63
+ 'answer in your own handler.');
64
+ }
62
65
  }
63
66
  }
64
67
  if (blank(password.label) || blank(password.revealLabel) || blank(password.hideLabel)) {
@@ -172,8 +175,8 @@ function assertSignup(input) {
172
175
  * function to a prop, so a caller rendering this from a server component gets a form
173
176
  * that submits and calls nothing and a reading that never appears.
174
177
  */
175
- export function Signup01({ eyebrow, title, description, onSubmit, fields, password, terms, submitLabel, submitting = false, status, signIn, headingLevel = 'h2', className, }) {
176
- assertSignup({ fields, password, signIn });
178
+ export function Signup01({ eyebrow, title, description, onSubmit, groups, password, terms, submitLabel, submitting = false, status, signIn, headingLevel = 'h2', className, }) {
179
+ assertSignup({ groups, password, signIn });
177
180
  const generated = useId();
178
181
  const working = submitting || status?.state === 'working';
179
182
  const done = status?.state === 'done';
@@ -193,17 +196,32 @@ export function Signup01({ eyebrow, title, description, onSubmit, fields, passwo
193
196
  if (done)
194
197
  setTyped(EMPTY_SIGNUP);
195
198
  }
196
- const setField = (id) => (value) => setTyped((was) => ({ ...was, fields: { ...was.fields, [id]: value } }));
199
+ const setField = (id) => (value) => setTyped((was) => ({
200
+ ...was,
201
+ fields: { ...was.fields, [id]: value === undefined || value === null ? '' : String(value) },
202
+ }));
197
203
  const setSecret = (value) => setTyped((was) => ({ ...was, password: value }));
198
204
  const handleSubmit = (event) => {
199
205
  event.preventDefault();
200
206
  onSubmit(typed);
201
207
  };
202
208
  const reading = password.strength === undefined ? null : password.strength(typed.password);
203
- return (_jsx(Section, { "data-slot": "signup-01", className: cn(className), children: _jsxs("div", { "data-slot": "signup-01-body", className: "flex flex-col gap-10", children: [_jsx(SectionHeading, { as: headingLevel, align: "left", eyebrow: eyebrow, title: title, description: description }), _jsx(Card, { "data-slot": "signup-01-card", className: "max-w-measure-narrow gap-0 px-6", children: _jsxs("form", { "data-slot": "signup-01-form", onSubmit: handleSubmit, className: "flex w-full flex-col gap-5", children: [_jsxs(FieldGroup, { "data-slot": "signup-01-fields", children: [fields.map((field) => {
204
- const controlId = `${generated}-${field.id}`;
205
- const descriptionId = `${controlId}-description`;
206
- return (_jsxs(Field, { "data-slot": "signup-01-field", "data-field": field.type, children: [_jsx(FieldLabel, { htmlFor: controlId, children: field.label }), field.type === 'select' ? (_jsx(NativeSelect, { id: controlId, name: field.id, value: typed.fields[field.id] ?? '', onChange: (event) => setField(field.id)(event.target.value), required: field.required, "aria-describedby": field.description === undefined ? undefined : descriptionId, children: field.options.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value))) })) : (_jsx(Input, { id: controlId, name: field.id, type: field.type, placeholder: field.placeholder, autoComplete: field.autoComplete, value: typed.fields[field.id] ?? '', onChange: (event) => setField(field.id)(event.target.value), required: field.required, "aria-describedby": field.description === undefined ? undefined : descriptionId })), field.description === undefined ? null : (_jsx(FieldDescription, { id: descriptionId, children: field.description }))] }, field.id));
207
- }), _jsx(PasswordField, { value: typed.password, onValueChange: setSecret, label: password.label, description: password.description, revealLabel: password.revealLabel, hideLabel: password.hideLabel, autoComplete: "new-password" })] }), reading === null ? null : (_jsxs("div", { "data-slot": "signup-01-strength", className: "flex flex-col gap-1.5", children: [_jsx(Progress, { value: reading.value, max: 100, valueText: reading.label }), _jsx("span", { "data-slot": "signup-01-strength-label", className: "text-muted-foreground text-sm", children: reading.label })] })), terms === undefined ? null : (_jsx("p", { "data-slot": "signup-01-terms", className: "text-muted-foreground text-sm text-pretty", children: terms })), _jsxs("div", { "data-slot": "signup-01-actions", className: "flex flex-col gap-3", children: [_jsx(Button, { "data-slot": "signup-01-submit", type: "submit", disabled: working, className: "w-full", children: submitLabel }), _jsx(LiveRegion, { "data-slot": "signup-01-status", politeness: status?.state === 'error' ? 'assertive' : 'polite', busy: working, className: cn('text-sm', status?.state === 'error' ? 'text-destructive font-medium' : 'text-muted-foreground'), children: status?.message })] })] }) }), signIn === undefined ? null : (_jsx(CtaLink, { "data-slot": "signup-01-sign-in", href: signIn.href, variant: "ghost", size: "sm", className: "self-start", children: signIn.label }))] }) }));
209
+ return (_jsx(Section, { "data-slot": "signup-01", className: cn(className), children: _jsxs("div", { "data-slot": "signup-01-body", className: "flex flex-col gap-10", children: [_jsx(SectionHeading, { as: headingLevel, align: "left", eyebrow: eyebrow, title: title, description: description }), _jsx(Card, { "data-slot": "signup-01-card", className: "max-w-measure-narrow gap-0 px-6", children: _jsxs("form", { "data-slot": "signup-01-form", onSubmit: handleSubmit, className: "flex w-full flex-col gap-5", children: [_jsxs(FieldGroup, { "data-slot": "signup-01-fields", children: [groups.map((group, groupIndex) => (_jsxs(FieldGroup, { "data-slot": "signup-01-group", className: "flex flex-col gap-5", children: [group.label === undefined ? null : (_jsx("p", { "data-slot": "signup-01-group-label", className: "text-foreground text-sm font-medium", children: group.label })), group.description === undefined ? null : (_jsx("p", { "data-slot": "signup-01-group-description", className: "text-muted-foreground text-sm", children: group.description })), group.fields.map((field, fieldIndex) => {
210
+ const id = `${generated}-${groupIndex}-${fieldIndex}`;
211
+ const labelId = `${id}-label`;
212
+ const helpId = field.help === undefined ? undefined : `${id}-help`;
213
+ const kind = field.kind.toLowerCase();
214
+ const ctx = {
215
+ id,
216
+ labelId,
217
+ describedBy: helpId,
218
+ invalid: false,
219
+ text: fieldText(field),
220
+ values: typed.fields,
221
+ setValue: (key, value) => setField(key)(value),
222
+ controlled: true,
223
+ };
224
+ return (_jsxs(Field, { "data-slot": "signup-01-field", "data-field": field.kind, children: [SELF_LABELLED.has(kind) ? null : (_jsx(Label, { id: labelId, htmlFor: id, required: field.required, disabled: field.disabled, children: field.label })), renderControl(field, ctx), field.help === undefined ? null : (_jsx(FieldDescription, { id: helpId, children: field.help }))] }, field.key));
225
+ })] }, group.id ?? groupIndex))), _jsx(PasswordField, { value: typed.password, onValueChange: setSecret, label: password.label, description: password.description, revealLabel: password.revealLabel, hideLabel: password.hideLabel, autoComplete: "new-password" })] }), reading === null ? null : (_jsxs("div", { "data-slot": "signup-01-strength", className: "flex flex-col gap-1.5", children: [_jsx(Progress, { value: reading.value, max: 100, valueText: reading.label }), _jsx("span", { "data-slot": "signup-01-strength-label", className: "text-muted-foreground text-sm", children: reading.label })] })), terms === undefined ? null : (_jsx("p", { "data-slot": "signup-01-terms", className: "text-muted-foreground text-sm text-pretty", children: terms })), _jsxs("div", { "data-slot": "signup-01-actions", className: "flex flex-col gap-3", children: [_jsx(Button, { "data-slot": "signup-01-submit", type: "submit", disabled: working, className: "w-full", children: submitLabel }), _jsx(LiveRegion, { "data-slot": "signup-01-status", politeness: status?.state === 'error' ? 'assertive' : 'polite', busy: working, className: cn('text-sm', status?.state === 'error' ? 'text-destructive font-medium' : 'text-muted-foreground'), children: status?.message })] })] }) }), signIn === undefined ? null : (_jsx(CtaLink, { "data-slot": "signup-01-sign-in", href: signIn.href, variant: "ghost", size: "sm", className: "self-start", children: signIn.label }))] }) }));
208
226
  }
209
227
  export default Signup01;
@@ -1,10 +1,10 @@
1
1
  /**
2
- * The subpath exports the Block and its props, and the props are exported because
3
- * a caller that builds a `Stat[]` has to be able to name the type it is building.
2
+ * The subpath exports the Block and its props, and the props are exported because a
3
+ * caller that builds a `MetricSpec[]` has to be able to name the type it is building.
4
4
  * A Block whose prop types are only reachable through inference is a Block whose
5
- * callers annotate their arrays as `any`, which is the point at which a rename
6
- * stops being caught by the compiler.
5
+ * callers annotate their arrays as `any`, which is the point at which a rename stops
6
+ * being caught by the compiler.
7
7
  */
8
8
  export { Stats01, default } from './stats.js';
9
- export type { Stat, Stats01Props } from './stats.js';
9
+ export type { Stats01Props } from './stats.js';
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/blocks/stats-01/index.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAC1C,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/blocks/stats-01/index.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAC1C,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA"}
@@ -1,8 +1,8 @@
1
1
  /**
2
- * The subpath exports the Block and its props, and the props are exported because
3
- * a caller that builds a `Stat[]` has to be able to name the type it is building.
2
+ * The subpath exports the Block and its props, and the props are exported because a
3
+ * caller that builds a `MetricSpec[]` has to be able to name the type it is building.
4
4
  * A Block whose prop types are only reachable through inference is a Block whose
5
- * callers annotate their arrays as `any`, which is the point at which a rename
6
- * stops being caught by the compiler.
5
+ * callers annotate their arrays as `any`, which is the point at which a rename stops
6
+ * being caught by the compiler.
7
7
  */
8
8
  export { Stats01, default } from './stats.js';
@@ -1,27 +1,62 @@
1
1
  import { type HeadingLevel } from '../../components/ui/section.js';
2
- export type Stat = {
3
- label: string;
4
- value: string;
5
- /** Percentage change. Positive reads as growth, negative as decline. */
6
- delta?: number;
7
- /** Period the delta compares against, e.g. "vs last month". */
8
- hint?: string;
9
- };
2
+ import type { MetricSpec } from '../../lib/spec.js';
3
+ /**
4
+ * The props a Stats01 takes. Every string and every figure is a prop and the Block
5
+ * ships none.
6
+ */
10
7
  export type Stats01Props = {
8
+ /** Optional label above the section title. See the No-Default-Eyebrow Rule. */
11
9
  eyebrow?: string;
10
+ /** The section title. Omit it for a row composed under its own heading. */
12
11
  title?: string;
13
- stats: Stat[];
12
+ /**
13
+ * The figures, in the order a reader should meet them.
14
+ *
15
+ * **The shared `MetricSpec` and not a shape of this Block's own.** `key`,
16
+ * `label` and `value` are required on each figure, `delta` is a number whose sign
17
+ * is the direction, `deltaFormat` is the caller's own words for that change,
18
+ * `hint` carries the period or the caveat, and a `series` and an `href` each travel
19
+ * with the label or the words they cannot be read without. The type is published as
20
+ * `@nanisoft/prism-ui/spec`, so the words a caller learns on a summary are the
21
+ * words on this row.
22
+ */
23
+ stats: MetricSpec[];
14
24
  /** Heading level for the section title. See `HeadingLevel`. */
15
25
  headingLevel?: HeadingLevel;
16
26
  };
17
27
  /**
18
- * A KPI row. Every number is a prop.
28
+ * A KPI row whose figures are the shared `MetricSpec` drawn by `Metric` and
29
+ * `Sparkline`. Every figure is a prop.
19
30
  *
20
- * This block used to carry a hardcoded set reading "128 blocks shipped" and "9.4k
21
- * weekly installs". Those were invented figures about a library nobody installs
22
- * yet, and because they lived in the component rather than at the call site, a
23
- * consumer who installed the block shipped fabricated metrics into their own
31
+ * **This block used to carry a hardcoded set reading "128 blocks shipped" and
32
+ * "9.4k weekly installs".** Those were invented figures about a library nobody
33
+ * installs yet, and because they lived in the component rather than at the call site,
34
+ * a consumer who installed the block shipped fabricated metrics into their own
24
35
  * dashboard. A stat block with invented stats in it is worse than no stat block.
36
+ *
37
+ * **It declared its own statistic type and guessed the unit, and that guess is the
38
+ * reason it is in this migration.** Its `delta` was documented as a percentage change
39
+ * and the Block appended a percent sign to the number, which is the one guess
40
+ * `Metric` refuses to make, because a delta of `0.12` is twelve percent, twelve cents
41
+ * or twelve milliseconds. It also carried no series, so the trend a metric summary
42
+ * names had nowhere to go. After this migration a delta with no formatter prints the
43
+ * number the caller passed and a delta with a formatter prints the caller's words,
44
+ * and the same consumer stops seeing a formatted change on one screen and a bare
45
+ * number on another.
46
+ *
47
+ * **The Block keeps its own name, kind and arrangement.** It is the same job as a row
48
+ * of figures with less of it declared, and a consumer who wants figures above
49
+ * supporting detail composes the dashboard Block beside them rather than either
50
+ * becoming the other. Only the statistic shape moves, because a shape is shared and a
51
+ * screen is not.
52
+ *
53
+ * **It still aggregates nothing and judges nothing.** It does not sum, count,
54
+ * average, bucket, rank or divide, it owns no threshold, no target, no severity and
55
+ * no freshness, and it draws no period switcher, no comparison toggle and no export.
56
+ * The period is the caller's words, carried by the `hint` and the delta formatter,
57
+ * and no period member exists.
58
+ *
59
+ * It is a server Component: no hook, no state and no client code.
25
60
  */
26
61
  export declare function Stats01({ eyebrow, title, stats, headingLevel }: Stats01Props): import("react").JSX.Element;
27
62
  export default Stats01;
@@ -1 +1 @@
1
- {"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../../src/blocks/stats-01/stats.tsx"],"names":[],"mappings":"AAGA,OAAO,EAA2B,KAAK,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAExF,MAAM,MAAM,IAAI,GAAG;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,IAAI,EAAE,CAAA;IACb,+DAA+D;IAC/D,YAAY,CAAC,EAAE,YAAY,CAAA;CAC5B,CAAA;AA2BD;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,YAAmB,EAAE,EAAE,YAAY,+BAiCnF;AAED,eAAe,OAAO,CAAA"}
1
+ {"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../../src/blocks/stats-01/stats.tsx"],"names":[],"mappings":"AAGA,OAAO,EAA2B,KAAK,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAExF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAEhD;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;;;;;OAUG;IACH,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,YAAY,CAAA;CAC5B,CAAA;AAiCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,YAAmB,EAAE,EAAE,YAAY,+BAqDnF;AAED,eAAe,OAAO,CAAA"}
@@ -1,27 +1,77 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ArrowDownRight, ArrowUpRight, Minus } from 'lucide-react';
3
2
  import { Card, CardContent } from '../../components/ui/card.js';
3
+ import { CtaLink } from '../../components/ui/cta-link.js';
4
+ import { Metric } from '../../components/ui/metric.js';
4
5
  import { Section, SectionHeading } from '../../components/ui/section.js';
5
- function Delta({ value }) {
6
- if (value === 0) {
7
- return (_jsxs("span", { className: "text-muted-foreground flex items-center gap-1 text-xs", children: [_jsx(Minus, { className: "size-3" }), "0%"] }));
6
+ import { Sparkline } from '../../components/ui/sparkline.js';
7
+ /**
8
+ * The refusals, as checks, so a tile that would render a link nobody can name is a
9
+ * diagnostic in a console rather than a rendered link.
10
+ *
11
+ * **The type already holds both pairs, and these checks exist for the caller the
12
+ * type never saw.** `MetricSpec` requires a destination's words wherever it has a
13
+ * destination and a series name wherever it has a series, so a typed caller cannot
14
+ * reach either diagnostic. A JavaScript caller can, and the cost of reaching one is
15
+ * a figure that draws a link with no words or a shape no reader can announce, so the
16
+ * same rule is stated once more where a runtime value can be read: the pair rule is
17
+ * the one `Dashboard01` makes.
18
+ */
19
+ function assertMetrics(metrics) {
20
+ for (const metric of metrics) {
21
+ if ((metric.href === undefined) !== (metric.hrefLabel === undefined)) {
22
+ throw new Error(`Stats01: the statistic "${metric.key}" declares one of href and hrefLabel without the other, so the ` +
23
+ 'tile would carry a link with no words on it, or a name with no link beside it. Pass the words that say ' +
24
+ 'what following it does, or omit the href.');
25
+ }
26
+ if ((metric.series === undefined) !== (metric.seriesLabel === undefined)) {
27
+ throw new Error(`Stats01: the statistic "${metric.key}" declares one of series and seriesLabel without the other, so the ` +
28
+ 'figure would be a picture with no name, which a screen reader cannot announce and two shapes in one row ' +
29
+ 'cannot be told apart. Pass the series name in the words the product uses, or omit the series.');
30
+ }
8
31
  }
9
- const up = value > 0;
10
- const Icon = up ? ArrowUpRight : ArrowDownRight;
11
- return (_jsxs("span", { className: up
12
- ? 'text-success flex items-center gap-1 text-xs'
13
- : 'text-destructive flex items-center gap-1 text-xs', children: [_jsx(Icon, { className: "size-3" }), Math.abs(value), "%"] }));
14
32
  }
15
33
  /**
16
- * A KPI row. Every number is a prop.
34
+ * A KPI row whose figures are the shared `MetricSpec` drawn by `Metric` and
35
+ * `Sparkline`. Every figure is a prop.
17
36
  *
18
- * This block used to carry a hardcoded set reading "128 blocks shipped" and "9.4k
19
- * weekly installs". Those were invented figures about a library nobody installs
20
- * yet, and because they lived in the component rather than at the call site, a
21
- * consumer who installed the block shipped fabricated metrics into their own
37
+ * **This block used to carry a hardcoded set reading "128 blocks shipped" and
38
+ * "9.4k weekly installs".** Those were invented figures about a library nobody
39
+ * installs yet, and because they lived in the component rather than at the call site,
40
+ * a consumer who installed the block shipped fabricated metrics into their own
22
41
  * dashboard. A stat block with invented stats in it is worse than no stat block.
42
+ *
43
+ * **It declared its own statistic type and guessed the unit, and that guess is the
44
+ * reason it is in this migration.** Its `delta` was documented as a percentage change
45
+ * and the Block appended a percent sign to the number, which is the one guess
46
+ * `Metric` refuses to make, because a delta of `0.12` is twelve percent, twelve cents
47
+ * or twelve milliseconds. It also carried no series, so the trend a metric summary
48
+ * names had nowhere to go. After this migration a delta with no formatter prints the
49
+ * number the caller passed and a delta with a formatter prints the caller's words,
50
+ * and the same consumer stops seeing a formatted change on one screen and a bare
51
+ * number on another.
52
+ *
53
+ * **The Block keeps its own name, kind and arrangement.** It is the same job as a row
54
+ * of figures with less of it declared, and a consumer who wants figures above
55
+ * supporting detail composes the dashboard Block beside them rather than either
56
+ * becoming the other. Only the statistic shape moves, because a shape is shared and a
57
+ * screen is not.
58
+ *
59
+ * **It still aggregates nothing and judges nothing.** It does not sum, count,
60
+ * average, bucket, rank or divide, it owns no threshold, no target, no severity and
61
+ * no freshness, and it draws no period switcher, no comparison toggle and no export.
62
+ * The period is the caller's words, carried by the `hint` and the delta formatter,
63
+ * and no period member exists.
64
+ *
65
+ * It is a server Component: no hook, no state and no client code.
23
66
  */
24
67
  export function Stats01({ eyebrow, title, stats, headingLevel = 'h2' }) {
25
- return (_jsxs(Section, { children: [title ? (_jsx(SectionHeading, { as: headingLevel, eyebrow: eyebrow, title: title, align: "left", className: "mb-10" })) : null, _jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-4", children: stats.map((stat) => (_jsx(Card, { className: "gap-2 py-5", children: _jsxs(CardContent, { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-muted-foreground text-sm", children: stat.label }), _jsx("span", { className: "text-2xl font-semibold tracking-tight", children: stat.value }), stat.delta === undefined && !stat.hint ? null : (_jsxs("div", { className: "flex items-center gap-2", children: [stat.delta === undefined ? null : _jsx(Delta, { value: stat.delta }), stat.hint ? (_jsx("span", { className: "text-muted-foreground text-xs", children: stat.hint })) : null] }))] }) }, stat.label))) })] }));
68
+ assertMetrics(stats);
69
+ return (_jsxs(Section, { children: [title ? (_jsx(SectionHeading, { as: headingLevel, eyebrow: eyebrow, title: title, align: "left", className: "mb-10" })) : null, _jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-4", children: stats.map((metric) => {
70
+ // The delta's words are the caller's own node, and `Metric` takes a
71
+ // formatter, so the node is placed through one. A delta with no words is
72
+ // left to `Metric`, which prints the number the caller passed.
73
+ const deltaWords = metric.deltaFormat;
74
+ return (_jsx(Card, { "data-slot": "stats-01-stat", "data-metric": metric.key, className: "gap-2 py-5", children: _jsxs(CardContent, { className: "flex flex-col gap-3", children: [_jsx(Metric, { value: metric.value, label: metric.label, delta: metric.delta, deltaFormat: deltaWords === undefined ? undefined : () => deltaWords, hint: metric.hint }), metric.series === undefined ? null : (_jsx("div", { "data-slot": "stats-01-sparkline", className: "flex", children: _jsx(Sparkline, { values: metric.series, label: metric.seriesLabel, className: "ms-auto" }) })), metric.href === undefined ? null : (_jsx(CtaLink, { href: metric.href, variant: "ghost", size: "sm", children: metric.hrefLabel }))] }) }, metric.key));
75
+ }) })] }));
26
76
  }
27
77
  export default Stats01;
@@ -1,3 +1,3 @@
1
1
  export { Trend01, default } from './trend.js';
2
- export type { Trend01Props, Trend01Item } from './trend.js';
2
+ export type { Trend01Props } from './trend.js';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/blocks/trend-01/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAI1C,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/blocks/trend-01/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAC1C,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA"}
@@ -1,64 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { type HeadingLevel } from '../../components/ui/section.js';
3
- /**
4
- * One row of a trend list: a name, what it is at now, how it moved, and a shape
5
- * beside it.
6
- *
7
- * Every field except `id` and `label` is optional because a trend list is
8
- * heterogeneous by nature. Some of the things a list tracks have a series behind
9
- * them and some have only a reading, some moved and some did not, and a shape
10
- * that made every field required would be a list where two thirds of every row is
11
- * an empty cell drawn as a placeholder.
12
- */
13
- export type Trend01Item = {
14
- /** A stable key for the row. */
15
- id: string;
16
- /**
17
- * The name of the thing being tracked, as a reader would name it in
18
- * conversation. It is the label the metric carries, and it is the last thing
19
- * read on the row because the figure and the direction come first.
20
- */
21
- label: string;
22
- /**
23
- * The current reading, already formatted by the caller.
24
- *
25
- * A `ReactNode` and not a `number`, for the reason `MetricProps.value` gives:
26
- * a caller's figure is often a value it formatted itself, with its own grouped
27
- * thousands, its own currency and its own unit, and a `number` here would push
28
- * that formatting onto every call site twice. Prism takes the node and formats
29
- * nothing.
30
- */
31
- value: ReactNode;
32
- /**
33
- * The change against the previous reading, as a number whose sign is the
34
- * direction. See `MetricDelta` on the Component.
35
- */
36
- delta?: number;
37
- /**
38
- * The words for that change, given the number. Required whenever `delta` is
39
- * set, and the Block throws without it. See the Component JSDoc for the full
40
- * argument.
41
- */
42
- deltaLabel?: (delta: number) => string;
43
- /**
44
- * The readings behind the current one, oldest first, in the caller's own units.
45
- *
46
- * Raw magnitudes rather than percentages of a maximum, which is the contract
47
- * `Sparkline` states: a caller that pre-normalises has two chances to get the
48
- * shape wrong, and a series drawn against an axis nobody can see is a picture
49
- * of somebody's arithmetic.
50
- */
51
- series?: number[];
52
- /**
53
- * The name of that series, and the only thing a screen reader reads from the
54
- * shape. Required whenever `series` is set, and the Block throws without it.
55
- */
56
- seriesLabel?: string;
57
- /** Where the row goes. Rendered as a native anchor, so the destination is real. */
58
- href?: string;
59
- /** The words on that link. Required whenever `href` is set. */
60
- hrefLabel?: string;
61
- };
3
+ import type { MetricSpec } from '../../lib/spec.js';
62
4
  /**
63
5
  * The props a Trend01 takes.
64
6
  *
@@ -78,8 +20,13 @@ export type Trend01Props = {
78
20
  /**
79
21
  * The rows, in the order a reader should meet them. See the Component JSDoc
80
22
  * for why the Block does not sort them.
23
+ *
24
+ * Each row is the shared `MetricSpec`: a stable `key` that is never the words of
25
+ * the label, a required `label` and `value`, a `delta` whose sign is the
26
+ * direction, an optional `deltaFormat` for the caller's own words, an optional
27
+ * `hint`, and an optional `series` and `href` with the names they travel with.
81
28
  */
82
- items: readonly Trend01Item[];
29
+ items: readonly MetricSpec[];
83
30
  /**
84
31
  * How many rows to render. Defaults to the whole set.
85
32
  *
@@ -133,19 +80,25 @@ export type Trend01Props = {
133
80
  * implementation would be the one without the table, because the table is the part
134
81
  * nobody draws by hand and it is the part that matters.
135
82
  *
136
- * **`deltaLabel` is required whenever any row carries a `delta`, and the reason
137
- * is that the Block cannot know which of three sentences the caller means.**
138
- * "up 12 percent", "+12%" and "12 higher than last week" are three sentences
139
- * about one number, and they are not formats of one another: the first is a
140
- * phrase with a unit, the second is a signed figure for a reader who is already
141
- * looking at a number, and the third is a comparison against a named period that
142
- * a reader who cannot see the period will misread as a comparison against
143
- * whatever they assumed. Worse, the fallback a Block would reach for is the worst
144
- * of the three, because a bare "+12" printed beside a figure is a number with an
145
- * arithmetic symbol on it, and a screen reader reads that as a formula rather
146
- * than as a change. So the prop is required and the Block throws without it
147
- * rather than printing the sign itself, and the cost of the throw is that a
148
- * caller who genuinely has no unit has to write the formatter that says so.
83
+ * **The rows are the shared `MetricSpec`, so a row here and a figure on a summary
84
+ * are one shape rather than two that disagree.** Each row carries a stable `key`
85
+ * that is never the words of the label, a required `label` and `value`, an optional
86
+ * `delta` whose sign is the direction, an optional `deltaFormat` carrying the
87
+ * caller's own words for the change, an optional `hint` for the period or the
88
+ * caveat, and an optional `series` and `href` each travelling with the name it
89
+ * cannot be read without. This closes the absence that put this Block in the
90
+ * migration: a reading now has a place for a series and for a destination, so the
91
+ * trend a metric summary names has somewhere to go when the two sit on one screen.
92
+ *
93
+ * **The words for the change stay the caller's, and the Block mints no period and
94
+ * no arithmetic.** `deltaFormat` is the caller's own node rather than a formatter
95
+ * this Block could write, because "up 12 percent", "+12%" and "12 higher than last
96
+ * week" are three sentences about one number and no single fallback is right for
97
+ * all three. A delta with a formatter prints the caller's words and one without
98
+ * prints the number the caller passed, which is `Metric`'s own contract. There is
99
+ * no period member, no derived rate of change and no period-over-period arithmetic
100
+ * over the series the caller passed, because each of those is a second arithmetic
101
+ * this package would be doing over a population it never fetched.
149
102
  *
150
103
  * **`seriesLabel` is required whenever any row carries a `series`, for the reason
151
104
  * the `Sparkline` JSDoc gives in full: a figure with no name is not announced
@@ -1 +1 @@
1
- {"version":3,"file":"trend.d.ts","sourceRoot":"","sources":["../../../src/blocks/trend-01/trend.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,OAAO,EAA2B,KAAK,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAIxF;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,gCAAgC;IAChC,EAAE,EAAE,MAAM,CAAA;IACV;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;;;OAQG;IACH,KAAK,EAAE,SAAS,CAAA;IAChB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mFAAmF;IACnF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iFAAiF;IACjF,KAAK,EAAE,SAAS,CAAA;IAChB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB;;;OAGG;IACH,KAAK,EAAE,SAAS,WAAW,EAAE,CAAA;IAC7B;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,wBAAgB,OAAO,CAAC,EACtB,OAAO,EACP,KAAK,EACL,WAAW,EACX,KAAK,EACL,KAAK,EACL,KAAK,EACL,YAAmB,EACnB,SAAS,GACV,EAAE,YAAY,sCA6Gd;AAED,eAAe,OAAO,CAAA"}
1
+ {"version":3,"file":"trend.d.ts","sourceRoot":"","sources":["../../../src/blocks/trend-01/trend.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,OAAO,EAA2B,KAAK,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAExF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAkChD;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iFAAiF;IACjF,KAAK,EAAE,SAAS,CAAA;IAChB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB;;;;;;;;OAQG;IACH,KAAK,EAAE,SAAS,UAAU,EAAE,CAAA;IAC5B;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AACH,wBAAgB,OAAO,CAAC,EACtB,OAAO,EACP,KAAK,EACL,WAAW,EACX,KAAK,EACL,KAAK,EACL,KAAK,EACL,YAAmB,EACnB,SAAS,GACV,EAAE,YAAY,sCA2Fd;AAED,eAAe,OAAO,CAAA"}