@marktiderman/genesis-forms-web 0.1.1 → 0.3.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/README.md CHANGED
@@ -6,9 +6,9 @@ The web renderer for [Genesis Forms](../forms/README.md), built on Genesis UI co
6
6
  import { Form } from "@marktiderman/genesis-forms-web";
7
7
 
8
8
  <Form
9
- form={afterClass} // a defineForm result or a canonical definition
9
+ form={eventFeedback} // a defineForm result or a canonical definition
10
10
  layout="focus" // one question at a time; or "classic"
11
- options={{ hidden: { coach: "Sam" } }}
11
+ options={{ hidden: { owner: "Sam" } }}
12
12
  onChange={(state) => saveDraft(state)}
13
13
  onSubmit={(engine) => submit(engine.getState().events)}
14
14
  />
@@ -24,7 +24,7 @@ import { Form } from "@marktiderman/genesis-forms-web";
24
24
  progress bar and Enter to continue (Ctrl/Cmd+Enter in long text; Enter on a focused radio or
25
25
  checkbox too). After a single pick (rating, NPS, yes/no, one choice) it moves on by itself
26
26
  (`autoAdvance={false}` turns it off): only a click, tap or Space counts as a pick, arrow keys just
27
- move through the options, and it never moves on from the last question, so Submit is always a
27
+ move through the options, and it never moves on from the last question, so Send is always a
28
28
  deliberate press. `classic` shows every visible question; a failed submit focuses an error summary
29
29
  above the fields that links to each invalid one.
30
30
  - **Presentation:** `presentation="embedded"` (the default) is a block inside your own page.
@@ -40,14 +40,14 @@ import { Form } from "@marktiderman/genesis-forms-web";
40
40
  button at the top-left. Each new question fades and slides in (CSS `@starting-style`, none with
41
41
  `prefers-reduced-motion`). The ending is centred and large. In classic layout, page mode is one
42
42
  scrolling page, Google Forms style: a header card with the title and description, each question
43
- in its own card (red-edged while invalid), Submit at the end.
44
- `brand={{ name: "CrossFit Fairfield", logo }}` shows a practice's logo (an image URL or an
43
+ in its own card (red-edged while invalid), Send at the end.
44
+ `brand={{ name: "Acme", logo }}` shows an organisation's or business's logo (an image URL or an
45
45
  element; its initials without one) and name small at the top of every step and on the ending.
46
46
  Custom slots and renderers can match with `useFormPresentation()`, which returns
47
47
  `{ presentation, layout }`.
48
48
 
49
49
  ```tsx
50
- <Form form={afterClass} presentation="page" brand={{ name: "CrossFit Fairfield" }} onSubmit={send} />
50
+ <Form form={eventFeedback} presentation="page" brand={{ name: "Acme" }} onSubmit={send} />
51
51
  ```
52
52
 
53
53
  - **Accessible by default:** group questions are a `fieldset` with a `legend`, and rating, NPS and
@@ -57,6 +57,18 @@ import { Form } from "@marktiderman/genesis-forms-web";
57
57
  question text), a failed Next is announced once through a live region, and the ending heading
58
58
  takes focus. Choice rows are full-width labels at least 44px tall; motion respects
59
59
  `prefers-reduced-motion`. Ids are unique per form, so two forms can share a page.
60
+ - **Send word and required note:** the send button says `submitLabel` (default **Send**) in every
61
+ layout. When any question is required, the title shows "\* Required" (`requiredLabel`, default
62
+ "Required"; `null` hides it).
63
+ - **Theme:** `theme={{ accent: "#f97316", headerImage: url, headerImageAlt }}` sets the form's own
64
+ primary colour (buttons, selected options, stars, progress, focus rings) and a banner over the
65
+ title. The accent is adjusted only as far as contrast needs: at least 3:1 against the page in light
66
+ and dark, and text on it at least 4.5:1. It takes `#rgb`, `#rrggbb` or `rgb()`; anything else is
67
+ ignored. `themeColors(accent)` returns the colours it would use.
68
+ - **Question types:** besides text, email, phone, rating, NPS, choices and yes/no, there are
69
+ `dropdown` (a native select), `number` (min, max, step; the decimal keypad on phones), `date` (the
70
+ browser's date picker) and `consent` (one checkbox). A rating can show end labels (`lowLabel`,
71
+ `highLabel`) and `display: "stars"`; NPS takes the same labels.
60
72
  - **Submitting:** the ending shows once `onSubmit` resolves. If it rejects, the completed response
61
73
  stays on screen read-only (a completed response is final), Back is hidden, and "Try again"
62
74
  (`retryLabel`) resends the same answers.
