@lifesg/react-design-system 4.0.0-alpha.13 → 4.0.0-alpha.14

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 (100) hide show
  1. package/cjs/external/dompurify/dist/purify.es.js +4 -2
  2. package/cjs/external/dompurify/dist/purify.es.js.map +1 -1
  3. package/cjs/modal-v2/slots/slot-styles.js +1 -1
  4. package/cjs/modal-v2/slots/slot-styles.js.map +1 -1
  5. package/cjs/modal-v2/slots/{slot-styles_ayb9il.css → slot-styles_ygu6zc.css} +1 -1
  6. package/cjs/notification-banner/notification-banner-hoc.js +1 -1
  7. package/cjs/notification-banner/notification-banner-hoc.js.map +1 -1
  8. package/external/dompurify/dist/purify.es.js +4 -2
  9. package/external/dompurify/dist/purify.es.js.map +1 -1
  10. package/modal-v2/slots/slot-styles.js +1 -1
  11. package/modal-v2/slots/slot-styles.js.map +1 -1
  12. package/modal-v2/slots/{slot-styles_ayb9il.css → slot-styles_ygu6zc.css} +1 -1
  13. package/notification-banner/notification-banner-hoc.js +1 -1
  14. package/notification-banner/notification-banner-hoc.js.map +1 -1
  15. package/package.json +2 -2
  16. package/scripts/cli.js +18 -0
  17. package/skills/fds-build/SKILL.md +167 -0
  18. package/skills/fds-build/catalogue.md +181 -0
  19. package/skills/fds-build/evals/evals.json +519 -0
  20. package/skills/fds-build/resources/components/accordion.md +128 -0
  21. package/skills/fds-build/resources/components/alert.md +54 -0
  22. package/skills/fds-build/resources/components/animation.md +18 -0
  23. package/skills/fds-build/resources/components/avatar.md +10 -0
  24. package/skills/fds-build/resources/components/badge.md +18 -0
  25. package/skills/fds-build/resources/components/box-container.md +61 -0
  26. package/skills/fds-build/resources/components/breadcrumb.md +60 -0
  27. package/skills/fds-build/resources/components/button.md +74 -0
  28. package/skills/fds-build/resources/components/calendar.md +113 -0
  29. package/skills/fds-build/resources/components/card.md +43 -0
  30. package/skills/fds-build/resources/components/checkbox.md +53 -0
  31. package/skills/fds-build/resources/components/countdown-timer.md +20 -0
  32. package/skills/fds-build/resources/components/data-table.md +142 -0
  33. package/skills/fds-build/resources/components/date-navigator.md +25 -0
  34. package/skills/fds-build/resources/components/divider.md +23 -0
  35. package/skills/fds-build/resources/components/drawer.md +78 -0
  36. package/skills/fds-build/resources/components/error-display.md +137 -0
  37. package/skills/fds-build/resources/components/feedback-rating.md +61 -0
  38. package/skills/fds-build/resources/components/file-download.md +54 -0
  39. package/skills/fds-build/resources/components/file-upload.md +105 -0
  40. package/skills/fds-build/resources/components/filter.md +228 -0
  41. package/skills/fds-build/resources/components/footer.md +94 -0
  42. package/skills/fds-build/resources/components/form-date-input.md +85 -0
  43. package/skills/fds-build/resources/components/form-date-range-input.md +146 -0
  44. package/skills/fds-build/resources/components/form-e-signature.md +55 -0
  45. package/skills/fds-build/resources/components/form-input.md +65 -0
  46. package/skills/fds-build/resources/components/form-masked-input.md +69 -0
  47. package/skills/fds-build/resources/components/form-multi-select.md +100 -0
  48. package/skills/fds-build/resources/components/form-nested-multi-select.md +159 -0
  49. package/skills/fds-build/resources/components/form-nested-select.md +151 -0
  50. package/skills/fds-build/resources/components/form-otp-verification.md +175 -0
  51. package/skills/fds-build/resources/components/form-phone-number-input.md +87 -0
  52. package/skills/fds-build/resources/components/form-predictive-text-input.md +108 -0
  53. package/skills/fds-build/resources/components/form-select.md +103 -0
  54. package/skills/fds-build/resources/components/form-slider.md +116 -0
  55. package/skills/fds-build/resources/components/form-textarea.md +116 -0
  56. package/skills/fds-build/resources/components/form-time-range-picker.md +98 -0
  57. package/skills/fds-build/resources/components/form-timepicker.md +77 -0
  58. package/skills/fds-build/resources/components/form-unit-number-input.md +138 -0
  59. package/skills/fds-build/resources/components/fullscreen-image-carousel.md +159 -0
  60. package/skills/fds-build/resources/components/histogram-slider.md +41 -0
  61. package/skills/fds-build/resources/components/icon.md +167 -0
  62. package/skills/fds-build/resources/components/image-button.md +12 -0
  63. package/skills/fds-build/resources/components/input.md +51 -0
  64. package/skills/fds-build/resources/components/language-switcher.md +15 -0
  65. package/skills/fds-build/resources/components/layout.md +156 -0
  66. package/skills/fds-build/resources/components/link-list.md +120 -0
  67. package/skills/fds-build/resources/components/local-nav.md +119 -0
  68. package/skills/fds-build/resources/components/masthead.md +14 -0
  69. package/skills/fds-build/resources/components/menu.md +109 -0
  70. package/skills/fds-build/resources/components/modal.md +150 -0
  71. package/skills/fds-build/resources/components/navbar.md +270 -0
  72. package/skills/fds-build/resources/components/notification-banner.md +106 -0
  73. package/skills/fds-build/resources/components/pagination.md +104 -0
  74. package/skills/fds-build/resources/components/pill.md +55 -0
  75. package/skills/fds-build/resources/components/popover.md +80 -0
  76. package/skills/fds-build/resources/components/progress-indicator.md +47 -0
  77. package/skills/fds-build/resources/components/radio-button.md +66 -0
  78. package/skills/fds-build/resources/components/schedule.md +78 -0
  79. package/skills/fds-build/resources/components/sidenav.md +183 -0
  80. package/skills/fds-build/resources/components/singpass-button.md +14 -0
  81. package/skills/fds-build/resources/components/smart-app-banner.md +28 -0
  82. package/skills/fds-build/resources/components/tab.md +107 -0
  83. package/skills/fds-build/resources/components/table.md +65 -0
  84. package/skills/fds-build/resources/components/tag.md +68 -0
  85. package/skills/fds-build/resources/components/text-list.md +44 -0
  86. package/skills/fds-build/resources/components/time-slot-bar-week.md +66 -0
  87. package/skills/fds-build/resources/components/time-slot-bar.md +57 -0
  88. package/skills/fds-build/resources/components/time-slot-week-view.md +70 -0
  89. package/skills/fds-build/resources/components/timeline.md +69 -0
  90. package/skills/fds-build/resources/components/timetable.md +123 -0
  91. package/skills/fds-build/resources/components/toast.md +72 -0
  92. package/skills/fds-build/resources/components/toggle.md +119 -0
  93. package/skills/fds-build/resources/components/typography.md +86 -0
  94. package/skills/fds-build/resources/components/uneditable-section.md +84 -0
  95. package/skills/fds-build/resources/foundations/layout-composition-patterns.md +53 -0
  96. package/skills/fds-build/resources/foundations/tokens.md +547 -0
  97. package/skills/fds-build/resources/patterns/states.md +300 -0
  98. package/skills/fds-build/resources/setup/setup.md +202 -0
  99. package/skills/fds-build/resources/templates/form.md +427 -0
  100. package/skills/fds-build/resources/templates/listing.md +435 -0
