@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,435 @@
1
+ # Listing Template
2
+
3
+ Pages whose primary goal is finding, scanning, or filtering a collection of records.
4
+
5
+ ## When to use
6
+
7
+ - Search results, service directories, record indexes
8
+ - Transaction histories, application lists
9
+ - Any screen presenting multiple instances of the same entity type
10
+
11
+ Do not use for:
12
+
13
+ - A single record's detail → use `detail.md`
14
+ - A mixed overview with stats and no primary table → use `dashboard.md`
15
+
16
+ ---
17
+
18
+ ## Page anatomy
19
+
20
+ ```
21
+ Navbar
22
+ ──────────────────────────────────────────────────────────
23
+ <main>
24
+ └─ pageWrapper (padding: spacing-32 0; lg: spacing-48)
25
+ └─ Layout.Container type="grid"
26
+ └─ Layout.ColDiv xxsCols={[1, -1]}
27
+ ├─ searchRow (optional)
28
+ └─ twoColumn
29
+ ├─ filterSidebar (278px) — omit for search-only variant
30
+ ├─ Filter
31
+ └─ resultsArea (flex 1)
32
+ ├─ resultCountRow
33
+ ├─ ErrorDisplay type="no-item-found" ← empty state
34
+ ├─ Card list / DataTable
35
+ └─ paginationWrapper (centred)
36
+ ──────────────────────────────────────────────────────────
37
+ Footer
38
+ ```
39
+
40
+ On mobile the `Filter` sidebar automatically collapses into a button that opens a modal.
41
+
42
+ ---
43
+
44
+ ## Required components
45
+
46
+ | Slot | Component | Required |
47
+ | -------------- | ----------------------------------------- | -------- |
48
+ | Navigation | `Navbar` (masthead enabled) | Yes |
49
+ | Results | `Card` list/grid **or** `DataTable` | Yes |
50
+ | Result count | `Typography.BodyMD` — "X result(s) found" | Yes |
51
+ | Footer | `Footer` | Yes |
52
+ | Search bar | `Form.Input` + `Button` | No\* |
53
+ | Filter sidebar | `Filter` (~278px wide) | No\* |
54
+ | Pagination | `Pagination showFirstAndLastNav` | No |
55
+
56
+ \* Search bar and filter sidebar are individually optional but **at least one must be present**.
57
+
58
+ ---
59
+
60
+ ## CSS (ListingPage.module.css)
61
+
62
+ ```css
63
+ .pageWrapper {
64
+ padding: var(--fds-spacing-32) 0;
65
+
66
+ :global(body.fds-breakpoint-lg-min) & {
67
+ padding: var(--fds-spacing-48) 0;
68
+ }
69
+ }
70
+
71
+ .twoColumn {
72
+ display: flex;
73
+ gap: var(--fds-spacing-32);
74
+ align-items: flex-start;
75
+ }
76
+
77
+ .filterSidebar {
78
+ width: 278px;
79
+ flex-shrink: 0;
80
+ }
81
+
82
+ .resultsArea {
83
+ flex: 1;
84
+ min-width: 0;
85
+ display: flex;
86
+ flex-direction: column;
87
+ gap: var(--fds-spacing-16);
88
+ }
89
+
90
+ .searchRow {
91
+ display: flex;
92
+ gap: var(--fds-spacing-16);
93
+ align-items: flex-end;
94
+ margin-bottom: var(--fds-spacing-24);
95
+ }
96
+
97
+ .resultCountRow {
98
+ display: flex;
99
+ justify-content: space-between;
100
+ align-items: center;
101
+ }
102
+
103
+ .cardBody {
104
+ padding: var(--fds-spacing-24);
105
+ }
106
+
107
+ .paginationWrapper {
108
+ display: flex;
109
+ justify-content: center;
110
+ margin-top: var(--fds-spacing-24);
111
+ }
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Rules
117
+
118
+ - Always show result count even when no filters are active — format: "X result(s) found"
119
+ - Always show `ErrorDisplay type="no-item-found"` with contextual copy when results are empty — never an empty container
120
+ - `Pagination` or lazy load — never both on the same list
121
+ - `showFirstAndLastNav` must be set on `Pagination`
122
+ - Filter sidebar width is fixed at ~278px; results area is `flex: 1`
123
+ - Inline action buttons (edit, delete) do not belong on listing cards — those go on the detail screen
124
+ - Changing `keyword`, `filters`, or `sortBy` must reset `page` to `1`
125
+
126
+ ---
127
+
128
+ ## Complete template
129
+
130
+ A single-file listing page with search, side filters, pagination, loading/error/empty states, and API stub wired in. Remove `filterSidebar` / `twoColumn` for search-only variant.
131
+
132
+ ```tsx
133
+ // ListingPage.tsx
134
+ import { useState, useEffect, useMemo } from "react";
135
+ import { Navbar } from "@lifesg/react-design-system/navbar";
136
+ import { Footer } from "@lifesg/react-design-system/footer";
137
+ import { Layout } from "@lifesg/react-design-system/layout";
138
+ import { Typography } from "@lifesg/react-design-system/typography";
139
+ import { Card } from "@lifesg/react-design-system/card";
140
+ import { Filter } from "@lifesg/react-design-system/filter";
141
+ import { RadioButton } from "@lifesg/react-design-system/radio-button";
142
+ import { Pagination } from "@lifesg/react-design-system/pagination";
143
+ import { ErrorDisplay } from "@lifesg/react-design-system/error-display";
144
+ import { Alert } from "@lifesg/react-design-system/alert";
145
+ import { Button } from "@lifesg/react-design-system/button";
146
+ import { Form } from "@lifesg/react-design-system/form";
147
+ import styles from "./ListingPage.module.css";
148
+
149
+ // ─── Types ────────────────────────────────────────────────────────────────────
150
+
151
+ interface Item {
152
+ id: string;
153
+ title: string;
154
+ description: string;
155
+ category: string;
156
+ }
157
+
158
+ interface ListingState {
159
+ keyword: string;
160
+ filters: Record<string, string[]>;
161
+ sortBy: string;
162
+ page: number;
163
+ }
164
+
165
+ // ─── Constants ────────────────────────────────────────────────────────────────
166
+
167
+ const PAGE_SIZE = 10;
168
+
169
+ const SORT_OPTIONS = [
170
+ { label: "Relevance", value: "relevance" },
171
+ { label: "Newest first", value: "newest" },
172
+ ];
173
+
174
+ const CATEGORY_OPTIONS = [
175
+ { label: "Category A", value: "a" },
176
+ { label: "Category B", value: "b" },
177
+ ];
178
+
179
+ // FIXME: replace with real seed data or remove once API is wired up
180
+ const MOCK_ITEMS: Item[] = [];
181
+
182
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
183
+
184
+ function matchesKeyword(item: Item, keyword: string): boolean {
185
+ if (!keyword.trim()) return true;
186
+ const q = keyword.toLowerCase();
187
+ return (
188
+ item.title.toLowerCase().includes(q) ||
189
+ item.description.toLowerCase().includes(q)
190
+ );
191
+ }
192
+
193
+ function matchesFilters(
194
+ item: Item,
195
+ filters: Record<string, string[]>
196
+ ): boolean {
197
+ const cats = filters.category;
198
+ if (!cats || cats.length === 0) return true;
199
+ return cats.includes(item.category);
200
+ }
201
+
202
+ function sortItems(items: Item[], sortBy: string): Item[] {
203
+ if (sortBy === "newest") return [...items].reverse();
204
+ return items;
205
+ }
206
+
207
+ // ─── Component ────────────────────────────────────────────────────────────────
208
+
209
+ export default function ListingPage() {
210
+ const [allItems, setAllItems] = useState<Item[]>(MOCK_ITEMS);
211
+ const [isLoading, setIsLoading] = useState(true);
212
+ const [fetchError, setFetchError] = useState<string | null>(null);
213
+
214
+ const [state, setState] = useState<ListingState>({
215
+ keyword: "",
216
+ filters: {},
217
+ sortBy: "relevance",
218
+ page: 1,
219
+ });
220
+
221
+ useEffect(() => {
222
+ setIsLoading(true);
223
+ setFetchError(null);
224
+ // FIXME: replace with actual API client and URL to fetch data, e.g.:
225
+ // apiClient
226
+ // .get<Item[]>("/api/v1/items")
227
+ // .then(({ data }) => setAllItems(data))
228
+ // .catch((err) => setFetchError(err.message))
229
+ // .finally(() => setIsLoading(false));
230
+ setAllItems(MOCK_ITEMS);
231
+ setIsLoading(false);
232
+ }, []);
233
+
234
+ const results = useMemo(() => {
235
+ let rows = allItems.filter(
236
+ (r) =>
237
+ matchesKeyword(r, state.keyword) &&
238
+ matchesFilters(r, state.filters)
239
+ );
240
+ return sortItems(rows, state.sortBy);
241
+ }, [allItems, state.keyword, state.filters, state.sortBy]);
242
+
243
+ const pageCount = Math.ceil(results.length / PAGE_SIZE);
244
+ const pageRows = results.slice(
245
+ (state.page - 1) * PAGE_SIZE,
246
+ state.page * PAGE_SIZE
247
+ );
248
+
249
+ function handleClear() {
250
+ setState((s) => ({ ...s, filters: {}, page: 1 }));
251
+ }
252
+
253
+ return (
254
+ <>
255
+ <Navbar masthead items={{ desktop: [] }} />
256
+ <main style={{ flex: 1 }}>
257
+ <div className={styles.pageWrapper}>
258
+ <Layout.Container type="grid">
259
+ <Layout.ColDiv xsCols={8} lgCols={12}>
260
+ {/* Search bar */}
261
+ <div className={styles.searchRow}>
262
+ <Form.Input
263
+ label="Search"
264
+ placeholder="Search by keyword"
265
+ value={state.keyword}
266
+ onChange={(e) =>
267
+ setState((s) => ({
268
+ ...s,
269
+ keyword: e.target.value,
270
+ page: 1,
271
+ }))
272
+ }
273
+ />
274
+ <Button
275
+ onClick={() =>
276
+ setState((s) => ({ ...s, page: 1 }))
277
+ }
278
+ >
279
+ Search
280
+ </Button>
281
+ </div>
282
+
283
+ {/* Loading */}
284
+ {isLoading && (
285
+ <Typography.BodyMD>
286
+ Loading...
287
+ </Typography.BodyMD>
288
+ )}
289
+
290
+ {/* Fetch error */}
291
+ {!isLoading && fetchError && (
292
+ <Alert
293
+ type="error"
294
+ actionLink={{
295
+ children: "Try again",
296
+ onClick: () => window.location.reload(),
297
+ }}
298
+ >
299
+ {fetchError}
300
+ </Alert>
301
+ )}
302
+
303
+ {/* Results */}
304
+ {!isLoading && !fetchError && (
305
+ <div className={styles.twoColumn}>
306
+ {/* Filter sidebar — omit for search-only variant */}
307
+ <div className={styles.filterSidebar}>
308
+ <Filter onClear={handleClear}>
309
+ <Filter.Item
310
+ title="Sort by"
311
+ collapsible={false}
312
+ >
313
+ {SORT_OPTIONS.map((opt) => (
314
+ <RadioButton
315
+ key={opt.value}
316
+ checked={
317
+ state.sortBy ===
318
+ opt.value
319
+ }
320
+ onChange={() =>
321
+ setState((s) => ({
322
+ ...s,
323
+ sortBy: opt.value,
324
+ page: 1,
325
+ }))
326
+ }
327
+ >
328
+ {opt.label}
329
+ </RadioButton>
330
+ ))}
331
+ </Filter.Item>
332
+ <Filter.Checkbox
333
+ title="Category"
334
+ options={CATEGORY_OPTIONS}
335
+ selected={
336
+ state.filters.category ?? []
337
+ }
338
+ onChange={(values) =>
339
+ setState((s) => ({
340
+ ...s,
341
+ page: 1,
342
+ filters: {
343
+ ...s.filters,
344
+ category: values,
345
+ },
346
+ }))
347
+ }
348
+ />
349
+ </Filter>
350
+ </div>
351
+
352
+ <div className={styles.resultsArea}>
353
+ {/* Result count */}
354
+ <div className={styles.resultCountRow}>
355
+ <Typography.BodyMD>
356
+ {results.length} result
357
+ {results.length !== 1
358
+ ? "s"
359
+ : ""} found
360
+ </Typography.BodyMD>
361
+ </div>
362
+
363
+ {/* Empty state */}
364
+ {results.length === 0 && (
365
+ <ErrorDisplay
366
+ type="no-item-found"
367
+ title="No results found"
368
+ description="Try adjusting your search or filters."
369
+ actionButton={{
370
+ children: "Clear filters",
371
+ onClick: handleClear,
372
+ }}
373
+ />
374
+ )}
375
+
376
+ {/* Card list */}
377
+ {pageRows.map((item) => (
378
+ <Card key={item.id}>
379
+ <div
380
+ className={styles.cardBody}
381
+ >
382
+ <Typography.HeadingSM>
383
+ {item.title}
384
+ </Typography.HeadingSM>
385
+ <Typography.BodyMD>
386
+ {item.description}
387
+ </Typography.BodyMD>
388
+ </div>
389
+ </Card>
390
+ ))}
391
+
392
+ {/* Pagination */}
393
+ {pageCount > 1 && (
394
+ <div
395
+ className={
396
+ styles.paginationWrapper
397
+ }
398
+ >
399
+ <Pagination
400
+ totalItems={results.length}
401
+ pageSize={PAGE_SIZE}
402
+ activePage={state.page}
403
+ onPageChange={(page) =>
404
+ setState((s) => ({
405
+ ...s,
406
+ page,
407
+ }))
408
+ }
409
+ showFirstAndLastNav
410
+ />
411
+ </div>
412
+ )}
413
+ </div>
414
+ </div>
415
+ )}
416
+ </Layout.ColDiv>
417
+ </Layout.Container>
418
+ </div>
419
+ </main>
420
+ <Footer />
421
+ </>
422
+ );
423
+ }
424
+ ```
425
+
426
+ ---
427
+
428
+ ## Anti-patterns
429
+
430
+ - Do not omit the result count
431
+ - Do not show an empty results area with no state message
432
+ - Do not use `DataTable` for citizen-facing records
433
+ - Do not use pagination AND lazy loading together
434
+ - Do not leave stale `page` values pointing past the end of a newly-filtered result set
435
+ - Do not split filtering and sorting into separate `useEffect` calls — derive from a single `useMemo`