@tsiky/components-r19 1.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 (287) hide show
  1. package/.prettierignore +4 -0
  2. package/.prettierrc +9 -0
  3. package/.storybook/main.ts +17 -0
  4. package/.storybook/preview.ts +21 -0
  5. package/.storybook/vitest.setup.ts +7 -0
  6. package/README.md +69 -0
  7. package/chart.ts +1 -0
  8. package/eslint.config.js +40 -0
  9. package/index.html +13 -0
  10. package/index.ts +33 -0
  11. package/package.json +63 -0
  12. package/public/vite.svg +1 -0
  13. package/src/App.css +42 -0
  14. package/src/App.tsx +12 -0
  15. package/src/assets/accessibility.png +0 -0
  16. package/src/assets/accessibility.svg +1 -0
  17. package/src/assets/addon-library.png +0 -0
  18. package/src/assets/assets.png +0 -0
  19. package/src/assets/avif-test-image.avif +0 -0
  20. package/src/assets/context.png +0 -0
  21. package/src/assets/discord.svg +1 -0
  22. package/src/assets/docs.png +0 -0
  23. package/src/assets/figma-plugin.png +0 -0
  24. package/src/assets/github.svg +1 -0
  25. package/src/assets/react.svg +1 -0
  26. package/src/assets/share.png +0 -0
  27. package/src/assets/styling.png +0 -0
  28. package/src/assets/testing.png +0 -0
  29. package/src/assets/theming.png +0 -0
  30. package/src/assets/tutorials.svg +1 -0
  31. package/src/assets/youtube.svg +1 -0
  32. package/src/components/AddItemModal/AddItemModal.module.css +72 -0
  33. package/src/components/AddItemModal/AddItemModal.tsx +82 -0
  34. package/src/components/AddItemModal/index.ts +1 -0
  35. package/src/components/Alert/Alert.css +54 -0
  36. package/src/components/Alert/Alert.stories.tsx +82 -0
  37. package/src/components/Alert/Alert.tsx +85 -0
  38. package/src/components/Alert/AlertContext.tsx +200 -0
  39. package/src/components/Alert/AlertModels.ts +34 -0
  40. package/src/components/Alert/index.ts +3 -0
  41. package/src/components/AnnouncementPanel/FlexRowContainer.css +17 -0
  42. package/src/components/AnnouncementPanel/FlexRowContainer.stories.tsx +329 -0
  43. package/src/components/AnnouncementPanel/FlexRowContainer.tsx +24 -0
  44. package/src/components/AnnouncementPanel/ListBox/CounterListBox.css +56 -0
  45. package/src/components/AnnouncementPanel/ListBox/CounterListBox.stories.tsx +292 -0
  46. package/src/components/AnnouncementPanel/ListBox/CounterListBox.tsx +106 -0
  47. package/src/components/AnnouncementPanel/ListBox/SimpleListBox.css +57 -0
  48. package/src/components/AnnouncementPanel/ListBox/SimpleListBox.stories.tsx +189 -0
  49. package/src/components/AnnouncementPanel/ListBox/SimpleListBox.tsx +138 -0
  50. package/src/components/AnnouncementPanel/ListBox/TrendListBox.css +61 -0
  51. package/src/components/AnnouncementPanel/ListBox/TrendListBox.stories.tsx +257 -0
  52. package/src/components/AnnouncementPanel/ListBox/TrendListBox.tsx +90 -0
  53. package/src/components/AnnouncementPanel/ListBox/index.ts +3 -0
  54. package/src/components/AnnouncementPanel/ListContentContainer.css +23 -0
  55. package/src/components/AnnouncementPanel/ListContentContainer.stories.tsx +212 -0
  56. package/src/components/AnnouncementPanel/ListContentContainer.tsx +33 -0
  57. package/src/components/AnnouncementPanel/index.ts +3 -0
  58. package/src/components/BandChart/index.tsx +282 -0
  59. package/src/components/Button/Button.module.css +165 -0
  60. package/src/components/Button/Button.stories.ts +132 -0
  61. package/src/components/Button/Button.tsx +55 -0
  62. package/src/components/Button/button.css +29 -0
  63. package/src/components/Button/index.ts +2 -0
  64. package/src/components/ChartContainer/ChartContainer.css +116 -0
  65. package/src/components/ChartContainer/ChartContainer.stories.tsx +159 -0
  66. package/src/components/ChartContainer/ChartContainer.tsx +155 -0
  67. package/src/components/ChartContainer/index.ts +1 -0
  68. package/src/components/Charts/area-chart-admission/AreaChartAdmission.stories.tsx +65 -0
  69. package/src/components/Charts/area-chart-admission/AreaChartAdmission.tsx +89 -0
  70. package/src/components/Charts/area-chart-admission/content.json +48 -0
  71. package/src/components/Charts/area-chart-hospitalisation/AreaChartHospitalisation.stories.tsx +141 -0
  72. package/src/components/Charts/area-chart-hospitalisation/AreaChartHospitalisation.tsx +262 -0
  73. package/src/components/Charts/area-chart-hospitalisation/content.json +55 -0
  74. package/src/components/Charts/bar-chart/BarChart.stories.tsx +50 -0
  75. package/src/components/Charts/bar-chart/BarChart.tsx +132 -0
  76. package/src/components/Charts/bar-chart/content.json +15 -0
  77. package/src/components/Charts/boxplot-chart/BoxPlotChart.stories.tsx +63 -0
  78. package/src/components/Charts/boxplot-chart/BoxPlotChart.tsx +114 -0
  79. package/src/components/Charts/boxplot-chart/boxUtils.ts +22 -0
  80. package/src/components/Charts/boxplot-chart/content.json +11 -0
  81. package/src/components/Charts/mixed-chart/MixedChart.stories.tsx +83 -0
  82. package/src/components/Charts/mixed-chart/MixedChart.tsx +625 -0
  83. package/src/components/Charts/mixed-chart/content.json +34 -0
  84. package/src/components/Charts/sankey-adaptation/sankey.tsx +70 -0
  85. package/src/components/Charts/sankey-chart/SankeyChart.stories.tsx +69 -0
  86. package/src/components/Charts/sankey-chart/SankeyChart.tsx +155 -0
  87. package/src/components/Charts/sankey-chart/content.json +15 -0
  88. package/src/components/Charts/stacked-column/StackedColumn.stories.tsx +72 -0
  89. package/src/components/Charts/stacked-column/StackedColumn.tsx +406 -0
  90. package/src/components/Charts/stacked-column/content.json +37 -0
  91. package/src/components/Charts/stacked-column-one-hundred-percent/StackedColumnOneHundredPercent.stories.tsx +43 -0
  92. package/src/components/Charts/stacked-column-one-hundred-percent/StackedColumnOneHundredPercent.tsx +75 -0
  93. package/src/components/Charts/stacked-column-one-hundred-percent/content.json +6 -0
  94. package/src/components/CircularProgress/CircularProgress.css +79 -0
  95. package/src/components/CircularProgress/CircularProgress.stories.tsx +251 -0
  96. package/src/components/CircularProgress/CircularProgress.tsx +101 -0
  97. package/src/components/CircularProgress/index.ts +2 -0
  98. package/src/components/Configure.mdx +369 -0
  99. package/src/components/DayStatCard/DayStatCard.css +50 -0
  100. package/src/components/DayStatCard/DayStatCard.stories.tsx +273 -0
  101. package/src/components/DayStatCard/DayStatCard.tsx +69 -0
  102. package/src/components/DayStatCard/index.ts +2 -0
  103. package/src/components/DraggableSwitcher/DraggableSwitcherButton.tsx +58 -0
  104. package/src/components/DraggableSwitcher/context/useDraggableSwitcher.tsx +45 -0
  105. package/src/components/DraggableSwitcher/index.ts +2 -0
  106. package/src/components/DropdownMenu/DropdownMenu.css +100 -0
  107. package/src/components/DropdownMenu/DropdownMenu.stories.tsx +174 -0
  108. package/src/components/DropdownMenu/DropdownMenu.tsx +106 -0
  109. package/src/components/DropdownMenu/index.ts +1 -0
  110. package/src/components/DynamicForm/DynamicFom.stories.ts +773 -0
  111. package/src/components/DynamicForm/DynamicForm.module.css +468 -0
  112. package/src/components/DynamicForm/DynamicForm.tsx +224 -0
  113. package/src/components/DynamicForm/index.ts +3 -0
  114. package/src/components/DynamicForm/tools/form-metadata.ts +82 -0
  115. package/src/components/DynamicForm/tools/validation.ts +168 -0
  116. package/src/components/DynamicInput/DynamicInput.module.css +126 -0
  117. package/src/components/DynamicInput/DynamicInput.stories.ts +350 -0
  118. package/src/components/DynamicInput/DynamicInput.tsx +144 -0
  119. package/src/components/DynamicInput/index.ts +2 -0
  120. package/src/components/DynamicInput/input/CheckboxInput.tsx +41 -0
  121. package/src/components/DynamicInput/input/ColorInput.tsx +46 -0
  122. package/src/components/DynamicInput/input/DateInput.tsx +57 -0
  123. package/src/components/DynamicInput/input/FileInput.tsx +174 -0
  124. package/src/components/DynamicInput/input/InputWrapper.tsx +26 -0
  125. package/src/components/DynamicInput/input/RadioInput.tsx +46 -0
  126. package/src/components/DynamicInput/input/RangeInput.tsx +46 -0
  127. package/src/components/DynamicInput/input/SelectInput.tsx +75 -0
  128. package/src/components/DynamicInput/input/TextInput.tsx +101 -0
  129. package/src/components/DynamicInput/input/TextareaInput.tsx +47 -0
  130. package/src/components/DynamicInput/input/assets/ColorInput.module.css +48 -0
  131. package/src/components/DynamicInput/input/assets/DateInput.module.css +83 -0
  132. package/src/components/DynamicInput/input/assets/FileInput.module.css +133 -0
  133. package/src/components/DynamicInput/input/assets/RangeInput.module.css +169 -0
  134. package/src/components/DynamicInput/input/assets/SelectInput.module.css +95 -0
  135. package/src/components/DynamicInput/input/index.ts +10 -0
  136. package/src/components/DynamicTable/AdvancedFilters.tsx +196 -0
  137. package/src/components/DynamicTable/ColumnSorter.tsx +185 -0
  138. package/src/components/DynamicTable/Pagination.tsx +115 -0
  139. package/src/components/DynamicTable/TableBody.tsx +42 -0
  140. package/src/components/DynamicTable/TableCell.tsx +30 -0
  141. package/src/components/DynamicTable/TableHeader.tsx +34 -0
  142. package/src/components/DynamicTable/TableRow.tsx +56 -0
  143. package/src/components/DynamicTable/TableauDynamic.stories.ts +1014 -0
  144. package/src/components/DynamicTable/TableauDynamique.module.css +1287 -0
  145. package/src/components/DynamicTable/TableauDynamique.tsx +154 -0
  146. package/src/components/DynamicTable/filters/BooleanFilter.tsx +30 -0
  147. package/src/components/DynamicTable/filters/DateFilter.tsx +28 -0
  148. package/src/components/DynamicTable/filters/DateRangeFilter.tsx +51 -0
  149. package/src/components/DynamicTable/filters/FilterRenderer.tsx +117 -0
  150. package/src/components/DynamicTable/filters/MultiSelectFilter.tsx +59 -0
  151. package/src/components/DynamicTable/filters/NumberFilter.tsx +37 -0
  152. package/src/components/DynamicTable/filters/OperatorFilter.tsx +64 -0
  153. package/src/components/DynamicTable/filters/SelectFilter.tsx +69 -0
  154. package/src/components/DynamicTable/filters/TextFilter.tsx +39 -0
  155. package/src/components/DynamicTable/filters/index.ts +9 -0
  156. package/src/components/DynamicTable/hooks/index.ts +2 -0
  157. package/src/components/DynamicTable/hooks/useAsyncActions.ts +36 -0
  158. package/src/components/DynamicTable/hooks/useFilters.ts +142 -0
  159. package/src/components/DynamicTable/hooks/useTableData.ts +216 -0
  160. package/src/components/DynamicTable/index.ts +11 -0
  161. package/src/components/DynamicTable/tools/filterTypes.ts +118 -0
  162. package/src/components/DynamicTable/tools/index.ts +3 -0
  163. package/src/components/DynamicTable/tools/tableConfig.ts +96 -0
  164. package/src/components/DynamicTable/tools/tableTypes.ts +63 -0
  165. package/src/components/EntryControl/EntryControl.module.css +218 -0
  166. package/src/components/EntryControl/EntryControl.stories.tsx +71 -0
  167. package/src/components/EntryControl/EntryControl.tsx +117 -0
  168. package/src/components/EntryControl/index.ts +2 -0
  169. package/src/components/Grid/Grid.stories.tsx +94 -0
  170. package/src/components/Grid/Grid.tsx +214 -0
  171. package/src/components/Grid/grid.css +285 -0
  172. package/src/components/Grid/index.ts +2 -0
  173. package/src/components/Header/Header.stories.tsx +164 -0
  174. package/src/components/Header/Header.tsx +59 -0
  175. package/src/components/Header/header.css +31 -0
  176. package/src/components/Header/index.ts +1 -0
  177. package/src/components/IconText/IconText..stories.tsx +135 -0
  178. package/src/components/IconText/IconText.css +43 -0
  179. package/src/components/IconText/IconText.tsx +43 -0
  180. package/src/components/IconText/index.ts +1 -0
  181. package/src/components/LanguageSelector/LanguageSelector.css +31 -0
  182. package/src/components/LanguageSelector/LanguageSelector.stories.tsx +38 -0
  183. package/src/components/LanguageSelector/LanguageSelector.tsx +37 -0
  184. package/src/components/LanguageSelector/index.ts +1 -0
  185. package/src/components/Logo/Logo.css +89 -0
  186. package/src/components/Logo/Logo.stories.tsx +79 -0
  187. package/src/components/Logo/Logo.tsx +22 -0
  188. package/src/components/Logo/index.ts +2 -0
  189. package/src/components/MetricsPanel/MetricsItem.tsx +128 -0
  190. package/src/components/MetricsPanel/MetricsPanel.module.css +636 -0
  191. package/src/components/MetricsPanel/MetricsPanel.stories.tsx +692 -0
  192. package/src/components/MetricsPanel/MetricsPanel.tsx +282 -0
  193. package/src/components/MetricsPanel/PanelHeader.tsx +19 -0
  194. package/src/components/MetricsPanel/SummaryCard.tsx +61 -0
  195. package/src/components/MetricsPanel/index.ts +4 -0
  196. package/src/components/MetricsPanel/renderers/CompactRenderer.tsx +125 -0
  197. package/src/components/MetricsPanel/renderers/ImageCard.tsx +62 -0
  198. package/src/components/MetricsPanel/renderers/PertinenceCard.tsx +55 -0
  199. package/src/components/MetricsPanel/tools/MetricsPanelTypes.ts +62 -0
  200. package/src/components/MetricsPanel/tools/chooseDefaultRender.ts +50 -0
  201. package/src/components/MetricsPanel/tools/colorUtils.ts +39 -0
  202. package/src/components/MetricsPanel/tools/index.ts +2 -0
  203. package/src/components/ModuleHeader/ModuleHeader.css +37 -0
  204. package/src/components/ModuleHeader/ModuleHeader.stories.tsx +37 -0
  205. package/src/components/ModuleHeader/ModuleHeader.tsx +42 -0
  206. package/src/components/ModuleHeader/index.ts +1 -0
  207. package/src/components/ModuleSideBar/ModuleSideBar.css +227 -0
  208. package/src/components/ModuleSideBar/ModuleSideBar.stories.tsx +40 -0
  209. package/src/components/ModuleSideBar/ModuleSideBar.tsx +155 -0
  210. package/src/components/ModuleSideBar/index.ts +1 -0
  211. package/src/components/NavBar/NavBar.css +58 -0
  212. package/src/components/NavBar/NavBar.stories.tsx +169 -0
  213. package/src/components/NavBar/NavBar.tsx +100 -0
  214. package/src/components/NavBar/NavContext.tsx +30 -0
  215. package/src/components/NavBar/index.ts +2 -0
  216. package/src/components/NavItem/NavItem.css +29 -0
  217. package/src/components/NavItem/NavItem.tsx +58 -0
  218. package/src/components/NavItem/index.ts +1 -0
  219. package/src/components/Page/Dashboard.tsx +93 -0
  220. package/src/components/Page/Page.stories.ts +33 -0
  221. package/src/components/Page/Page.tsx +73 -0
  222. package/src/components/Page/page.css +81 -0
  223. package/src/components/PerformanceCard/PerformanceCard.module.css +232 -0
  224. package/src/components/PerformanceCard/PerformanceCard.stories.tsx +441 -0
  225. package/src/components/PerformanceCard/PerformanceCard.tsx +198 -0
  226. package/src/components/PerformanceCard/defaultHistogramRenderer.tsx +32 -0
  227. package/src/components/PerformanceCard/tools/types.ts +50 -0
  228. package/src/components/PerformanceCard/tools/usePerformanceCard.ts +93 -0
  229. package/src/components/PeriodRange/PeriodRange.module.css +158 -0
  230. package/src/components/PeriodRange/PeriodRange.stories.tsx +66 -0
  231. package/src/components/PeriodRange/PeriodRange.tsx +130 -0
  232. package/src/components/PeriodSelect/PeriodSelect.module.css +65 -0
  233. package/src/components/PeriodSelect/PeriodSelect.stories.tsx +40 -0
  234. package/src/components/PeriodSelect/PeriodSelect.tsx +42 -0
  235. package/src/components/PeriodSelect/index.ts +1 -0
  236. package/src/components/ScrollableHorizontale/ScrollableHorizontale.css +40 -0
  237. package/src/components/ScrollableHorizontale/ScrollableHorizontale.stories.tsx +133 -0
  238. package/src/components/ScrollableHorizontale/ScrollableHorizontale.tsx +29 -0
  239. package/src/components/ScrollableHorizontale/index.ts +1 -0
  240. package/src/components/SearchBar/SearchBar.css +40 -0
  241. package/src/components/SearchBar/SearchBar.stories.tsx +36 -0
  242. package/src/components/SearchBar/SearchBar.tsx +30 -0
  243. package/src/components/SearchBar/index.ts +1 -0
  244. package/src/components/SectionTitle/SectionTitle.css +21 -0
  245. package/src/components/SectionTitle/SectionTitle.stories.tsx +39 -0
  246. package/src/components/SectionTitle/SectionTitle.tsx +18 -0
  247. package/src/components/SideComponent/PatientEditor.tsx +64 -0
  248. package/src/components/SideComponent/SideComponent.css +179 -0
  249. package/src/components/SideComponent/SideComponent.stories.tsx +547 -0
  250. package/src/components/SideComponent/SideComponent.tsx +243 -0
  251. package/src/components/SideComponent/hooks/useBodyScrollLock.ts +15 -0
  252. package/src/components/SideComponent/index.ts +2 -0
  253. package/src/components/SideComponent/portal.ts +11 -0
  254. package/src/components/SubNavBar/SubNavBar.tsx +41 -0
  255. package/src/components/SubNavBar/index.ts +1 -0
  256. package/src/components/Switcher/Switcher.css +65 -0
  257. package/src/components/Switcher/Switcher.stories.tsx +153 -0
  258. package/src/components/Switcher/Switcher.tsx +83 -0
  259. package/src/components/Switcher/index.ts +1 -0
  260. package/src/components/Title/Title.stories.tsx +18 -0
  261. package/src/components/Title/Title.tsx +26 -0
  262. package/src/components/Title/index.ts +1 -0
  263. package/src/components/TranslationKey/TranslationKey.css +272 -0
  264. package/src/components/TranslationKey/TranslationKey.stories.tsx +50 -0
  265. package/src/components/TranslationKey/TranslationKey.tsx +245 -0
  266. package/src/components/TranslationKey/index.ts +1 -0
  267. package/src/components/TrendItem/TrendItem.css +36 -0
  268. package/src/components/TrendItem/TrendItem.stories.tsx +276 -0
  269. package/src/components/TrendItem/TrendItem.tsx +46 -0
  270. package/src/components/TrendItem/index.ts +1 -0
  271. package/src/components/TrendList/TrendList.css +16 -0
  272. package/src/components/TrendList/TrendList.stories.tsx +337 -0
  273. package/src/components/TrendList/TrendList.tsx +45 -0
  274. package/src/components/TrendList/index.ts +1 -0
  275. package/src/components/theme/ThemeSwitcherButton.tsx +64 -0
  276. package/src/components/theme/context/ThemeContext.tsx +61 -0
  277. package/src/components/theme/context/index.ts +2 -0
  278. package/src/components/theme/context/useThemeSwitcher.ts +18 -0
  279. package/src/components/theme/index.ts +1 -0
  280. package/src/index.css +68 -0
  281. package/src/main.tsx +10 -0
  282. package/src/vite-env.d.ts +1 -0
  283. package/tsconfig.app.json +27 -0
  284. package/tsconfig.json +4 -0
  285. package/tsconfig.node.json +25 -0
  286. package/vite.config.ts +43 -0
  287. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,468 @@