@@ -0,0 +1,427 @@
1
+ # Form Template
2
+
3
+ Single or multi-step form page.
4
+
5
+ ## When to use
6
+
7
+ - Collecting user input across one or more steps
8
+ - Profile, settings, or data-entry pages
9
+
10
+ ---
11
+
12
+ ## Page anatomy
13
+
14
+ ```
15
+ Navbar
16
+ ──────────────────────────────────────────────────────────
17
+ <main>
18
+ └─ pageWrapper (padding: spacing-32 0; lg: spacing-64)
19
+ └─ Layout.Container type="grid"
20
+ ├─ Layout.ColDiv xxsCols={[1, -1]}
21
+ │ └─ ProgressIndicator (multi-step only)
22
+ └─ Layout.ColDiv xxsCols={12} lgCols={8}
23
+ │ flex column · gap: spacing-32
24
+ ├─ pageHeadingBlock (flex column · gap: 16)
25
+ │ ├─ h1
26
+ │ └─ p (optional)
27
+ ├─ div (form content)
28
+ │ └─ Form fields (built-in margin-bottom: 32)
29
+ │ Non-form components → margin-bottom: 32
30
+ └─ FormNav
31
+ ──────────────────────────────────────────────────────────
32
+ Footer
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Rules
38
+
39
+ | | Wrong | Correct |
40
+ | ------------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
41
+ | Content column width | `lgCols={12}` | `xxsCols={12} lgCols={8}` |
42
+ | Progress indicator | Inside 8-col div | Separate `lgCols={12}` ColDiv |
43
+ | Page heading | Static string | Defaults to `{STEPS[step]}`; override per step as needed |
44
+ | Field spacing | `gap` on wrapper div | Rely on component built-in `margin-bottom: 32`; add `margin-bottom: var(--fds-spacing-32)` via CSS Module class for components without it (BoxContainer, Toggle, FileUpload, ESignature) |
45
+ | Constrained-width inputs | `width` on element | `max-width` — works alongside internal `width: 100%` |
46
+ | Review step | Omitting | Required as last step — one `BoxContainer` per preceding step |
47
+
48
+ > **Setup:** If the project is not yet set up, read `../setup/setup.md` first before writing any code here.
49
+
50
+ ---
51
+
52
+ ## FormNav
53
+
54
+ ```tsx
55
+ // FormNav.tsx
56
+ import { useRef, useLayoutEffect } from "react";
57
+ import { Button } from "@lifesg/react-design-system/button";
58
+ import styles from "./FormNav.module.css";
59
+
60
+ interface FormNavProps {
61
+ onBack?: () => void;
62
+ onNext: () => void;
63
+ nextLabel?: string;
64
+ isSubmitting?: boolean;
65
+ actions?: React.ReactNode;
66
+ }
67
+
68
+ export default function FormNav({
69
+ onBack,
70
+ onNext,
71
+ nextLabel = "Next",
72
+ isSubmitting = false,
73
+ actions,
74
+ }: FormNavProps) {
75
+ const navRef = useRef<HTMLDivElement>(null);
76
+
77
+ useLayoutEffect(() => {
78
+ if (!navRef.current) return;
79
+ const buttons = Array.from(
80
+ navRef.current.querySelectorAll<HTMLElement>("button")
81
+ );
82
+ buttons.forEach((b) => {
83
+ b.style.width = "";
84
+ });
85
+ if (!window.matchMedia("(min-width: 769px)").matches) return;
86
+ const widest = Math.max(...buttons.map((b) => b.offsetWidth));
87
+ buttons.forEach((b) => {
88
+ b.style.width = `${widest}px`;
89
+ });
90
+ });
91
+
92
+ return (
93
+ <div ref={navRef} className={styles.nav}>
94
+ <div className={styles.primaryGroup}>
95
+ <Button
96
+ styleType="default"
97
+ onClick={onNext}
98
+ loading={isSubmitting}
99
+ disabled={isSubmitting}
100
+ className={styles.nextButton}
101
+ >
102
+ {nextLabel}
103
+ </Button>
104
+ {actions && (
105
+ <div className={styles.actionsGroup}>{actions}</div>
106
+ )}
107
+ </div>
108
+ {onBack && (
109
+ <Button
110
+ styleType="secondary"
111
+ onClick={onBack}
112
+ className={styles.backButton}
113
+ >
114
+ Back
115
+ </Button>
116
+ )}
117
+ </div>
118
+ );
119
+ }
120
+ ```
121
+
122
+ ```css
123
+ /* FormNav.module.css */
124
+ .nav {
125
+ margin-top: var(--fds-spacing-32);
126
+ display: flex;
127
+ flex-direction: column;
128
+ gap: var(--fds-spacing-24);
129
+
130
+ :global(body.fds-breakpoint-lg-min) & {
131
+ flex-direction: row;
132
+ align-items: center;
133
+ justify-content: space-between;
134
+ gap: 0;
135
+ }
136
+ }
137
+
138
+ .primaryGroup {
139
+ display: flex;
140
+ flex-direction: column;
141
+ gap: var(--fds-spacing-16);
142
+
143
+ :global(body.fds-breakpoint-lg-min) & {
144
+ flex-direction: row;
145
+ gap: var(--fds-spacing-16);
146
+ order: 2;
147
+ margin-left: auto;
148
+ }
149
+ }
150
+
151
+ .nextButton {
152
+ box-sizing: border-box;
153
+ width: 100%;
154
+
155
+ :global(body.fds-breakpoint-lg-min) & {
156
+ width: auto;
157
+ min-width: 10rem;
158
+ order: 1;
159
+ }
160
+ }
161
+
162
+ .actionsGroup {
163
+ display: flex;
164
+ flex-direction: column;
165
+ gap: var(--fds-spacing-16);
166
+
167
+ & > button {
168
+ box-sizing: border-box;
169
+ width: 100%;
170
+ }
171
+
172
+ :global(body.fds-breakpoint-lg-min) & {
173
+ flex-direction: row;
174
+ gap: var(--fds-spacing-16);
175
+ & > button {
176
+ width: auto;
177
+ min-width: 10rem;
178
+ }
179
+ }
180
+ }
181
+
182
+ .backButton {
183
+ box-sizing: border-box;
184
+ width: 100%;
185
+
186
+ :global(body.fds-breakpoint-lg-min) & {
187
+ width: auto;
188
+ min-width: 10rem;
189
+ order: 1;
190
+ }
191
+ }
192
+ ```
193
+
194
+ ---
195
+
196
+ ## Review step
197
+
198
+ ```tsx
199
+ import { BoxContainer } from "@lifesg/react-design-system/box-container";
200
+ import { UneditableSection } from "@lifesg/react-design-system/uneditable-section";
201
+ import styles from "./FormPage.module.css";
202
+
203
+ {
204
+ step === STEPS.length - 1 && (
205
+ <BoxContainer
206
+ title="Step one"
207
+ collapsible
208
+ expanded
209
+ className={styles.boxContainer}
210
+ >
211
+ <div className={styles.summaryPadding}>
212
+ <UneditableSection
213
+ fullWidth
214
+ background={false}
215
+ items={[
216
+ {
217
+ label: "Short field",
218
+ value: valueA || "—",
219
+ displayWidth: "half",
220
+ },
221
+ {
222
+ label: "Short field",
223
+ value: valueB || "—",
224
+ displayWidth: "half",
225
+ },
226
+ { label: "Long field", value: longValue || "—" },
227
+ ]}
228
+ />
229
+ </div>
230
+ </BoxContainer>
231
+ );
232
+ }
233
+ ```
234
+
235
+ ```css
236
+ /* in FormPage.module.css */
237
+ .boxContainer {
238
+ margin-bottom: var(--fds-spacing-32);
239
+ }
240
+
241
+ .summaryPadding {
242
+ padding: var(--fds-spacing-20);
243
+
244
+ :global(body.fds-breakpoint-md-min) & {
245
+ padding: var(--fds-spacing-32);
246
+ }
247
+ }
248
+ ```
249
+
250
+ ---
251
+
252
+ ## Complete template
253
+
254
+ ```tsx
255
+ // FormPage.tsx
256
+ import { useState } from "react";
257
+ import { Form } from "@lifesg/react-design-system/form";
258
+ import { ProgressIndicator } from "@lifesg/react-design-system/progress-indicator";
259
+ import { Layout } from "@lifesg/react-design-system/layout";
260
+ import { Alert } from "@lifesg/react-design-system/alert";
261
+ import FormNav from "./FormNav";
262
+ import styles from "./FormPage.module.css";
263
+
264
+ const STEPS = ["Step one", "Step two", "Review"];
265
+
266
+ const CATEGORY_OPTIONS = [
267
+ { label: "Option A", value: "a" },
268
+ { label: "Option B", value: "b" },
269
+ ];
270
+
271
+ export default function FormPage() {
272
+ const [step, setStep] = useState(0);
273
+ const isLastStep = step === STEPS.length - 1;
274
+
275
+ const [name, setName] = useState("");
276
+ const [category, setCategory] = useState("");
277
+ const [errors, setErrors] = useState<Record<string, string>>({});
278
+ const [isSubmitting, setIsSubmitting] = useState(false);
279
+ const [submitError, setSubmitError] = useState<string | null>(null);
280
+
281
+ function validateStep(s: number): Record<string, string> {
282
+ const e: Record<string, string> = {};
283
+ if (s === 0) {
284
+ if (!name.trim()) e.name = "Enter your full name";
285
+ if (!category) e.category = "Select a category";
286
+ }
287
+ return e;
288
+ }
289
+
290
+ function handleNext() {
291
+ const stepErrors = validateStep(step);
292
+ setErrors(stepErrors);
293
+ if (Object.keys(stepErrors).length > 0) return;
294
+ if (!isLastStep) setStep((s) => s + 1);
295
+ else handleSubmit();
296
+ }
297
+
298
+ async function handleSubmit() {
299
+ setIsSubmitting(true);
300
+ setSubmitError(null);
301
+ try {
302
+ // FIXME: replace with actual API call, e.g.:
303
+ // await apiClient.post("/api/v1/submissions", { name, category });
304
+ console.log("submitted", { name, category });
305
+ } catch (err: unknown) {
306
+ setSubmitError(
307
+ err instanceof Error
308
+ ? err.message
309
+ : "Submission failed. Please try again."
310
+ );
311
+ } finally {
312
+ setIsSubmitting(false);
313
+ }
314
+ }
315
+
316
+ return (
317
+ <>
318
+ <Layout.ColDiv
319
+ xsCols={12}
320
+ lgCols={12}
321
+ className={styles.progressColDiv}
322
+ >
323
+ <ProgressIndicator
324
+ steps={STEPS}
325
+ currentIndex={step}
326
+ displayExtractor={(s) => s}
327
+ />
328
+ </Layout.ColDiv>
329
+
330
+ <Layout.ColDiv
331
+ xsCols={12}
332
+ lgCols={9}
333
+ xlCols={8}
334
+ className={styles.formColDiv}
335
+ >
336
+ <div className={styles.pageHeadingBlock}>
337
+ <h1 className={styles.pageTitle}>{STEPS[step]}</h1>
338
+ {/* <p className={styles.pageDescription}>Optional description</p> */}
339
+ </div>
340
+
341
+ {submitError && <Alert type="error">{submitError}</Alert>}
342
+
343
+ <div>
344
+ {step === 0 && (
345
+ <>
346
+ <Form.Input
347
+ label="Full name"
348
+ value={name}
349
+ errorMessage={errors.name}
350
+ onChange={(e) => {
351
+ setName(e.target.value);
352
+ if (errors.name)
353
+ setErrors((prev) => ({
354
+ ...prev,
355
+ name: "",
356
+ }));
357
+ }}
358
+ />
359
+ <Form.Select
360
+ label="Category"
361
+ options={CATEGORY_OPTIONS}
362
+ selectedOption={CATEGORY_OPTIONS.find(
363
+ (o) => o.value === category
364
+ )}
365
+ listExtractor={(o) => o.label}
366
+ displayValueExtractor={(o) => o.label}
367
+ valueExtractor={(o) => o.value}
368
+ onSelectOption={(opt) => {
369
+ setCategory(opt.value);
370
+ if (errors.category)
371
+ setErrors((prev) => ({
372
+ ...prev,
373
+ category: "",
374
+ }));
375
+ }}
376
+ errorMessage={errors.category}
377
+ />
378
+ </>
379
+ )}
380
+ {step === 1 && <>{/* step 2 fields */}</>}
381
+ </div>
382
+
383
+ <FormNav
384
+ onBack={step > 0 ? () => setStep((s) => s - 1) : undefined}
385
+ onNext={handleNext}
386
+ nextLabel={isLastStep ? "Submit" : "Next"}
387
+ isSubmitting={isSubmitting}
388
+ />
389
+ </Layout.ColDiv>
390
+ </>
391
+ );
392
+ }
393
+ ```
394
+
395
+ ```css
396
+ /* FormPage.module.css */
397
+ .progressColDiv {
398
+ margin-bottom: var(--fds-spacing-32);
399
+ }
400
+
401
+ .formColDiv {
402
+ display: flex;
403
+ flex-direction: column;
404
+ gap: var(--fds-spacing-32);
405
+ }
406
+
407
+ .pageHeadingBlock {
408
+ display: flex;
409
+ flex-direction: column;
410
+ gap: var(--fds-spacing-16);
411
+ }
412
+
413
+ .pageTitle {
414
+ margin: 0;
415
+ }
416
+
417
+ .pageDescription {
418
+ margin: 0;
419
+ }
420
+ ```
421
+
422
+ ---
423
+
424
+ ## Notes
425
+
426
+ - Single-step: omit `ProgressIndicator` and pass no `onBack` to `FormNav`
427
+ - Pass `actions` to `FormNav` for secondary actions like Save draft