@@ -74,7 +86,31 @@ import { Form } from "@marktiderman/genesis-forms-web";
74
86
  `defaultRegistry.register(entry)` mutates the shared registry in place, for every form in the app.
75
87
 
76
88
  Renderers: short and long text, email, phone (`type="tel"`), rating, NPS, single choice, multiple
77
- choice, yes/no.
89
+ choice, yes/no, and the structured types:
90
+
91
+ - **`list`** (goals, action items, three wins) and **`table`** (an action plan with What / Who /
92
+ By when, pros and cons) add, remove and reorder entries (SortableList: drag, keyboard and a Move
93
+ menu).
94
+ - **`media-notes`** (lecture notes, reviewing a recorded call) takes a typed time (`m:ss`,
95
+ `h:mm:ss` or seconds). Wrap the form in `MediaTimeContext` to link it to your video or audio
96
+ player: `currentTime` prefills "Add note at 1:23" and `seek` plays from a note.
97
+ - **`text-highlights`** (annotating an article, a policy or a contract) takes highlights by
98
+ selecting words in the passage or typing them. The passage is the field's `text` prop, or the
99
+ value of `SourceTextContext` around the form.
100
+
101
+ ```tsx
102
+ <MediaTimeContext.Provider value={{ currentTime: () => player.currentTime, seek: (s) => player.seek(s) }}>
103
+ <SourceTextContext.Provider value={article.body}>
104
+ <Form form={videoNotes} onSubmit={send} />
105
+ </SourceTextContext.Provider>
106
+ </MediaTimeContext.Provider>
107
+ ```
108
+
109
+ `useMediaTime()` and `useSourceText()` read them in a custom renderer.
110
+
111
+ A **`calculated`** field shows the value the engine worked out from other answers, read-only, in an
112
+ `<output>` named by the question that announces changes politely. It follows the field's `format`
113
+ and `decimals`, and shows a dash until there is a value.
78
114
 
79
115
  ## License
80
116
 
