@rfdtech/components 1.22.0 → 2.0.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 (218) hide show
  1. package/README.md +108 -38
  2. package/dist/adapters/next-adapter.d.ts.map +1 -1
  3. package/dist/components/app-header/AppHeader.d.ts.map +1 -1
  4. package/dist/components/app-header/AppHeaderNotificationItem.d.ts +3 -0
  5. package/dist/components/app-header/AppHeaderNotificationItem.d.ts.map +1 -0
  6. package/dist/components/app-header/AppHeaderProfile.d.ts +5 -0
  7. package/dist/components/app-header/AppHeaderProfile.d.ts.map +1 -1
  8. package/dist/components/app-header/index.d.ts +3 -1
  9. package/dist/components/app-header/index.d.ts.map +1 -1
  10. package/dist/components/app-switcher/AppIconTile.d.ts +10 -0
  11. package/dist/components/app-switcher/AppIconTile.d.ts.map +1 -0
  12. package/dist/components/app-switcher/AppSwitcher.d.ts +1 -1
  13. package/dist/components/app-switcher/AppSwitcher.d.ts.map +1 -1
  14. package/dist/components/app-switcher/AppSwitcherItem.d.ts.map +1 -1
  15. package/dist/components/app-switcher/IconPatterns.d.ts +8 -0
  16. package/dist/components/app-switcher/IconPatterns.d.ts.map +1 -0
  17. package/dist/components/app-switcher/SystemAppIcon.d.ts.map +1 -1
  18. package/dist/components/app-switcher/index.d.ts +2 -0
  19. package/dist/components/app-switcher/index.d.ts.map +1 -1
  20. package/dist/components/avatar/Avatar.d.ts.map +1 -1
  21. package/dist/components/bulk-import-modal/BulkImportModal.d.ts.map +1 -1
  22. package/dist/components/bulk-import-modal/steps/ValidateDataStep.d.ts.map +1 -1
  23. package/dist/components/button/Button.d.ts +1 -1
  24. package/dist/components/button/Button.d.ts.map +1 -1
  25. package/dist/components/button/Spinner.d.ts.map +1 -1
  26. package/dist/components/combobox/Combobox.d.ts +4 -0
  27. package/dist/components/combobox/Combobox.d.ts.map +1 -0
  28. package/dist/components/combobox/Combobox.test.d.ts +2 -0
  29. package/dist/components/combobox/Combobox.test.d.ts.map +1 -0
  30. package/dist/components/combobox/index.d.ts +3 -0
  31. package/dist/components/combobox/index.d.ts.map +1 -0
  32. package/dist/components/country-selector/CountrySelector.d.ts +7 -0
  33. package/dist/components/country-selector/CountrySelector.d.ts.map +1 -1
  34. package/dist/components/country-selector/index.d.ts +1 -0
  35. package/dist/components/country-selector/index.d.ts.map +1 -1
  36. package/dist/components/date-range-selector/DateRangeSelector.d.ts +2 -2
  37. package/dist/components/date-range-selector/DateRangeSelector.d.ts.map +1 -1
  38. package/dist/components/date-range-selector/index.d.ts +1 -1
  39. package/dist/components/date-range-selector/index.d.ts.map +1 -1
  40. package/dist/components/date-selector/DateSelector.d.ts.map +1 -1
  41. package/dist/components/dropdown/Dropdown.d.ts +1 -1
  42. package/dist/components/dropdown/Dropdown.d.ts.map +1 -1
  43. package/dist/components/export-button/ExportButton.d.ts +3 -0
  44. package/dist/components/export-button/ExportButton.d.ts.map +1 -0
  45. package/dist/components/export-button/ExportButton.test.d.ts +2 -0
  46. package/dist/components/export-button/ExportButton.test.d.ts.map +1 -0
  47. package/dist/components/export-button/index.d.ts +3 -0
  48. package/dist/components/export-button/index.d.ts.map +1 -0
  49. package/dist/components/launchpad/Launchpad.d.ts +4 -0
  50. package/dist/components/launchpad/Launchpad.d.ts.map +1 -0
  51. package/dist/components/launchpad/Launchpad.test.d.ts +2 -0
  52. package/dist/components/launchpad/Launchpad.test.d.ts.map +1 -0
  53. package/dist/components/launchpad/LaunchpadGridIcon.d.ts +2 -0
  54. package/dist/components/launchpad/LaunchpadGridIcon.d.ts.map +1 -0
  55. package/dist/components/launchpad/LaunchpadIconTile.d.ts +10 -0
  56. package/dist/components/launchpad/LaunchpadIconTile.d.ts.map +1 -0
  57. package/dist/components/launchpad/LaunchpadItem.d.ts +8 -0
  58. package/dist/components/launchpad/LaunchpadItem.d.ts.map +1 -0
  59. package/dist/components/launchpad/LaunchpadOverlays.d.ts +3 -0
  60. package/dist/components/launchpad/LaunchpadOverlays.d.ts.map +1 -0
  61. package/dist/components/launchpad/SystemLaunchpadIcon.d.ts +5 -0
  62. package/dist/components/launchpad/SystemLaunchpadIcon.d.ts.map +1 -0
  63. package/dist/components/launchpad/hooks/useLaunchpad.d.ts +3 -0
  64. package/dist/components/launchpad/hooks/useLaunchpad.d.ts.map +1 -0
  65. package/dist/components/launchpad/index.d.ts +10 -0
  66. package/dist/components/launchpad/index.d.ts.map +1 -0
  67. package/dist/components/metric-card/MetricCard.d.ts.map +1 -1
  68. package/dist/components/network-operator/NetworkOperator.d.ts +4 -0
  69. package/dist/components/network-operator/NetworkOperator.d.ts.map +1 -1
  70. package/dist/components/network-operator/index.d.ts +1 -0
  71. package/dist/components/network-operator/index.d.ts.map +1 -1
  72. package/dist/components/notice/Notice.d.ts +4 -0
  73. package/dist/components/notice/Notice.d.ts.map +1 -0
  74. package/dist/components/notice/Notice.test.d.ts +2 -0
  75. package/dist/components/notice/Notice.test.d.ts.map +1 -0
  76. package/dist/components/notice/index.d.ts +3 -0
  77. package/dist/components/notice/index.d.ts.map +1 -0
  78. package/dist/components/phone-number-input/PhoneNumberInput.d.ts.map +1 -1
  79. package/dist/components/popup/Popup.d.ts +15 -0
  80. package/dist/components/popup/Popup.d.ts.map +1 -0
  81. package/dist/components/popup/Popup.test.d.ts +2 -0
  82. package/dist/components/popup/Popup.test.d.ts.map +1 -0
  83. package/dist/components/popup/index.d.ts +3 -0
  84. package/dist/components/popup/index.d.ts.map +1 -0
  85. package/dist/components/profile-popover/ProfilePopover.d.ts +4 -0
  86. package/dist/components/profile-popover/ProfilePopover.d.ts.map +1 -0
  87. package/dist/components/profile-popover/ProfilePopover.test.d.ts +2 -0
  88. package/dist/components/profile-popover/ProfilePopover.test.d.ts.map +1 -0
  89. package/dist/components/profile-popover/index.d.ts +3 -0
  90. package/dist/components/profile-popover/index.d.ts.map +1 -0
  91. package/dist/components/role-select/RoleSelect.d.ts +4 -0
  92. package/dist/components/role-select/RoleSelect.d.ts.map +1 -0
  93. package/dist/components/role-select/RoleSelect.test.d.ts +2 -0
  94. package/dist/components/role-select/RoleSelect.test.d.ts.map +1 -0
  95. package/dist/components/role-select/index.d.ts +3 -0
  96. package/dist/components/role-select/index.d.ts.map +1 -0
  97. package/dist/components/section-header/SectionHeader.d.ts +7 -0
  98. package/dist/components/section-header/SectionHeader.d.ts.map +1 -0
  99. package/dist/components/section-header/SectionHeader.test.d.ts +2 -0
  100. package/dist/components/section-header/SectionHeader.test.d.ts.map +1 -0
  101. package/dist/components/section-header/index.d.ts +3 -0
  102. package/dist/components/section-header/index.d.ts.map +1 -0
  103. package/dist/components/sidebar/Sidebar.d.ts +1 -1
  104. package/dist/components/sidebar/Sidebar.d.ts.map +1 -1
  105. package/dist/components/sidebar/SidebarContext.d.ts +6 -0
  106. package/dist/components/sidebar/SidebarContext.d.ts.map +1 -1
  107. package/dist/components/sidebar/index.d.ts +1 -1
  108. package/dist/components/sidebar/index.d.ts.map +1 -1
  109. package/dist/components/switch/Switch.d.ts +4 -0
  110. package/dist/components/switch/Switch.d.ts.map +1 -0
  111. package/dist/components/switch/Switch.test.d.ts +2 -0
  112. package/dist/components/switch/Switch.test.d.ts.map +1 -0
  113. package/dist/components/switch/index.d.ts +3 -0
  114. package/dist/components/switch/index.d.ts.map +1 -0
  115. package/dist/components/table/Table.d.ts.map +1 -1
  116. package/dist/components/table/TableHeader.d.ts.map +1 -1
  117. package/dist/components/table/TablePagination.d.ts.map +1 -1
  118. package/dist/components/table/index.d.ts +1 -1
  119. package/dist/components/table/index.d.ts.map +1 -1
  120. package/dist/components/theme/ThemeProvider.d.ts.map +1 -1
  121. package/dist/components/theme/cletTheme.d.ts +11 -0
  122. package/dist/components/theme/cletTheme.d.ts.map +1 -0
  123. package/dist/components/theme/cletTheme.test-d.d.ts +2 -0
  124. package/dist/components/theme/cletTheme.test-d.d.ts.map +1 -0
  125. package/dist/components/theme/cletTheme.test.d.ts +2 -0
  126. package/dist/components/theme/cletTheme.test.d.ts.map +1 -0
  127. package/dist/components/theme/index.d.ts +3 -1
  128. package/dist/components/theme/index.d.ts.map +1 -1
  129. package/dist/components/upload-field/UploadField.d.ts +5 -0
  130. package/dist/components/upload-field/UploadField.d.ts.map +1 -1
  131. package/dist/components/upload-field/index.d.ts +2 -2
  132. package/dist/components/upload-field/index.d.ts.map +1 -1
  133. package/dist/generated/components.theme.d.ts +313 -0
  134. package/dist/generated/components.theme.d.ts.map +1 -0
  135. package/dist/hooks/index.d.ts +1 -0
  136. package/dist/hooks/index.d.ts.map +1 -1
  137. package/dist/hooks/useHasMounted.d.ts +10 -0
  138. package/dist/hooks/useHasMounted.d.ts.map +1 -0
  139. package/dist/hooks/useTableFilter.d.ts.map +1 -1
  140. package/dist/hooks/useTablePagination.d.ts.map +1 -1
  141. package/dist/hooks/useTableState.d.ts.map +1 -1
  142. package/dist/index.cjs +84 -56
  143. package/dist/index.css +1 -1
  144. package/dist/index.d.ts +12 -1
  145. package/dist/index.d.ts.map +1 -1
  146. package/dist/index.js +19021 -31595
  147. package/dist/mcp/cli.js +75 -0
  148. package/dist/mcp/docs.js +122 -0
  149. package/dist/mcp/generated/components.json +3593 -0
  150. package/dist/mcp/generated/examples.json +1233 -0
  151. package/dist/mcp/generated/index.meta.json +252 -0
  152. package/dist/mcp/generated/rules.json +629 -0
  153. package/dist/mcp/generated/search.json +1618 -0
  154. package/dist/mcp/generated/tokens.json +1022 -0
  155. package/dist/mcp/index.js +337 -0
  156. package/dist/mcp/indexer.js +370 -0
  157. package/dist/mcp/installers/claude.js +37 -0
  158. package/dist/mcp/installers/codex.js +24 -0
  159. package/dist/mcp/installers/cursor.js +33 -0
  160. package/dist/mcp/installers/doctor.js +36 -0
  161. package/dist/mcp/installers/opencode.js +36 -0
  162. package/dist/mcp/installers/setup.js +34 -0
  163. package/dist/mcp/installers/util.js +41 -0
  164. package/dist/mcp/paths.js +63 -0
  165. package/dist/mcp/skill/SKILL.md +36 -0
  166. package/dist/mcp/skill/image-to-components.md +49 -0
  167. package/dist/next-index.d.ts.map +1 -1
  168. package/dist/next.cjs +104 -94
  169. package/dist/next.css +1 -1
  170. package/dist/next.js +16272 -29979
  171. package/dist/types/app-header.d.ts +52 -13
  172. package/dist/types/app-header.d.ts.map +1 -1
  173. package/dist/types/app-switcher.d.ts +10 -1
  174. package/dist/types/app-switcher.d.ts.map +1 -1
  175. package/dist/types/avatar.d.ts +9 -0
  176. package/dist/types/avatar.d.ts.map +1 -1
  177. package/dist/types/combobox.d.ts +43 -0
  178. package/dist/types/combobox.d.ts.map +1 -0
  179. package/dist/types/country-selector.d.ts +4 -0
  180. package/dist/types/country-selector.d.ts.map +1 -1
  181. package/dist/types/date-range-selector.d.ts +11 -0
  182. package/dist/types/date-range-selector.d.ts.map +1 -1
  183. package/dist/types/dropdown.d.ts +8 -0
  184. package/dist/types/dropdown.d.ts.map +1 -1
  185. package/dist/types/export-button.d.ts +24 -0
  186. package/dist/types/export-button.d.ts.map +1 -0
  187. package/dist/types/launchpad.d.ts +79 -0
  188. package/dist/types/launchpad.d.ts.map +1 -0
  189. package/dist/types/metric-card.d.ts +4 -0
  190. package/dist/types/metric-card.d.ts.map +1 -1
  191. package/dist/types/network-operator.d.ts +8 -0
  192. package/dist/types/network-operator.d.ts.map +1 -1
  193. package/dist/types/notice.d.ts +24 -0
  194. package/dist/types/notice.d.ts.map +1 -0
  195. package/dist/types/popup.d.ts +43 -0
  196. package/dist/types/popup.d.ts.map +1 -0
  197. package/dist/types/profile-popover.d.ts +97 -0
  198. package/dist/types/profile-popover.d.ts.map +1 -0
  199. package/dist/types/role-select.d.ts +28 -0
  200. package/dist/types/role-select.d.ts.map +1 -0
  201. package/dist/types/section-header.d.ts +26 -0
  202. package/dist/types/section-header.d.ts.map +1 -0
  203. package/dist/types/sidebar.d.ts +5 -0
  204. package/dist/types/sidebar.d.ts.map +1 -1
  205. package/dist/types/switch.d.ts +24 -0
  206. package/dist/types/switch.d.ts.map +1 -0
  207. package/dist/types/table.d.ts +21 -1
  208. package/dist/types/table.d.ts.map +1 -1
  209. package/dist/types/theme.d.ts +28 -8
  210. package/dist/types/theme.d.ts.map +1 -1
  211. package/dist/types/upload-field.d.ts +23 -0
  212. package/dist/types/upload-field.d.ts.map +1 -1
  213. package/dist/utils/export.d.ts +15 -0
  214. package/dist/utils/export.d.ts.map +1 -0
  215. package/dist/utils/stringToColor.d.ts +25 -0
  216. package/dist/utils/stringToColor.d.ts.map +1 -1
  217. package/package.json +29 -10
  218. package/scripts/postinstall.mjs +40 -0