1
+ /* DynamicForm.module.css - corrected for overlap & textarea sizing */
2
+
3
+ /* ===== Variables (facile à ajuster) ===== */
4
+ /* :root {
5
+ --bg: #ffffff;
6
+ --card-border: rgba(15, 23, 42, 0.04);
7
+ --muted: #6b7280;
8
+ --text: #111827;
9
+ --primary-600: #6b21a8;
10
+ --primary-700: #5b21a6;
11
+ --focus-ring: rgba(107, 33, 168, 0.08);
12
+ --error: #dc3545;
13
+ --success: #198754;
14
+ --radius: 10px;
15
+ --gap: 16px;
16
+ --field-height: 44px;
17
+ --font-stack: Inter, 'Helvetica Neue', Arial, sans-serif;
18
+ } */
19
+
20
+ /* Global box-sizing for the form to avoid width calculation issues */
21
+ .formContainer,
22
+ .formContainer * {
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ /* ===== Form container & header ===== */
27
+ .formContainer {
28
+ background: var(--bg);
29
+ border-radius: var(--radius);
30
+ padding: 28px;
31
+ box-shadow: 0 6px 18px rgba(16, 24, 40, 0.06);
32
+ border: 1px solid var(--card-border);
33
+ color: var(--text);
34
+ line-height: 1.35;
35
+ width: 100%;
36
+ overflow: visible;
37
+ }
38
+
39
+ /* header */
40
+ .headerLine {
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: space-between;
44
+ gap: 12px;
45
+ }
46
+
47
+ /* ... (conserve tes autres styles d'en-tête) ... */
48
+ .formTitle {
49
+ font-size: 18px;
50
+ font-weight: 700;
51
+ color: var(--text);
52
+ margin: 0;
53
+ }
54
+ .formDescription {
55
+ margin: 8px 0 18px 0;
56
+ color: var(--muted);
57
+ font-size: 13px;
58
+ }
59
+
60
+ /* ===== Sections & helper text ===== */
61
+ .sectionTitle {
62
+ font-size: 15px;
63
+ font-weight: 700;
64
+ color: var(--text);
65
+ margin: 20px 0 8px 0;
66
+ }
67
+ .sectionHelp {
68
+ margin: 0 0 12px 0;
69
+ color: var(--muted);
70
+ font-size: 13px;
71
+ }
72
+ .fieldHelp {
73
+ display: block;
74
+ margin-top: 6px;
75
+ color: var(--muted);
76
+ font-size: 12px;
77
+ }
78
+
79
+ /* ===== Grid (container) ===== */
80
+ .gridContainer {
81
+ display: grid;
82
+ gap: var(--gap);
83
+ align-items: start;
84
+ margin-bottom: 12px;
85
+ /* gridTemplateColumns set inline from React; recommend using minmax(0,1fr) there */
86
+ grid-auto-flow: row;
87
+ /* critical: allow cells to shrink (avoid overflow) */
88
+ min-width: 0;
89
+ }
90
+
91
+ /* each grid cell must be able to shrink */
92
+ .gridContainer > .fieldGridItem {
93
+ display: flex;
94
+ flex-direction: column;
95
+ width: 100%;
96
+ min-width: 0; /* prevents children from forcing cell wider than available */
97
+ }
98
+
99
+ /* Ensure any element inside a grid cell can shrink */
100
+ .gridContainer > .fieldGridItem * {
101
+ min-width: 0;
102
+ }
103
+
104
+ /* Optional: equalize sibling fields when you can add wrapper .equalize-group */
105
+ .equalize-group {
106
+ display: flex;
107
+ gap: var(--gap);
108
+ align-items: stretch;
109
+ width: 100%;
110
+ }
111
+ .equalize-group > .fieldGridItem {
112
+ flex: 1 1 0;
113
+ min-width: 0;
114
+ }
115
+
116
+ /* Field item */
117
+ .fieldGridItem {
118
+ display: flex;
119
+ flex-direction: column;
120
+ width: 100%;
121
+ min-width: 0;
122
+ }
123
+
124
+ /* Section full width */
125
+ .sectionFull {
126
+ grid-column: 1 / -1;
127
+ }
128
+
129
+ /* ===== Form footer ===== */
130
+ .formFooter {
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: space-between;
134
+ gap: 12px;
135
+ margin-top: 20px;
136
+ }
137
+ .footerLeft,
138
+ .footerRight {
139
+ display: flex;
140
+ align-items: center;
141
+ }
142
+
143
+ /* cancel link / submit button keep same styles as before */
144
+ .cancelLink {
145
+ background: none;
146
+ border: none;
147
+ color: #374151;
148
+ font-size: 14px;
149
+ cursor: pointer;
150
+ padding: 0;
151
+ display: inline-flex;
152
+ align-items: center;
153
+ }
154
+ .submitButton {
155
+ background: linear-gradient(180deg, var(--primary-600) 0%, var(--primary-700) 100%);
156
+ border: none;
157
+ color: #fff !important;
158
+ padding: 10px 20px;
159
+ border-radius: 10px;
160
+ box-shadow: 0 6px 12px rgba(107, 33, 168, 0.12);
161
+ font-weight: 700;
162
+ letter-spacing: 0.4px;
163
+ }
164
+
165
+ /* ===== Labels ===== */
166
+ .fieldLabel {
167
+ font-weight: 600;
168
+ color: var(--text);
169
+ margin-bottom: 8px;
170
+ display: block;
171
+ font-size: 14px;
172
+ }
173
+ .fieldLabel.required::after {
174
+ content: ' *';
175
+ color: var(--error);
176
+ font-weight: 700;
177
+ margin-left: 2px;
178
+ }
179
+
180
+ /* ===== Input general layout ===== */
181
+ .inputWrapper {
182
+ display: block;
183
+ width: 100%;
184
+ min-width: 0;
185
+ }
186
+ .controlRow {
187
+ display: flex;
188
+ align-items: center;
189
+ gap: 12px;
190
+ }
191
+ .controlRow .fieldLabel {
192
+ width: 160px;
193
+ margin-bottom: 0;
194
+ text-align: right;
195
+ }
196
+ .controlRow .inputWrapper {
197
+ flex: 1;
198
+ width: auto;
199
+ }
200
+
201
+ /* ===== Input inner: icon + input positioning ===== */
202
+ .inputInner {
203
+ position: relative;
204
+ display: flex;
205
+ align-items: center;
206
+ width: 100%;
207
+ min-width: 0;
208
+ }
209
+
210
+ /* ensure padding for prefix/suffix */
211
+ .inputInner.hasPrefix .inputBase {
212
+ padding-left: 40px;
213
+ }
214
+ .inputInner.hasSuffix .inputBase {
215
+ padding-right: 40px;
216
+ }
217
+
218
+ /* ===== Input base (text/select/textarea/file) ===== */
219
+ .inputBase,
220
+ .selectBase,
221
+ .textareaBase {
222
+ width: 100%;
223
+ max-width: 100%;
224
+ min-height: var(--field-height);
225
+ padding: 10px 12px;
226
+ border: 1px solid #e6e6e9;
227
+ border-radius: 10px;
228
+ font-size: 14px;
229
+ transition:
230
+ border-color 160ms ease,
231
+ box-shadow 160ms ease,
232
+ transform 120ms ease;
233
+ background: #fff;
234
+ color: var(--text);
235
+ -webkit-appearance: none;
236
+ min-width: 0; /* critical */
237
+ box-sizing: border-box;
238
+ }
239
+
240
+ /* placeholder color */
241
+ .inputBase::placeholder,
242
+ .textareaBase::placeholder {
243
+ color: #9ca3af;
244
+ }
245
+
246
+ /* focus / hover states */
247
+ .inputBase:focus,
248
+ .selectBase:focus,
249
+ .textareaBase:focus {
250
+ outline: none;
251
+ border-color: var(--primary-600);
252
+ box-shadow: 0 6px 18px var(--focus-ring);
253
+ transform: translateY(-1px);
254
+ }
255
+ .inputBase:hover,
256
+ .selectBase:hover,
257
+ .textareaBase:hover {
258
+ border-color: #dcdce0;
259
+ }
260
+
261
+ /* textarea specifics - prevent it overflowing its grid cell */
262
+ .textareaBase {
263
+ min-height: 120px;
264
+ resize: vertical;
265
+ padding-top: 12px;
266
+ padding-bottom: 12px;
267
+ width: 100%;
268
+ max-width: 100%;
269
+ min-width: 0;
270
+ overflow: auto;
271
+ box-sizing: border-box;
272
+ }
273
+
274
+ /* select specifics */
275
+ .selectBase {
276
+ min-height: var(--field-height);
277
+ padding-right: 36px;
278
+ }
279
+
280
+ /* file input */
281
+ .fileWrap {
282
+ display: flex;
283
+ gap: 8px;
284
+ align-items: center;
285
+ }
286
+ .fileButton {
287
+ padding: 8px 12px;
288
+ border-radius: 8px;
289
+ background: #f3f4f6;
290
+ border: 1px solid #e6e6e9;
291
+ cursor: pointer;
292
+ font-size: 14px;
293
+ }
294
+ .fileName {
295
+ color: var(--muted);
296
+ font-size: 13px;
297
+ }
298
+
299
+ /* disabled state */
300
+ .inputBase:disabled,
301
+ .selectBase:disabled,
302
+ .textareaBase:disabled {
303
+ background: #f8f9fb;
304
+ color: #6b7280;
305
+ cursor: not-allowed;
306
+ opacity: 0.95;
307
+ }
308
+
309
+ /* ===== Icons (prefix/suffix & password toggle) ===== */
310
+ .iconPrefix,
311
+ .iconSuffix {
312
+ position: absolute;
313
+ display: inline-flex;
314
+ align-items: center;
315
+ justify-content: center;
316
+ color: var(--muted);
317
+ padding: 6px;
318
+ border-radius: 6px;
319
+ transition:
320
+ background 120ms ease,
321
+ color 120ms ease;
322
+ }
323
+ .iconPrefix {
324
+ left: 8px;
325
+ top: 50%;
326
+ transform: translateY(-50%);
327
+ }
328
+ .iconSuffix {
329
+ right: 8px;
330
+ top: 50%;
331
+ transform: translateY(-50%);
332
+ }
333
+
334
+ /* password toggle */
335
+ .passwordToggle {
336
+ position: absolute;
337
+ right: 8px;
338
+ top: 50%;
339
+ transform: translateY(-50%);
340
+ background: transparent;
341
+ border: none;
342
+ cursor: pointer;
343
+ padding: 4px;
344
+ color: var(--muted);
345
+ display: inline-flex;
346
+ align-items: center;
347
+ justify-content: center;
348
+ }
349
+
350
+ /* ensure padding if suffix icon present */
351
+ .inputInner .iconSuffix ~ .inputBase,
352
+ .inputInner .passwordToggle ~ .inputBase {
353
+ padding-right: 44px;
354
+ }
355
+
356
+ /* ===== Choice controls ===== */
357
+ .choiceGroup {
358
+ display: flex;
359
+ gap: 12px;
360
+ align-items: center;
361
+ flex-wrap: wrap;
362
+ margin-top: 6px;
363
+ }
364
+ .choiceItem {
365
+ display: inline-flex;
366
+ align-items: center;
367
+ gap: 8px;
368
+ font-size: 14px;
369
+ color: var(--text);
370
+ }
371
+ .choiceItem input {
372
+ width: 16px;
373
+ height: 16px;
374
+ margin: 0;
375
+ accent-color: var(--primary-600);
376
+ }
377
+
378
+ /* ===== Error / valid states ===== */
379
+ .fieldError {
380
+ color: var(--error);
381
+ font-size: 13px;
382
+ margin-top: 8px;
383
+ display: block;
384
+ }
385
+ .fieldInvalid .inputBase,
386
+ .fieldInvalid .selectBase,
387
+ .fieldInvalid .textareaBase,
388
+ .inputBase.fieldInvalid,
389
+ .selectBase.fieldInvalid {
390
+ border-color: var(--error) !important;
391
+ box-shadow: 0 6px 18px rgba(220, 53, 69, 0.06);
392
+ }
393
+ .fieldValid .inputBase,
394
+ .fieldValid .selectBase {
395
+ border-color: var(--success) !important;
396
+ box-shadow: 0 6px 18px rgba(25, 135, 84, 0.04);
397
+ }
398
+
399
+ /* subtle label hint */
400
+ .optionalHint {
401
+ font-weight: 400;
402
+ color: #9ca3af;
403
+ font-size: 12px;
404
+ margin-left: 6px;
405
+ }
406
+
407
+ /* ===== Non-grid fallback (bootstrap .row override) =====
408
+ We keep columns flexible but ensure min-width:0 to avoid overflow & overlap.
409
+ */
410
+ .formContainer > .row {
411
+ display: flex;
412
+ gap: var(--gap);
413
+ flex-wrap: wrap;
414
+ align-items: flex-start;
415
+ margin-bottom: 12px;
416
+ min-width: 0;
417
+ }
418
+ .formContainer > .row > [class*='col-'] {
419
+ flex: 1 1 0;
420
+ max-width: none !important;
421
+ min-width: 0; /* critical to allow shrinking */
422
+ box-sizing: border-box;
423
+ padding-left: 0.5rem;
424
+ padding-right: 0.5rem;
425
+ }
426
+ .formContainer > .row > [class*='col-'] .inputWrapper {
427
+ width: 100%;
428
+ min-width: 0;
429
+ }
430
+
431
+ /* keep ability to allow static width columns if you want */
432
+ .formContainer > .row > .allow-static {
433
+ flex: 0 0 auto;
434
+ max-width: none;
435
+ }
436
+
437
+ /* ===== small screens: responsive stacking ===== */
438
+ @media (max-width: 768px) {
439
+ .formContainer {
440
+ padding: 18px;
441
+ }
442
+ .gridContainer {
443
+ grid-template-columns: 1fr !important;
444
+ }
445
+ .formContainer > .row {
446
+ display: block;
447
+ }
448
+ .formContainer > .row > [class*='col-'] {
449
+ width: 100%;
450
+ max-width: 100% !important;
451
+ padding-left: 0;
452
+ padding-right: 0;
453
+ margin-bottom: 12px;
454
+ }
455
+ .controlRow .fieldLabel {
456
+ width: auto;
457
+ text-align: left;
458
+ margin-bottom: 6px;
459
+ }
460
+ .formFooter {
461
+ flex-direction: column;
462
+ align-items: stretch;
463
+ gap: 10px;
464
+ }
465
+ .footerRight {
466
+ justify-content: flex-end;
467
+ }
468
+ }
@@ -0,0 +1,224 @@
1
+ /* DynamicForm.tsx */
2
+ import React, { useState, useEffect, useRef } from 'react';
3
+ import type { FormSchema, FieldMetadata } from './tools/form-metadata';
4
+ import { validateField, validateForm } from './tools/validation';
5
+ import { DynamicInput } from '../DynamicInput/DynamicInput';
6
+ import isEqual from 'lodash/isEqual';
7
+ import Button from '../Button/Button';
8
+ import styles from './DynamicForm.module.css';
9
+ import * as Icons from 'lucide-react';
10
+
11
+ export interface DynamicFormProps {
12
+ schema: FormSchema;
13
+ initialData?: Record<string, unknown>;
14
+ onSubmit: (data: Record<string, unknown>) => Promise<unknown> | void;
15
+ submitLabel?: string;
16
+ submitIcon?: string; // nom de l'icône lucide-react (ex: "Check", "Send", "Save")
17
+ submitIconPosition?: 'left' | 'right';
18
+ onCancel?: () => void;
19
+ cancelLabel?: string;
20
+ cancelIcon?: string;
21
+ }
22
+
23
+ export const DynamicForm: React.FC<DynamicFormProps> = ({
24
+ schema,
25
+ initialData = {},
26
+ onSubmit,
27
+ submitLabel,
28
+ submitIcon,
29
+ submitIconPosition = 'left',
30
+ onCancel,
31
+ cancelLabel = 'Retour',
32
+ cancelIcon,
33
+ }) => {
34
+ const [formData, setFormData] = useState<Record<string, unknown>>(initialData);
35
+ const [errors, setErrors] = useState<Record<string, string>>({});
36
+ const [touched, setTouched] = useState<Record<string, boolean>>({});
37
+ const [isSubmitting, setIsSubmitting] = useState(false);
38
+ const initialDataRef = useRef(initialData);
39
+
40
+ useEffect(() => {
41
+ if (!isEqual(initialDataRef.current, initialData)) {
42
+ setFormData(initialData);
43
+ initialDataRef.current = initialData;
44
+ }
45
+ }, [initialData]);
46
+
47
+ const handleFieldChange = (fieldName: string, value: unknown) => {
48
+ const newData = { ...formData, [fieldName]: value };
49
+ setFormData(newData);
50
+
51
+ if (touched[fieldName]) {
52
+ const field = schema.fields.find((f) => f.name === fieldName);
53
+ if (field) {
54
+ const validationResult = validateField(value, field);
55
+ setErrors((prev) => ({
56
+ ...prev,
57
+ [fieldName]: validationResult.isValid ? '' : validationResult.error!,
58
+ }));
59
+ }
60
+ }
61
+ };
62
+
63
+ const handleFieldBlur = (fieldName: string) => {
64
+ if (!touched[fieldName]) {
65
+ setTouched((prev) => ({ ...prev, [fieldName]: true }));
66
+
67
+ const field = schema.fields.find((f) => f.name === fieldName);
68
+ if (field) {
69
+ const validationResult = validateField(formData[fieldName], field);
70
+ setErrors((prev) => ({
71
+ ...prev,
72
+ [fieldName]: validationResult.isValid ? '' : validationResult.error!,
73
+ }));
74
+ }
75
+ }
76
+ };
77
+
78
+ const handleSubmit = async (e: React.FormEvent) => {
79
+ e.preventDefault();
80
+ setIsSubmitting(true);
81
+
82
+ const newTouched = schema.fields.reduce(
83
+ (acc, field) => {
84
+ if (field.type !== 'section') acc[field.name] = true;
85
+ return acc;
86
+ },
87
+ {} as Record<string, boolean>
88
+ );
89
+
90
+ setTouched(newTouched);
91
+
92
+ const validation = validateForm(
93
+ formData,
94
+ schema.fields.filter((f) => f.type !== 'section')
95
+ );
96
+ setErrors(validation.errors);
97
+
98
+ if (validation.isValid) {
99
+ try {
100
+ await onSubmit(formData);
101
+ } catch (error) {
102
+ console.error('Erreur lors de la soumission:', error);
103
+ setErrors((prev) => ({
104
+ ...prev,
105
+ __global: 'Une erreur est survenue lors de la soumission',
106
+ }));
107
+ }
108
+ }
109
+
110
+ setIsSubmitting(false);
111
+ };
112
+
113
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
+ const CancelIconComp = cancelIcon ? (Icons as any)[cancelIcon] : null;
115
+
116
+ // Submit icon resolution (prop > schema > none)
117
+
118
+ const SubmitIconComp =
119
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
120
+ (submitIcon && (Icons as any)[submitIcon]) ||
121
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
+ (schema.submitIcon && (Icons as any)[schema.submitIcon]) ||
123
+ null;
124
+
125
+ // layout config
126
+ const columns = schema.columns && schema.columns > 0 ? Math.floor(schema.columns) : 12;
127
+
128
+ return (
129
+ <div className={styles.formContainer}>
130
+ <div className={styles.headerLine}>
131
+ <h2 className={styles.formTitle}>{schema.title}</h2>
132
+ </div>
133
+
134
+ {schema.description && <p className={styles.formDescription}>{schema.description}</p>}
135
+
136
+ {errors.__global && (
137
+ <div className={`alert alert-danger ${styles.globalError}`}>{errors.__global}</div>
138
+ )}
139
+
140
+ <form onSubmit={handleSubmit}>
141
+ {/* Unified grid: utilisé pour layout:grid et pour le fallback "classic" */}
142
+ <div
143
+ className={styles.gridContainer}
144
+ // important: use minmax(0,1fr) to allow shrinking and avoid overflow
145
+ style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}
146
+ >
147
+ {schema.fields.map((field: FieldMetadata) => {
148
+ if (field.type === 'section') {
149
+ return (
150
+ <div key={field.name} className={styles.sectionFull}>
151
+ <h5 className={styles.sectionTitle}>{field.label}</h5>
152
+ {field.help && <p className={styles.sectionHelp}>{field.help}</p>}
153
+ </div>
154
+ );
155
+ }
156
+
157
+ // clamp gridCol pour éviter span > columns
158
+ const span = Math.max(1, Math.min(columns, field.gridCol ?? columns));
159
+ const gridStyle: React.CSSProperties = { gridColumn: `span ${span}` };
160
+
161
+ return (
162
+ <div key={field.name} className={styles.fieldGridItem} style={gridStyle}>
163
+ <DynamicInput
164
+ field={field}
165
+ value={formData[field.name]}
166
+ onChange={(value) => handleFieldChange(field.name, value)}
167
+ onBlur={() => handleFieldBlur(field.name)}
168
+ error={errors[field.name]}
169
+ touched={touched[field.name]}
170
+ />
171
+ {field.help && <small className={styles.fieldHelp}>{field.help}</small>}
172
+ </div>
173
+ );
174
+ })}
175
+ </div>
176
+
177
+ <div className={styles.formFooter}>
178
+ <div className={styles.footerLeft}>
179
+ {onCancel && (
180
+ <button type='button' className={styles.cancelLink} onClick={onCancel}>
181
+ {CancelIconComp && <CancelIconComp size={16} className='me-2' />} {cancelLabel}
182
+ </button>
183
+ )}
184
+ </div>
185
+
186
+ <div className={styles.footerRight}>
187
+ <Button
188
+ type='submit'
189
+ variant='primary'
190
+ size='md'
191
+ className={styles.submitButton}
192
+ disabled={isSubmitting}
193
+ >
194
+ {isSubmitting ? (
195
+ <>
196
+ <span className='spinner-border spinner-border-sm me-2' role='status' />
197
+ Validation...
198
+ </>
199
+ ) : (
200
+ <>
201
+ {/* icon left */}
202
+ {SubmitIconComp &&
203
+ (submitIconPosition === 'left' || schema.submitIconPosition === 'left') && (
204
+ <SubmitIconComp size={16} className='me-2' />
205
+ )}
206
+
207
+ {submitLabel || schema.submitText || 'Valider'}
208
+
209
+ {/* icon right */}
210
+ {SubmitIconComp &&
211
+ (submitIconPosition === 'right' || schema.submitIconPosition === 'right') && (
212
+ <SubmitIconComp size={16} className='ms-2' />
213
+ )}
214
+ </>
215
+ )}
216
+ </Button>
217
+ </div>
218
+ </div>
219
+ </form>
220
+ </div>
221
+ );
222
+ };
223
+
224
+ export default DynamicForm;
@@ -0,0 +1,3 @@
1
+ export * from './DynamicForm';
2
+ export * from './tools/validation';
3
+ export * from './tools/form-metadata';