package/dist/index.d.ts CHANGED
@@ -21,14 +21,60 @@ interface FormPresentationValue {
21
21
  */
22
22
  declare function useFormPresentation(): FormPresentationValue;
23
23
 
24
+ /**
25
+ * A form's own look: an accent colour and a header image. The accent becomes
26
+ * the Genesis `--primary` (and `--ring`) inside the form, so every primary
27
+ * surface follows it: buttons, selected options, stars, progress, focus rings.
28
+ */
29
+ interface FormTheme {
30
+ /**
31
+ * The accent colour, as `#rgb`, `#rrggbb` or `rgb(r, g, b)`, such as a
32
+ * brand's colour. It is adjusted only as far as needed for contrast: at
33
+ * least 3:1 against the page (light and dark), with text on it at least
34
+ * 4.5:1 (white or near-black, whichever reads better). Anything else is ignored.
35
+ */
36
+ accent?: string | undefined;
37
+ /** A banner image URL shown over the form's title. */
38
+ headerImage?: string | undefined;
39
+ /** The banner's text alternative. Default "" (decorative). */
40
+ headerImageAlt?: string | undefined;
41
+ }
42
+ /** Shadcn HSL components (`"221 83% 53%"`), as Genesis tokens hold them. */
43
+ interface ThemeColors {
44
+ primary: string;
45
+ primaryForeground: string;
46
+ }
47
+ /**
48
+ * The accent as Genesis primary colours for a light and a dark page, each
49
+ * adjusted only as far as contrast needs. Null when `accent` is not a colour
50
+ * this understands.
51
+ */
52
+ declare function themeColors(accent: string): {
53
+ light: ThemeColors;
54
+ dark: ThemeColors;
55
+ } | null;
56
+ /** The CSS that applies `theme` to the elements matching `selector`, or "" when it changes no colour. */
57
+ declare function themeCss(theme: FormTheme | undefined, selector: string): string;
58
+
24
59
  type SlotOverride<P> = ComponentType<P> | null;
25
60
  interface HeaderSlotProps {
26
61
  title: string;
27
62
  description?: string;
63
+ /** `theme.headerImage`: a banner over the title. */
64
+ image?: {
65
+ src: string;
66
+ alt: string;
67
+ } | undefined;
68
+ /**
69
+ * The note that marks required questions ("Required", drawn as "* Required"),
70
+ * set when the form has at least one required question. Empty when it has none
71
+ * or `requiredLabel` is null.
72
+ */
73
+ requiredNote?: string | undefined;
28
74
  className?: string;
29
75
  }
30
76
  interface BrandSlotProps {
31
- /** The practice or business name, shown beside the logo. */
77
+ /** The organisation or business name, shown beside the logo. */
32
78
  name: string;
33
79
  /** An image URL or your own element. Without one, the name's initials are shown. */
34
80
  logo?: ReactNode;
@@ -110,8 +156,15 @@ interface FormProps {
110
156
  * layout the questions are sections on one scrolling page.
111
157
  */
112
158
  presentation?: FormPresentation | undefined;
113
- /** A practice's name and logo, shown small at the top of every step and on the ending. */
159
+ /** An organisation's or business's name and logo, shown small at the top of every step and on the ending. */
114
160
  brand?: FormBrand | undefined;
161
+ /**
162
+ * Colours and a header image: `accent` replaces the primary colour (buttons,
163
+ * selected options, progress, focus ring), adjusted where needed so it keeps
164
+ * at least 3:1 contrast with the page and its text 4.5:1; `headerImage` is a
165
+ * banner over the title.
166
+ */
167
+ theme?: FormTheme | undefined;
115
168
  registry?: RendererRegistry | undefined;
116
169
  /**
117
170
  * Called once the form completes. The ending shows only after it resolves.
@@ -130,8 +183,15 @@ interface FormProps {
130
183
  * Never moves on from the last question. Default true.
131
184
  */
132
185
  autoAdvance?: boolean | undefined;
186
+ /** The send button's word, the same in every layout. Default "Send". */
133
187
  submitLabel?: string | undefined;
188
+ /** The button that moves to the next question in focus layout. Default "OK". */
134
189
  nextLabel?: string | undefined;
190
+ /**
191
+ * The note shown with the title when any question is required, after an
192
+ * asterisk ("* Required"). `null` hides it. Default "Required".
193
+ */
194
+ requiredLabel?: string | null | undefined;
135
195
  /** The submit button's label after `onSubmit` failed. Default "Try again". */
136
196
  retryLabel?: string | undefined;
137
197
  slots?: {
@@ -164,6 +224,7 @@ interface FieldChromeProps {
164
224
  */
165
225
  declare function FieldChrome({ props, children, className }: FieldChromeProps): react.JSX.Element;
166
226
  declare function describedBy(props: FieldRendererProps): string | undefined;
227
+
167
228
  declare const builtInRenderers: RendererEntry[];
168
229
  /**
169
230
  * The built-in web renderers, shared by every `<Form>` that gets no
@@ -172,4 +233,29 @@ declare const builtInRenderers: RendererEntry[];
172
233
  */
173
234
  declare const defaultRegistry: _marktiderman_genesis_forms_react.RendererRegistry;
174
235
 
175
- export { type BrandSlotProps, type EndingSlotProps, type ErrorSummaryItem, type ErrorSummarySlotProps, FieldChrome, type FieldChromeProps, Form, type FormBrand, type FormPresentation, type FormPresentationValue, type FormProps, type FormSlotMap, type HeaderSlotProps, type NavigationSlotProps, type ProgressSlotProps, builtInRenderers, defaultRegistry, describedBy, useFormPresentation };
236
+ /**
237
+ * The video or audio a form is about, as the host app plays it. `media-notes`
238
+ * fields read it: with `currentTime` the add row starts at the current moment
239
+ * ("Add note at 1:23"), with `seek` a note's time plays the media from there.
240
+ * Both are in seconds from the start of the full recording.
241
+ */
242
+ interface MediaTime {
243
+ /** The playhead now, or undefined when it isn't known yet. */
244
+ currentTime?: (() => number | undefined) | undefined;
245
+ /** Move the playhead. */
246
+ seek?: ((seconds: number) => void) | undefined;
247
+ }
248
+ /** Provide it around `<Form>` to link `media-notes` fields to a media player. */
249
+ declare const MediaTimeContext: react.Context<MediaTime | null>;
250
+ /** The enclosing `MediaTimeContext` value, or null. */
251
+ declare function useMediaTime(): MediaTime | null;
252
+ /**
253
+ * The text a form is about (an article, a policy, a contract). `text-highlights`
254
+ * fields highlight passages of it; a field's own `props.text` is used when no
255
+ * host provides one. Offsets are UTF-16 indices into this exact string.
256
+ */
257
+ declare const SourceTextContext: react.Context<string | null>;
258
+ /** The enclosing `SourceTextContext` value, or null. */
259
+ declare function useSourceText(): string | null;
260
+
261
+ export { type BrandSlotProps, type EndingSlotProps, type ErrorSummaryItem, type ErrorSummarySlotProps, FieldChrome, type FieldChromeProps, Form, type FormBrand, type FormPresentation, type FormPresentationValue, type FormProps, type FormSlotMap, type FormTheme, type HeaderSlotProps, type MediaTime, MediaTimeContext, type NavigationSlotProps, type ProgressSlotProps, SourceTextContext, type ThemeColors, builtInRenderers, defaultRegistry, describedBy, themeColors, themeCss, useFormPresentation, useMediaTime, useSourceText };