@@ -0,0 +1,1233 @@
1
+ {
2
+ "app-header": [
3
+ {
4
+ "file": "app-header-branding-declarative.example.tsx",
5
+ "importName": "appHeaderBrandingDeclarativeSource",
6
+ "title": "App Header Branding Declarative",
7
+ "components": [
8
+ "AppHeader",
9
+ "AppHeaderBranding",
10
+ "Badge"
11
+ ],
12
+ "source": "import { AppHeader, AppHeaderBranding, Badge } from \"@rfdtech/components\";\n\nexport function AppHeaderBrandingDeclarativeExample() {\n return (\n <div\n style={{\n padding: 16,\n borderRadius: \"var(--clet-radius-2xl)\",\n background: \"var(--clet-surface-subtle)\",\n }}\n >\n <AppHeader variant=\"plain\">\n <AppHeaderBranding\n logo={\n <img\n src=\"/clet-logo.png\"\n alt=\"\"\n width={28}\n height={28}\n className=\"demo-home__sidebar-logo\"\n />\n }\n >\n <span className=\"clet-app-header__branding-text\">\n <span className=\"clet-app-header__branding-title\">CLET PORTAL</span>\n <Badge variant=\"outline\">Beta</Badge>\n </span>\n </AppHeaderBranding>\n </AppHeader>\n </div>\n );\n}\n"
13
+ },
14
+ {
15
+ "file": "app-header-plain.example.tsx",
16
+ "importName": "appHeaderPlainSource",
17
+ "title": "App Header Plain",
18
+ "components": [
19
+ "AppHeader",
20
+ "AppHeaderActions",
21
+ "AppHeaderBranding",
22
+ "AppHeaderNotifications",
23
+ "AppSwitcher",
24
+ "ProfilePopover",
25
+ "RoleSelect"
26
+ ],
27
+ "source": "import { useState } from \"react\";\nimport {\n AppHeader,\n AppHeaderActions,\n AppHeaderBranding,\n AppHeaderNotifications,\n AppSwitcher,\n ProfilePopover,\n RoleSelect,\n} from \"@rfdtech/components\";\nimport { Shield, Eye, ScrollText } from \"lucide-react\";\n\nconst user = {\n name: \"Kwame Asante\",\n role: \"Admin\",\n initials: \"KA\",\n email: \"kwame@clet.edu.gh\",\n};\n\nconst roles = [\n { id: \"admin\", name: \"Admin\", icon: <Shield size={16} strokeWidth={1.5} /> },\n { id: \"reviewer\", name: \"Reviewer\", icon: <Eye size={16} strokeWidth={1.5} /> },\n { id: \"auditor\", name: \"Auditor\", icon: <ScrollText size={16} strokeWidth={1.5} /> },\n];\n\nconst apps = [\n {\n id: \"gov-portal\",\n name: \"Governance Portal\",\n icon: \"https://ui-avatars.com/api/?name=Governance+Portal&background=1d4ed8&color=fff&size=96\",\n href: \"https://portal.example.com\",\n },\n {\n id: \"finance-hub\",\n name: \"Finance Hub\",\n icon: \"https://ui-avatars.com/api/?name=Finance+Hub&background=047857&color=fff&size=96\",\n },\n];\n\nconst notifications = [\n { id: \"1\", text: \"A new member joined the Ghana chapter.\", time: \"2m ago\", unread: true },\n { id: \"2\", text: \"Your monthly report is ready.\", time: \"1h ago\", unread: false },\n];\n\nexport function AppHeaderPlainExample() {\n const [selectedRole, setSelectedRole] = useState(\"admin\");\n\n return (\n <div\n style={{\n padding: 16,\n borderRadius: \"var(--clet-radius-2xl)\",\n background: \"var(--clet-surface-subtle)\",\n }}\n >\n <AppHeader variant=\"plain\">\n <AppHeaderBranding title=\"CLET PORTAL\" subtitle=\"Component Library\" />\n <AppHeaderActions>\n <AppSwitcher apps={apps} title=\"System directory\" />\n <AppHeaderNotifications>\n {notifications.map((n) => (\n <div key={n.id} className=\"clet-notif-popover__item\">\n {n.unread && <div className=\"clet-notif-popover__dot\" />}\n <div className=\"clet-notif-popover__body\">\n <div className=\"clet-notif-popover__body-text\">{n.text}</div>\n <div className=\"clet-notif-popover__body-time\">{n.time}</div>\n </div>\n </div>\n ))}\n </AppHeaderNotifications>\n <ProfilePopover\n variant=\"avatar\"\n user={user}\n onSignOut={() => console.log(\"Sign out\")}\n >\n <RoleSelect\n title=\"View as\"\n roles={roles}\n selectedRole={selectedRole}\n onClickRole={(role) => setSelectedRole(role.id)}\n />\n </ProfilePopover>\n </AppHeaderActions>\n </AppHeader>\n </div>\n );\n}\n"
28
+ },
29
+ {
30
+ "file": "app-header.example.tsx",
31
+ "importName": "appHeaderSource",
32
+ "title": "App Header",
33
+ "components": [
34
+ "AppHeader",
35
+ "AppHeaderActions",
36
+ "AppHeaderNotificationItem",
37
+ "AppHeaderNotifications",
38
+ "AppHeaderSearch",
39
+ "AppHeaderSearchDataGroup",
40
+ "AppSwitcher",
41
+ "ProfilePopover",
42
+ "RoleSelect",
43
+ "SystemAppIcon"
44
+ ],
45
+ "source": "import { useState } from \"react\";\nimport {\n AppHeader,\n AppHeaderActions,\n AppHeaderSearch,\n AppHeaderNotifications,\n AppHeaderNotificationItem,\n AppSwitcher,\n ProfilePopover,\n RoleSelect,\n SystemAppIcon,\n} from \"@rfdtech/components\";\nimport type { AppHeaderSearchDataGroup } from \"@rfdtech/components\";\nimport { Shield, Eye, ScrollText } from \"lucide-react\";\n\nconst user = {\n name: \"Kwame Asante\",\n role: \"Admin\",\n initials: \"KA\",\n email: \"kwame@clet.edu.gh\",\n};\n\nconst roles = [\n { id: \"admin\", name: \"Admin\", icon: <Shield size={16} strokeWidth={1.5} /> },\n { id: \"reviewer\", name: \"Reviewer\", icon: <Eye size={16} strokeWidth={1.5} /> },\n { id: \"auditor\", name: \"Auditor\", icon: <ScrollText size={16} strokeWidth={1.5} /> },\n];\n\nconst apps = [\n {\n id: \"gov-portal\",\n name: \"Governance Portal\",\n icon: <SystemAppIcon name=\"Governance Portal\" />,\n href: \"https://portal.example.com\",\n },\n {\n id: \"finance-hub\",\n name: \"Finance Hub\",\n icon: <SystemAppIcon name=\"Finance Hub\" />,\n href: \"https://finance.example.com\",\n },\n {\n id: \"hr-suite\",\n name: \"HR Suite\",\n icon: <SystemAppIcon name=\"HR Suite\" />,\n },\n];\n\nconst notifications = [\n { id: \"1\", text: \"A new member joined the Ghana chapter.\", time: \"2m ago\", unread: true },\n { id: \"2\", text: \"Your monthly report is ready.\", time: \"1h ago\", unread: true },\n { id: \"3\", text: \"System maintenance scheduled for 2:00 AM.\", time: \"3h ago\", unread: false },\n];\n\nexport function AppHeaderExample() {\n const [search, setSearch] = useState(\"\");\n const [profileVariant, setProfileVariant] = useState<\"full\" | \"avatar\">(\n \"full\",\n );\n const [selectedRole, setSelectedRole] = useState(\"admin\");\n\n const nextProfileVariant = ({ full: \"avatar\", avatar: \"full\" } as const)[\n profileVariant\n ];\n\n const searchGroups: AppHeaderSearchDataGroup[] = search\n ? [\n {\n heading: \"Members\",\n items: [\n {\n value: \"kwame-asante\",\n label: \"Kwame Asante\",\n onSelect: () => console.log(\"Selected member:\", \"Kwame Asante\"),\n },\n {\n value: \"abena-mensah\",\n label: \"Abena Mensah\",\n onSelect: () => console.log(\"Selected member:\", \"Abena Mensah\"),\n },\n ],\n },\n {\n heading: \"Projects\",\n items: [\n {\n value: \"clet-platform\",\n label: \"CLET Platform\",\n onSelect: () => console.log(\"Selected project:\", \"CLET Platform\"),\n },\n ],\n },\n ]\n : [];\n\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 16 }}>\n <button\n type=\"button\"\n className=\"clet-profile-menu__item\"\n style={{ alignSelf: \"flex-start\" }}\n onClick={() => setProfileVariant(nextProfileVariant)}\n >\n <span>Switch profile trigger to {nextProfileVariant}</span>\n </button>\n <AppHeader>\n <AppHeaderSearch\n data={searchGroups}\n onSearch={setSearch}\n showEmpty\n placeholder=\"Search members, projects…\"\n />\n <AppHeaderActions>\n <AppSwitcher apps={apps} />\n <AppHeaderNotifications>\n {notifications.map((n) => (\n <AppHeaderNotificationItem\n key={n.id}\n text={n.text}\n time={n.time}\n unread={n.unread}\n />\n ))}\n </AppHeaderNotifications>\n <ProfilePopover\n user={user}\n variant={profileVariant}\n onSignOut={() => console.log(\"Sign out\")}\n >\n <RoleSelect\n title=\"View as\"\n roles={roles}\n selectedRole={selectedRole}\n onClickRole={(role) => setSelectedRole(role.id)}\n />\n </ProfilePopover>\n </AppHeaderActions>\n </AppHeader>\n <div\n style={{\n height: 120,\n borderRadius: \"var(--clet-radius-2xl)\",\n background: \"var(--clet-surface-subtle)\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n fontSize: 14,\n color: \"var(--clet-text-muted)\",\n border: \"1px dashed var(--clet-border)\",\n }}\n >\n Page content goes here\n </div>\n </div>\n );\n}\n"
46
+ }
47
+ ],
48
+ "app-layout": [
49
+ {
50
+ "file": "app-layout-stacked.example.tsx",
51
+ "importName": "appLayoutStackedSource",
52
+ "title": "App Layout Stacked",
53
+ "components": [
54
+ "AppBody",
55
+ "AppHeader",
56
+ "AppHeaderActions",
57
+ "AppHeaderBranding",
58
+ "AppHeaderNotifications",
59
+ "AppLayout",
60
+ "AppSidebar",
61
+ "AppSwitcher",
62
+ "ProfilePopover",
63
+ "RoleSelect",
64
+ "Sidebar",
65
+ "SidebarBadge",
66
+ "SidebarContent",
67
+ "SidebarFooter",
68
+ "SidebarGroup",
69
+ "SidebarGroupLabel",
70
+ "SidebarLink",
71
+ "SidebarNav"
72
+ ],
73
+ "source": "import { useState } from \"react\";\nimport {\n AppLayout,\n AppHeader,\n AppHeaderActions,\n AppHeaderBranding,\n AppHeaderNotifications,\n AppSwitcher,\n AppSidebar,\n AppBody,\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarNav,\n SidebarGroup,\n SidebarGroupLabel,\n SidebarLink,\n SidebarBadge,\n ProfilePopover,\n RoleSelect,\n} from \"@rfdtech/components\";\nimport {\n Bell,\n BookOpen,\n ChevronRight,\n FileText,\n LayoutDashboard,\n Shield,\n Users,\n User,\n Settings,\n HelpCircle,\n} from \"lucide-react\";\n\nconst user = {\n name: \"Kwame Asante\",\n role: \"Admin\",\n initials: \"KA\",\n email: \"kwame@clet.edu.gh\",\n};\n\nconst roles = [\n { id: \"admin\", name: \"Admin\" },\n { id: \"reviewer\", name: \"Reviewer\" },\n { id: \"auditor\", name: \"Auditor\" },\n];\n\nconst apps = [\n {\n id: \"gov-portal\",\n name: \"Governance Portal\",\n icon: \"https://ui-avatars.com/api/?name=Governance+Portal&background=1d4ed8&color=fff&size=96\",\n },\n {\n id: \"finance-hub\",\n name: \"Finance Hub\",\n icon: \"https://ui-avatars.com/api/?name=Finance+Hub&background=047857&color=fff&size=96\",\n },\n];\n\nconst notifications = [\n { id: \"1\", text: \"A new member joined the Ghana chapter.\", time: \"2m ago\", unread: true },\n { id: \"2\", text: \"Your monthly report is ready.\", time: \"1h ago\", unread: false },\n];\n\nconst navGroups = [\n {\n label: \"Main\",\n links: [\n { id: \"dashboard\", label: \"Dashboard\", icon: LayoutDashboard },\n { id: \"members\", label: \"Members\", icon: Users },\n { id: \"docs\", label: \"Documentation\", icon: BookOpen, badge: \"New\" },\n ],\n },\n {\n label: \"Analytics\",\n links: [\n { id: \"reports\", label: \"Reports\", icon: FileText },\n { id: \"alerts\", label: \"Alerts\", icon: Bell, badge: \"8\" },\n { id: \"permissions\", label: \"Permissions\", icon: Shield },\n ],\n },\n];\n\nexport function AppLayoutStackedExample() {\n const [active, setActive] = useState(\"members\");\n const [selectedRole, setSelectedRole] = useState(\"admin\");\n const [expandedGroups, setExpandedGroups] = useState<Set<string>>(\n () => new Set([\"Main\"]),\n );\n\n const toggleGroup = (label: string, expanded: boolean) => {\n setExpandedGroups((prev) => {\n const next = new Set(prev);\n if (expanded) next.add(label);\n else next.delete(label);\n return next;\n });\n };\n\n return (\n <div style={{ height: 480, borderRadius: \"var(--clet-radius-2xl)\", overflow: \"hidden\" }}>\n {/* Dashboard shell: variant=\"stacked\" AppLayout + variant=\"plain\" AppHeader/Sidebar */}\n <AppLayout variant=\"stacked\">\n <AppHeader variant=\"plain\">\n <AppHeaderBranding title=\"CLET PORTAL\" subtitle=\"Component Library\" />\n <AppHeaderActions>\n <AppSwitcher apps={apps} title=\"System directory\" />\n <AppHeaderNotifications>\n {notifications.map((n) => (\n <div key={n.id} className=\"clet-notif-popover__item\">\n {n.unread && <div className=\"clet-notif-popover__dot\" />}\n <div className=\"clet-notif-popover__body\">\n <div className=\"clet-notif-popover__body-text\">{n.text}</div>\n <div className=\"clet-notif-popover__body-time\">{n.time}</div>\n </div>\n </div>\n ))}\n </AppHeaderNotifications>\n <ProfilePopover\n variant=\"avatar\"\n user={user}\n onSignOut={() => console.log(\"Sign out\")}\n >\n <RoleSelect\n title=\"View as\"\n roles={roles}\n selectedRole={selectedRole}\n onClickRole={(role) => setSelectedRole(role.id)}\n />\n </ProfilePopover>\n </AppHeaderActions>\n </AppHeader>\n <AppSidebar>\n <Sidebar variant=\"plain\">\n <SidebarContent>\n <SidebarNav>\n {navGroups.map((group) => (\n <SidebarGroup\n key={group.label}\n collapsible\n expanded={expandedGroups.has(group.label)}\n onExpandedChange={(expanded) => toggleGroup(group.label, expanded)}\n >\n <SidebarGroupLabel>{group.label}</SidebarGroupLabel>\n {group.links.map((link) => (\n <SidebarLink\n key={link.id}\n active={active === link.id}\n icon={<link.icon size={18} strokeWidth={1.5} />}\n onClick={() => setActive(link.id)}\n >\n {link.label}\n {link.badge && <SidebarBadge>{link.badge}</SidebarBadge>}\n </SidebarLink>\n ))}\n </SidebarGroup>\n ))}\n </SidebarNav>\n <button type=\"button\" className=\"clet-sidebar__link\" style={{ width: \"100%\" }}>\n <ChevronRight size={16} strokeWidth={1.5} />\n <span className=\"clet-sidebar__link-label\">Back to main dashboard</span>\n </button>\n </SidebarContent>\n <SidebarFooter>\n <ProfilePopover\n fullName={user.name}\n email={user.email}\n items={[\n {\n icon: <User size={20} strokeWidth={1.5} aria-hidden />,\n label: \"My Profile\",\n },\n {\n icon: <Settings size={20} strokeWidth={1.5} aria-hidden />,\n label: \"Account Settings\",\n },\n {\n icon: <HelpCircle size={20} strokeWidth={1.5} aria-hidden />,\n label: \"Help & Support\",\n },\n ]}\n onSignOut={() => console.log(\"Sign out\")}\n >\n <RoleSelect\n title=\"View as\"\n roles={roles}\n selectedRole={selectedRole}\n onClickRole={(role) => setSelectedRole(role.id)}\n />\n </ProfilePopover>\n </SidebarFooter>\n </Sidebar>\n </AppSidebar>\n <AppBody>\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n height: \"100%\",\n color: \"var(--clet-text-muted)\",\n fontSize: 14,\n }}\n >\n Page content — full-width plain header on top, plain sidebar and\n content side by side below\n </div>\n </AppBody>\n </AppLayout>\n </div>\n );\n}\n"
74
+ },
75
+ {
76
+ "file": "app-layout.example.tsx",
77
+ "importName": "appLayoutSource",
78
+ "title": "App Layout",
79
+ "components": [
80
+ "AppBody",
81
+ "AppHeader",
82
+ "AppHeaderActions",
83
+ "AppHeaderSearch",
84
+ "AppHeaderSearchDataGroup",
85
+ "AppLayout",
86
+ "AppSidebar",
87
+ "ProfilePopover",
88
+ "RoleSelect",
89
+ "Sidebar",
90
+ "SidebarContent",
91
+ "SidebarGroup",
92
+ "SidebarGroupLabel",
93
+ "SidebarLink",
94
+ "SidebarNav"
95
+ ],
96
+ "source": "import { useState } from \"react\";\nimport {\n AppLayout,\n AppHeader,\n AppHeaderActions,\n AppHeaderSearch,\n ProfilePopover,\n AppSidebar,\n AppBody,\n Sidebar,\n SidebarContent,\n SidebarNav,\n SidebarGroup,\n SidebarGroupLabel,\n SidebarLink,\n RoleSelect,\n} from \"@rfdtech/components\";\nimport { LayoutDashboard, Users, Settings, Shield, Eye, ScrollText } from \"lucide-react\";\nimport type { AppHeaderSearchDataGroup } from \"@rfdtech/components\";\n\nconst user = {\n name: \"Kwame Asante\",\n role: \"Admin\",\n initials: \"KA\",\n email: \"kwame@clet.edu.gh\",\n};\n\nconst roles = [\n { id: \"admin\", name: \"Admin\", icon: <Shield size={16} strokeWidth={1.5} /> },\n { id: \"reviewer\", name: \"Reviewer\", icon: <Eye size={16} strokeWidth={1.5} /> },\n { id: \"auditor\", name: \"Auditor\", icon: <ScrollText size={16} strokeWidth={1.5} /> },\n];\n\nconst links = [\n { id: \"dashboard\", label: \"Dashboard\", icon: LayoutDashboard, active: true },\n { id: \"members\", label: \"Members\", icon: Users },\n { id: \"settings\", label: \"Settings\", icon: Settings },\n];\n\nexport function AppLayoutExample() {\n const [search, setSearch] = useState(\"\");\n const [selectedRole, setSelectedRole] = useState(\"admin\");\n\n const searchGroups: AppHeaderSearchDataGroup[] = search\n ? [\n {\n heading: \"Members\",\n items: [\n {\n value: \"kwame-asante\",\n label: \"Kwame Asante\",\n onSelect: () => console.log(\"Selected member:\", \"Kwame Asante\"),\n },\n {\n value: \"abena-mensah\",\n label: \"Abena Mensah\",\n onSelect: () => console.log(\"Selected member:\", \"Abena Mensah\"),\n },\n ],\n },\n {\n heading: \"Projects\",\n items: [\n {\n value: \"clet-platform\",\n label: \"CLET Platform\",\n onSelect: () => console.log(\"Selected project:\", \"CLET Platform\"),\n },\n ],\n },\n ]\n : [];\n\n return (\n <div style={{ height: 400, borderRadius: \"var(--clet-radius-2xl)\", overflow: \"hidden\" }}>\n <AppLayout>\n <AppHeader>\n <AppHeaderSearch\n data={searchGroups}\n onSearch={setSearch}\n showEmpty\n placeholder=\"Search members, projects…\"\n />\n <AppHeaderActions>\n <ProfilePopover\n user={user}\n onSignOut={() => console.log(\"Sign out\")}\n >\n <RoleSelect\n title=\"View as\"\n roles={roles}\n selectedRole={selectedRole}\n onClickRole={(role) => setSelectedRole(role.id)}\n />\n </ProfilePopover>\n </AppHeaderActions>\n </AppHeader>\n <AppSidebar>\n <Sidebar>\n <SidebarContent>\n <SidebarNav>\n <SidebarGroup>\n <SidebarGroupLabel>Navigation</SidebarGroupLabel>\n {links.map((link) => (\n <SidebarLink key={link.id} icon={<link.icon size={18} strokeWidth={1.5} />}>\n {link.label}\n </SidebarLink>\n ))}\n </SidebarGroup>\n </SidebarNav>\n </SidebarContent>\n </Sidebar>\n </AppSidebar>\n <AppBody>\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n height: \"100%\",\n color: \"var(--clet-text-muted)\",\n fontSize: 14,\n }}\n >\n Page content — auto-positioned header, sidebar, and body\n </div>\n </AppBody>\n </AppLayout>\n </div>\n );\n}\n"
97
+ }
98
+ ],
99
+ "app-switcher": [
100
+ {
101
+ "file": "app-switcher-loading.example.tsx",
102
+ "importName": "appSwitcherLoadingSource",
103
+ "title": "App Switcher Loading",
104
+ "components": [
105
+ "AppSwitcher"
106
+ ],
107
+ "source": "import { AppSwitcher } from \"@rfdtech/components\";\n\nexport function AppSwitcherLoadingExample() {\n return (\n <AppSwitcher\n apps={[]}\n loading\n loadingLabel=\"Loading systems...\"\n title=\"System directory\"\n />\n );\n}\n"
108
+ },
109
+ {
110
+ "file": "app-switcher.example.tsx",
111
+ "importName": "appSwitcherSource",
112
+ "title": "App Switcher",
113
+ "components": [
114
+ "AppSwitcher",
115
+ "SystemAppIcon"
116
+ ],
117
+ "source": "import { AppSwitcher, SystemAppIcon } from \"@rfdtech/components\";\n\nconst apps = [\n {\n id: \"gov-portal\",\n name: \"Governance Portal\",\n icon: \"https://ui-avatars.com/api/?name=Governance+Portal&background=1d4ed8&color=fff&size=96\",\n href: \"https://portal.example.com\",\n },\n {\n id: \"finance-hub\",\n name: \"Finance Hub\",\n icon: \"https://ui-avatars.com/api/?name=Finance+Hub&background=047857&color=fff&size=96\",\n href: \"https://finance.example.com\",\n },\n {\n id: \"hr-suite\",\n name: \"HR Suite\",\n icon: \"https://ui-avatars.com/api/?name=HR+Suite&background=7c3aed&color=fff&size=96\",\n },\n {\n id: \"accreditation\",\n name: \"Accreditation\",\n icon: <SystemAppIcon name=\"Accreditation\" />,\n },\n {\n id: \"facilities\",\n name: \"Facilities\",\n icon: <SystemAppIcon name=\"Facilities\" />,\n },\n {\n id: \"fleet-management\",\n name: \"Fleet Management\",\n icon: <SystemAppIcon name=\"Fleet Management\" />,\n },\n];\n\nexport function AppSwitcherExample() {\n return (\n <AppSwitcher\n apps={apps}\n title=\"System directory\"\n onAppSelect={(app) => console.log(\"Selected:\", app.name)}\n />\n );\n}\n"
118
+ }
119
+ ],
120
+ "badge": [
121
+ {
122
+ "file": "badge-variants.example.tsx",
123
+ "importName": "badgeVariantsSource",
124
+ "title": "Badge Variants",
125
+ "components": [
126
+ "Badge"
127
+ ],
128
+ "source": "import { Badge } from \"@rfdtech/components\";\n\nexport function BadgeVariantsExample() {\n return (\n <>\n <Badge variant=\"default\">Default</Badge>\n <Badge variant=\"primary\">Primary</Badge>\n <Badge variant=\"success\">Success</Badge>\n <Badge variant=\"warning\">Warning</Badge>\n <Badge variant=\"error\">Error</Badge>\n <Badge variant=\"outline\">Outline</Badge>\n <Badge size=\"md\">Medium</Badge>\n </>\n );\n}\n"
129
+ }
130
+ ],
131
+ "breadcrumb": [
132
+ {
133
+ "file": "breadcrumb.example.tsx",
134
+ "importName": "breadcrumbSource",
135
+ "title": "Breadcrumb",
136
+ "components": [
137
+ "Breadcrumb",
138
+ "BreadcrumbItem",
139
+ "BreadcrumbLink",
140
+ "BreadcrumbList",
141
+ "BreadcrumbPage",
142
+ "BreadcrumbSeparator"
143
+ ],
144
+ "source": "import {\n Breadcrumb,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbPage,\n BreadcrumbSeparator,\n} from \"@rfdtech/components\";\n\nexport function BreadcrumbExample() {\n return (\n <Breadcrumb>\n <BreadcrumbList>\n <BreadcrumbItem>\n <BreadcrumbLink href=\"#\">Home</BreadcrumbLink>\n </BreadcrumbItem>\n <BreadcrumbSeparator />\n <BreadcrumbItem>\n <BreadcrumbLink href=\"#\">Settings</BreadcrumbLink>\n </BreadcrumbItem>\n <BreadcrumbSeparator />\n <BreadcrumbItem>\n <BreadcrumbPage>Profile</BreadcrumbPage>\n </BreadcrumbItem>\n </BreadcrumbList>\n </Breadcrumb>\n );\n}\n"
145
+ }
146
+ ],
147
+ "bulk-import-modal": [
148
+ {
149
+ "file": "bulk-import.example.tsx",
150
+ "importName": "bulkImportSource",
151
+ "title": "Bulk Import",
152
+ "components": [
153
+ "BulkImportField",
154
+ "BulkImportModal",
155
+ "BulkImportResult",
156
+ "useModalSearchParam"
157
+ ],
158
+ "source": "import type { BulkImportField, BulkImportResult } from \"@rfdtech/components\";\nimport { BulkImportModal, useModalSearchParam } from \"@rfdtech/components\";\nimport { useCallback } from \"react\";\n\nconst fields: BulkImportField[] = [\n {\n key: \"organisation_name\",\n label: \"Organisation Name\",\n required: true,\n example: \"CLET Corporation\",\n },\n {\n key: \"organisation_email\",\n type: \"email\",\n label: \"Organisation Email\",\n required: true,\n unique: true,\n pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/,\n example: \"info@clet.edu.gh\",\n },\n {\n key: \"document_name\",\n label: \"Document Name\",\n required: true,\n example: \"2024 Annual Budget\",\n },\n {\n key: \"document_type\",\n label: \"Document Type\",\n required: false,\n example: \"Financial\",\n },\n {\n key: \"date_submitted\",\n label: \"Date Submitted\",\n required: false,\n example: \"2024-01-15\",\n },\n {\n key: \"status\",\n label: \"Status\",\n required: false,\n example: \"Approved\",\n },\n {\n key: \"amount\",\n label: \"Amount (GHS)\",\n required: false,\n example: \"150000\",\n },\n];\n\nexport function BulkImportExample() {\n const { open, onOpenChange, openWith } = useModalSearchParam(\"bulk-import\");\n\n const handleComplete = useCallback(\n (result: BulkImportResult) => {\n console.log(\"Import complete:\", result);\n onOpenChange(false);\n },\n [onOpenChange],\n );\n\n return (\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: 12,\n alignItems: \"flex-start\",\n }}\n >\n <div style={{ display: \"flex\", gap: 8 }}>\n <button\n type=\"button\"\n className=\"demo-button\"\n onClick={() => openWith()}\n >\n Open bulk import\n </button>\n </div>\n <BulkImportModal\n maxFileSizeBytes={200 * 1024 * 1024}\n open={open}\n onOpenChange={onOpenChange}\n title=\"Import documents\"\n fields={fields}\n onComplete={handleComplete}\n />\n </div>\n );\n}\n"
159
+ }
160
+ ],
161
+ "button": [
162
+ {
163
+ "file": "button-variants.example.tsx",
164
+ "importName": "buttonVariantsSource",
165
+ "title": "Button Variants",
166
+ "components": [
167
+ "Button"
168
+ ],
169
+ "source": "import { Button } from \"@rfdtech/components\";\n\nexport function ButtonVariantsExample() {\n return (\n <>\n <Button variant=\"primary\">Primary</Button>\n <Button variant=\"secondary\">Secondary</Button>\n <Button variant=\"outline\">Outline</Button>\n <Button variant=\"ghost\">Ghost</Button>\n <Button loading loadingLabel=\"Saving\">\n Loading\n </Button>\n <Button disabled>Disabled</Button>\n </>\n );\n}\n"
170
+ }
171
+ ],
172
+ "card": [
173
+ {
174
+ "file": "card.example.tsx",
175
+ "importName": "cardSource",
176
+ "title": "Card",
177
+ "components": [
178
+ "Card",
179
+ "CardActions",
180
+ "CardHeader",
181
+ "CardTitle"
182
+ ],
183
+ "source": "import { Card, CardHeader, CardTitle, CardActions } from \"@rfdtech/components\";\n\nexport function CardExample() {\n return (\n <div\n style={{\n background: \"var(--clet-surface-subtle)\",\n padding: 20,\n borderRadius: \"var(--clet-radius-xl)\",\n }}\n >\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: 16,\n maxWidth: 480,\n }}\n >\n <Card>\n <CardHeader>\n <CardTitle>Profile summary</CardTitle>\n <CardActions>\n <button\n style={{\n padding: \"4px 12px\",\n fontSize: 12,\n border: \"1px solid var(--clet-border)\",\n borderRadius: \"var(--clet-radius)\",\n background: \"var(--clet-bg)\",\n cursor: \"pointer\",\n }}\n >\n Edit\n </button>\n </CardActions>\n </CardHeader>\n <p\n style={{\n margin: 0,\n fontSize: 14,\n color: \"var(--clet-text-secondary)\",\n lineHeight: 1.6,\n }}\n >\n Kwame Asante is an administrator with full system access.\n </p>\n </Card>\n <Card>\n <p\n style={{\n margin: 0,\n fontSize: 14,\n color: \"var(--clet-text-secondary)\",\n lineHeight: 1.6,\n }}\n >\n A card without a header, useful for wrapping standalone content like\n tables or text.\n </p>\n </Card>\n <Card>\n <CardHeader>\n <CardTitle>Settings</CardTitle>\n </CardHeader>\n <p\n style={{\n margin: 0,\n fontSize: 14,\n color: \"var(--clet-text-secondary)\",\n lineHeight: 1.6,\n }}\n >\n Header with title only, no actions. Use <code>className</code> for\n root overrides.\n </p>\n </Card>\n </div>\n </div>\n );\n}\n"
184
+ }
185
+ ],
186
+ "changelog": [],
187
+ "checkbox": [
188
+ {
189
+ "file": "checkbox.example.tsx",
190
+ "importName": "checkboxSource",
191
+ "title": "Checkbox",
192
+ "components": [
193
+ "Checkbox"
194
+ ],
195
+ "source": "import { useState } from \"react\";\nimport { Checkbox } from \"@rfdtech/components\";\n\nexport function CheckboxExample() {\n const [terms, setTerms] = useState(false);\n const [updates, setUpdates] = useState(true);\n\n return (\n <>\n <Checkbox\n label=\"Accept terms and conditions\"\n checked={terms}\n onCheckedChange={setTerms}\n />\n <Checkbox\n label=\"Send me product updates\"\n checked={updates}\n onCheckedChange={setUpdates}\n />\n <Checkbox label=\"Disabled option\" disabled />\n </>\n );\n}\n"
196
+ }
197
+ ],
198
+ "combobox": [
199
+ {
200
+ "file": "combobox-icons.example.tsx",
201
+ "importName": "comboboxIconsSource",
202
+ "title": "Combobox Icons",
203
+ "components": [
204
+ "Combobox"
205
+ ],
206
+ "source": "import { Combobox } from \"@rfdtech/components\";\nimport { Star } from \"lucide-react\";\nimport { useState } from \"react\";\n\nconst options = [\n { value: \"1\", label: \"Option 1\", icon: <Star size={16} /> },\n { value: \"2\", label: \"Option 2\", icon: <Star size={16} /> },\n { value: \"3\", label: \"Option 3\", icon: <Star size={16} /> },\n];\n\nexport function ComboboxIconsExample() {\n const [value, setValue] = useState<string[]>([\"1\", \"2\"]);\n\n return (\n <Combobox\n aria-label=\"Select options\"\n placeholder=\"Select\"\n options={options}\n value={value}\n onValueChange={setValue}\n multiple\n />\n );\n}\n"
207
+ },
208
+ {
209
+ "file": "combobox-multi.example.tsx",
210
+ "importName": "comboboxMultiSource",
211
+ "title": "Combobox Multi",
212
+ "components": [
213
+ "Combobox"
214
+ ],
215
+ "source": "import { Combobox } from \"@rfdtech/components\";\nimport { useState } from \"react\";\n\nconst options = [\n { value: \"1\", label: \"Option 1\" },\n { value: \"2\", label: \"Option 2\" },\n { value: \"3\", label: \"Option 3\" },\n { value: \"4\", label: \"Option 4\" },\n];\n\nexport function ComboboxMultiExample() {\n const [value, setValue] = useState<string[]>([\"2\", \"4\"]);\n\n return (\n <Combobox\n aria-label=\"Select options\"\n placeholder=\"Select\"\n options={options}\n value={value}\n onValueChange={setValue}\n multiple\n />\n );\n}\n"
216
+ },
217
+ {
218
+ "file": "combobox-single.example.tsx",
219
+ "importName": "comboboxSingleSource",
220
+ "title": "Combobox Single",
221
+ "components": [
222
+ "Combobox"
223
+ ],
224
+ "source": "import { Combobox } from \"@rfdtech/components\";\nimport { useState } from \"react\";\n\nconst options = [\n { value: \"1\", label: \"Option 1\" },\n { value: \"2\", label: \"Option 2\" },\n { value: \"3\", label: \"Option 3\" },\n { value: \"4\", label: \"Option 4\" },\n { value: \"5\", label: \"Option 5\" },\n];\n\nexport function ComboboxSingleExample() {\n const [value, setValue] = useState<string | null>(null);\n\n return (\n <Combobox\n aria-label=\"Select\"\n placeholder=\"Select\"\n options={options}\n value={value}\n onValueChange={setValue}\n clearable\n />\n );\n}\n"
225
+ }
226
+ ],
227
+ "command": [
228
+ {
229
+ "file": "command-dialog.example.tsx",
230
+ "importName": "commandDialogSource",
231
+ "title": "Command Dialog",
232
+ "components": [
233
+ "Button",
234
+ "CommandDialog",
235
+ "CommandEmpty",
236
+ "CommandGroup",
237
+ "CommandInput",
238
+ "CommandItem",
239
+ "CommandList",
240
+ "CommandSeparator",
241
+ "CommandShortcut",
242
+ "useDialogSearchParam"
243
+ ],
244
+ "source": "import { useCallback, useEffect, useState } from \"react\";\nimport {\n Button,\n CommandDialog,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n CommandShortcut,\n useDialogSearchParam,\n} from \"@rfdtech/components\";\n\ntype MockItem = { value: string; label: string };\n\nconst MOCK_NAV: MockItem[] = [\n { value: \"dashboard\", label: \"Dashboard\" },\n { value: \"users\", label: \"Users\" },\n];\n\nconst MOCK_ACTIONS: MockItem[] = [{ value: \"sign-out\", label: \"Sign out\" }];\n\nfunction filterItems(query: string, items: readonly MockItem[]) {\n const normalized = query.trim().toLowerCase();\n if (!normalized) {\n return [];\n }\n\n return items.filter(\n (item) =>\n item.label.toLowerCase().includes(normalized) ||\n item.value.toLowerCase().includes(normalized),\n );\n}\n\nfunction useMockGroupFetch(\n query: string,\n items: readonly MockItem[],\n delayMs: number,\n) {\n const [results, setResults] = useState<MockItem[]>([]);\n const [loading, setLoading] = useState(false);\n\n useEffect(() => {\n const normalized = query.trim();\n if (!normalized) {\n setResults([]);\n setLoading(false);\n return;\n }\n\n setLoading(true);\n const timer = window.setTimeout(() => {\n setResults(filterItems(normalized, items));\n setLoading(false);\n }, delayMs);\n\n return () => {\n window.clearTimeout(timer);\n };\n }, [query, items, delayMs]);\n\n return { results, loading };\n}\n\nexport function CommandDialogExample() {\n const { open, onOpenChange, openWith } = useDialogSearchParam(\"command-menu\");\n const [search, setSearch] = useState(\"\");\n const [lastAction, setLastAction] = useState(\"Open the command menu\");\n const navigation = useMockGroupFetch(search, MOCK_NAV, 350);\n const actions = useMockGroupFetch(search, MOCK_ACTIONS, 700);\n const hasSearch = Boolean(search.trim());\n const anyLoading = navigation.loading || actions.loading;\n const totalResults = navigation.results.length + actions.results.length;\n const showEmpty = hasSearch && !anyLoading && totalResults === 0;\n\n const handleOpenChange = useCallback(\n (nextOpen: boolean) => {\n onOpenChange(nextOpen);\n if (!nextOpen) {\n setSearch(\"\");\n }\n },\n [onOpenChange],\n );\n\n return (\n <div style={{ display: \"grid\", gap: 12 }}>\n <Button variant=\"secondary\" onClick={() => openWith()}>\n Open command menu\n </Button>\n <p style={{ margin: 0, fontSize: 14, color: \"var(--clet-text-secondary)\" }}>\n Press <kbd>Cmd</kbd>+<kbd>K</kbd> or <kbd>Ctrl</kbd>+<kbd>K</kbd>.{\" \"}\n {lastAction}\n </p>\n <CommandDialog\n open={open}\n onOpenChange={handleOpenChange}\n shortcut\n label=\"Command menu\"\n shouldFilter={false}\n >\n <CommandInput\n value={search}\n onValueChange={setSearch}\n placeholder=\"Type a command...\"\n aria-label=\"Command search\"\n />\n {hasSearch ? (\n <CommandList>\n {showEmpty ? <CommandEmpty>No results found.</CommandEmpty> : null}\n <CommandGroup\n heading=\"Navigation\"\n loading={navigation.loading}\n loadingLabel=\"Loading navigation\"\n >\n {navigation.results.map((item) => (\n <CommandItem\n key={item.value}\n value={item.value}\n onSelect={() => {\n setLastAction(`Navigated to ${item.label}`);\n handleOpenChange(false);\n }}\n >\n {item.label}\n </CommandItem>\n ))}\n </CommandGroup>\n <CommandSeparator />\n <CommandGroup\n heading=\"Actions\"\n loading={actions.loading}\n loadingLabel=\"Loading actions\"\n >\n {actions.results.map((item) => (\n <CommandItem\n key={item.value}\n value={item.value}\n onSelect={() => {\n setLastAction(\n item.value === \"sign-out\" ? \"Signed out\" : item.label,\n );\n handleOpenChange(false);\n }}\n >\n {item.label}\n {item.value === \"sign-out\" ? (\n <CommandShortcut>\n <span>⌘</span>\n <span>Q</span>\n </CommandShortcut>\n ) : null}\n </CommandItem>\n ))}\n </CommandGroup>\n </CommandList>\n ) : null}\n </CommandDialog>\n </div>\n );\n}\n"
245
+ },
246
+ {
247
+ "file": "command-inline.example.tsx",
248
+ "importName": "commandInlineSource",
249
+ "title": "Command Inline",
250
+ "components": [
251
+ "Command",
252
+ "CommandEmpty",
253
+ "CommandGroup",
254
+ "CommandInput",
255
+ "CommandItem",
256
+ "CommandList"
257
+ ],
258
+ "source": "import { useEffect, useState } from \"react\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@rfdtech/components\";\n\ntype MockItem = { value: string; label: string };\n\nconst MOCK_CONTACT: MockItem[] = [\n { value: \"email\", label: \"Email\" },\n { value: \"name\", label: \"Full name\" },\n { value: \"phone\", label: \"Phone number\" },\n];\n\nconst MOCK_ORG: MockItem[] = [\n { value: \"company\", label: \"Company name\" },\n { value: \"address\", label: \"Street address\" },\n];\n\nfunction filterItems(query: string, items: readonly MockItem[]) {\n const normalized = query.trim().toLowerCase();\n if (!normalized) {\n return [];\n }\n\n return items.filter(\n (item) =>\n item.label.toLowerCase().includes(normalized) ||\n item.value.toLowerCase().includes(normalized),\n );\n}\n\nfunction useMockGroupFetch(\n query: string,\n items: readonly MockItem[],\n delayMs: number,\n) {\n const [results, setResults] = useState<MockItem[]>([]);\n const [loading, setLoading] = useState(false);\n\n useEffect(() => {\n const normalized = query.trim();\n if (!normalized) {\n setResults([]);\n setLoading(false);\n return;\n }\n\n setLoading(true);\n const timer = window.setTimeout(() => {\n setResults(filterItems(normalized, items));\n setLoading(false);\n }, delayMs);\n\n return () => {\n window.clearTimeout(timer);\n };\n }, [query, items, delayMs]);\n\n return { results, loading };\n}\n\nexport function CommandInlineExample() {\n const [search, setSearch] = useState(\"\");\n const [field, setField] = useState<string>(\"email\");\n const contact = useMockGroupFetch(search, MOCK_CONTACT, 350);\n const organization = useMockGroupFetch(search, MOCK_ORG, 700);\n const hasSearch = Boolean(search.trim());\n const anyLoading = contact.loading || organization.loading;\n const totalResults = contact.results.length + organization.results.length;\n const showEmpty = hasSearch && !anyLoading && totalResults === 0;\n\n return (\n <div style={{ display: \"grid\", gap: 12, maxWidth: 360, width: \"100%\" }}>\n <Command label=\"Field picker\" shouldFilter={false}>\n <CommandInput\n value={search}\n onValueChange={setSearch}\n placeholder=\"Search fields...\"\n aria-label=\"Search fields\"\n />\n {hasSearch ? (\n <CommandList>\n {showEmpty ? <CommandEmpty>No results.</CommandEmpty> : null}\n <CommandGroup\n heading=\"Contact fields\"\n loading={contact.loading}\n loadingLabel=\"Loading contact fields\"\n >\n {contact.results.map((item) => (\n <CommandItem\n key={item.value}\n value={item.value}\n onSelect={() => setField(item.value)}\n >\n {item.label}\n </CommandItem>\n ))}\n </CommandGroup>\n <CommandGroup\n heading=\"Organization\"\n loading={organization.loading}\n loadingLabel=\"Loading organization fields\"\n >\n {organization.results.map((item) => (\n <CommandItem\n key={item.value}\n value={item.value}\n onSelect={() => setField(item.value)}\n >\n {item.label}\n </CommandItem>\n ))}\n </CommandGroup>\n </CommandList>\n ) : null}\n </Command>\n <p style={{ margin: 0, fontSize: 14, color: \"var(--clet-text-secondary)\" }}>\n Selected field: <strong style={{ color: \"var(--clet-text)\" }}>{field}</strong>\n </p>\n </div>\n );\n}\n"
259
+ }
260
+ ],
261
+ "country-selector": [
262
+ {
263
+ "file": "country-selector.example.tsx",
264
+ "importName": "countrySelectorSource",
265
+ "title": "Country Selector",
266
+ "components": [
267
+ "CountrySelector"
268
+ ],
269
+ "source": "import { CountrySelector } from \"@rfdtech/components\";\n\nexport function CountrySelectorExample() {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 16, maxWidth: 300 }}>\n <CountrySelector />\n <CountrySelector invalid />\n <CountrySelector disabled />\n </div>\n );\n}\n"
270
+ }
271
+ ],
272
+ "date-range-selector": [
273
+ {
274
+ "file": "date-range-filter.example.tsx",
275
+ "importName": "dateRangeFilterSource",
276
+ "title": "Date Range Filter",
277
+ "components": [
278
+ "Badge",
279
+ "Button",
280
+ "DateRangeSelector",
281
+ "DateRangeValue",
282
+ "Table",
283
+ "TableColumn",
284
+ "TableContent",
285
+ "TableFilter",
286
+ "TableFooter",
287
+ "TableHeader",
288
+ "TablePagination",
289
+ "TableSearch",
290
+ "useTableState"
291
+ ],
292
+ "source": "import { useState } from \"react\";\nimport { useTableState } from \"@rfdtech/components\";\nimport type { TableColumn, DateRangeValue } from \"@rfdtech/components\";\nimport {\n Table,\n TableHeader,\n TableSearch,\n TableFilter,\n TableContent,\n TableFooter,\n TablePagination,\n DateRangeSelector,\n Badge,\n Button,\n} from \"@rfdtech/components\";\nimport { ShieldAlert, SearchX } from \"lucide-react\";\n\ninterface AuditLogEntry {\n id: number;\n actor: string;\n action: string;\n target: string;\n status: \"success\" | \"failed\" | \"warning\";\n timestamp: Date;\n}\n\nconst auditLog: AuditLogEntry[] = [\n { id: 1, actor: \"kwame@clet.edu.gh\", action: \"Login\", target: \"Portal\", status: \"success\", timestamp: new Date(\"2026-06-01T09:12:00\") },\n { id: 2, actor: \"abena@clet.edu.gh\", action: \"Export report\", target: \"Finance\", status: \"success\", timestamp: new Date(\"2026-06-04T14:03:00\") },\n { id: 3, actor: \"kofi@clet.edu.gh\", action: \"Failed login\", target: \"Portal\", status: \"failed\", timestamp: new Date(\"2026-06-10T08:45:00\") },\n { id: 4, actor: \"esi@clet.edu.gh\", action: \"Update permissions\", target: \"User: yaw@clet.edu.gh\", status: \"warning\", timestamp: new Date(\"2026-06-15T11:20:00\") },\n { id: 5, actor: \"yaw@clet.edu.gh\", action: \"Delete record\", target: \"Claims\", status: \"warning\", timestamp: new Date(\"2026-06-18T16:32:00\") },\n { id: 6, actor: \"nana@clet.edu.gh\", action: \"Login\", target: \"Portal\", status: \"success\", timestamp: new Date(\"2026-06-22T09:01:00\") },\n { id: 7, actor: \"akua@clet.edu.gh\", action: \"Approve claim\", target: \"Claims\", status: \"success\", timestamp: new Date(\"2026-06-25T13:47:00\") },\n { id: 8, actor: \"kwesi@clet.edu.gh\", action: \"Failed login\", target: \"Portal\", status: \"failed\", timestamp: new Date(\"2026-06-29T07:58:00\") },\n { id: 9, actor: \"adwoa@clet.edu.gh\", action: \"Export report\", target: \"Finance\", status: \"success\", timestamp: new Date(\"2026-07-02T10:15:00\") },\n { id: 10, actor: \"kobina@clet.edu.gh\", action: \"Update permissions\", target: \"User: esi@clet.edu.gh\", status: \"warning\", timestamp: new Date(\"2026-07-06T15:40:00\") },\n];\n\nfunction statusVariant(status: AuditLogEntry[\"status\"]) {\n switch (status) {\n case \"success\":\n return \"success\" as const;\n case \"failed\":\n return \"error\" as const;\n case \"warning\":\n return \"warning\" as const;\n default:\n return \"default\" as const;\n }\n}\n\nfunction formatDate(date: Date) {\n return date.toLocaleDateString(\"en-US\", {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n });\n}\n\nconst columns: TableColumn<AuditLogEntry>[] = [\n { id: \"actor\", header: \"Actor\", accessorKey: \"actor\", sortable: true },\n { id: \"action\", header: \"Action\", accessorKey: \"action\", sortable: true },\n { id: \"target\", header: \"Target\", accessorKey: \"target\" },\n {\n id: \"status\",\n header: \"Status\",\n accessorKey: \"status\",\n sortable: true,\n cell: ({ value }) => (\n <Badge variant={statusVariant(value as AuditLogEntry[\"status\"])}>\n {String(value)}\n </Badge>\n ),\n },\n {\n id: \"timestamp\",\n header: \"Date\",\n accessorFn: (row) => formatDate(row.timestamp),\n sortable: true,\n },\n];\n\nexport function DateRangeFilterExample() {\n const { page, pageSize, pageSizeOptions, search } = useTableState({\n defaultPageSize: 5,\n });\n\n // Local draft state for the date range picker inside the filter popover.\n const [draftRange, setDraftRange] = useState<DateRangeValue>({\n start: null,\n end: null,\n });\n // Only committed to `appliedRange` when the user clicks \"Apply\".\n const [appliedRange, setAppliedRange] = useState<DateRangeValue>({\n start: null,\n end: null,\n });\n\n // Demo-only toggle to simulate a genuine error/permission failure, where we\n // render an inline message in place of the table body instead of letting\n // TableContent render its own (empty ≠ error) state.\n const [simulateError, setSimulateError] = useState(false);\n\n const filtered = auditLog.filter((entry) => {\n const matchesSearch =\n !search ||\n entry.actor.toLowerCase().includes(search.toLowerCase()) ||\n entry.action.toLowerCase().includes(search.toLowerCase());\n if (!matchesSearch) return false;\n\n if (appliedRange.start && entry.timestamp < appliedRange.start) return false;\n if (appliedRange.end && entry.timestamp > appliedRange.end) return false;\n\n return true;\n });\n\n const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));\n const paged = filtered.slice((page - 1) * pageSize, page * pageSize);\n\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 12 }}>\n <Table paramPrefix=\"audit-log\">\n <TableHeader>\n <TableSearch placeholder=\"Search audit log...\" />\n <TableFilter\n applyLabel=\"Apply\"\n onApply={() => setAppliedRange(draftRange)}\n onReset={() => {\n setDraftRange({ start: null, end: null });\n setAppliedRange({ start: null, end: null });\n }}\n >\n <div style={{ display: \"grid\", gap: 8 }}>\n <label style={{ fontSize: 13, fontWeight: 500 }}>Date range</label>\n <DateRangeSelector\n value={draftRange}\n onChange={setDraftRange}\n placeholder=\"Select date range\"\n />\n </div>\n </TableFilter>\n </TableHeader>\n\n {simulateError ? (\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: 8,\n padding: \"40px 16px\",\n textAlign: \"center\",\n }}\n >\n <ShieldAlert size={32} strokeWidth={1.5} color=\"var(--clet-error)\" />\n <p style={{ margin: 0, fontSize: 14, fontWeight: 500, color: \"var(--clet-error-text)\" }}>\n You don&apos;t have permission to view this audit log.\n </p>\n <p style={{ margin: 0, fontSize: 13, color: \"var(--clet-text-muted)\" }}>\n Contact an administrator if you believe this is a mistake.\n </p>\n </div>\n ) : (\n <TableContent\n columns={columns}\n data={paged}\n rowKey={(row) => row.id}\n emptyIcon={<SearchX size={40} strokeWidth={1} />}\n emptyText=\"No audit events in the selected range\"\n />\n )}\n\n <TableFooter>\n <TablePagination\n totalPages={totalPages}\n totalItems={filtered.length}\n pageSizeOptions={pageSizeOptions}\n />\n </TableFooter>\n </Table>\n\n <Button\n variant=\"outline\"\n size=\"sm\"\n style={{ alignSelf: \"flex-start\" }}\n onClick={() => setSimulateError((v) => !v)}\n >\n {simulateError ? \"Show table\" : \"Simulate permission error\"}\n </Button>\n </div>\n );\n}\n"
293
+ },
294
+ {
295
+ "file": "date-range-presets.example.tsx",
296
+ "importName": "dateRangePresetsSource",
297
+ "title": "Date Range Presets",
298
+ "components": [
299
+ "DateRangePreset",
300
+ "DateRangeSelector",
301
+ "DateRangeValue"
302
+ ],
303
+ "source": "import { DateRangeSelector } from \"@rfdtech/components\";\nimport type { DateRangePreset, DateRangeValue } from \"@rfdtech/components\";\nimport { useState } from \"react\";\n\nfunction startOfDay(date: Date) {\n return new Date(date.getFullYear(), date.getMonth(), date.getDate());\n}\n\nfunction daysAgo(count: number) {\n const d = startOfDay(new Date());\n d.setDate(d.getDate() - count);\n return d;\n}\n\nfunction monthsAgo(count: number) {\n const d = startOfDay(new Date());\n d.setMonth(d.getMonth() - count);\n return d;\n}\n\nconst presets: DateRangePreset[] = [\n { label: \"Today\", getRange: () => ({ start: startOfDay(new Date()), end: startOfDay(new Date()) }) },\n { label: \"Last 7 days\", getRange: () => ({ start: daysAgo(7), end: startOfDay(new Date()) }) },\n { label: \"Last 30 days\", getRange: () => ({ start: daysAgo(30), end: startOfDay(new Date()) }) },\n { label: \"Last 3 months\", getRange: () => ({ start: monthsAgo(3), end: startOfDay(new Date()) }) },\n { label: \"Last 12 months\", getRange: () => ({ start: monthsAgo(12), end: startOfDay(new Date()) }) },\n {\n label: \"Month to date\",\n getRange: () => {\n const now = new Date();\n return { start: new Date(now.getFullYear(), now.getMonth(), 1), end: startOfDay(now) };\n },\n },\n {\n label: \"Year to date\",\n getRange: () => {\n const now = new Date();\n return { start: new Date(now.getFullYear(), 0, 1), end: startOfDay(now) };\n },\n },\n { label: \"All time\", getRange: () => ({ start: null, end: null }) },\n];\n\nexport function DateRangePresetsExample() {\n const [value, setValue] = useState<DateRangeValue>({ start: null, end: null });\n\n return (\n <DateRangeSelector value={value} onChange={setValue} presets={presets} />\n );\n}\n"
304
+ },
305
+ {
306
+ "file": "date-range-selector.example.tsx",
307
+ "importName": "dateRangeSource",
308
+ "title": "Date Range Selector",
309
+ "components": [
310
+ "DateRangeSelector"
311
+ ],
312
+ "source": "import { DateRangeSelector } from \"@rfdtech/components\";\n\nexport function DateRangeSelectorExample() {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 16, maxWidth: 320 }}>\n <DateRangeSelector\n onChange={(range) => console.log(range)}\n />\n <DateRangeSelector invalid placeholder=\"Pick dates\" />\n <DateRangeSelector disabled />\n </div>\n );\n}\n"
313
+ }
314
+ ],
315
+ "date-selector": [
316
+ {
317
+ "file": "date-selector.example.tsx",
318
+ "importName": "dateSelectorSource",
319
+ "title": "Date Selector",
320
+ "components": [
321
+ "DateSelector"
322
+ ],
323
+ "source": "import { DateSelector } from \"@rfdtech/components\";\n\nexport function DateSelectorExample() {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 16, maxWidth: 300 }}>\n <DateSelector />\n <DateSelector invalid />\n <DateSelector disabled />\n </div>\n );\n}\n"
324
+ }
325
+ ],
326
+ "dialog": [
327
+ {
328
+ "file": "dialog.example.tsx",
329
+ "importName": "dialogSource",
330
+ "title": "Dialog",
331
+ "components": [
332
+ "Button",
333
+ "Dialog",
334
+ "DialogContent",
335
+ "DialogDescription",
336
+ "DialogOverlay",
337
+ "DialogPortal",
338
+ "DialogTitle",
339
+ "useDialogSearchParam"
340
+ ],
341
+ "source": "import {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n useDialogSearchParam,\n} from \"@rfdtech/components\";\n\nexport function DialogExample() {\n const { open, data, onOpenChange, openWith } = useDialogSearchParam<{\n userId: string;\n }>(\"edit-profile\");\n\n return (\n <>\n <Button\n variant=\"secondary\"\n onClick={() => openWith({ userId: \"42\" })}\n >\n Edit profile\n </Button>\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogPortal>\n <DialogOverlay />\n <DialogContent showCloseButton>\n <DialogTitle>Edit profile</DialogTitle>\n <DialogDescription>\n Update your name and contact details. Changes are saved when you\n submit the form.\n {data?.userId ? (\n <>\n {\" \"}\n Editing user <strong>{data.userId}</strong> (\n <code>?dialog=edit-profile&dialog.userId={data.userId}</code>\n ).\n </>\n ) : null}\n </DialogDescription>\n <div style={{ display: \"grid\", gap: 12 }}>\n <label style={{ display: \"grid\", gap: 6, fontSize: 14 }}>\n Full name\n <input\n type=\"text\"\n defaultValue=\"Alex Johnson\"\n style={{\n height: 40,\n padding: \"0 12px\",\n border: \"1px solid var(--clet-border)\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"var(--clet-bg)\",\n color: \"var(--clet-text)\",\n }}\n />\n </label>\n <label style={{ display: \"grid\", gap: 6, fontSize: 14 }}>\n Email\n <input\n type=\"email\"\n defaultValue=\"alex@example.com\"\n style={{\n height: 40,\n padding: \"0 12px\",\n border: \"1px solid var(--clet-border)\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"var(--clet-bg)\",\n color: \"var(--clet-text)\",\n }}\n />\n </label>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"flex-end\", gap: 8 }}>\n <Button variant=\"ghost\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={() => onOpenChange(false)}>Save changes</Button>\n </div>\n </DialogContent>\n </DialogPortal>\n </Dialog>\n </>\n );\n}\n"
342
+ }
343
+ ],
344
+ "draggable": [
345
+ {
346
+ "file": "draggable-root.example.tsx",
347
+ "importName": "draggableRootSource",
348
+ "title": "Draggable Root",
349
+ "components": [
350
+ "Draggable"
351
+ ],
352
+ "source": "import { Draggable } from \"@rfdtech/components\";\n\nexport function DraggableRootExample() {\n return (\n <div\n style={{\n position: \"relative\",\n width: \"100%\",\n maxWidth: 360,\n height: 240,\n border: \"1px dashed var(--clet-border)\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"var(--clet-surface-subtle)\",\n overflow: \"hidden\",\n }}\n >\n <Draggable defaultPosition={{ x: 24, y: 24 }} bounds=\"parent\">\n <div\n style={{\n padding: 12,\n border: \"1px solid var(--clet-border)\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"var(--clet-bg)\",\n boxShadow: \"0 1px 2px color-mix(in srgb, var(--clet-text) 8%, transparent)\",\n maxWidth: 220,\n cursor: \"grab\",\n }}\n >\n <div style={{ fontSize: 14, fontWeight: 600 }}>Draggable card</div>\n <div style={{ fontSize: 13, color: \"var(--clet-text-secondary)\" }}>\n Drag anywhere on the card to reposition within the canvas.\n </div>\n </div>\n </Draggable>\n </div>\n );\n}\n"
353
+ },
354
+ {
355
+ "file": "draggable.example.tsx",
356
+ "importName": "draggableHandleSource",
357
+ "title": "Draggable",
358
+ "components": [
359
+ "Draggable",
360
+ "DraggableHandle"
361
+ ],
362
+ "source": "import { Draggable, DraggableHandle } from \"@rfdtech/components\";\n\nexport function DraggableHandleExample() {\n return (\n <div\n style={{\n position: \"relative\",\n width: \"100%\",\n maxWidth: 360,\n height: 240,\n border: \"1px dashed var(--clet-border)\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"var(--clet-surface-subtle)\",\n overflow: \"hidden\",\n }}\n >\n <Draggable defaultPosition={{ x: 24, y: 24 }} bounds=\"parent\">\n <div\n style={{\n display: \"flex\",\n alignItems: \"flex-start\",\n gap: 8,\n padding: 12,\n border: \"1px solid var(--clet-border)\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"var(--clet-bg)\",\n boxShadow: \"0 1px 2px color-mix(in srgb, var(--clet-text) 8%, transparent)\",\n maxWidth: 220,\n }}\n >\n <DraggableHandle aria-label=\"Drag card\" />\n <div>\n <div style={{ fontSize: 14, fontWeight: 600 }}>Draggable card</div>\n <div style={{ fontSize: 13, color: \"var(--clet-text-secondary)\" }}>\n Drag the handle to reposition within the canvas.\n </div>\n </div>\n </div>\n </Draggable>\n </div>\n );\n}\n"
363
+ },
364
+ {
365
+ "file": "sortable.example.tsx",
366
+ "importName": "sortableSource",
367
+ "title": "Sortable",
368
+ "components": [
369
+ "Sortable",
370
+ "SortableHandle",
371
+ "SortableItem",
372
+ "SortableList"
373
+ ],
374
+ "source": "import { useState } from \"react\";\nimport {\n Sortable,\n SortableHandle,\n SortableItem,\n SortableList,\n} from \"@rfdtech/components\";\n\nconst labels: Record<string, string> = {\n alpha: \"Review submissions\",\n beta: \"Approve grades\",\n gamma: \"Publish results\",\n delta: \"Send notifications\",\n};\n\nexport function SortableExample() {\n const [items, setItems] = useState([\"alpha\", \"beta\", \"gamma\", \"delta\"]);\n\n return (\n <div\n style={{\n width: \"100%\",\n maxWidth: 360,\n padding: 12,\n border: \"1px dashed var(--clet-border)\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"var(--clet-surface-subtle)\",\n }}\n >\n <Sortable items={items} onReorder={(ids) => setItems(ids as string[])}>\n <SortableList>\n {items.map((id) => (\n <SortableItem key={id} id={id}>\n <SortableHandle aria-label={`Reorder ${labels[id]}`} />\n <span style={{ fontSize: 14 }}>{labels[id]}</span>\n </SortableItem>\n ))}\n </SortableList>\n </Sortable>\n </div>\n );\n}\n"
375
+ }
376
+ ],
377
+ "dropdown": [
378
+ {
379
+ "file": "dropdown-variants.example.tsx",
380
+ "importName": "dropdownVariantsSource",
381
+ "title": "Dropdown Variants",
382
+ "components": [
383
+ "Dropdown"
384
+ ],
385
+ "source": "import { Dropdown } from \"@rfdtech/components\";\nimport { Check } from \"lucide-react\";\nimport { useState } from \"react\";\n\nconst options = [\n { value: \"1\", label: \"Option A\" },\n { value: \"2\", label: \"Option B\" },\n];\n\nexport function DropdownVariantsExample() {\n const [value, setValue] = useState<string | null>(\"1\");\n\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: \"16px\", width: \"240px\" }}>\n <Dropdown\n aria-label=\"Disabled field\"\n value=\"1\"\n onValueChange={() => {}}\n options={options}\n disabled\n />\n\n <Dropdown\n aria-label=\"Custom rendering\"\n value={value}\n onValueChange={setValue}\n options={options}\n formatOption={(option, state) => {\n if (state === \"empty\") return <span>Pick one...</span>;\n if (state === \"selected\") {\n return (\n <span style={{ display: \"inline-flex\", alignItems: \"center\", gap: 6 }}>\n <Check size={14} />\n <span>{option!.label}</span>\n </span>\n );\n }\n return <span>{option!.label}</span>;\n }}\n />\n </div>\n );\n}\n"
386
+ },
387
+ {
388
+ "file": "dropdown.example.tsx",
389
+ "importName": "dropdownSource",
390
+ "title": "Dropdown",
391
+ "components": [
392
+ "Dropdown"
393
+ ],
394
+ "source": "import { Dropdown } from \"@rfdtech/components\";\n\nconst options = [\n { value: \"email\", label: \"Email\" },\n { value: \"name\", label: \"Full name\" },\n { value: \"year\", label: \"Year\" },\n];\n\nexport function DropdownExample() {\n return (\n <div style={{ width: \"240px\" }}>\n <Dropdown\n aria-label=\"Field\"\n value=\"email\"\n onValueChange={() => {}}\n options={options}\n clearable\n />\n </div>\n );\n}\n"
395
+ }
396
+ ],
397
+ "export-button": [
398
+ {
399
+ "file": "export-button-formats.example.tsx",
400
+ "importName": "exportButtonFormatsSource",
401
+ "title": "Export Button Formats",
402
+ "components": [
403
+ "ExportButton",
404
+ "ExportColumn"
405
+ ],
406
+ "source": "import { ExportButton } from \"@rfdtech/components\";\nimport type { ExportColumn } from \"@rfdtech/components\";\n\ninterface Member {\n name: string;\n email: string;\n}\n\nconst members: Member[] = [\n { name: \"Kwame Asante\", email: \"kwame.asante@clet.edu.gh\" },\n { name: \"Abena Mensah\", email: \"abena.mensah@clet.edu.gh\" },\n];\n\nconst columns: ExportColumn<Member>[] = [\n { header: \"Name\", accessor: (row) => row.name },\n { header: \"Email\", accessor: (row) => row.email },\n];\n\nexport function ExportButtonFormatsExample() {\n return (\n <ExportButton\n data={members}\n columns={columns}\n title=\"Members\"\n formats={[\"csv\"]}\n label=\"Export CSV\"\n variant=\"secondary\"\n />\n );\n}\n"
407
+ },
408
+ {
409
+ "file": "export-button.example.tsx",
410
+ "importName": "exportButtonSource",
411
+ "title": "Export Button",
412
+ "components": [
413
+ "ExportButton",
414
+ "ExportColumn"
415
+ ],
416
+ "source": "import { ExportButton } from \"@rfdtech/components\";\nimport type { ExportColumn } from \"@rfdtech/components\";\n\ninterface Member {\n id: number;\n name: string;\n email: string;\n role: string;\n status: string;\n joined: string;\n}\n\nconst members: Member[] = [\n { id: 1, name: \"Kwame Asante\", email: \"kwame.asante@clet.edu.gh\", role: \"Admin\", status: \"Active\", joined: \"2024-01-15\" },\n { id: 2, name: \"Abena Mensah\", email: \"abena.mensah@clet.edu.gh\", role: \"Editor\", status: \"Active\", joined: \"2024-02-03\" },\n { id: 3, name: \"Kofi Owusu\", email: \"kofi.owusu@clet.edu.gh\", role: \"Viewer\", status: \"Inactive\", joined: \"2024-03-08\" },\n];\n\nconst columns: ExportColumn<Member>[] = [\n { header: \"Name\", accessor: (row) => row.name },\n { header: \"Email\", accessor: (row) => row.email },\n { header: \"Role\", accessor: (row) => row.role },\n { header: \"Status\", accessor: (row) => row.status },\n { header: \"Joined\", accessor: (row) => row.joined },\n];\n\nexport function ExportButtonExample() {\n return (\n <ExportButton\n data={members}\n columns={columns}\n title=\"Members\"\n filtersDescription=\"Active only\"\n />\n );\n}\n"
417
+ }
418
+ ],
419
+ "form-field": [
420
+ {
421
+ "file": "form.example.tsx",
422
+ "importName": "formSource",
423
+ "title": "Form",
424
+ "components": [
425
+ "Button",
426
+ "Field",
427
+ "FieldControl",
428
+ "FieldDescription",
429
+ "FieldError",
430
+ "FieldLabel",
431
+ "Form",
432
+ "FormField",
433
+ "Input",
434
+ "Textarea"
435
+ ],
436
+ "source": "import { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\nimport {\n Button,\n Field,\n FieldControl,\n FieldDescription,\n FieldError,\n FieldLabel,\n Form,\n FormField,\n Input,\n Textarea,\n} from \"@rfdtech/components\";\n\nconst contactFormSchema = z.object({\n email: z\n .string()\n .min(1, \"Email is required\")\n .email(\"Enter a valid email address\"),\n message: z.string().min(10, \"Message must be at least 10 characters\"),\n});\n\ntype ContactFormValues = z.infer<typeof contactFormSchema>;\n\nfunction ContactFormFields() {\n const form = useForm<ContactFormValues>({\n resolver: zodResolver(contactFormSchema),\n defaultValues: {\n email: \"\",\n message: \"\",\n },\n });\n\n return (\n <Form {...form}>\n <form\n onSubmit={form.handleSubmit(() => {})}\n style={{ display: \"grid\", gap: 16, maxWidth: 420 }}\n >\n <FormField\n control={form.control}\n name=\"email\"\n render={({ field, fieldState }) => (\n <Field invalid={!!fieldState.error}>\n <FieldLabel>Email</FieldLabel>\n <FieldControl>\n <Input type=\"email\" placeholder=\"you@example.com\" {...field} />\n </FieldControl>\n <FieldDescription>We will never share your email.</FieldDescription>\n <FieldError>{fieldState.error?.message}</FieldError>\n </Field>\n )}\n />\n\n <FormField\n control={form.control}\n name=\"message\"\n render={({ field, fieldState }) => (\n <Field invalid={!!fieldState.error}>\n <FieldLabel>Message</FieldLabel>\n <FieldControl>\n <Textarea placeholder=\"How can we help?\" rows={4} {...field} />\n </FieldControl>\n <FieldError>{fieldState.error?.message}</FieldError>\n </Field>\n )}\n />\n\n <Button type=\"submit\" variant=\"primary\">\n Send message\n </Button>\n </form>\n </Form>\n );\n}\n\nexport function FormExample() {\n return <ContactFormFields />;\n}\n"
437
+ }
438
+ ],
439
+ "form": [
440
+ {
441
+ "file": "field.example.tsx",
442
+ "importName": "fieldSource",
443
+ "title": "Field",
444
+ "components": [
445
+ "Field",
446
+ "FieldControl",
447
+ "FieldDescription",
448
+ "FieldLabel",
449
+ "Input",
450
+ "Textarea"
451
+ ],
452
+ "source": "import {\n Field,\n FieldControl,\n FieldDescription,\n FieldLabel,\n Input,\n Textarea,\n} from \"@rfdtech/components\";\n\nexport function FieldExample() {\n return (\n <div style={{ display: \"grid\", gap: 16, maxWidth: 420 }}>\n <Field>\n <FieldLabel>Email</FieldLabel>\n <FieldControl>\n <Input type=\"email\" placeholder=\"you@example.com\" />\n </FieldControl>\n <FieldDescription>We will never share your email.</FieldDescription>\n </Field>\n\n <Field>\n <FieldLabel>Message</FieldLabel>\n <FieldControl>\n <Textarea rows={4} placeholder=\"How can we help?\" />\n </FieldControl>\n </Field>\n </div>\n );\n}\n"
453
+ },
454
+ {
455
+ "file": "form-modal.example.tsx",
456
+ "importName": "formModalSource",
457
+ "title": "Form Modal",
458
+ "components": [
459
+ "Button",
460
+ "Dropdown",
461
+ "Field",
462
+ "FieldControl",
463
+ "FieldError",
464
+ "FieldLabel",
465
+ "Form",
466
+ "FormField",
467
+ "Input",
468
+ "Modal",
469
+ "ModalBody",
470
+ "ModalContent",
471
+ "ModalFooter",
472
+ "ModalHeader",
473
+ "ModalOverlay",
474
+ "ModalPortal",
475
+ "ModalTitle"
476
+ ],
477
+ "source": "import { useEffect, useState } from \"react\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\nimport {\n Button,\n Dropdown,\n Field,\n FieldControl,\n FieldError,\n FieldLabel,\n Form,\n FormField,\n Input,\n Modal,\n ModalBody,\n ModalContent,\n ModalFooter,\n ModalHeader,\n ModalOverlay,\n ModalPortal,\n ModalTitle,\n} from \"@rfdtech/components\";\n\nconst createUserSchema = z.object({\n name: z.string().min(1, \"Name is required\"),\n email: z.string().min(1, \"Email is required\").email(\"Enter a valid email address\"),\n roleId: z.string().min(1, \"Select a role\"),\n});\n\ntype CreateUserValues = z.infer<typeof createUserSchema>;\n\ninterface RoleOption {\n value: string;\n label: string;\n}\n\n/** Simulates fetching a roles list from the server. */\nfunction loadRoles(): Promise<RoleOption[]> {\n return new Promise((resolve) => {\n setTimeout(() => {\n resolve([\n { value: \"admin\", label: \"Admin\" },\n { value: \"editor\", label: \"Editor\" },\n { value: \"viewer\", label: \"Viewer\" },\n ]);\n }, 900);\n });\n}\n\n/** Simulates an async create-user API call. Rejects for a \"taken\" email so\n * the failure path (root-level form error) is exercised too. */\nfunction createUser(values: CreateUserValues): Promise<void> {\n return new Promise((resolve, reject) => {\n setTimeout(() => {\n if (values.email === \"taken@example.com\") {\n reject(new Error(\"A user with this email already exists.\"));\n return;\n }\n resolve();\n }, 1200);\n });\n}\n\nfunction CreateUserForm({ onDone }: { onDone: () => void }) {\n const [roles, setRoles] = useState<RoleOption[] | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n loadRoles().then((options) => {\n if (!cancelled) setRoles(options);\n });\n return () => {\n cancelled = true;\n };\n }, []);\n\n const form = useForm<CreateUserValues>({\n resolver: zodResolver(createUserSchema),\n defaultValues: { name: \"\", email: \"\", roleId: \"\" },\n });\n\n const onSubmit = async (values: CreateUserValues) => {\n try {\n await createUser(values);\n onDone();\n } catch (error) {\n form.setError(\"root\", {\n message: error instanceof Error ? error.message : \"Something went wrong. Please try again.\",\n });\n }\n };\n\n const rolesLoading = roles === null;\n\n return (\n <Form {...form}>\n <ModalBody style={{ display: \"grid\", gap: 16 }}>\n {form.formState.errors.root ? (\n <div\n role=\"alert\"\n style={{\n padding: \"10px 12px\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"var(--clet-error-bg, #fef2f2)\",\n color: \"var(--clet-error-text, #b91c1c)\",\n fontSize: 14,\n }}\n >\n {form.formState.errors.root.message}\n </div>\n ) : null}\n\n <FormField\n control={form.control}\n name=\"name\"\n render={({ field, fieldState }) => (\n <Field invalid={!!fieldState.error}>\n <FieldLabel>Full name</FieldLabel>\n <FieldControl>\n <Input placeholder=\"Ama Serwaa\" {...field} />\n </FieldControl>\n <FieldError>{fieldState.error?.message}</FieldError>\n </Field>\n )}\n />\n\n <FormField\n control={form.control}\n name=\"email\"\n render={({ field, fieldState }) => (\n <Field invalid={!!fieldState.error}>\n <FieldLabel>Email</FieldLabel>\n <FieldControl>\n <Input type=\"email\" placeholder=\"ama@clet.edu.gh\" {...field} />\n </FieldControl>\n <FieldError>{fieldState.error?.message}</FieldError>\n </Field>\n )}\n />\n\n <FormField\n control={form.control}\n name=\"roleId\"\n render={({ field, fieldState }) => (\n <Field invalid={!!fieldState.error}>\n <FieldLabel>Role</FieldLabel>\n <FieldControl>\n <Dropdown\n aria-label=\"Role\"\n value={field.value || null}\n onValueChange={(value) => field.onChange(value ?? \"\")}\n options={roles ?? []}\n disabled={rolesLoading}\n placeholder={rolesLoading ? \"Loading roles…\" : \"Select a role\"}\n invalid={!!fieldState.error}\n />\n </FieldControl>\n <FieldError>{fieldState.error?.message}</FieldError>\n </Field>\n )}\n />\n </ModalBody>\n\n <ModalFooter>\n <Button variant=\"ghost\" type=\"button\" onClick={onDone}>\n Cancel\n </Button>\n <Button\n type=\"button\"\n loading={form.formState.isSubmitting}\n loadingLabel=\"Creating…\"\n onClick={() => form.handleSubmit(onSubmit)()}\n >\n Create user\n </Button>\n </ModalFooter>\n </Form>\n );\n}\n\nexport function FormModalExample() {\n const [open, setOpen] = useState(true);\n // Remounts the form (and its async role-loading effect) each time the\n // modal is reopened, mirroring how a real \"create\" modal starts fresh.\n const [formKey, setFormKey] = useState(0);\n\n const openModal = () => {\n setFormKey((k) => k + 1);\n setOpen(true);\n };\n\n return (\n <>\n <Button variant=\"secondary\" onClick={openModal}>\n Add user\n </Button>\n <Modal open={open} onOpenChange={setOpen}>\n <ModalPortal>\n <ModalOverlay />\n <ModalContent showCloseButton size=\"md\">\n <ModalHeader>\n <ModalTitle>Add user</ModalTitle>\n </ModalHeader>\n <CreateUserForm key={formKey} onDone={() => setOpen(false)} />\n </ModalContent>\n </ModalPortal>\n </Modal>\n </>\n );\n}\n"
478
+ }
479
+ ],
480
+ "getting-started": [],
481
+ "hooks": [
482
+ {
483
+ "file": "dialog.example.tsx",
484
+ "importName": "dialogSource",
485
+ "title": "Dialog",
486
+ "components": [
487
+ "Button",
488
+ "Dialog",
489
+ "DialogContent",
490
+ "DialogDescription",
491
+ "DialogOverlay",
492
+ "DialogPortal",
493
+ "DialogTitle",
494
+ "useDialogSearchParam"
495
+ ],
496
+ "source": "import {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n useDialogSearchParam,\n} from \"@rfdtech/components\";\n\nexport function DialogExample() {\n const { open, data, onOpenChange, openWith } = useDialogSearchParam<{\n userId: string;\n }>(\"edit-profile\");\n\n return (\n <>\n <Button\n variant=\"secondary\"\n onClick={() => openWith({ userId: \"42\" })}\n >\n Edit profile\n </Button>\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogPortal>\n <DialogOverlay />\n <DialogContent showCloseButton>\n <DialogTitle>Edit profile</DialogTitle>\n <DialogDescription>\n Update your name and contact details. Changes are saved when you\n submit the form.\n {data?.userId ? (\n <>\n {\" \"}\n Editing user <strong>{data.userId}</strong> (\n <code>?dialog=edit-profile&dialog.userId={data.userId}</code>\n ).\n </>\n ) : null}\n </DialogDescription>\n <div style={{ display: \"grid\", gap: 12 }}>\n <label style={{ display: \"grid\", gap: 6, fontSize: 14 }}>\n Full name\n <input\n type=\"text\"\n defaultValue=\"Alex Johnson\"\n style={{\n height: 40,\n padding: \"0 12px\",\n border: \"1px solid var(--clet-border)\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"var(--clet-bg)\",\n color: \"var(--clet-text)\",\n }}\n />\n </label>\n <label style={{ display: \"grid\", gap: 6, fontSize: 14 }}>\n Email\n <input\n type=\"email\"\n defaultValue=\"alex@example.com\"\n style={{\n height: 40,\n padding: \"0 12px\",\n border: \"1px solid var(--clet-border)\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"var(--clet-bg)\",\n color: \"var(--clet-text)\",\n }}\n />\n </label>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"flex-end\", gap: 8 }}>\n <Button variant=\"ghost\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={() => onOpenChange(false)}>Save changes</Button>\n </div>\n </DialogContent>\n </DialogPortal>\n </Dialog>\n </>\n );\n}\n"
497
+ }
498
+ ],
499
+ "launchpad": [
500
+ {
501
+ "file": "launchpad-loading.example.tsx",
502
+ "importName": "launchpadLoadingSource",
503
+ "title": "Launchpad Loading",
504
+ "components": [
505
+ "Launchpad",
506
+ "RoleSelect"
507
+ ],
508
+ "source": "import { Launchpad, RoleSelect } from \"@rfdtech/components\";\n\nexport function LaunchpadLoadingExample() {\n return (\n <Launchpad apps={[]} loading>\n <RoleSelect\n title=\"View as\"\n roles={[{ id: \"admin\", name: \"Admin\" }]}\n selectedRole=\"admin\"\n onClickRole={() => {}}\n />\n </Launchpad>\n );\n}\n"
509
+ },
510
+ {
511
+ "file": "launchpad.example.tsx",
512
+ "importName": "launchpadSource",
513
+ "title": "Launchpad",
514
+ "components": [
515
+ "Launchpad",
516
+ "LaunchpadIconTile",
517
+ "RoleSelect"
518
+ ],
519
+ "source": "import { useState } from \"react\";\nimport { Launchpad, RoleSelect, LaunchpadIconTile } from \"@rfdtech/components\";\nimport {\n Shield,\n Eye,\n ScrollText,\n BarChart3,\n BadgeCheck,\n Building2,\n ShieldCheck,\n Truck,\n Megaphone,\n CalendarDays,\n Boxes,\n Users,\n Scale,\n Briefcase,\n GraduationCap,\n FolderLock,\n} from \"lucide-react\";\n\nconst apps = [\n {\n id: \"analytics\",\n name: \"Analytics\",\n icon: (\n <LaunchpadIconTile name=\"Analytics\">\n <BarChart3 size={26} strokeWidth={1.75} />\n </LaunchpadIconTile>\n ),\n },\n {\n id: \"accreditation\",\n name: \"Accreditation\",\n icon: (\n <LaunchpadIconTile name=\"Accreditation\">\n <BadgeCheck size={26} strokeWidth={1.75} />\n </LaunchpadIconTile>\n ),\n },\n {\n id: \"facilities\",\n name: \"Facilities\",\n icon: (\n <LaunchpadIconTile name=\"Facilities\">\n <Building2 size={26} strokeWidth={1.75} />\n </LaunchpadIconTile>\n ),\n },\n {\n id: \"compliance\",\n name: \"Compliance\",\n icon: (\n <LaunchpadIconTile name=\"Compliance\">\n <ShieldCheck size={26} strokeWidth={1.75} />\n </LaunchpadIconTile>\n ),\n },\n {\n id: \"fleet-management\",\n name: \"Fleet Management\",\n icon: (\n <LaunchpadIconTile name=\"Fleet Management\">\n <Truck size={26} strokeWidth={1.75} />\n </LaunchpadIconTile>\n ),\n },\n {\n id: \"outreach\",\n name: \"Outreach\",\n icon: (\n <LaunchpadIconTile name=\"Outreach\">\n <Megaphone size={26} strokeWidth={1.75} />\n </LaunchpadIconTile>\n ),\n },\n {\n id: \"scheduling\",\n name: \"Scheduling\",\n icon: (\n <LaunchpadIconTile name=\"Scheduling\">\n <CalendarDays size={26} strokeWidth={1.75} />\n </LaunchpadIconTile>\n ),\n },\n {\n id: \"asset-registry\",\n name: \"Asset Registry\",\n icon: (\n <LaunchpadIconTile name=\"Asset Registry\">\n <Boxes size={26} strokeWidth={1.75} />\n </LaunchpadIconTile>\n ),\n },\n {\n id: \"hr-suite\",\n name: \"HR Suite\",\n icon: (\n <LaunchpadIconTile name=\"HR Suite\">\n <Users size={26} strokeWidth={1.75} />\n </LaunchpadIconTile>\n ),\n },\n {\n id: \"legal-research\",\n name: \"Legal Research\",\n icon: (\n <LaunchpadIconTile name=\"Legal Research\">\n <Scale size={26} strokeWidth={1.75} />\n </LaunchpadIconTile>\n ),\n },\n {\n id: \"case-management\",\n name: \"Case Management\",\n icon: (\n <LaunchpadIconTile name=\"Case Management\">\n <Briefcase size={26} strokeWidth={1.75} />\n </LaunchpadIconTile>\n ),\n },\n {\n id: \"training-academy\",\n name: \"Training Academy\",\n icon: (\n <LaunchpadIconTile name=\"Training Academy\">\n <GraduationCap size={26} strokeWidth={1.75} />\n </LaunchpadIconTile>\n ),\n },\n {\n id: \"document-vault\",\n name: \"Document Vault\",\n icon: (\n <LaunchpadIconTile name=\"Document Vault\">\n <FolderLock size={26} strokeWidth={1.75} />\n </LaunchpadIconTile>\n ),\n },\n];\n\nconst roles = [\n { id: \"admin\", name: \"Admin\", icon: <Shield size={16} strokeWidth={1.5} /> },\n { id: \"reviewer\", name: \"Reviewer\", icon: <Eye size={16} strokeWidth={1.5} /> },\n { id: \"auditor\", name: \"Auditor\", icon: <ScrollText size={16} strokeWidth={1.5} /> },\n];\n\nexport function LaunchpadExample() {\n const [selectedRole, setSelectedRole] = useState(\"admin\");\n\n return (\n <Launchpad apps={apps} onAppSelect={(app) => console.log(\"Selected:\", app.name)}>\n <RoleSelect\n title=\"View as\"\n roles={roles}\n selectedRole={selectedRole}\n onClickRole={(role) => setSelectedRole(role.id)}\n />\n </Launchpad>\n );\n}\n"
520
+ }
521
+ ],
522
+ "metric-card": [
523
+ {
524
+ "file": "metric-card-loading.example.tsx",
525
+ "importName": "metricCardLoadingSource",
526
+ "title": "Metric Card Loading",
527
+ "components": [
528
+ "MetricCard"
529
+ ],
530
+ "source": "import { useState } from \"react\";\nimport { MetricCard } from \"@rfdtech/components\";\nimport { Users, UserCheck, Activity } from \"lucide-react\";\n\nexport function MetricCardLoadingExample() {\n const [loading, setLoading] = useState(true);\n\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 16 }}>\n <button\n type=\"button\"\n className=\"clet-profile-menu__item\"\n style={{ alignSelf: \"flex-start\" }}\n onClick={() => {\n setLoading(true);\n setTimeout(() => setLoading(false), 1500);\n }}\n >\n <span>Simulate loading</span>\n </button>\n\n <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 16 }}>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n loading={loading}\n label=\"Revenue\"\n value=\"$128.4k\"\n icon={<Users size={16} strokeWidth={1.5} />}\n description=\"Total revenue this quarter\"\n trend=\"up\"\n trendValue=\"+12.5%\"\n />\n </div>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n loading={loading}\n label=\"Active users\"\n value=\"2,847\"\n icon={<UserCheck size={16} strokeWidth={1.5} />}\n description=\"Current active users\"\n trend=\"down\"\n trendValue=\"-3.2%\"\n />\n </div>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n loading={loading}\n label=\"Avg response\"\n value=\"1.2s\"\n icon={<Activity size={16} strokeWidth={1.5} />}\n description=\"Average API response time\"\n />\n </div>\n </div>\n </div>\n );\n}\n"
531
+ },
532
+ {
533
+ "file": "metric-card.example.tsx",
534
+ "importName": "metricCardSource",
535
+ "title": "Metric Card",
536
+ "components": [
537
+ "MetricCard"
538
+ ],
539
+ "source": "import { MetricCard } from \"@rfdtech/components\";\n\nexport function MetricCardExample() {\n return (\n <div\n style={{\n background: \"var(--clet-surface-subtle)\",\n padding: 20,\n borderRadius: \"var(--clet-radius-xl)\",\n width: \"100%\",\n }}\n >\n <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 16 }}>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n label=\"Revenue\"\n value=\"$128.4k\"\n description=\"Total revenue this quarter\"\n trend=\"up\"\n trendValue=\"+12.5%\"\n animate\n />\n </div>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n label=\"Active users\"\n value=\"2,847\"\n description=\"Current active users\"\n trend=\"down\"\n trendValue=\"-3.2%\"\n animate\n />\n </div>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n label=\"Avg response\"\n value=\"1.2s\"\n description=\"Average API response time\"\n animate\n />\n </div>\n </div>\n\n <div\n style={{ display: \"flex\", flexWrap: \"wrap\", gap: 16, marginTop: 16 }}\n >\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n variant=\"outline\"\n label=\"Revenue\"\n value=\"$128.4k\"\n description=\"Total revenue this quarter\"\n trend=\"up\"\n trendValue=\"12.5%\"\n />\n </div>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n variant=\"outline\"\n label=\"Active users\"\n value=\"2,847\"\n description=\"Current active users\"\n trend=\"down\"\n trendValue=\"3.2%\"\n />\n </div>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n variant=\"outline\"\n label=\"Avg response\"\n value=\"1.2s\"\n description=\"Average API response time\"\n trend=\"neutral\"\n trendValue=\"0.0%\"\n />\n </div>\n </div>\n </div>\n );\n}\n"
540
+ }
541
+ ],
542
+ "migration-v2": [],
543
+ "modal": [
544
+ {
545
+ "file": "modal.example.tsx",
546
+ "importName": "modalSource",
547
+ "title": "Modal",
548
+ "components": [
549
+ "Button",
550
+ "Modal",
551
+ "ModalBody",
552
+ "ModalContent",
553
+ "ModalFooter",
554
+ "ModalHeader",
555
+ "ModalOverlay",
556
+ "ModalPortal",
557
+ "ModalTitle",
558
+ "useModalSearchParam"
559
+ ],
560
+ "source": "import { useState } from \"react\";\nimport {\n Button,\n Modal,\n ModalBody,\n ModalContent,\n ModalFooter,\n ModalHeader,\n ModalOverlay,\n ModalPortal,\n ModalTitle,\n useModalSearchParam,\n} from \"@rfdtech/components\";\n\nconst reviewItems = [\n \"Update homepage hero copy\",\n \"Publish Q2 enrollment announcement\",\n \"Archive outdated policy PDFs\",\n \"Notify department heads by email\",\n];\n\nexport function ModalExample() {\n const { open, onOpenChange, openWith } = useModalSearchParam(\"review-changes\");\n const [dirty, setDirty] = useState(false);\n\n return (\n <div style={{ display: \"flex\", gap: 8, flexWrap: \"wrap\" }}>\n <Button variant=\"secondary\" onClick={() => { setDirty(false); openWith(); }}>\n Review changes\n </Button>\n <Button variant=\"secondary\" onClick={() => { setDirty(true); openWith(); }}>\n With preventClose\n </Button>\n <Modal open={open} onOpenChange={onOpenChange}>\n <ModalPortal>\n <ModalOverlay />\n <ModalContent\n showCloseButton\n size=\"lg\"\n preventClose={dirty}\n >\n <ModalHeader>\n <ModalTitle>Review changes</ModalTitle>\n </ModalHeader>\n <ModalBody>\n <p style={{ margin: \"0 0 12px\", fontSize: 14, color: \"var(--clet-text-secondary)\" }}>\n Confirm the items below before publishing.\n {dirty && \" Close prevention is active — try clicking the X or overlay.\"}\n </p>\n <ul\n style={{\n margin: 0,\n paddingLeft: 20,\n display: \"grid\",\n gap: 10,\n fontSize: 14,\n lineHeight: 1.5,\n }}\n >\n {reviewItems.map((item) => (\n <li key={item}>{item}</li>\n ))}\n </ul>\n </ModalBody>\n <ModalFooter>\n <Button variant=\"ghost\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={() => onOpenChange(false)}>Publish</Button>\n </ModalFooter>\n </ModalContent>\n </ModalPortal>\n </Modal>\n </div>\n );\n}\n"
561
+ }
562
+ ],
563
+ "network-operator": [
564
+ {
565
+ "file": "network-operator.example.tsx",
566
+ "importName": "networkOperatorSource",
567
+ "title": "Network Operator",
568
+ "components": [
569
+ "NetworkOperator"
570
+ ],
571
+ "source": "import { NetworkOperator } from \"@rfdtech/components\";\n\nexport function NetworkOperatorExample() {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 16, maxWidth: 300 }}>\n <NetworkOperator />\n <NetworkOperator invalid />\n <NetworkOperator disabled />\n </div>\n );\n}\n"
572
+ }
573
+ ],
574
+ "notice": [
575
+ {
576
+ "file": "notice-inline.example.tsx",
577
+ "importName": "noticeInlineSource",
578
+ "title": "Notice Inline",
579
+ "components": [
580
+ "Notice",
581
+ "NoticeVariant"
582
+ ],
583
+ "source": "import { Notice } from \"@rfdtech/components\";\nimport type { NoticeVariant } from \"@rfdtech/components\";\n\nconst dot = (\n <span\n style={{\n display: \"inline-block\",\n width: 8,\n height: 8,\n marginTop: 6,\n borderRadius: \"50%\",\n background: \"currentcolor\",\n }}\n />\n);\n\nconst ROWS: { variant: NoticeVariant; message: string }[] = [\n {\n variant: \"info\",\n message: \"This field is optional if you are applying under the equity provision.\",\n },\n { variant: \"success\", message: \"Document uploaded and virus scan passed.\" },\n { variant: \"warning\", message: \"This section will be locked once you continue.\" },\n {\n variant: \"error\",\n message: \"We need a valid email address. The one entered does not include an @.\",\n },\n];\n\nexport function NoticeInlineExample() {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: \"12px\" }}>\n {ROWS.map((row) => (\n <Notice key={row.variant} variant={row.variant} icon={dot}>\n {row.message}\n </Notice>\n ))}\n </div>\n );\n}\n"
584
+ },
585
+ {
586
+ "file": "notice-variants.example.tsx",
587
+ "importName": "noticeVariantsSource",
588
+ "title": "Notice Variants",
589
+ "components": [
590
+ "Notice"
591
+ ],
592
+ "source": "import { Notice } from \"@rfdtech/components\";\n\nexport function NoticeVariantsExample() {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: \"16px\" }}>\n <Notice\n variant=\"warning\"\n leftBorder\n dashed\n title=\"Your scope for this composer\"\n >\n <ul>\n <li>\n Templates: Admissions Directorate only · Audience: 4 pre-approved\n groups · Classification: Standard only · Channels: Email, Portal.\n </li>\n <li>\n Only what&apos;s listed above appears below — there&apos;s\n nothing hidden or disabled to request access to. Contact CCP\n Comms Admin if you need a new template or group.\n </li>\n </ul>\n </Notice>\n\n <Notice\n variant=\"success\"\n icon={\n <span\n style={{\n display: \"inline-block\",\n width: 8,\n height: 8,\n marginTop: 6,\n borderRadius: \"50%\",\n background: \"currentcolor\",\n }}\n />\n }\n >\n This will send directly. Standard classification within your assigned\n scope meets direct-send policy criteria — no approval routing\n required (SRS-S074-C-01).\n </Notice>\n\n <Notice variant=\"info\" title=\"Info\">\n Neutral, non-blocking information for the current context.\n </Notice>\n\n <Notice variant=\"error\" title=\"Something went wrong\">\n The request could not be completed. Try again in a moment.\n </Notice>\n\n <Notice color=\"#7c3aed\" title=\"Custom accent color\">\n Any CSS color can be passed via the `color` prop when none of the\n built-in variants fit.\n </Notice>\n </div>\n );\n}\n"
593
+ }
594
+ ],
595
+ "otp-input": [
596
+ {
597
+ "file": "otp-input.example.tsx",
598
+ "importName": "otpInputSource",
599
+ "title": "Otp Input",
600
+ "components": [
601
+ "OtpInput"
602
+ ],
603
+ "source": "import { OtpInput } from \"@rfdtech/components\";\n\nexport function OtpInputExample() {\n return (\n <div>\n <OtpInput />\n <p style={{ margin: \"8px 0 0\", fontSize: 13, color: \"var(--clet-text-muted)\" }}>\n Enter 6-digit code\n </p>\n </div>\n );\n}\n"
604
+ },
605
+ {
606
+ "file": "otp-verify-dialog.example.tsx",
607
+ "importName": "otpVerifyDialogSource",
608
+ "title": "Otp Verify Dialog",
609
+ "components": [
610
+ "Button",
611
+ "Dialog",
612
+ "DialogContent",
613
+ "DialogDescription",
614
+ "DialogOverlay",
615
+ "DialogPortal",
616
+ "DialogTitle",
617
+ "OtpInput"
618
+ ],
619
+ "source": "import { useState } from \"react\";\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n OtpInput,\n} from \"@rfdtech/components\";\n\n// Mock \"correct\" code — stands in for a real server-side verification call.\nconst MOCK_VALID_CODE = \"123456\";\n\ntype VerifyStatus = \"idle\" | \"verifying\" | \"error\" | \"success\";\n\nexport function OtpVerifyDialogExample() {\n const [open, setOpen] = useState(false);\n const [code, setCode] = useState(\"\");\n const [status, setStatus] = useState<VerifyStatus>(\"idle\");\n\n const reset = () => {\n setCode(\"\");\n setStatus(\"idle\");\n };\n\n const verify = (value: string) => {\n if (value.length < 6 || status === \"verifying\") return;\n setStatus(\"verifying\");\n // Simulate an async verification request.\n setTimeout(() => {\n if (value === MOCK_VALID_CODE) {\n setStatus(\"success\");\n setTimeout(() => {\n setOpen(false);\n reset();\n }, 900);\n } else {\n setStatus(\"error\");\n }\n }, 1200);\n };\n\n return (\n <>\n <Button\n variant=\"secondary\"\n onClick={() => {\n reset();\n setOpen(true);\n }}\n >\n Verify identity\n </Button>\n\n <Dialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) reset();\n }}\n >\n <DialogPortal>\n <DialogOverlay />\n <DialogContent showCloseButton>\n <DialogTitle>Two-factor verification</DialogTitle>\n <DialogDescription>\n Enter the 6-digit code sent to your registered device to confirm\n this action. (Hint: the mock code is <code>123456</code>.)\n </DialogDescription>\n\n <div style={{ display: \"grid\", gap: 8 }}>\n <OtpInput\n length={6}\n value={code}\n disabled={status === \"verifying\" || status === \"success\"}\n invalid={status === \"error\"}\n onChange={(value) => {\n setCode(value);\n if (status === \"error\") setStatus(\"idle\");\n }}\n onComplete={(value) => verify(value)}\n />\n {status === \"error\" && (\n <p style={{ margin: 0, fontSize: 13, color: \"var(--clet-error-text)\" }}>\n Incorrect code. Please try again.\n </p>\n )}\n {status === \"success\" && (\n <p style={{ margin: 0, fontSize: 13, color: \"var(--clet-success)\" }}>\n Verified successfully.\n </p>\n )}\n </div>\n\n <div style={{ display: \"flex\", justifyContent: \"flex-end\", gap: 8 }}>\n <Button\n variant=\"ghost\"\n onClick={() => setOpen(false)}\n disabled={status === \"verifying\"}\n >\n Cancel\n </Button>\n <Button\n onClick={() => verify(code)}\n disabled={code.length < 6 || status === \"verifying\" || status === \"success\"}\n loading={status === \"verifying\"}\n loadingLabel=\"Verifying...\"\n >\n Verify\n </Button>\n </div>\n </DialogContent>\n </DialogPortal>\n </Dialog>\n </>\n );\n}\n"
620
+ }
621
+ ],
622
+ "phone-number-input": [
623
+ {
624
+ "file": "phone-number-input-field.example.tsx",
625
+ "importName": "phoneNumberInputFieldSource",
626
+ "title": "Phone Number Input Field",
627
+ "components": [
628
+ "Field",
629
+ "FieldControl",
630
+ "FieldDescription",
631
+ "FieldError",
632
+ "FieldLabel",
633
+ "PhoneNumberInput",
634
+ "Tooltip"
635
+ ],
636
+ "source": "import { Info } from \"lucide-react\";\nimport {\n Field,\n FieldControl,\n FieldDescription,\n FieldError,\n FieldLabel,\n PhoneNumberInput,\n Tooltip,\n} from \"@rfdtech/components\";\n\nfunction OptionalLabel() {\n return (\n <span style={{ display: \"inline-flex\", alignItems: \"center\", gap: 6 }}>\n Phone Number <span style={{ fontWeight: 400 }}>(Optional)</span>\n <Tooltip content=\"We'll only use this to reach you about your application.\">\n <Info size={14} style={{ color: \"var(--clet-text-secondary)\" }} />\n </Tooltip>\n </span>\n );\n}\n\nfunction Hint({ children }: { children: string }) {\n return (\n <span style={{ display: \"inline-flex\", alignItems: \"center\", gap: 6 }}>\n <Info size={14} style={{ flexShrink: 0 }} />\n {children}\n </span>\n );\n}\n\nexport function PhoneNumberInputFieldExample() {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: \"20px\", maxWidth: 360 }}>\n <Field>\n <FieldLabel>\n <OptionalLabel />\n </FieldLabel>\n <FieldControl>\n <PhoneNumberInput />\n </FieldControl>\n <FieldDescription>\n <Hint>This is a hint text to help user.</Hint>\n </FieldDescription>\n </Field>\n\n <Field>\n <FieldLabel>\n <OptionalLabel />\n </FieldLabel>\n <FieldControl>\n <PhoneNumberInput value=\"+18020000000\" onChange={() => {}} />\n </FieldControl>\n <FieldDescription>\n <Hint>This is a hint text to help user.</Hint>\n </FieldDescription>\n </Field>\n\n <Field invalid>\n <FieldLabel>\n <OptionalLabel />\n </FieldLabel>\n <FieldControl>\n <PhoneNumberInput invalid value=\"+18020000000\" onChange={() => {}} />\n </FieldControl>\n <FieldError>\n <Hint>This is a hint text to help user.</Hint>\n </FieldError>\n </Field>\n\n <Field>\n <FieldLabel>\n <OptionalLabel />\n </FieldLabel>\n <FieldControl>\n <PhoneNumberInput disabled />\n </FieldControl>\n <FieldDescription>\n <Hint>This is a hint text to help user.</Hint>\n </FieldDescription>\n </Field>\n </div>\n );\n}\n"
637
+ },
638
+ {
639
+ "file": "phone-number-input-form.example.tsx",
640
+ "importName": "phoneNumberInputFormSource",
641
+ "title": "Phone Number Input Form",
642
+ "components": [
643
+ "Button",
644
+ "Dialog",
645
+ "DialogContent",
646
+ "DialogDescription",
647
+ "DialogTitle",
648
+ "Field",
649
+ "FieldControl",
650
+ "FieldError",
651
+ "FieldLabel",
652
+ "Form",
653
+ "FormField",
654
+ "PhoneNumberInput"
655
+ ],
656
+ "source": "import { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useState } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogTitle,\n Field,\n FieldControl,\n FieldError,\n FieldLabel,\n Form,\n FormField,\n PhoneNumberInput,\n} from \"@rfdtech/components\";\n\nconst phoneSchema = z.object({\n phone: z\n .string()\n .min(1, \"Phone number is required\")\n .regex(/^\\+\\d{7,15}$/, \"Please enter a valid phone number\"),\n});\n\ntype PhoneFormValues = z.infer<typeof phoneSchema>;\n\nexport function PhoneNumberInputFormExample() {\n const [dialogOpen, setDialogOpen] = useState(false);\n const [result, setResult] = useState<{\n success: boolean;\n phone: string;\n } | null>(null);\n\n const form = useForm<PhoneFormValues>({\n resolver: zodResolver(phoneSchema),\n defaultValues: { phone: \"\" },\n });\n\n function onSubmit(data: PhoneFormValues) {\n setResult({ success: true, phone: data.phone });\n setDialogOpen(true);\n }\n\n function onInvalid() {\n setResult({ success: false, phone: \"\" });\n setDialogOpen(true);\n }\n\n return (\n <>\n <Form {...form}>\n <form\n onSubmit={form.handleSubmit(onSubmit, onInvalid)}\n style={{ display: \"grid\", gap: 16, maxWidth: 360 }}\n >\n <FormField\n control={form.control}\n name=\"phone\"\n render={({ field, fieldState }) => (\n <Field invalid={!!fieldState.error}>\n <FieldLabel>Phone number</FieldLabel>\n <FieldControl>\n <PhoneNumberInput\n value={field.value}\n onChange={field.onChange}\n name={field.name}\n ref={field.ref}\n />\n </FieldControl>\n <FieldError>{fieldState.error?.message}</FieldError>\n </Field>\n )}\n />\n <Button type=\"submit\" variant=\"primary\">\n Validate phone\n </Button>\n </form>\n </Form>\n\n <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>\n <DialogContent showCloseButton>\n <DialogTitle>\n {result?.success ? \"Valid phone number\" : \"Validation failed\"}\n </DialogTitle>\n <DialogDescription>\n {result?.success\n ? `The phone number ${result.phone} is valid.`\n : \"Please fix the validation errors and try again.\"}\n </DialogDescription>\n </DialogContent>\n </Dialog>\n </>\n );\n}\n"
657
+ },
658
+ {
659
+ "file": "phone-number-input.example.tsx",
660
+ "importName": "phoneNumberInputSource",
661
+ "title": "Phone Number Input",
662
+ "components": [
663
+ "PhoneNumberInput"
664
+ ],
665
+ "source": "import { PhoneNumberInput } from \"@rfdtech/components\";\n\nexport function PhoneNumberInputExample() {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 16, maxWidth: 360 }}>\n <div>\n <p style={{ margin: \"0 0 8px\", fontSize: 14, color: \"var(--clet-text-secondary)\" }}>Default</p>\n <PhoneNumberInput onChange={(v) => console.log(v)} />\n </div>\n <div>\n <p style={{ margin: \"0 0 8px\", fontSize: 14, color: \"var(--clet-text-secondary)\" }}>Invalid</p>\n <PhoneNumberInput invalid />\n </div>\n <div>\n <p style={{ margin: \"0 0 8px\", fontSize: 14, color: \"var(--clet-text-secondary)\" }}>Disabled</p>\n <PhoneNumberInput disabled />\n </div>\n </div>\n );\n}\n"
666
+ }
667
+ ],
668
+ "popover": [
669
+ {
670
+ "file": "popover.example.tsx",
671
+ "importName": "popoverSource",
672
+ "title": "Popover",
673
+ "components": [
674
+ "Button",
675
+ "Popover",
676
+ "PopoverClose",
677
+ "PopoverContent",
678
+ "PopoverTrigger"
679
+ ],
680
+ "source": "import {\n Button,\n Popover,\n PopoverClose,\n PopoverContent,\n PopoverTrigger,\n} from \"@rfdtech/components\";\nimport { Copy, Pencil, Trash2 } from \"lucide-react\";\n\nconst menuItems = [\n { label: \"Edit\", icon: Pencil, destructive: false },\n { label: \"Duplicate\", icon: Copy, destructive: false },\n { label: \"Delete\", icon: Trash2, destructive: true },\n] as const;\n\nexport function PopoverExample() {\n return (\n <Popover>\n <PopoverTrigger asChild>\n <Button variant=\"secondary\">Actions</Button>\n </PopoverTrigger>\n <PopoverContent\n side=\"bottom\"\n align=\"end\"\n sideOffset={4}\n className=\"clet-popover--menu\"\n >\n <div className=\"clet-popover__menu\" role=\"menu\">\n {menuItems.map(({ label, icon: Icon, destructive }) => (\n <PopoverClose asChild key={label}>\n <button\n type=\"button\"\n className={[\n \"clet-popover__menu-item\",\n destructive ? \"clet-popover__menu-item--destructive\" : \"\",\n ]\n .filter(Boolean)\n .join(\" \")}\n role=\"menuitem\"\n >\n <Icon size={16} strokeWidth={2} aria-hidden />\n {label}\n </button>\n </PopoverClose>\n ))}\n </div>\n </PopoverContent>\n </Popover>\n );\n}\n"
681
+ }
682
+ ],
683
+ "popup": [
684
+ {
685
+ "file": "popup-variants.example.tsx",
686
+ "importName": "popupVariantsSource",
687
+ "title": "Popup Variants",
688
+ "components": [
689
+ "Button",
690
+ "Input",
691
+ "Popup",
692
+ "PopupBody",
693
+ "PopupClose",
694
+ "PopupContent",
695
+ "PopupDescription",
696
+ "PopupFooter",
697
+ "PopupHeader",
698
+ "PopupTitle",
699
+ "PopupTrigger"
700
+ ],
701
+ "source": "import {\n Button,\n Input,\n Popup,\n PopupBody,\n PopupClose,\n PopupContent,\n PopupDescription,\n PopupFooter,\n PopupHeader,\n PopupTitle,\n PopupTrigger,\n} from \"@rfdtech/components\";\n\nconst description =\n \"Keep your messages short, but make sure they cover everything you need to say.\";\n\nexport function PopupVariantsExample() {\n return (\n <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: \"12px\" }}>\n <Popup>\n <PopupTrigger asChild>\n <Button variant=\"secondary\">Title + description, row</Button>\n </PopupTrigger>\n <PopupContent side=\"bottom\" align=\"center\" sideOffset={8}>\n <PopupHeader>\n <PopupTitle>Title</PopupTitle>\n <PopupDescription>{description}</PopupDescription>\n </PopupHeader>\n <PopupFooter>\n <PopupClose asChild>\n <Button variant=\"primary\">Button</Button>\n </PopupClose>\n <PopupClose asChild>\n <Button variant=\"outline\">Button</Button>\n </PopupClose>\n </PopupFooter>\n </PopupContent>\n </Popup>\n\n <Popup>\n <PopupTrigger asChild>\n <Button variant=\"secondary\">Title + description, stack</Button>\n </PopupTrigger>\n <PopupContent side=\"bottom\" align=\"center\" sideOffset={8}>\n <PopupHeader>\n <PopupTitle>Title</PopupTitle>\n <PopupDescription>{description}</PopupDescription>\n </PopupHeader>\n <PopupFooter layout=\"stack\">\n <PopupClose asChild>\n <Button variant=\"primary\">Button</Button>\n </PopupClose>\n <PopupClose asChild>\n <Button variant=\"outline\">Button</Button>\n </PopupClose>\n </PopupFooter>\n </PopupContent>\n </Popup>\n\n <Popup>\n <PopupTrigger asChild>\n <Button variant=\"secondary\">\n Title + description + input, stack\n </Button>\n </PopupTrigger>\n <PopupContent side=\"bottom\" align=\"center\" sideOffset={8}>\n <PopupHeader>\n <PopupTitle>Title</PopupTitle>\n <PopupDescription>{description}</PopupDescription>\n </PopupHeader>\n <PopupBody>\n <Input placeholder=\"Placeholder\" />\n </PopupBody>\n <PopupFooter layout=\"stack\">\n <PopupClose asChild>\n <Button variant=\"primary\">Button</Button>\n </PopupClose>\n <PopupClose asChild>\n <Button variant=\"outline\">Button</Button>\n </PopupClose>\n </PopupFooter>\n </PopupContent>\n </Popup>\n\n <Popup>\n <PopupTrigger asChild>\n <Button variant=\"secondary\">Description only, row</Button>\n </PopupTrigger>\n <PopupContent side=\"bottom\" align=\"center\" sideOffset={8}>\n <PopupDescription>{description}</PopupDescription>\n <PopupFooter>\n <PopupClose asChild>\n <Button variant=\"primary\">Button</Button>\n </PopupClose>\n <PopupClose asChild>\n <Button variant=\"outline\">Button</Button>\n </PopupClose>\n </PopupFooter>\n </PopupContent>\n </Popup>\n\n <Popup>\n <PopupTrigger asChild>\n <Button variant=\"secondary\">Description only, stack</Button>\n </PopupTrigger>\n <PopupContent side=\"bottom\" align=\"center\" sideOffset={8}>\n <PopupDescription>{description}</PopupDescription>\n <PopupFooter layout=\"stack\">\n <PopupClose asChild>\n <Button variant=\"primary\">Button</Button>\n </PopupClose>\n <PopupClose asChild>\n <Button variant=\"outline\">Button</Button>\n </PopupClose>\n </PopupFooter>\n </PopupContent>\n </Popup>\n\n <Popup>\n <PopupTrigger asChild>\n <Button variant=\"secondary\">Title only, single button</Button>\n </PopupTrigger>\n <PopupContent side=\"bottom\" align=\"center\" sideOffset={8}>\n <PopupHeader>\n <PopupTitle>Title</PopupTitle>\n </PopupHeader>\n <PopupFooter>\n <PopupClose asChild>\n <Button variant=\"primary\">Button</Button>\n </PopupClose>\n </PopupFooter>\n </PopupContent>\n </Popup>\n\n <Popup>\n <PopupTrigger asChild>\n <Button variant=\"secondary\">Title only, stack</Button>\n </PopupTrigger>\n <PopupContent side=\"bottom\" align=\"center\" sideOffset={8}>\n <PopupHeader>\n <PopupTitle>Title</PopupTitle>\n </PopupHeader>\n <PopupFooter layout=\"stack\">\n <PopupClose asChild>\n <Button variant=\"primary\">Button</Button>\n </PopupClose>\n <PopupClose asChild>\n <Button variant=\"outline\">Button</Button>\n </PopupClose>\n </PopupFooter>\n </PopupContent>\n </Popup>\n </div>\n );\n}\n"
702
+ },
703
+ {
704
+ "file": "popup.example.tsx",
705
+ "importName": "popupSource",
706
+ "title": "Popup",
707
+ "components": [
708
+ "Button",
709
+ "Input",
710
+ "Popup",
711
+ "PopupBody",
712
+ "PopupClose",
713
+ "PopupContent",
714
+ "PopupDescription",
715
+ "PopupFooter",
716
+ "PopupHeader",
717
+ "PopupTitle",
718
+ "PopupTrigger"
719
+ ],
720
+ "source": "import {\n Button,\n Input,\n Popup,\n PopupBody,\n PopupClose,\n PopupContent,\n PopupDescription,\n PopupFooter,\n PopupHeader,\n PopupTitle,\n PopupTrigger,\n} from \"@rfdtech/components\";\n\nexport function PopupExample() {\n return (\n <Popup>\n <PopupTrigger asChild>\n <Button variant=\"secondary\">Send message</Button>\n </PopupTrigger>\n <PopupContent side=\"bottom\" align=\"center\" sideOffset={8}>\n <PopupHeader>\n <PopupTitle>Title</PopupTitle>\n <PopupDescription>\n Keep your messages short, but make sure they cover everything you\n need to say.\n </PopupDescription>\n </PopupHeader>\n <PopupBody>\n <Input placeholder=\"Placeholder\" />\n </PopupBody>\n <PopupFooter>\n <PopupClose asChild>\n <Button variant=\"primary\">Button</Button>\n </PopupClose>\n <PopupClose asChild>\n <Button variant=\"outline\">Button</Button>\n </PopupClose>\n </PopupFooter>\n </PopupContent>\n </Popup>\n );\n}\n"
721
+ }
722
+ ],
723
+ "profile-popover": [
724
+ {
725
+ "file": "profile-popover-loading.example.tsx",
726
+ "importName": "profilePopoverLoadingSource",
727
+ "title": "Profile Popover Loading",
728
+ "components": [
729
+ "ProfilePopover",
730
+ "RoleSelect"
731
+ ],
732
+ "source": "import { useState } from \"react\";\nimport { ProfilePopover, RoleSelect } from \"@rfdtech/components\";\nimport { Shield, Eye, ScrollText } from \"lucide-react\";\n\nconst roles = [\n { id: \"admin\", name: \"Admin\", icon: <Shield size={16} strokeWidth={1.5} /> },\n { id: \"reviewer\", name: \"Reviewer\", icon: <Eye size={16} strokeWidth={1.5} /> },\n { id: \"auditor\", name: \"Auditor\", icon: <ScrollText size={16} strokeWidth={1.5} /> },\n];\n\nexport function ProfilePopoverLoadingExample() {\n const [loading, setLoading] = useState(true);\n const [selectedRole, setSelectedRole] = useState(\"admin\");\n\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 16 }}>\n <button\n type=\"button\"\n className=\"clet-profile-menu__item\"\n style={{ alignSelf: \"flex-start\" }}\n onClick={() => {\n setLoading(true);\n setTimeout(() => setLoading(false), 1500);\n }}\n >\n <span>Simulate loading</span>\n </button>\n\n <ProfilePopover\n fullName=\"Yaw Boateng\"\n email=\"y.boateng@clet.gov.gh\"\n loading={loading}\n loadingLabel=\"Loading profile...\"\n onSignOut={() => console.log(\"Sign out\")}\n >\n <RoleSelect\n title=\"View as\"\n roles={roles}\n selectedRole={selectedRole}\n onClickRole={(role) => setSelectedRole(role.id)}\n />\n </ProfilePopover>\n </div>\n );\n}\n"
733
+ },
734
+ {
735
+ "file": "profile-popover-user-trigger.example.tsx",
736
+ "importName": "profilePopoverUserTriggerSource",
737
+ "title": "Profile Popover User Trigger",
738
+ "components": [
739
+ "ProfilePopover",
740
+ "RoleSelect"
741
+ ],
742
+ "source": "import { useState } from \"react\";\nimport { ProfilePopover, RoleSelect } from \"@rfdtech/components\";\nimport { Shield, Eye, ScrollText, User, Settings, HelpCircle } from \"lucide-react\";\n\nconst user = {\n name: \"Kwame Asante\",\n role: \"Admin\",\n initials: \"KA\",\n email: \"kwame@clet.edu.gh\",\n};\n\nconst roles = [\n { id: \"admin\", name: \"Admin\", icon: <Shield size={16} strokeWidth={1.5} /> },\n { id: \"reviewer\", name: \"Reviewer\", icon: <Eye size={16} strokeWidth={1.5} /> },\n { id: \"auditor\", name: \"Auditor\", icon: <ScrollText size={16} strokeWidth={1.5} /> },\n];\n\nexport function ProfilePopoverUserTriggerExample() {\n const [variant, setVariant] = useState<\"full\" | \"avatar\">(\"full\");\n const [selectedRole, setSelectedRole] = useState(\"admin\");\n\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 16 }}>\n <button\n type=\"button\"\n className=\"clet-profile-menu__item\"\n style={{ alignSelf: \"flex-start\" }}\n onClick={() => setVariant((v) => (v === \"full\" ? \"avatar\" : \"full\"))}\n >\n <span>Switch trigger to {variant === \"full\" ? \"avatar\" : \"full\"}</span>\n </button>\n\n <div\n style={{\n display: \"inline-flex\",\n padding: 8,\n borderRadius: \"var(--clet-radius-2xl)\",\n background: \"var(--clet-surface-subtle)\",\n }}\n >\n <ProfilePopover\n user={user}\n variant={variant}\n items={[\n {\n icon: <User size={20} strokeWidth={1.5} aria-hidden />,\n label: \"My Profile\",\n onClick: () => console.log(\"My Profile\"),\n },\n {\n icon: <Settings size={20} strokeWidth={1.5} aria-hidden />,\n label: \"Account Settings\",\n onClick: () => console.log(\"Account Settings\"),\n },\n {\n icon: <HelpCircle size={20} strokeWidth={1.5} aria-hidden />,\n label: \"Help & Support\",\n onClick: () => console.log(\"Help & Support\"),\n },\n ]}\n onSignOut={() => console.log(\"Sign out\")}\n >\n <RoleSelect\n title=\"View as\"\n roles={roles}\n selectedRole={selectedRole}\n onClickRole={(role) => setSelectedRole(role.id)}\n />\n </ProfilePopover>\n </div>\n </div>\n );\n}\n"
743
+ },
744
+ {
745
+ "file": "profile-popover.example.tsx",
746
+ "importName": "profilePopoverSource",
747
+ "title": "Profile Popover",
748
+ "components": [
749
+ "ProfilePopover",
750
+ "RoleSelect"
751
+ ],
752
+ "source": "import { useState } from \"react\";\nimport { ProfilePopover, RoleSelect } from \"@rfdtech/components\";\nimport { User, Settings, HelpCircle, Shield, Eye, ScrollText } from \"lucide-react\";\n\nconst roles = [\n { id: \"admin\", name: \"Admin\", icon: <Shield size={16} strokeWidth={1.5} /> },\n { id: \"reviewer\", name: \"Reviewer\", icon: <Eye size={16} strokeWidth={1.5} /> },\n { id: \"auditor\", name: \"Auditor\", icon: <ScrollText size={16} strokeWidth={1.5} /> },\n];\n\nexport function ProfilePopoverExample() {\n const [selectedRole, setSelectedRole] = useState(\"admin\");\n\n return (\n <ProfilePopover\n fullName=\"Yaw Boateng\"\n email=\"y.boateng@clet.gov.gh\"\n items={[\n {\n icon: <User size={20} strokeWidth={1.5} />,\n label: \"My Profile\",\n onClick: () => console.log(\"My Profile\"),\n },\n {\n icon: <Settings size={20} strokeWidth={1.5} />,\n label: \"Account Settings\",\n onClick: () => console.log(\"Account Settings\"),\n },\n {\n icon: <HelpCircle size={20} strokeWidth={1.5} />,\n label: \"Help & Support\",\n onClick: () => console.log(\"Help & Support\"),\n },\n ]}\n onSignOut={() => console.log(\"Sign out\")}\n >\n <RoleSelect\n title=\"View as\"\n roles={roles}\n selectedRole={selectedRole}\n onClickRole={(role) => setSelectedRole(role.id)}\n />\n </ProfilePopover>\n );\n}\n"
753
+ }
754
+ ],
755
+ "progress-bar": [
756
+ {
757
+ "file": "progress-bar.example.tsx",
758
+ "importName": "progressBarSource",
759
+ "title": "Progress Bar",
760
+ "components": [
761
+ "ProgressBar"
762
+ ],
763
+ "source": "import { ProgressBar } from \"@rfdtech/components\";\n\nexport function ProgressBarExample() {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: \"16px\", width: \"100%\" }}>\n <ProgressBar value={60} label=\"Upload progress\" showValue />\n <ProgressBar value={100} variant=\"success\" showValue />\n <ProgressBar value={45} variant=\"warning\" />\n <ProgressBar value={20} variant=\"error\" size=\"md\" />\n <ProgressBar indeterminate label=\"Loading\" size=\"md\" />\n </div>\n );\n}\n"
764
+ }
765
+ ],
766
+ "radio-group": [
767
+ {
768
+ "file": "radio-decision-form.example.tsx",
769
+ "importName": "radioDecisionFormSource",
770
+ "title": "Radio Decision Form",
771
+ "components": [
772
+ "Button",
773
+ "Field",
774
+ "FieldControl",
775
+ "FieldError",
776
+ "FieldLabel",
777
+ "Form",
778
+ "FormField",
779
+ "Radio",
780
+ "RadioGroup",
781
+ "Textarea"
782
+ ],
783
+ "source": "import { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\nimport {\n Button,\n Field,\n FieldControl,\n FieldError,\n FieldLabel,\n Form,\n FormField,\n Radio,\n RadioGroup,\n Textarea,\n} from \"@rfdtech/components\";\n\nconst decisionSchema = z\n .object({\n verdict: z.enum([\"approve\", \"reject\", \"needs_changes\"]),\n reason: z.string().optional(),\n })\n .superRefine((data, ctx) => {\n // `reason` is only required when the verdict isn't a clean approval.\n if (data.verdict === \"approve\") return;\n const trimmed = (data.reason ?? \"\").trim();\n if (trimmed.length < 10) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"Please provide a reason of at least 10 characters.\",\n path: [\"reason\"],\n });\n }\n });\n\ntype DecisionFormValues = z.infer<typeof decisionSchema>;\n\nfunction DecisionFormFields() {\n const form = useForm<DecisionFormValues>({\n resolver: zodResolver(decisionSchema),\n defaultValues: {\n verdict: \"approve\",\n reason: \"\",\n },\n });\n\n const verdict = form.watch(\"verdict\");\n\n return (\n <Form {...form}>\n <form\n onSubmit={form.handleSubmit(() => {})}\n style={{ display: \"grid\", gap: 16, maxWidth: 480 }}\n >\n <FormField\n control={form.control}\n name=\"verdict\"\n render={({ field }) => (\n <Field>\n <FieldLabel>Recommendation</FieldLabel>\n <FieldControl>\n <RadioGroup\n variant=\"card\"\n value={field.value}\n onValueChange={field.onChange}\n >\n <Radio\n value=\"approve\"\n label=\"Approve\"\n description=\"The submission meets all requirements as-is.\"\n />\n <Radio\n value=\"needs_changes\"\n label=\"Needs changes\"\n description=\"Minor issues must be addressed before approval.\"\n />\n <Radio\n value=\"reject\"\n label=\"Reject\"\n description=\"The submission does not meet requirements.\"\n />\n </RadioGroup>\n </FieldControl>\n </Field>\n )}\n />\n\n {verdict !== \"approve\" && (\n <FormField\n control={form.control}\n name=\"reason\"\n render={({ field, fieldState }) => (\n <Field invalid={!!fieldState.error}>\n <FieldLabel>Reason</FieldLabel>\n <FieldControl>\n <Textarea\n placeholder=\"Explain the reasoning behind this decision...\"\n rows={4}\n {...field}\n />\n </FieldControl>\n <FieldError>{fieldState.error?.message}</FieldError>\n </Field>\n )}\n />\n )}\n\n <Button type=\"submit\" variant=\"primary\">\n Submit recommendation\n </Button>\n </form>\n </Form>\n );\n}\n\nexport function RadioDecisionFormExample() {\n return <DecisionFormFields />;\n}\n"
784
+ },
785
+ {
786
+ "file": "radio-group-card.example.tsx",
787
+ "importName": "radioGroupCardSource",
788
+ "title": "Radio Group Card",
789
+ "components": [
790
+ "Radio",
791
+ "RadioGroup"
792
+ ],
793
+ "source": "import { useState } from \"react\";\nimport { Radio, RadioGroup } from \"@rfdtech/components\";\n\nexport function RadioGroupCardExample() {\n const [plan, setPlan] = useState(\"starter\");\n\n return (\n <RadioGroup variant=\"card\" value={plan} onValueChange={setPlan}>\n <Radio\n value=\"starter\"\n label=\"Starter\"\n description=\"For individuals getting started.\"\n />\n <Radio\n value=\"team\"\n label=\"Team\"\n description=\"Collaborate with up to 10 members.\"\n />\n <Radio\n value=\"enterprise\"\n label=\"Enterprise\"\n description=\"Advanced controls and dedicated support.\"\n />\n </RadioGroup>\n );\n}\n"
794
+ },
795
+ {
796
+ "file": "radio-group.example.tsx",
797
+ "importName": "radioGroupSource",
798
+ "title": "Radio Group",
799
+ "components": [
800
+ "Radio",
801
+ "RadioGroup"
802
+ ],
803
+ "source": "import { useState } from \"react\";\nimport { Radio, RadioGroup } from \"@rfdtech/components\";\n\nexport function RadioGroupExample() {\n const [method, setMethod] = useState(\"email\");\n\n return (\n <RadioGroup value={method} onValueChange={setMethod}>\n <Radio value=\"email\" label=\"Email\" />\n <Radio value=\"sms\" label=\"SMS\" />\n <Radio value=\"phone\" label=\"Phone\" disabled />\n </RadioGroup>\n );\n}\n"
804
+ }
805
+ ],
806
+ "role-select": [
807
+ {
808
+ "file": "role-select.example.tsx",
809
+ "importName": "roleSelectSource",
810
+ "title": "Role Select",
811
+ "components": [
812
+ "RoleSelect"
813
+ ],
814
+ "source": "import { RoleSelect } from \"@rfdtech/components\";\nimport { Eye, ScrollText, Shield } from \"lucide-react\";\nimport { useState } from \"react\";\n\nconst roles = [\n { id: \"admin\", name: \"Admin\", icon: <Shield size={16} /> },\n { id: \"reviewer\", name: \"Reviewer\", icon: <Eye size={16} /> },\n { id: \"auditor\", name: \"Auditor\", icon: <ScrollText size={16} />, disabled: true },\n];\n\nexport function RoleSelectExample() {\n const [selectedRole, setSelectedRole] = useState(\"admin\");\n\n return (\n <RoleSelect\n title=\"View as\"\n roles={roles}\n selectedRole={selectedRole}\n onClickRole={(role) => setSelectedRole(role.id)}\n />\n );\n}\n"
815
+ }
816
+ ],
817
+ "router-adapter": [],
818
+ "section-header": [
819
+ {
820
+ "file": "section-header.example.tsx",
821
+ "importName": "sectionHeaderSource",
822
+ "title": "Section Header",
823
+ "components": [
824
+ "Button",
825
+ "SectionActions",
826
+ "SectionDescription",
827
+ "SectionHeader",
828
+ "SectionTitle"
829
+ ],
830
+ "source": "import {\n Button,\n SectionActions,\n SectionDescription,\n SectionHeader,\n SectionTitle,\n} from \"@rfdtech/components\";\nimport { Plus } from \"lucide-react\";\n\nexport function SectionHeaderExample() {\n return (\n <SectionHeader>\n <SectionTitle>Users</SectionTitle>\n <SectionDescription>\n Manage who has access to this workspace and what they can do.\n </SectionDescription>\n <SectionActions>\n <Button variant=\"outline\" size=\"md\">\n Export\n </Button>\n <Button variant=\"primary\" size=\"md\">\n <Plus size={14} strokeWidth={1.5} />\n Add user\n </Button>\n </SectionActions>\n </SectionHeader>\n );\n}\n"
831
+ }
832
+ ],
833
+ "sheet": [
834
+ {
835
+ "file": "sheet.example.tsx",
836
+ "importName": "sheetSource",
837
+ "title": "Sheet",
838
+ "components": [
839
+ "Button",
840
+ "Checkbox",
841
+ "Sheet",
842
+ "SheetBody",
843
+ "SheetContent",
844
+ "SheetDescription",
845
+ "SheetFooter",
846
+ "SheetHeader",
847
+ "SheetOverlay",
848
+ "SheetPortal",
849
+ "SheetTitle",
850
+ "SheetTrigger"
851
+ ],
852
+ "source": "import { useState } from \"react\";\nimport {\n Button,\n Checkbox,\n Sheet,\n SheetBody,\n SheetContent,\n SheetDescription,\n SheetFooter,\n SheetHeader,\n SheetOverlay,\n SheetPortal,\n SheetTitle,\n SheetTrigger,\n} from \"@rfdtech/components\";\n\nconst filterOptions = [\n { id: \"published\", label: \"Published only\" },\n { id: \"drafts\", label: \"Include drafts\" },\n { id: \"archived\", label: \"Include archived\" },\n { id: \"assigned\", label: \"Assigned to me\" },\n { id: \"mentions\", label: \"Mentions me\" },\n { id: \"recent\", label: \"Updated in last 7 days\" },\n];\n\nexport function SheetExample() {\n const [open, setOpen] = useState(false);\n const [filters, setFilters] = useState<Record<string, boolean>>({\n published: true,\n drafts: false,\n archived: false,\n assigned: true,\n mentions: false,\n recent: true,\n });\n\n return (\n <Sheet open={open} onOpenChange={setOpen}>\n <SheetTrigger asChild>\n <Button variant=\"secondary\">Open filters</Button>\n </SheetTrigger>\n <SheetPortal>\n <SheetOverlay />\n <SheetContent side=\"right\" showCloseButton>\n <SheetHeader>\n <SheetTitle>Filters</SheetTitle>\n <SheetDescription>\n Refine the list below. Changes apply when you click Apply.\n </SheetDescription>\n </SheetHeader>\n <SheetBody>\n <div style={{ display: \"grid\", gap: 12 }}>\n {filterOptions.map(({ id, label }) => (\n <Checkbox\n key={id}\n label={label}\n checked={filters[id]}\n onCheckedChange={(checked) =>\n setFilters((current) => ({ ...current, [id]: checked }))\n }\n />\n ))}\n </div>\n </SheetBody>\n <SheetFooter>\n <Button\n variant=\"ghost\"\n onClick={() => {\n setFilters({\n published: true,\n drafts: false,\n archived: false,\n assigned: false,\n mentions: false,\n recent: false,\n });\n }}\n >\n Reset\n </Button>\n <Button onClick={() => setOpen(false)}>Apply</Button>\n </SheetFooter>\n </SheetContent>\n </SheetPortal>\n </Sheet>\n );\n}\n"
853
+ }
854
+ ],
855
+ "sidebar": [
856
+ {
857
+ "file": "sidebar-loading.example.tsx",
858
+ "importName": "sidebarLoadingSource",
859
+ "title": "Sidebar Loading",
860
+ "components": [
861
+ "Sidebar",
862
+ "SidebarBadge",
863
+ "SidebarContent",
864
+ "SidebarGroup",
865
+ "SidebarHeader",
866
+ "SidebarItem",
867
+ "SidebarLink",
868
+ "SidebarNav",
869
+ "SidebarProvider"
870
+ ],
871
+ "source": "import { useState } from \"react\";\nimport { Bell, LayoutGrid, Shield, Users } from \"lucide-react\";\nimport {\n Sidebar,\n SidebarBadge,\n SidebarContent,\n SidebarGroup,\n SidebarHeader,\n SidebarItem,\n SidebarLink,\n SidebarNav,\n SidebarProvider,\n} from \"@rfdtech/components\";\n\nconst links = [\n { id: \"dashboard\", label: \"Dashboard\", icon: LayoutGrid, badge: undefined },\n { id: \"users\", label: \"Users\", icon: Users, badge: undefined },\n { id: \"roles\", label: \"Roles & Permissions\", icon: Shield, badge: undefined },\n {\n id: \"notifications\",\n label: \"Notification Templates\",\n icon: Bell,\n badge: \"New\",\n },\n] as const;\n\nexport function SidebarLoadingExample() {\n const [loading, setLoading] = useState(true);\n const [active, setActive] = useState<string>(\"users\");\n\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 16 }}>\n <button\n type=\"button\"\n className=\"clet-profile-menu__item\"\n style={{ alignSelf: \"flex-start\" }}\n onClick={() => {\n setLoading(true);\n setTimeout(() => setLoading(false), 1500);\n }}\n >\n <span>Simulate loading</span>\n </button>\n\n <SidebarProvider>\n <div style={{ maxWidth: 280 }}>\n <Sidebar>\n <SidebarHeader>\n <span className=\"clet-sidebar__header-title\">Navigation</span>\n </SidebarHeader>\n <SidebarContent>\n <SidebarNav aria-label=\"Main navigation\">\n <SidebarGroup>\n {loading\n ? Array.from({ length: 4 }).map((_, i) => (\n <SidebarItem key={i}>\n <SidebarLink loading icon={<span />}>\n Loading\n </SidebarLink>\n </SidebarItem>\n ))\n : links.map(({ id, label, icon: Icon, badge }) => (\n <SidebarItem key={id}>\n <SidebarLink\n icon={<Icon size={20} strokeWidth={1.75} />}\n active={active === id}\n onClick={() => setActive(id)}\n >\n {label}\n {badge ? <SidebarBadge>{badge}</SidebarBadge> : null}\n </SidebarLink>\n </SidebarItem>\n ))}\n </SidebarGroup>\n </SidebarNav>\n </SidebarContent>\n </Sidebar>\n </div>\n </SidebarProvider>\n </div>\n );\n}\n"
872
+ },
873
+ {
874
+ "file": "sidebar-plain.example.tsx",
875
+ "importName": "sidebarPlainSource",
876
+ "title": "Sidebar Plain",
877
+ "components": [
878
+ "Sidebar",
879
+ "SidebarContent",
880
+ "SidebarGroup",
881
+ "SidebarGroupLabel",
882
+ "SidebarItem",
883
+ "SidebarLink",
884
+ "SidebarNav",
885
+ "SidebarProvider"
886
+ ],
887
+ "source": "import { useState } from \"react\";\nimport { LayoutGrid, Shield, Users } from \"lucide-react\";\nimport {\n Sidebar,\n SidebarContent,\n SidebarGroup,\n SidebarGroupLabel,\n SidebarItem,\n SidebarLink,\n SidebarNav,\n SidebarProvider,\n} from \"@rfdtech/components\";\n\nconst links = [\n { id: \"dashboard\", label: \"Dashboard\", icon: LayoutGrid },\n { id: \"users\", label: \"Users\", icon: Users },\n { id: \"roles\", label: \"Roles & Permissions\", icon: Shield },\n] as const;\n\nexport function SidebarPlainExample() {\n const [active, setActive] = useState<string>(\"users\");\n\n return (\n <SidebarProvider>\n <div\n style={{\n display: \"flex\",\n height: 280,\n borderRadius: \"var(--clet-radius-base)\",\n overflow: \"hidden\",\n background: \"var(--clet-bg)\",\n }}\n >\n <Sidebar variant=\"plain\">\n <SidebarContent>\n <SidebarNav>\n <SidebarGroup>\n <SidebarGroupLabel>Navigation</SidebarGroupLabel>\n {links.map(({ id, label, icon: Icon }) => (\n <SidebarItem key={id}>\n <SidebarLink\n icon={<Icon size={18} strokeWidth={1.5} />}\n active={active === id}\n onClick={() => setActive(id)}\n >\n {label}\n </SidebarLink>\n </SidebarItem>\n ))}\n </SidebarGroup>\n </SidebarNav>\n </SidebarContent>\n </Sidebar>\n <div\n style={{\n flex: 1,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n color: \"var(--clet-text-muted)\",\n fontSize: 14,\n padding: 16,\n textAlign: \"center\",\n }}\n >\n Page content — plain sidebar has a transparent background and a\n right border instead of the card panel surface\n </div>\n </div>\n </SidebarProvider>\n );\n}\n"
888
+ },
889
+ {
890
+ "file": "sidebar.example.tsx",
891
+ "importName": "sidebarSource",
892
+ "title": "Sidebar",
893
+ "components": [
894
+ "Sidebar",
895
+ "SidebarBadge",
896
+ "SidebarCollapse",
897
+ "SidebarContent",
898
+ "SidebarGroup",
899
+ "SidebarGroupLabel",
900
+ "SidebarHeader",
901
+ "SidebarItem",
902
+ "SidebarLink",
903
+ "SidebarNav",
904
+ "SidebarOverlay",
905
+ "SidebarProvider",
906
+ "SidebarTrigger"
907
+ ],
908
+ "source": "import { useState } from \"react\";\nimport {\n Bell,\n GitBranch,\n LayoutGrid,\n Monitor,\n Plug,\n ScrollText,\n Shield,\n Users,\n Workflow,\n} from \"lucide-react\";\nimport {\n Sidebar,\n SidebarBadge,\n SidebarCollapse,\n SidebarContent,\n SidebarGroup,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarItem,\n SidebarLink,\n SidebarNav,\n SidebarOverlay,\n SidebarProvider,\n SidebarTrigger,\n} from \"@rfdtech/components\";\n\nconst mainLinks = [\n { id: \"dashboard\", label: \"Dashboard\", icon: LayoutGrid, badge: undefined },\n { id: \"users\", label: \"Users\", icon: Users, badge: undefined },\n { id: \"roles\", label: \"Roles & Permissions\", icon: Shield, badge: undefined },\n {\n id: \"notifications\",\n label: \"Notification Templates\",\n icon: Bell,\n badge: \"New\",\n },\n { id: \"workflow\", label: \"Workflow Parameters\", icon: GitBranch, badge: undefined },\n { id: \"integrations\", label: \"Integrations\", icon: Plug, badge: undefined },\n] as const;\n\nconst cbtLinks = [\n { id: \"secure-browser\", label: \"Secure Browser Policy\", icon: ScrollText, badge: undefined },\n { id: \"exam-monitoring\", label: \"Exam Monitoring Settings\", icon: Monitor, badge: undefined },\n] as const;\n\nconst monitoringLinks = [\n { id: \"audit-logs\", label: \"Audit Logs\", icon: ScrollText, badge: \"12\" },\n { id: \"system-health\", label: \"System Health\", icon: Workflow, badge: undefined },\n] as const;\n\nexport function SidebarExample() {\n const [active, setActive] = useState<string>(\"users\");\n\n return (\n <SidebarProvider>\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"minmax(0, 280px) minmax(0, 1fr)\",\n gap: 24,\n alignItems: \"start\",\n maxWidth: \"100%\",\n }}\n >\n <div style={{ minWidth: 0 }}>\n <SidebarTrigger>Open menu</SidebarTrigger>\n <SidebarOverlay />\n <Sidebar>\n <SidebarHeader>\n <div className=\"clet-sidebar__header-brand\">\n <span\n aria-hidden=\"true\"\n style={{\n display: \"inline-flex\",\n flexShrink: 0,\n alignItems: \"center\",\n justifyContent: \"center\",\n width: 32,\n height: 32,\n borderRadius: \"50%\",\n background: \"var(--clet-primary-light)\",\n color: \"var(--clet-primary)\",\n fontSize: 11,\n fontWeight: 700,\n }}\n >\n CLET\n </span>\n <span className=\"clet-sidebar__header-title\">NBES</span>\n </div>\n <SidebarCollapse />\n </SidebarHeader>\n <SidebarContent>\n <SidebarNav aria-label=\"Main navigation\">\n <SidebarGroup>\n {mainLinks.map(({ id, label, icon: Icon, badge }) => (\n <SidebarItem key={id}>\n <SidebarLink\n icon={<Icon size={20} strokeWidth={1.75} />}\n active={active === id}\n onClick={() => setActive(id)}\n >\n {label}\n {badge ? <SidebarBadge>{badge}</SidebarBadge> : null}\n </SidebarLink>\n </SidebarItem>\n ))}\n </SidebarGroup>\n <SidebarGroup>\n <SidebarGroupLabel>CBT Engine</SidebarGroupLabel>\n {cbtLinks.map(({ id, label, icon: Icon }) => (\n <SidebarItem key={id}>\n <SidebarLink\n icon={<Icon size={20} strokeWidth={1.75} />}\n active={active === id}\n onClick={() => setActive(id)}\n >\n {label}\n </SidebarLink>\n </SidebarItem>\n ))}\n </SidebarGroup>\n <SidebarGroup collapsible defaultExpanded={false}>\n <SidebarGroupLabel>Monitoring</SidebarGroupLabel>\n {monitoringLinks.map(({ id, label, icon: Icon, badge }) => (\n <SidebarItem key={id}>\n <SidebarLink\n icon={<Icon size={20} strokeWidth={1.75} />}\n active={active === id}\n onClick={() => setActive(id)}\n >\n {label}\n {badge ? <SidebarBadge>{badge}</SidebarBadge> : null}\n </SidebarLink>\n </SidebarItem>\n ))}\n </SidebarGroup>\n </SidebarNav>\n </SidebarContent>\n </Sidebar>\n </div>\n <div\n style={{\n minWidth: 0,\n padding: 16,\n border: \"1px solid var(--clet-border)\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"var(--clet-bg)\",\n }}\n >\n <p style={{ margin: 0, fontSize: 14, color: \"var(--clet-text-secondary)\" }}>\n Main content area. On desktop, use the collapse control to switch to\n an icon-only rail. On viewports 768px and below, use the menu button\n to open the offcanvas sidebar.\n </p>\n </div>\n </div>\n </SidebarProvider>\n );\n}\n"
909
+ }
910
+ ],
911
+ "sortable": [
912
+ {
913
+ "file": "sortable.example.tsx",
914
+ "importName": "sortableSource",
915
+ "title": "Sortable",
916
+ "components": [
917
+ "Sortable",
918
+ "SortableHandle",
919
+ "SortableItem",
920
+ "SortableList"
921
+ ],
922
+ "source": "import { useState } from \"react\";\nimport {\n Sortable,\n SortableHandle,\n SortableItem,\n SortableList,\n} from \"@rfdtech/components\";\n\nconst labels: Record<string, string> = {\n alpha: \"Review submissions\",\n beta: \"Approve grades\",\n gamma: \"Publish results\",\n delta: \"Send notifications\",\n};\n\nexport function SortableExample() {\n const [items, setItems] = useState([\"alpha\", \"beta\", \"gamma\", \"delta\"]);\n\n return (\n <div\n style={{\n width: \"100%\",\n maxWidth: 360,\n padding: 12,\n border: \"1px dashed var(--clet-border)\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"var(--clet-surface-subtle)\",\n }}\n >\n <Sortable items={items} onReorder={(ids) => setItems(ids as string[])}>\n <SortableList>\n {items.map((id) => (\n <SortableItem key={id} id={id}>\n <SortableHandle aria-label={`Reorder ${labels[id]}`} />\n <span style={{ fontSize: 14 }}>{labels[id]}</span>\n </SortableItem>\n ))}\n </SortableList>\n </Sortable>\n </div>\n );\n}\n"
923
+ }
924
+ ],
925
+ "stepper": [
926
+ {
927
+ "file": "stepper.example.tsx",
928
+ "importName": "stepperSource",
929
+ "title": "Stepper",
930
+ "components": [
931
+ "Step",
932
+ "StepLabel",
933
+ "Stepper"
934
+ ],
935
+ "source": "import { useState } from \"react\";\nimport { Step, StepLabel, Stepper } from \"@rfdtech/components\";\n\nconst steps = [\n { value: 1, label: \"Upload Document\" },\n { value: 2, label: \"Select header row\" },\n { value: 3, label: \"Match Columns\" },\n { value: 4, label: \"Validate data\" },\n];\n\nexport function StepperExample() {\n const [value, setValue] = useState(3);\n\n return (\n <section\n className=\"clet-card\"\n style={{\n padding: \"24px\",\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"24px\",\n width: \"100%\",\n }}\n >\n <Stepper value={value} clickable onValueChange={setValue}>\n {steps.map((step) => (\n <Step key={step.value} value={step.value}>\n <StepLabel>{step.label}</StepLabel>\n </Step>\n ))}\n </Stepper>\n\n <div style={{ display: \"flex\", gap: \"8px\", paddingTop: \"32px\" }}>\n <button\n type=\"button\"\n className=\"clet-button clet-button--secondary\"\n onClick={() => setValue((v) => Math.max(1, v - 1))}\n disabled={value === 1}\n >\n Back\n </button>\n <button\n type=\"button\"\n className=\"clet-button clet-button--primary\"\n onClick={() => setValue((v) => Math.min(steps.length, v + 1))}\n disabled={value === steps.length}\n >\n Next\n </button>\n </div>\n </section>\n );\n}\n"
936
+ }
937
+ ],
938
+ "switch": [
939
+ {
940
+ "file": "switch-variants.example.tsx",
941
+ "importName": "switchVariantsSource",
942
+ "title": "Switch Variants",
943
+ "components": [
944
+ "Switch"
945
+ ],
946
+ "source": "import { Switch } from \"@rfdtech/components\";\nimport { useState } from \"react\";\n\nexport function SwitchVariantsExample() {\n const [checked, setChecked] = useState(true);\n\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: \"16px\" }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"24px\" }}>\n <Switch aria-label=\"Unchecked\" checked={false} onCheckedChange={() => {}} />\n <Switch aria-label=\"Checked\" checked onCheckedChange={() => {}} />\n <Switch aria-label=\"Disabled unchecked\" disabled />\n <Switch aria-label=\"Disabled checked\" disabled defaultChecked />\n </div>\n <Switch\n label=\"Notify me about updates\"\n checked={checked}\n onCheckedChange={setChecked}\n />\n <Switch\n label=\"Left-aligned label\"\n labelPosition=\"left\"\n defaultChecked\n />\n </div>\n );\n}\n"
947
+ }
948
+ ],
949
+ "table": [
950
+ {
951
+ "file": "table-actions.example.tsx",
952
+ "importName": "tableActionsSource",
953
+ "title": "Table Actions",
954
+ "components": [
955
+ "Badge",
956
+ "Dropdown",
957
+ "Table",
958
+ "TableActions",
959
+ "TableColumn",
960
+ "TableContent",
961
+ "TableFooter",
962
+ "TableHeader",
963
+ "TablePagination",
964
+ "TableSearch",
965
+ "useTableState"
966
+ ],
967
+ "source": "import type { TableColumn } from \"@rfdtech/components\";\nimport {\n Table,\n TableHeader,\n TableSearch,\n TableActions,\n TableContent,\n TableFooter,\n TablePagination,\n Badge,\n Dropdown,\n useTableState,\n} from \"@rfdtech/components\";\n\ninterface RoleAssignment {\n id: number;\n name: string;\n email: string;\n role: string;\n status: string;\n}\n\nconst assignments: RoleAssignment[] = [\n { id: 1, name: \"Kwame Asante\", email: \"kwame@clet.edu.gh\", role: \"Admin\", status: \"Active\" },\n { id: 2, name: \"Abena Mensah\", email: \"abena@clet.edu.gh\", role: \"Editor\", status: \"Active\" },\n { id: 3, name: \"Kofi Owusu\", email: \"kofi@clet.edu.gh\", role: \"Viewer\", status: \"Inactive\" },\n { id: 4, name: \"Esi Boateng\", email: \"esi@clet.edu.gh\", role: \"Editor\", status: \"Active\" },\n { id: 5, name: \"Yaw Adom\", email: \"yaw@clet.edu.gh\", role: \"Viewer\", status: \"Pending\" },\n { id: 6, name: \"Nana Yeboah\", email: \"nana@clet.edu.gh\", role: \"Admin\", status: \"Active\" },\n { id: 7, name: \"Akua Donkor\", email: \"akua@clet.edu.gh\", role: \"Editor\", status: \"Active\" },\n { id: 8, name: \"Kwesi Appiah\", email: \"kwesi@clet.edu.gh\", role: \"Viewer\", status: \"Inactive\" },\n];\n\nconst roleOptions = [\n { value: \"Admin\", label: \"Admin\" },\n { value: \"Editor\", label: \"Editor\" },\n { value: \"Viewer\", label: \"Viewer\" },\n];\n\nfunction statusVariant(status: string) {\n switch (status) {\n case \"Active\":\n return \"success\" as const;\n case \"Pending\":\n return \"warning\" as const;\n case \"Inactive\":\n return \"outline\" as const;\n default:\n return \"default\" as const;\n }\n}\n\nconst columns: TableColumn<RoleAssignment>[] = [\n { id: \"name\", header: \"Name\", accessorKey: \"name\", sortable: true },\n { id: \"email\", header: \"Email\", accessorKey: \"email\", sortable: true },\n { id: \"role\", header: \"Role\", accessorKey: \"role\", sortable: true },\n {\n id: \"status\",\n header: \"Status\",\n accessorKey: \"status\",\n sortable: true,\n cell: ({ value }) => (\n <Badge variant={statusVariant(String(value))}>{String(value)}</Badge>\n ),\n },\n];\n\ninterface RolesFilters {\n role: string | null;\n [key: string]: string | null | undefined;\n}\n\nexport function TableActionsExample() {\n // `useTableState`'s `filters` bag is exactly what a `TableActions` filter\n // control (here a `Dropdown`) reads from and writes to — the toolbar slot\n // itself is just a styled flex wrapper, it has no filtering logic of its own.\n const { page, pageSize, pageSizeOptions, search, filters, setFilter } =\n useTableState<RolesFilters>({\n defaultPageSize: 5,\n paramPrefix: \"role-assignments\",\n });\n\n const filtered = assignments.filter((a) => {\n if (filters.role && a.role !== filters.role) return false;\n return (\n a.name.toLowerCase().includes(search.toLowerCase()) ||\n a.email.toLowerCase().includes(search.toLowerCase())\n );\n });\n const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));\n const paged = filtered.slice((page - 1) * pageSize, page * pageSize);\n\n return (\n <Table paramPrefix=\"role-assignments\">\n <TableHeader>\n <TableSearch placeholder=\"Search by name or email...\" />\n <TableActions>\n <div style={{ width: 180 }}>\n <Dropdown\n aria-label=\"Filter by role\"\n value={filters.role ?? null}\n onValueChange={(value) => setFilter(\"role\", value)}\n options={roleOptions}\n placeholder=\"All roles\"\n clearable\n />\n </div>\n </TableActions>\n </TableHeader>\n <TableContent columns={columns} data={paged} rowKey={(a) => a.id} />\n <TableFooter>\n <TablePagination\n totalPages={totalPages}\n totalItems={filtered.length}\n pageSizeOptions={pageSizeOptions}\n />\n </TableFooter>\n </Table>\n );\n}\n"
968
+ },
969
+ {
970
+ "file": "table-crud.example.tsx",
971
+ "importName": "tableCrudSource",
972
+ "title": "Table Crud",
973
+ "components": [
974
+ "Badge",
975
+ "Button",
976
+ "Dialog",
977
+ "DialogContent",
978
+ "DialogDescription",
979
+ "DialogOverlay",
980
+ "DialogPortal",
981
+ "DialogTitle",
982
+ "Popover",
983
+ "PopoverClose",
984
+ "PopoverContent",
985
+ "PopoverTrigger",
986
+ "Table",
987
+ "TableBulkActions",
988
+ "TableColumn",
989
+ "TableContent",
990
+ "TableFooter",
991
+ "TableHeader",
992
+ "TablePagination",
993
+ "TableSearch",
994
+ "useTableState"
995
+ ],
996
+ "source": "import { useCallback, useState } from \"react\";\nimport type { TableColumn } from \"@rfdtech/components\";\nimport {\n Table,\n TableHeader,\n TableSearch,\n TableContent,\n TableBulkActions,\n TableFooter,\n TablePagination,\n Badge,\n Button,\n Dialog,\n DialogPortal,\n DialogOverlay,\n DialogContent,\n DialogTitle,\n DialogDescription,\n Popover,\n PopoverTrigger,\n PopoverContent,\n PopoverClose,\n useTableState,\n} from \"@rfdtech/components\";\nimport { Trash2, Eye, Edit, MoreHorizontal } from \"lucide-react\";\n\ninterface User {\n id: number;\n name: string;\n email: string;\n role: string;\n status: string;\n}\n\nconst initialUsers: User[] = [\n { id: 1, name: \"Kwame Asante\", email: \"kwame@clet.edu.gh\", role: \"Admin\", status: \"Active\" },\n { id: 2, name: \"Abena Mensah\", email: \"abena@clet.edu.gh\", role: \"Editor\", status: \"Active\" },\n { id: 3, name: \"Kofi Owusu\", email: \"kofi@clet.edu.gh\", role: \"Viewer\", status: \"Inactive\" },\n { id: 4, name: \"Esi Boateng\", email: \"esi@clet.edu.gh\", role: \"Editor\", status: \"Active\" },\n { id: 5, name: \"Yaw Adom\", email: \"yaw@clet.edu.gh\", role: \"Viewer\", status: \"Pending\" },\n { id: 6, name: \"Nana Yeboah\", email: \"nana@clet.edu.gh\", role: \"Admin\", status: \"Active\" },\n { id: 7, name: \"Akua Donkor\", email: \"akua@clet.edu.gh\", role: \"Editor\", status: \"Active\" },\n { id: 8, name: \"Kwesi Appiah\", email: \"kwesi@clet.edu.gh\", role: \"Viewer\", status: \"Inactive\" },\n];\n\nfunction statusVariant(status: string) {\n switch (status) {\n case \"Active\":\n return \"success\" as const;\n case \"Pending\":\n return \"warning\" as const;\n case \"Inactive\":\n return \"outline\" as const;\n default:\n return \"default\" as const;\n }\n}\n\n/** Per-row action menu — an alternative to the built-in `rowActions` prop,\n * composed by hand from `Popover` so the menu content can be fully custom. */\nfunction RowActionsMenu({\n user,\n onView,\n onEdit,\n onDelete,\n}: {\n user: User;\n onView: (user: User) => void;\n onEdit: (user: User) => void;\n onDelete: (user: User) => void;\n}) {\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n aria-label={`Actions for ${user.name}`}\n className=\"clet-dropdown__trigger\"\n style={{\n width: 32,\n height: 32,\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n border: \"1px solid var(--clet-border)\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"transparent\",\n }}\n >\n <MoreHorizontal size={16} strokeWidth={1.5} />\n </button>\n </PopoverTrigger>\n <PopoverContent side=\"bottom\" align=\"end\" sideOffset={4} className=\"clet-popover--menu\">\n <div className=\"clet-popover__menu\" role=\"menu\">\n <PopoverClose asChild>\n <button\n type=\"button\"\n className=\"clet-popover__menu-item\"\n role=\"menuitem\"\n onClick={() => onView(user)}\n >\n <Eye size={16} strokeWidth={1.5} aria-hidden />\n View\n </button>\n </PopoverClose>\n <PopoverClose asChild>\n <button\n type=\"button\"\n className=\"clet-popover__menu-item\"\n role=\"menuitem\"\n onClick={() => onEdit(user)}\n >\n <Edit size={16} strokeWidth={1.5} aria-hidden />\n Edit\n </button>\n </PopoverClose>\n <PopoverClose asChild>\n <button\n type=\"button\"\n className=\"clet-popover__menu-item clet-popover__menu-item--destructive\"\n role=\"menuitem\"\n onClick={() => onDelete(user)}\n >\n <Trash2 size={16} strokeWidth={1.5} aria-hidden />\n Delete\n </button>\n </PopoverClose>\n </div>\n </PopoverContent>\n </Popover>\n );\n}\n\nexport function TableCrudExample() {\n const [users, setUsers] = useState(initialUsers);\n const [selected, setSelected] = useState<Set<string | number>>(new Set());\n const [pendingDelete, setPendingDelete] = useState<Set<string | number> | null>(null);\n\n const { page, pageSize, pageSizeOptions, search } = useTableState({\n defaultPageSize: 5,\n });\n\n const filtered = users.filter(\n (u) =>\n u.name.toLowerCase().includes(search.toLowerCase()) ||\n u.email.toLowerCase().includes(search.toLowerCase()),\n );\n const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));\n const paged = filtered.slice((page - 1) * pageSize, page * pageSize);\n\n const handleView = useCallback((user: User) => {\n window.alert(`Viewing ${user.name}`);\n }, []);\n\n const handleEdit = useCallback((user: User) => {\n window.alert(`Editing ${user.name}`);\n }, []);\n\n const handleRowDelete = useCallback((user: User) => {\n setUsers((prev) => prev.filter((u) => u.id !== user.id));\n }, []);\n\n // Bulk delete never deletes directly — it stages the selection and opens a\n // confirm Dialog. The actual mutation only happens once the user confirms.\n const requestBulkDelete = useCallback((ids: Set<string | number>) => {\n setPendingDelete(ids);\n }, []);\n\n const confirmBulkDelete = useCallback(() => {\n if (!pendingDelete) return;\n setUsers((prev) => prev.filter((u) => !pendingDelete.has(u.id)));\n setSelected(new Set());\n setPendingDelete(null);\n }, [pendingDelete]);\n\n const columns: TableColumn<User>[] = [\n { id: \"name\", header: \"Name\", accessorKey: \"name\", sortable: true },\n { id: \"email\", header: \"Email\", accessorKey: \"email\", sortable: true },\n { id: \"role\", header: \"Role\", accessorKey: \"role\", sortable: true },\n {\n id: \"status\",\n header: \"Status\",\n accessorKey: \"status\",\n sortable: true,\n cell: ({ value }) => (\n <Badge variant={statusVariant(String(value))}>{String(value)}</Badge>\n ),\n },\n {\n id: \"actions\",\n header: \"\",\n width: 56,\n align: \"right\",\n cell: ({ row }) => (\n <RowActionsMenu\n user={row}\n onView={handleView}\n onEdit={handleEdit}\n onDelete={handleRowDelete}\n />\n ),\n },\n ];\n\n return (\n <>\n <Table paramPrefix=\"crud-users\">\n <TableHeader>\n <TableSearch placeholder=\"Search users...\" />\n </TableHeader>\n <TableContent\n selectable\n selectedIds={selected}\n onSelectionChange={setSelected}\n columns={columns}\n data={paged}\n rowKey={(u) => u.id}\n />\n <TableBulkActions\n selectedIds={selected}\n onClear={() => setSelected(new Set())}\n actions={[\n {\n id: \"delete\",\n label: \"Delete\",\n icon: <Trash2 size={14} strokeWidth={1.5} />,\n onClick: requestBulkDelete,\n destructive: true,\n },\n ]}\n />\n <TableFooter>\n <TablePagination\n totalPages={totalPages}\n totalItems={filtered.length}\n pageSizeOptions={pageSizeOptions}\n />\n </TableFooter>\n </Table>\n\n <Dialog\n open={pendingDelete !== null}\n onOpenChange={(open) => {\n if (!open) setPendingDelete(null);\n }}\n >\n <DialogPortal>\n <DialogOverlay />\n <DialogContent showCloseButton>\n <DialogTitle>Delete selected users?</DialogTitle>\n <DialogDescription>\n {pendingDelete?.size ?? 0} user{(pendingDelete?.size ?? 0) === 1 ? \"\" : \"s\"}{\" \"}\n will be permanently removed. This action cannot be undone.\n </DialogDescription>\n <div className=\"flex justify-end gap-2\">\n <Button variant=\"ghost\" onClick={() => setPendingDelete(null)}>\n Cancel\n </Button>\n <Button variant=\"destructive\" onClick={confirmBulkDelete}>\n Delete\n </Button>\n </div>\n </DialogContent>\n </DialogPortal>\n </Dialog>\n </>\n );\n}\n"
997
+ },
998
+ {
999
+ "file": "table-empty.example.tsx",
1000
+ "importName": "tableEmptySource",
1001
+ "title": "Table Empty",
1002
+ "components": [
1003
+ "Table",
1004
+ "TableColumn",
1005
+ "TableContent",
1006
+ "TableFooter",
1007
+ "TableHeader",
1008
+ "TablePagination",
1009
+ "TableSearch",
1010
+ "useTableState"
1011
+ ],
1012
+ "source": "import { useState } from \"react\";\nimport { useTableState } from \"@rfdtech/components\";\nimport type { TableColumn } from \"@rfdtech/components\";\nimport {\n Table,\n TableHeader,\n TableSearch,\n TableContent,\n TableFooter,\n TablePagination,\n} from \"@rfdtech/components\";\n\ninterface User {\n id: number;\n name: string;\n email: string;\n role: string;\n}\n\nconst columns: TableColumn<User>[] = [\n { id: \"name\", header: \"Name\", accessorKey: \"name\", sortable: true },\n { id: \"email\", header: \"Email\", accessorKey: \"email\", sortable: true },\n { id: \"role\", header: \"Role\", accessorKey: \"role\", sortable: true },\n];\n\nexport function TableEmptyExample() {\n const [selected, setSelected] = useState<Set<string | number>>(new Set());\n useTableState({ defaultPageSize: 5 });\n\n const filtered: User[] = [];\n const totalPages = 0;\n\n return (\n <Table paramPrefix=\"users-empty\">\n <TableHeader>\n <TableSearch placeholder=\"Search users...\" />\n </TableHeader>\n <TableContent\n selectable\n selectedIds={selected}\n onSelectionChange={setSelected}\n columns={columns}\n data={filtered}\n rowKey={(u) => u.id}\n emptyText=\"No users found\"\n />\n <TableFooter>\n <TablePagination\n totalPages={totalPages}\n pageSizeOptions={[5, 10, 20]}\n />\n </TableFooter>\n </Table>\n );\n}\n"
1013
+ },
1014
+ {
1015
+ "file": "table-filter-spread.example.tsx",
1016
+ "importName": "tableFilterSpreadSource",
1017
+ "title": "Table Filter Spread",
1018
+ "components": [
1019
+ "Badge",
1020
+ "Dropdown",
1021
+ "Table",
1022
+ "TableColumn",
1023
+ "TableContent",
1024
+ "TableFilter",
1025
+ "TableFooter",
1026
+ "TableHeader",
1027
+ "TablePagination",
1028
+ "TableSearch",
1029
+ "useTableState"
1030
+ ],
1031
+ "source": "import { useMemo, useState } from \"react\";\nimport { useTableState } from \"@rfdtech/components\";\nimport type { TableColumn } from \"@rfdtech/components\";\nimport {\n Table,\n TableHeader,\n TableSearch,\n TableFilter,\n TableContent,\n TableFooter,\n TablePagination,\n Dropdown,\n Badge,\n} from \"@rfdtech/components\";\n\ninterface Member {\n id: number;\n name: string;\n role: string;\n status: string;\n}\n\nconst members: Member[] = [\n { id: 1, name: \"Kwame Asante\", role: \"Admin\", status: \"Active\" },\n { id: 2, name: \"Abena Mensah\", role: \"Editor\", status: \"Active\" },\n { id: 3, name: \"Kofi Owusu\", role: \"Viewer\", status: \"Inactive\" },\n { id: 4, name: \"Esi Boateng\", role: \"Editor\", status: \"Active\" },\n { id: 5, name: \"Yaw Adom\", role: \"Viewer\", status: \"Pending\" },\n { id: 6, name: \"Nana Yeboah\", role: \"Admin\", status: \"Active\" },\n];\n\nfunction statusVariant(status: string) {\n switch (status) {\n case \"Active\":\n return \"success\" as const;\n case \"Pending\":\n return \"warning\" as const;\n default:\n return \"outline\" as const;\n }\n}\n\nconst columns: TableColumn<Member>[] = [\n { id: \"name\", header: \"Name\", accessorKey: \"name\", sortable: true },\n { id: \"role\", header: \"Role\", accessorKey: \"role\", sortable: true },\n {\n id: \"status\",\n header: \"Status\",\n accessorKey: \"status\",\n sortable: true,\n cell: ({ value }) => (\n <Badge variant={statusVariant(String(value))}>{String(value)}</Badge>\n ),\n },\n];\n\nexport function TableFilterSpreadExample() {\n const { page, pageSize, pageSizeOptions, search, filters } = useTableState({\n defaultPageSize: 5,\n paramPrefix: \"spread-filter\",\n });\n const [roleValue, setRoleValue] = useState(filters.role ?? \"\");\n const [statusValue, setStatusValue] = useState(filters.status ?? \"\");\n\n const filtered = useMemo(\n () =>\n members.filter((m) => {\n const matchSearch = m.name.toLowerCase().includes(search.toLowerCase());\n const matchRole = !filters.role || m.role === filters.role;\n const matchStatus = !filters.status || m.status === filters.status;\n return matchSearch && matchRole && matchStatus;\n }),\n [search, filters],\n );\n\n const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));\n const paged = filtered.slice((page - 1) * pageSize, page * pageSize);\n\n return (\n <Table paramPrefix=\"spread-filter\">\n <TableHeader>\n <TableSearch placeholder=\"Search members...\" />\n <TableFilter variant=\"spread\">\n <Dropdown\n name=\"role\"\n value={roleValue || null}\n onValueChange={(v) => setRoleValue(v ?? \"\")}\n options={[\n { value: \"Admin\", label: \"Admin\" },\n { value: \"Editor\", label: \"Editor\" },\n { value: \"Viewer\", label: \"Viewer\" },\n ]}\n placeholder=\"All roles\"\n aria-label=\"Filter by role\"\n />\n <Dropdown\n name=\"status\"\n value={statusValue || null}\n onValueChange={(v) => setStatusValue(v ?? \"\")}\n options={[\n { value: \"Active\", label: \"Active\" },\n { value: \"Inactive\", label: \"Inactive\" },\n { value: \"Pending\", label: \"Pending\" },\n ]}\n placeholder=\"All statuses\"\n aria-label=\"Filter by status\"\n />\n </TableFilter>\n </TableHeader>\n <TableContent columns={columns} data={paged} rowKey={(m) => m.id} />\n <TableFooter>\n <TablePagination\n totalPages={totalPages}\n totalItems={filtered.length}\n pageSizeOptions={pageSizeOptions}\n />\n </TableFooter>\n </Table>\n );\n}\n"
1032
+ },
1033
+ {
1034
+ "file": "table-panel.example.tsx",
1035
+ "importName": "tablePanelSource",
1036
+ "title": "Table Panel",
1037
+ "components": [
1038
+ "Badge",
1039
+ "Table",
1040
+ "TableColumn",
1041
+ "TableContent",
1042
+ "TableFooter",
1043
+ "TablePagination",
1044
+ "Tabs",
1045
+ "TabsContent",
1046
+ "TabsList",
1047
+ "TabsTrigger",
1048
+ "useTableState"
1049
+ ],
1050
+ "source": "import { useState } from \"react\";\nimport { useTableState } from \"@rfdtech/components\";\nimport type { TableColumn } from \"@rfdtech/components\";\nimport {\n Tabs,\n TabsList,\n TabsTrigger,\n TabsContent,\n Table,\n TableContent,\n TableFooter,\n TablePagination,\n Badge,\n} from \"@rfdtech/components\";\n\ninterface Member {\n id: number;\n name: string;\n role: string;\n status: string;\n}\n\nconst members: Member[] = [\n { id: 1, name: \"Kwame Asante\", role: \"Admin\", status: \"Active\" },\n { id: 2, name: \"Abena Mensah\", role: \"Editor\", status: \"Active\" },\n { id: 3, name: \"Kofi Owusu\", role: \"Viewer\", status: \"Inactive\" },\n { id: 4, name: \"Esi Boateng\", role: \"Editor\", status: \"Active\" },\n { id: 5, name: \"Yaw Adom\", role: \"Viewer\", status: \"Pending\" },\n { id: 6, name: \"Nana Yeboah\", role: \"Admin\", status: \"Active\" },\n];\n\nfunction statusVariant(status: string) {\n switch (status) {\n case \"Active\":\n return \"success\" as const;\n case \"Pending\":\n return \"warning\" as const;\n default:\n return \"outline\" as const;\n }\n}\n\nconst columns: TableColumn<Member>[] = [\n { id: \"name\", header: \"Name\", accessorKey: \"name\", sortable: true },\n { id: \"role\", header: \"Role\", accessorKey: \"role\", sortable: true },\n {\n id: \"status\",\n header: \"Status\",\n accessorKey: \"status\",\n sortable: true,\n cell: ({ value }) => (\n <Badge variant={statusVariant(String(value))}>{String(value)}</Badge>\n ),\n },\n];\n\nconst TABS = [\n { value: \"all\", label: \"All\", data: members },\n {\n value: \"active\",\n label: \"Active\",\n data: members.filter((m) => m.status === \"Active\"),\n },\n {\n value: \"pending\",\n label: \"Pending\",\n data: members.filter((m) => m.status === \"Pending\"),\n },\n];\n\nfunction PanelTable({ prefix, data }: { prefix: string; data: Member[] }) {\n const { page, pageSize, pageSizeOptions } = useTableState({\n defaultPageSize: 5,\n paramPrefix: prefix,\n });\n const totalPages = Math.max(1, Math.ceil(data.length / pageSize));\n const paged = data.slice((page - 1) * pageSize, page * pageSize);\n\n return (\n <Table paramPrefix={prefix}>\n <TableContent\n variant=\"panel\"\n columns={columns}\n data={paged}\n rowKey={(m) => m.id}\n />\n <TableFooter>\n <TablePagination\n totalPages={totalPages}\n totalItems={data.length}\n pageSizeOptions={pageSizeOptions}\n />\n </TableFooter>\n </Table>\n );\n}\n\nexport function TablePanelExample() {\n const [tab, setTab] = useState(\"all\");\n\n return (\n <Tabs variant=\"pill\" value={tab} onValueChange={setTab}>\n <TabsList>\n {TABS.map((t) => (\n <TabsTrigger key={t.value} value={t.value}>\n {t.label}\n </TabsTrigger>\n ))}\n </TabsList>\n {TABS.map((t) => (\n <TabsContent key={t.value} value={t.value}>\n <PanelTable prefix={`panel-${t.value}`} data={t.data} />\n </TabsContent>\n ))}\n </Tabs>\n );\n}\n"
1051
+ },
1052
+ {
1053
+ "file": "table.example.tsx",
1054
+ "importName": "tableSource",
1055
+ "title": "Table",
1056
+ "components": [
1057
+ "Badge",
1058
+ "Dropdown",
1059
+ "Table",
1060
+ "TableBulkActions",
1061
+ "TableColumn",
1062
+ "TableContent",
1063
+ "TableFilter",
1064
+ "TableFooter",
1065
+ "TableHeader",
1066
+ "TablePagination",
1067
+ "TableRowAction",
1068
+ "TableSearch",
1069
+ "useTableState"
1070
+ ],
1071
+ "source": "import { useCallback, useState } from \"react\";\nimport { useTableState } from \"@rfdtech/components\";\nimport type { TableColumn, TableRowAction } from \"@rfdtech/components\";\nimport {\n Table,\n TableHeader,\n TableSearch,\n TableFilter,\n TableContent,\n TableBulkActions,\n TableFooter,\n TablePagination,\n Dropdown,\n Badge,\n} from \"@rfdtech/components\";\nimport { Trash2, Eye, Edit } from \"lucide-react\";\n\ninterface User {\n id: number;\n name: string;\n email: string;\n role: string;\n status: string;\n}\n\nconst initialUsers: User[] = [\n { id: 1, name: \"Kwame Asante\", email: \"kwame@clet.edu.gh\", role: \"Admin\", status: \"Active\" },\n { id: 2, name: \"Abena Mensah\", email: \"abena@clet.edu.gh\", role: \"Editor\", status: \"Active\" },\n { id: 3, name: \"Kofi Owusu\", email: \"kofi@clet.edu.gh\", role: \"Viewer\", status: \"Inactive\" },\n { id: 4, name: \"Esi Boateng\", email: \"esi@clet.edu.gh\", role: \"Editor\", status: \"Active\" },\n { id: 5, name: \"Yaw Adom\", email: \"yaw@clet.edu.gh\", role: \"Viewer\", status: \"Pending\" },\n { id: 6, name: \"Nana Yeboah\", email: \"nana@clet.edu.gh\", role: \"Admin\", status: \"Active\" },\n { id: 7, name: \"Akua Donkor\", email: \"akua@clet.edu.gh\", role: \"Editor\", status: \"Active\" },\n { id: 8, name: \"Kwesi Appiah\", email: \"kwesi@clet.edu.gh\", role: \"Viewer\", status: \"Inactive\" },\n { id: 9, name: \"Adwoa Sarpong\", email: \"adwoa@clet.edu.gh\", role: \"Editor\", status: \"Active\" },\n { id: 10, name: \"Kobina Ennin\", email: \"kobina@clet.edu.gh\", role: \"Viewer\", status: \"Pending\" },\n { id: 11, name: \"Efua Sutherland\", email: \"efua@clet.edu.gh\", role: \"Editor\", status: \"Active\" },\n { id: 12, name: \"Paapa Essiedu\", email: \"paapa@clet.edu.gh\", role: \"Viewer\", status: \"Inactive\" },\n];\n\nfunction statusVariant(status: string) {\n switch (status) {\n case \"Active\":\n return \"success\" as const;\n case \"Pending\":\n return \"warning\" as const;\n case \"Inactive\":\n return \"outline\" as const;\n default:\n return \"default\" as const;\n }\n}\n\nconst columns: TableColumn<User>[] = [\n { id: \"name\", header: \"Name\", accessorKey: \"name\", sortable: true },\n { id: \"email\", header: \"Email\", accessorKey: \"email\", sortable: true },\n { id: \"role\", header: \"Role\", accessorKey: \"role\", sortable: true },\n {\n id: \"status\",\n header: \"Status\",\n accessorKey: \"status\",\n sortable: true,\n cell: ({ value }) => (\n <Badge variant={statusVariant(String(value))}>{String(value)}</Badge>\n ),\n },\n];\n\nconst rowActions: TableRowAction<User>[] = [\n { id: \"view\", label: \"View\", icon: <Eye size={14} strokeWidth={1.5} />, onClick: () => {} },\n { id: \"edit\", label: \"Edit\", icon: <Edit size={14} strokeWidth={1.5} />, onClick: () => {} },\n { id: \"delete\", label: \"Delete\", icon: <Trash2 size={14} strokeWidth={1.5} />, onClick: () => {}, variant: \"destructive\" },\n];\n\nexport function TableExample() {\n const [users, setUsers] = useState(initialUsers);\n const [selected, setSelected] = useState<Set<string | number>>(new Set());\n\n const { page, pageSize, pageSizeOptions, search, filters } = useTableState({\n defaultPageSize: 5,\n });\n const [roleValue, setRoleValue] = useState(filters.role ?? \"\");\n\n const filtered = users.filter(\n (u) => {\n if (filters.role && u.role !== filters.role) return false;\n return (\n u.name.toLowerCase().includes(search.toLowerCase()) ||\n u.email.toLowerCase().includes(search.toLowerCase())\n );\n },\n );\n const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));\n const paged = filtered.slice((page - 1) * pageSize, page * pageSize);\n\n const handleDelete = useCallback((ids: Set<string | number>) => {\n setUsers((prev) => prev.filter((u) => !ids.has(u.id)));\n setSelected(new Set());\n }, []);\n\n return (\n <Table paramPrefix=\"users\">\n <TableHeader>\n <TableSearch placeholder=\"Search users...\" />\n <TableFilter>\n <Dropdown\n name=\"role\"\n value={roleValue || null}\n onValueChange={(v) => setRoleValue(v ?? \"\")}\n options={[\n { value: \"Admin\", label: \"Admin\" },\n { value: \"Editor\", label: \"Editor\" },\n { value: \"Viewer\", label: \"Viewer\" },\n ]}\n placeholder=\"All roles\"\n aria-label=\"Filter by role\"\n />\n </TableFilter>\n </TableHeader>\n <TableContent\n selectable\n selectedIds={selected}\n onSelectionChange={setSelected}\n rowActions={rowActions}\n columns={columns}\n data={paged}\n rowKey={(u) => u.id}\n />\n <TableBulkActions\n selectedIds={selected}\n onClear={() => setSelected(new Set())}\n actions={[\n {\n id: \"delete\",\n label: \"Delete\",\n icon: <Trash2 size={14} strokeWidth={1.5} />,\n onClick: handleDelete,\n destructive: true,\n },\n ]}\n />\n <TableFooter>\n <TablePagination\n totalPages={totalPages}\n totalItems={filtered.length}\n pageSizeOptions={pageSizeOptions}\n />\n </TableFooter>\n </Table>\n );\n}\n"
1072
+ }
1073
+ ],
1074
+ "tabs": [
1075
+ {
1076
+ "file": "table-tabs-filter.example.tsx",
1077
+ "importName": "tableTabsFilterSource",
1078
+ "title": "Table Tabs Filter",
1079
+ "components": [
1080
+ "Badge",
1081
+ "Table",
1082
+ "TableColumn",
1083
+ "TableContent",
1084
+ "TableFooter",
1085
+ "TablePagination",
1086
+ "Tabs",
1087
+ "TabsList",
1088
+ "TabsTrigger",
1089
+ "useTableState"
1090
+ ],
1091
+ "source": "import { useState } from \"react\";\nimport type { TableColumn } from \"@rfdtech/components\";\nimport {\n Tabs,\n TabsList,\n TabsTrigger,\n Table,\n TableContent,\n TableFooter,\n TablePagination,\n Badge,\n useTableState,\n} from \"@rfdtech/components\";\n\ninterface Registrant {\n id: number;\n name: string;\n institution: string;\n status: string;\n}\n\nconst registrants: Registrant[] = [\n { id: 1, name: \"Kwame Asante\", institution: \"Achimota School\", status: \"Active\" },\n { id: 2, name: \"Abena Mensah\", institution: \"Wesley Girls' High\", status: \"Pending\" },\n { id: 3, name: \"Kofi Owusu\", institution: \"Prempeh College\", status: \"Active\" },\n { id: 4, name: \"Esi Boateng\", institution: \"Holy Child School\", status: \"Active\" },\n { id: 5, name: \"Yaw Adom\", institution: \"Mfantsipim School\", status: \"Pending\" },\n { id: 6, name: \"Nana Yeboah\", institution: \"Aburi Girls'\", status: \"Active\" },\n { id: 7, name: \"Akua Donkor\", institution: \"St. Augustine's\", status: \"Pending\" },\n { id: 8, name: \"Kwesi Appiah\", institution: \"Adisadel College\", status: \"Active\" },\n { id: 9, name: \"Adwoa Sarpong\", institution: \"Opoku Ware School\", status: \"Active\" },\n { id: 10, name: \"Kobina Ennin\", institution: \"Ghana National College\", status: \"Pending\" },\n];\n\nfunction statusVariant(status: string) {\n switch (status) {\n case \"Active\":\n return \"success\" as const;\n case \"Pending\":\n return \"warning\" as const;\n default:\n return \"outline\" as const;\n }\n}\n\nconst columns: TableColumn<Registrant>[] = [\n { id: \"name\", header: \"Name\", accessorKey: \"name\", sortable: true },\n { id: \"institution\", header: \"Institution\", accessorKey: \"institution\", sortable: true },\n {\n id: \"status\",\n header: \"Status\",\n accessorKey: \"status\",\n sortable: true,\n cell: ({ value }) => (\n <Badge variant={statusVariant(String(value))}>{String(value)}</Badge>\n ),\n },\n];\n\nconst STATUS_TABS = [\n { value: \"all\", label: \"All\" },\n { value: \"active\", label: \"Active\" },\n { value: \"pending\", label: \"Pending\" },\n] as const;\n\ntype StatusTab = (typeof STATUS_TABS)[number][\"value\"];\n\nfunction filterByTab(data: Registrant[], tab: StatusTab): Registrant[] {\n if (tab === \"active\") return data.filter((r) => r.status === \"Active\");\n if (tab === \"pending\") return data.filter((r) => r.status === \"Pending\");\n return data;\n}\n\n/**\n * Status tabs act as a segment filter above ONE shared table — switching tabs\n * re-filters the same `data` array and resets pagination, rather than\n * mounting a separate `<Table>`/`<TableContent>` per tab.\n */\nexport function TableTabsFilterExample() {\n const [tab, setTab] = useState<StatusTab>(\"all\");\n const { page, pageSize, pageSizeOptions, setPage } = useTableState({\n defaultPageSize: 5,\n paramPrefix: \"registry-results\",\n });\n\n const filtered = filterByTab(registrants, tab);\n const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));\n const paged = filtered.slice((page - 1) * pageSize, page * pageSize);\n\n return (\n <Tabs\n variant=\"pill\"\n value={tab}\n onValueChange={(value) => {\n setTab(value as StatusTab);\n setPage(1);\n }}\n >\n <TabsList>\n {STATUS_TABS.map((t) => (\n <TabsTrigger key={t.value} value={t.value}>\n {t.label}\n </TabsTrigger>\n ))}\n </TabsList>\n\n <Table paramPrefix=\"registry-results\">\n <TableContent columns={columns} data={paged} rowKey={(r) => r.id} />\n <TableFooter>\n <TablePagination\n totalPages={totalPages}\n totalItems={filtered.length}\n pageSizeOptions={pageSizeOptions}\n />\n </TableFooter>\n </Table>\n </Tabs>\n );\n}\n"
1092
+ },
1093
+ {
1094
+ "file": "tabs-line.example.tsx",
1095
+ "importName": "tabsLineSource",
1096
+ "title": "Tabs Line",
1097
+ "components": [
1098
+ "Tabs",
1099
+ "TabsContent",
1100
+ "TabsList",
1101
+ "TabsTrigger"
1102
+ ],
1103
+ "source": "import { Tabs, TabsContent, TabsList, TabsTrigger } from \"@rfdtech/components\";\n\nexport function TabsLineExample() {\n return (\n <Tabs defaultValue=\"account\" variant=\"line\">\n <TabsList>\n <TabsTrigger value=\"account\">Account</TabsTrigger>\n <TabsTrigger value=\"security\">Security</TabsTrigger>\n <TabsTrigger value=\"notifications\">Notifications</TabsTrigger>\n </TabsList>\n <TabsContent value=\"account\">\n Manage your profile details and contact information.\n </TabsContent>\n <TabsContent value=\"security\">\n Update your password and two-factor authentication settings.\n </TabsContent>\n <TabsContent value=\"notifications\">\n Choose which email and in-app alerts you receive.\n </TabsContent>\n </Tabs>\n );\n}\n"
1104
+ },
1105
+ {
1106
+ "file": "tabs-pill.example.tsx",
1107
+ "importName": "tabsPillSource",
1108
+ "title": "Tabs Pill",
1109
+ "components": [
1110
+ "Tabs",
1111
+ "TabsContent",
1112
+ "TabsList",
1113
+ "TabsTrigger"
1114
+ ],
1115
+ "source": "import { Tabs, TabsContent, TabsList, TabsTrigger } from \"@rfdtech/components\";\n\nexport function TabsPillExample() {\n return (\n <Tabs defaultValue=\"account\" variant=\"pill\">\n <TabsList>\n <TabsTrigger value=\"account\">Account</TabsTrigger>\n <TabsTrigger value=\"security\">Security</TabsTrigger>\n <TabsTrigger value=\"notifications\">Notifications</TabsTrigger>\n </TabsList>\n <TabsContent value=\"account\">\n Manage your profile details and contact information.\n </TabsContent>\n <TabsContent value=\"security\">\n Update your password and two-factor authentication settings.\n </TabsContent>\n <TabsContent value=\"notifications\">\n Choose which email and in-app alerts you receive.\n </TabsContent>\n </Tabs>\n );\n}\n"
1116
+ },
1117
+ {
1118
+ "file": "tabs.example.tsx",
1119
+ "importName": "tabsSource",
1120
+ "title": "Tabs",
1121
+ "components": [
1122
+ "Tabs",
1123
+ "TabsContent",
1124
+ "TabsList",
1125
+ "TabsTrigger"
1126
+ ],
1127
+ "source": "import { Tabs, TabsContent, TabsList, TabsTrigger } from \"@rfdtech/components\";\n\nexport function TabsExample() {\n return (\n <Tabs defaultValue=\"account\">\n <TabsList>\n <TabsTrigger value=\"account\">Account</TabsTrigger>\n <TabsTrigger value=\"security\">Security</TabsTrigger>\n <TabsTrigger value=\"notifications\">Notifications</TabsTrigger>\n </TabsList>\n <TabsContent value=\"account\">\n Manage your profile details and contact information.\n </TabsContent>\n <TabsContent value=\"security\">\n Update your password and two-factor authentication settings.\n </TabsContent>\n <TabsContent value=\"notifications\">\n Choose which email and in-app alerts you receive.\n </TabsContent>\n </Tabs>\n );\n}\n"
1128
+ }
1129
+ ],
1130
+ "theme": [],
1131
+ "timeline": [
1132
+ {
1133
+ "file": "timeline.example.tsx",
1134
+ "importName": "timelineSource",
1135
+ "title": "Timeline",
1136
+ "components": [
1137
+ "Timeline",
1138
+ "TimelineData",
1139
+ "TimelineFooter",
1140
+ "TimelineItem",
1141
+ "TimelineTitle"
1142
+ ],
1143
+ "source": "import { Check } from \"lucide-react\";\nimport {\n Timeline,\n TimelineData,\n TimelineFooter,\n TimelineItem,\n TimelineTitle,\n} from \"@rfdtech/components\";\n\nconst history = [\n {\n title: \"Case reported\",\n date: \"12 Jun 2026\",\n description: \"HR Officer intake\",\n mode: \"success\" as const,\n },\n {\n title: \"Assigned to HR Manager\",\n date: \"13 Jun 2026\",\n description: \"You\",\n mode: \"success\" as const,\n },\n {\n title: \"Initial interview scheduled\",\n date: \"20 Jun 2026\",\n description: \"Complainant & respondent\",\n mode: \"success\" as const,\n },\n {\n title: \"Response overdue\",\n date: \"Since 3 Jul 2026\",\n description: \"You\",\n mode: \"warning\" as const,\n },\n];\n\nexport function TimelineExample() {\n return (\n <section className=\"clet-card\" style={{ padding: \"24px\" }}>\n <h2\n className=\"clet-card__title\"\n style={{\n margin: \"0 0 24px\",\n fontSize: \"20px\",\n fontWeight: 600,\n color: \"var(--clet-text)\",\n }}\n >\n Case History\n </h2>\n <Timeline>\n {history.map((item) => (\n <TimelineItem\n key={item.title}\n mode={item.mode}\n icon={\n item.mode === \"warning\" ? null : (\n <Check size={10} strokeWidth={3} />\n )\n }\n >\n <TimelineTitle>{item.title}</TimelineTitle>\n <TimelineData>{item.date}</TimelineData>\n <TimelineFooter>{item.description}</TimelineFooter>\n </TimelineItem>\n ))}\n </Timeline>\n </section>\n );\n}\n"
1144
+ }
1145
+ ],
1146
+ "toast": [
1147
+ {
1148
+ "file": "toast-icons.example.tsx",
1149
+ "importName": "toastIconsSource",
1150
+ "title": "Toast Icons",
1151
+ "components": [
1152
+ "Button",
1153
+ "ToastProvider",
1154
+ "Toaster",
1155
+ "useToast"
1156
+ ],
1157
+ "source": "import {\n Bell,\n CheckCircle2,\n Info,\n Paperclip,\n} from \"lucide-react\";\nimport { Button, ToastProvider, Toaster, useToast } from \"@rfdtech/components\";\n\nfunction ToastIconTriggers() {\n const { toast } = useToast();\n\n return (\n <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 8 }}>\n <Button\n variant=\"secondary\"\n onClick={() =>\n toast({\n title: \"New message\",\n description: \"Alex sent you a file.\",\n icon: <Bell size={18} strokeWidth={2} aria-hidden />,\n })\n }\n >\n Default + icon\n </Button>\n <Button\n variant=\"secondary\"\n onClick={() =>\n toast({\n title: \"Backup complete\",\n variant: \"success\",\n icon: <CheckCircle2 size={18} strokeWidth={2} aria-hidden />,\n })\n }\n >\n Title only\n </Button>\n <Button\n variant=\"secondary\"\n onClick={() =>\n toast({\n title: \"Attachment added\",\n description: \"report-q2.pdf\",\n variant: \"default\",\n icon: <Paperclip size={18} strokeWidth={2} aria-hidden />,\n })\n }\n >\n Custom SVG\n </Button>\n <Button\n variant=\"secondary\"\n onClick={() =>\n toast({\n title: \"Tip\",\n description: \"Any React node works — emoji, SVG, or component.\",\n icon: <span aria-hidden>💡</span>,\n })\n }\n >\n Emoji icon\n </Button>\n <Button\n variant=\"secondary\"\n onClick={() =>\n toast({\n title: \"Session expiring\",\n description: \"Save your work to keep changes.\",\n variant: \"warning\",\n icon: <Info size={18} strokeWidth={2} aria-hidden />,\n action: {\n label: \"Extend\",\n onClick: () => {},\n },\n })\n }\n >\n Icon + action\n </Button>\n </div>\n );\n}\n\nexport function ToastIconsExample() {\n return (\n <ToastProvider>\n <ToastIconTriggers />\n <Toaster />\n </ToastProvider>\n );\n}\n"
1158
+ },
1159
+ {
1160
+ "file": "toast.example.tsx",
1161
+ "importName": "toastSource",
1162
+ "title": "Toast",
1163
+ "components": [
1164
+ "Button",
1165
+ "ToastProvider",
1166
+ "Toaster",
1167
+ "useToast"
1168
+ ],
1169
+ "source": "import {\n AlertTriangle,\n CheckCircle2,\n CircleX,\n Info,\n} from \"lucide-react\";\nimport { Button, ToastProvider, Toaster, useToast } from \"@rfdtech/components\";\n\nfunction ToastTriggers() {\n const { toast } = useToast();\n\n return (\n <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 8 }}>\n <Button\n variant=\"secondary\"\n onClick={() =>\n toast({\n title: \"Profile saved\",\n description: \"Your changes were applied.\",\n variant: \"success\",\n icon: <CheckCircle2 size={18} strokeWidth={2} aria-hidden />,\n })\n }\n >\n Success\n </Button>\n <Button\n variant=\"secondary\"\n onClick={() =>\n toast({\n title: \"Sync in progress\",\n description: \"This may take a few seconds.\",\n variant: \"warning\",\n icon: <AlertTriangle size={18} strokeWidth={2} aria-hidden />,\n })\n }\n >\n Warning\n </Button>\n <Button\n variant=\"secondary\"\n onClick={() =>\n toast({\n title: \"Upload failed\",\n description: \"The file could not be uploaded.\",\n variant: \"error\",\n icon: <CircleX size={18} strokeWidth={2} aria-hidden />,\n action: {\n label: \"Retry\",\n onClick: () => {\n toast({\n title: \"Retrying upload\",\n variant: \"default\",\n icon: <Info size={18} strokeWidth={2} aria-hidden />,\n });\n },\n },\n })\n }\n >\n Error with action\n </Button>\n </div>\n );\n}\n\nexport function ToastExample() {\n return (\n <ToastProvider>\n <ToastTriggers />\n <Toaster />\n </ToastProvider>\n );\n}\n"
1170
+ }
1171
+ ],
1172
+ "tooltip": [
1173
+ {
1174
+ "file": "tooltip.example.tsx",
1175
+ "importName": "tooltipSource",
1176
+ "title": "Tooltip",
1177
+ "components": [
1178
+ "Tooltip"
1179
+ ],
1180
+ "source": "import { Tooltip } from \"@rfdtech/components\";\n\nexport function TooltipExample() {\n return (\n <div\n style={{\n display: \"flex\",\n gap: 16,\n alignItems: \"center\",\n justifyContent: \"center\",\n padding: 48,\n }}\n >\n <Tooltip content=\"Top tooltip\" side=\"top\">\n <button\n type=\"button\"\n style={{\n padding: \"8px 14px\",\n border: \"1px solid var(--clet-border)\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"var(--clet-bg)\",\n color: \"var(--clet-text)\",\n fontFamily: \"var(--clet-font)\",\n fontSize: 14,\n cursor: \"default\",\n }}\n >\n Top\n </button>\n </Tooltip>\n <Tooltip content=\"Right tooltip\" side=\"right\">\n <button\n type=\"button\"\n style={{\n padding: \"8px 14px\",\n border: \"1px solid var(--clet-border)\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"var(--clet-bg)\",\n color: \"var(--clet-text)\",\n fontFamily: \"var(--clet-font)\",\n fontSize: 14,\n cursor: \"default\",\n }}\n >\n Right\n </button>\n </Tooltip>\n <Tooltip content=\"Bottom tooltip\" side=\"bottom\">\n <button\n type=\"button\"\n style={{\n padding: \"8px 14px\",\n border: \"1px solid var(--clet-border)\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"var(--clet-bg)\",\n color: \"var(--clet-text)\",\n fontFamily: \"var(--clet-font)\",\n fontSize: 14,\n cursor: \"default\",\n }}\n >\n Bottom\n </button>\n </Tooltip>\n <Tooltip content=\"Left tooltip\" side=\"left\">\n <button\n type=\"button\"\n style={{\n padding: \"8px 14px\",\n border: \"1px solid var(--clet-border)\",\n borderRadius: \"var(--clet-radius-base)\",\n background: \"var(--clet-bg)\",\n color: \"var(--clet-text)\",\n fontFamily: \"var(--clet-font)\",\n fontSize: 14,\n cursor: \"default\",\n }}\n >\n Left\n </button>\n </Tooltip>\n </div>\n );\n}\n"
1181
+ }
1182
+ ],
1183
+ "upload-field": [
1184
+ {
1185
+ "file": "upload-field-form.example.tsx",
1186
+ "importName": "uploadFieldFormSource",
1187
+ "title": "Upload Field Form",
1188
+ "components": [
1189
+ "Button",
1190
+ "Dialog",
1191
+ "DialogContent",
1192
+ "DialogDescription",
1193
+ "DialogTitle",
1194
+ "Field",
1195
+ "FieldControl",
1196
+ "FieldError",
1197
+ "FieldLabel",
1198
+ "Form",
1199
+ "FormField",
1200
+ "UploadField"
1201
+ ],
1202
+ "source": "import { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useState } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogTitle,\n Field,\n FieldControl,\n FieldError,\n FieldLabel,\n Form,\n FormField,\n UploadField,\n} from \"@rfdtech/components\";\n\nconst ACCEPTED_TYPES = [\"image/png\", \"image/jpeg\", \"application/pdf\"];\nconst MAX_SIZE = 5 * 1024 * 1024; // 5 MB\nconst MIN_SIZE = 1024; // 1 KB\nconst MAX_NAME_LENGTH = 100;\n\nfunction formatSize(bytes: number) {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nconst uploadSchema = z.object({\n file: z\n .instanceof(File, { message: \"Please select a file\" })\n .refine((f) => ACCEPTED_TYPES.includes(f.type), {\n message: \"Only PNG, JPEG, and PDF files are accepted\",\n })\n .refine((f) => f.size >= MIN_SIZE, {\n message: \"File must be at least 1 KB\",\n })\n .refine((f) => f.size <= MAX_SIZE, {\n message: \"File size must be 5 MB or less\",\n })\n .refine((f) => f.name.length <= MAX_NAME_LENGTH, {\n message: \"File name must be 100 characters or less\",\n }),\n});\n\ntype UploadFormValues = z.infer<typeof uploadSchema>;\n\nexport function UploadFieldFormExample() {\n const [dialogOpen, setDialogOpen] = useState(false);\n const [result, setResult] = useState<{\n success: boolean;\n fileName?: string;\n fileSize?: number;\n } | null>(null);\n\n const form = useForm<UploadFormValues>({\n resolver: zodResolver(uploadSchema),\n defaultValues: { file: undefined },\n });\n\n function onSubmit(data: UploadFormValues) {\n const file = data.file!;\n setResult({ success: true, fileName: file.name, fileSize: file.size });\n setDialogOpen(true);\n }\n\n function onInvalid() {\n setResult({ success: false });\n setDialogOpen(true);\n }\n\n return (\n <div style={{ maxWidth: 480, width: \"100%\" }}>\n <Form {...form}>\n <form\n onSubmit={form.handleSubmit(onSubmit, onInvalid)}\n style={{ display: \"grid\", gap: 16, width: \"100%\" }}\n >\n <FormField\n control={form.control}\n name=\"file\"\n render={({ field, fieldState }) => (\n <Field invalid={!!fieldState.error} style={{ minWidth: \"100%\", flex: 1 }}>\n <FieldLabel>Upload file</FieldLabel>\n <FieldControl>\n <div style={{ width: \"100%\" }}>\n <UploadField\n accept=\".png,.jpg,.jpeg,.pdf\"\n maxSize={MAX_SIZE}\n value={field.value ?? null}\n onChange={field.onChange}\n name={field.name}\n ref={field.ref}\n />\n </div>\n </FieldControl>\n <FieldError>{fieldState.error?.message}</FieldError>\n </Field>\n )}\n />\n <Button type=\"submit\" variant=\"primary\" style={{ width: \"100%\" }}>\n Submit\n </Button>\n </form>\n </Form>\n\n <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>\n <DialogContent showCloseButton>\n <DialogTitle>\n {result?.success ? \"File accepted\" : \"Validation failed\"}\n </DialogTitle>\n <DialogDescription>\n {result?.success\n ? `\"${result.fileName}\" (${formatSize(result.fileSize!)}) is valid.`\n : \"Please fix the validation errors and try again.\"}\n </DialogDescription>\n </DialogContent>\n </Dialog>\n </div>\n );\n}\n"
1203
+ },
1204
+ {
1205
+ "file": "upload-field-multiple.example.tsx",
1206
+ "importName": "uploadFieldMultipleSource",
1207
+ "title": "Upload Field Multiple",
1208
+ "components": [
1209
+ "UploadField"
1210
+ ],
1211
+ "source": "import { UploadField } from \"@rfdtech/components\";\nimport { useState } from \"react\";\n\nexport function UploadFieldMultipleExample() {\n const [files, setFiles] = useState<File[] | null>(null);\n\n return (\n <UploadField\n multiple\n accept=\"image/*,.pdf\"\n value={files}\n onChange={(v) => setFiles(v as File[] | null)}\n />\n );\n}\n"
1212
+ },
1213
+ {
1214
+ "file": "upload-field-status-cards.example.tsx",
1215
+ "importName": "uploadFieldStatusCardsSource",
1216
+ "title": "Upload Field Status Cards",
1217
+ "components": [
1218
+ "UploadField",
1219
+ "UploadFieldFileStatus"
1220
+ ],
1221
+ "source": "import { UploadField } from \"@rfdtech/components\";\nimport type { UploadFieldFileStatus } from \"@rfdtech/components\";\n\nfunction fakeFile(name: string, type: string, size: number) {\n return new File([new ArrayBuffer(size)], name, { type });\n}\n\nconst files = [\n fakeFile(\"my-cv.pdf\", \"application/pdf\", 120 * 1024),\n fakeFile(\"my-cv.pdf\", \"application/pdf\", 120 * 1024),\n fakeFile(\"my-cv.pdf\", \"application/pdf\", 120 * 1024),\n];\n\nconst statuses: UploadFieldFileStatus[] = [\n { status: \"uploading\", progress: 45 },\n { status: \"completed\" },\n { status: \"failed\", error: \"Upload failed\" },\n];\n\nexport function UploadFieldStatusCardsExample() {\n return (\n <UploadField\n multiple\n value={files}\n fileStatuses={statuses}\n onChange={() => {}}\n onCancel={(file) => console.log(\"cancel\", file.name)}\n onRetry={(file) => console.log(\"retry\", file.name)}\n />\n );\n}\n"
1222
+ },
1223
+ {
1224
+ "file": "upload-field.example.tsx",
1225
+ "importName": "uploadFieldSource",
1226
+ "title": "Upload Field",
1227
+ "components": [
1228
+ "UploadField"
1229
+ ],
1230
+ "source": "import { UploadField } from \"@rfdtech/components\";\n\nexport function UploadFieldExample() {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 16, maxWidth: 480 }}>\n <UploadField accept=\".csv\" maxSize={10 * 1024 * 1024} onChange={(f) => console.log(f)} />\n <UploadField invalid />\n <UploadField disabled />\n </div>\n );\n}\n"
1231
+ }
1232
+ ]
1233
+ }