@spaceinvoices/react-ui 0.1.1

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 (352) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +340 -0
  3. package/cli/dist/index.js +922 -0
  4. package/package.json +87 -0
  5. package/registry.json +600 -0
  6. package/spaceinvoices.schema.json +47 -0
  7. package/src/app.tsx +25 -0
  8. package/src/common/autocomplete.tsx +135 -0
  9. package/src/components/activities/activity-timeline.tsx +160 -0
  10. package/src/components/activities/index.ts +1 -0
  11. package/src/components/activities/locales/de.ts +30 -0
  12. package/src/components/activities/locales/sl.ts +30 -0
  13. package/src/components/advance-invoices/advance-invoices.hooks.ts +75 -0
  14. package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +702 -0
  15. package/src/components/advance-invoices/create/locales/de.ts +29 -0
  16. package/src/components/advance-invoices/create/locales/sl.ts +25 -0
  17. package/src/components/advance-invoices/create/prepare-advance-invoice-submission.ts +74 -0
  18. package/src/components/advance-invoices/index.ts +5 -0
  19. package/src/components/advance-invoices/list/index.ts +3 -0
  20. package/src/components/advance-invoices/list/list-row-actions.tsx +119 -0
  21. package/src/components/advance-invoices/list/list-table.tsx +178 -0
  22. package/src/components/advance-invoices/list/locales/de.ts +32 -0
  23. package/src/components/advance-invoices/list/locales/sl.ts +32 -0
  24. package/src/components/advance-invoices/list/use-advance-invoice-download.ts +63 -0
  25. package/src/components/button-loader.tsx +11 -0
  26. package/src/components/combobox.tsx +96 -0
  27. package/src/components/company-registry/company-registry-autocomplete.tsx +151 -0
  28. package/src/components/company-registry/company-registry.hooks.ts +67 -0
  29. package/src/components/company-registry/index.ts +7 -0
  30. package/src/components/credit-notes/create/create-credit-note-form.tsx +332 -0
  31. package/src/components/credit-notes/create/index.ts +1 -0
  32. package/src/components/credit-notes/create/locales/de.ts +69 -0
  33. package/src/components/credit-notes/create/locales/sl.ts +67 -0
  34. package/src/components/credit-notes/credit-notes.hooks.ts +22 -0
  35. package/src/components/credit-notes/index.ts +10 -0
  36. package/src/components/credit-notes/list/index.ts +3 -0
  37. package/src/components/credit-notes/list/list-row-actions.tsx +116 -0
  38. package/src/components/credit-notes/list/list-table.tsx +183 -0
  39. package/src/components/credit-notes/list/locales/de.ts +33 -0
  40. package/src/components/credit-notes/list/locales/sl.ts +33 -0
  41. package/src/components/credit-notes/list/use-credit-note-download.ts +65 -0
  42. package/src/components/customers/create-customer-form/create-customer-form.tsx +134 -0
  43. package/src/components/customers/create-customer-form/locales/de.ts +20 -0
  44. package/src/components/customers/create-customer-form/locales/sl.ts +20 -0
  45. package/src/components/customers/customer-autocomplete.tsx +173 -0
  46. package/src/components/customers/customer-combobox.tsx +130 -0
  47. package/src/components/customers/customer-list-table/customer-list-row-actions.tsx +48 -0
  48. package/src/components/customers/customer-list-table/customer-list-table.tsx +124 -0
  49. package/src/components/customers/customer-list-table/index.ts +2 -0
  50. package/src/components/customers/customer-list-table/locales/de.ts +16 -0
  51. package/src/components/customers/customer-list-table/locales/sl.ts +16 -0
  52. package/src/components/customers/customers.hooks.test.ts +348 -0
  53. package/src/components/customers/customers.hooks.ts +57 -0
  54. package/src/components/customers/index.ts +5 -0
  55. package/src/components/dashboard/chart-empty-state.tsx +29 -0
  56. package/src/components/dashboard/collection-rate-card/collection-rate-card.tsx +80 -0
  57. package/src/components/dashboard/collection-rate-card/index.ts +4 -0
  58. package/src/components/dashboard/collection-rate-card/locales/sl.ts +3 -0
  59. package/src/components/dashboard/collection-rate-card/use-collection-rate.ts +74 -0
  60. package/src/components/dashboard/index.ts +54 -0
  61. package/src/components/dashboard/invoice-status-chart/index.ts +4 -0
  62. package/src/components/dashboard/invoice-status-chart/invoice-status-chart.tsx +130 -0
  63. package/src/components/dashboard/invoice-status-chart/locales/sl.ts +9 -0
  64. package/src/components/dashboard/invoice-status-chart/use-invoice-status.ts +105 -0
  65. package/src/components/dashboard/loading-card.tsx +19 -0
  66. package/src/components/dashboard/payment-methods-chart/index.ts +4 -0
  67. package/src/components/dashboard/payment-methods-chart/locales/sl.ts +12 -0
  68. package/src/components/dashboard/payment-methods-chart/payment-methods-chart.tsx +152 -0
  69. package/src/components/dashboard/payment-methods-chart/use-payment-methods.ts +50 -0
  70. package/src/components/dashboard/payment-trend-chart/index.ts +4 -0
  71. package/src/components/dashboard/payment-trend-chart/locales/sl.ts +5 -0
  72. package/src/components/dashboard/payment-trend-chart/payment-trend-chart.tsx +137 -0
  73. package/src/components/dashboard/payment-trend-chart/use-payment-trend.ts +92 -0
  74. package/src/components/dashboard/revenue-card.tsx +49 -0
  75. package/src/components/dashboard/revenue-trend-chart/index.ts +4 -0
  76. package/src/components/dashboard/revenue-trend-chart/locales/sl.ts +5 -0
  77. package/src/components/dashboard/revenue-trend-chart/revenue-trend-chart.tsx +137 -0
  78. package/src/components/dashboard/revenue-trend-chart/use-revenue-trend.ts +93 -0
  79. package/src/components/dashboard/shared/index.ts +5 -0
  80. package/src/components/dashboard/shared/use-revenue-data.ts +160 -0
  81. package/src/components/dashboard/shared/use-stats-counts.ts +89 -0
  82. package/src/components/dashboard/shared/use-stats-query.ts +38 -0
  83. package/src/components/dashboard/stat-card.tsx +41 -0
  84. package/src/components/dashboard/tax-collected-card/index.ts +2 -0
  85. package/src/components/dashboard/tax-collected-card/tax-collected-card.tsx +77 -0
  86. package/src/components/dashboard/tax-collected-card/use-tax-collected.ts +145 -0
  87. package/src/components/dashboard/top-customers-chart/index.ts +4 -0
  88. package/src/components/dashboard/top-customers-chart/locales/sl.ts +5 -0
  89. package/src/components/dashboard/top-customers-chart/top-customers-chart.tsx +130 -0
  90. package/src/components/dashboard/top-customers-chart/use-top-customers.ts +72 -0
  91. package/src/components/documents/create/document-add-item-form.tsx +379 -0
  92. package/src/components/documents/create/document-add-item-tax-rate-field.tsx +120 -0
  93. package/src/components/documents/create/document-details-section.tsx +597 -0
  94. package/src/components/documents/create/document-items-section.tsx +133 -0
  95. package/src/components/documents/create/document-recipient-section.tsx +101 -0
  96. package/src/components/documents/create/form-types.ts +36 -0
  97. package/src/components/documents/create/index.ts +9 -0
  98. package/src/components/documents/create/live-preview.tsx +235 -0
  99. package/src/components/documents/create/mark-as-paid-section.tsx +82 -0
  100. package/src/components/documents/create/prepare-document-submission.test.ts +132 -0
  101. package/src/components/documents/create/prepare-document-submission.ts +187 -0
  102. package/src/components/documents/create/prepare-preview-data.test.ts +155 -0
  103. package/src/components/documents/create/prepare-preview-data.ts +16 -0
  104. package/src/components/documents/create/smart-code-insert-button.tsx +139 -0
  105. package/src/components/documents/create/use-document-customer-form.ts +161 -0
  106. package/src/components/documents/document-preview.tsx +13 -0
  107. package/src/components/documents/documents.hooks.ts +146 -0
  108. package/src/components/documents/index.ts +23 -0
  109. package/src/components/documents/shared/document-preview-display.tsx +172 -0
  110. package/src/components/documents/shared/index.ts +3 -0
  111. package/src/components/documents/shared/scaled-document-preview.tsx +70 -0
  112. package/src/components/documents/shared/use-a4-scaling.ts +62 -0
  113. package/src/components/documents/types.ts +61 -0
  114. package/src/components/documents/view/document-actions-bar.tsx +328 -0
  115. package/src/components/documents/view/document-details-card.tsx +179 -0
  116. package/src/components/documents/view/document-payments-list.tsx +256 -0
  117. package/src/components/documents/view/index.ts +4 -0
  118. package/src/components/documents/view/locales/de.ts +85 -0
  119. package/src/components/documents/view/locales/sl.ts +84 -0
  120. package/src/components/documents/view/use-document-download.ts +125 -0
  121. package/src/components/entities/create-entity-form.tsx +105 -0
  122. package/src/components/entities/entities.hooks.ts +50 -0
  123. package/src/components/entities/entity-settings-form/email-template-variables-info.tsx +103 -0
  124. package/src/components/entities/entity-settings-form/entity-settings-form.tsx +1326 -0
  125. package/src/components/entities/entity-settings-form/image-upload-with-crop.tsx +222 -0
  126. package/src/components/entities/entity-settings-form/index.ts +2 -0
  127. package/src/components/entities/entity-settings-form/input-with-preview.tsx +190 -0
  128. package/src/components/entities/entity-settings-form/locales/de.ts +192 -0
  129. package/src/components/entities/entity-settings-form/locales/sl.ts +188 -0
  130. package/src/components/entities/furs-settings-form/furs-settings-form.tsx +410 -0
  131. package/src/components/entities/furs-settings-form/furs-settings.hooks.ts +320 -0
  132. package/src/components/entities/furs-settings-form/index.ts +3 -0
  133. package/src/components/entities/furs-settings-form/locales/de.ts +233 -0
  134. package/src/components/entities/furs-settings-form/locales/en.ts +194 -0
  135. package/src/components/entities/furs-settings-form/locales/sl.ts +196 -0
  136. package/src/components/entities/furs-settings-form/sections/certificate-settings-section.tsx +242 -0
  137. package/src/components/entities/furs-settings-form/sections/enable-fiscalization-section.tsx +139 -0
  138. package/src/components/entities/furs-settings-form/sections/general-settings-section.tsx +252 -0
  139. package/src/components/entities/furs-settings-form/sections/premises-management-section.tsx +370 -0
  140. package/src/components/entities/furs-settings-form/sections/register-premise-dialog.tsx +420 -0
  141. package/src/components/entities/keys.ts +2 -0
  142. package/src/components/entities/settings/branding-settings-form.tsx +274 -0
  143. package/src/components/entities/settings/company-settings-form.tsx +256 -0
  144. package/src/components/entities/settings/defaults-settings-form.tsx +501 -0
  145. package/src/components/entities/settings/email-settings-form.tsx +288 -0
  146. package/src/components/entities/settings/eslog-settings-form.tsx +113 -0
  147. package/src/components/entities/settings/index.ts +8 -0
  148. package/src/components/entities/settings/number-format-settings-form.tsx +244 -0
  149. package/src/components/entities/settings/pdf-template-selector/demo-invoice-data.ts +164 -0
  150. package/src/components/entities/settings/pdf-template-selector/index.ts +2 -0
  151. package/src/components/entities/settings/pdf-template-selector/locales/de.ts +18 -0
  152. package/src/components/entities/settings/pdf-template-selector/locales/sl.ts +18 -0
  153. package/src/components/entities/settings/pdf-template-selector/pdf-template-cards.tsx +49 -0
  154. package/src/components/entities/settings/settings-footer.tsx +16 -0
  155. package/src/components/entities/settings/tax-rules-settings-form.tsx +346 -0
  156. package/src/components/estimates/create/create-estimate-form.tsx +384 -0
  157. package/src/components/estimates/create/locales/de.ts +64 -0
  158. package/src/components/estimates/create/locales/sl.ts +63 -0
  159. package/src/components/estimates/create/prepare-estimate-submission.ts +39 -0
  160. package/src/components/estimates/create/use-estimate-customer-form.ts +5 -0
  161. package/src/components/estimates/estimates.hooks.ts +15 -0
  162. package/src/components/estimates/index.ts +6 -0
  163. package/src/components/estimates/list/index.ts +3 -0
  164. package/src/components/estimates/list/list-row-actions.tsx +103 -0
  165. package/src/components/estimates/list/list-table.tsx +171 -0
  166. package/src/components/estimates/list/locales/de.ts +26 -0
  167. package/src/components/estimates/list/locales/sl.ts +26 -0
  168. package/src/components/estimates/list/use-estimate-download.ts +63 -0
  169. package/src/components/export/document-export-form.tsx +288 -0
  170. package/src/components/export/index.ts +2 -0
  171. package/src/components/form/form-input.tsx +89 -0
  172. package/src/components/form/index.ts +1 -0
  173. package/src/components/invoices/create/create-invoice-form.tsx +852 -0
  174. package/src/components/invoices/create/eslog-validation.test.ts +242 -0
  175. package/src/components/invoices/create/eslog-validation.ts +208 -0
  176. package/src/components/invoices/create/locales/de.ts +118 -0
  177. package/src/components/invoices/create/locales/sl.ts +114 -0
  178. package/src/components/invoices/create/prepare-invoice-submission.test.ts +777 -0
  179. package/src/components/invoices/create/prepare-invoice-submission.ts +79 -0
  180. package/src/components/invoices/create/use-invoice-customer-form.ts +5 -0
  181. package/src/components/invoices/index.ts +9 -0
  182. package/src/components/invoices/invoices-furs.hooks.ts +28 -0
  183. package/src/components/invoices/invoices.hooks.ts +110 -0
  184. package/src/components/invoices/list/index.ts +3 -0
  185. package/src/components/invoices/list/list-row-actions.tsx +132 -0
  186. package/src/components/invoices/list/list-table.tsx +165 -0
  187. package/src/components/invoices/list/locales/de.ts +33 -0
  188. package/src/components/invoices/list/locales/sl.ts +33 -0
  189. package/src/components/invoices/list/use-invoice-download.ts +62 -0
  190. package/src/components/invoices/send-email-dialog/index.ts +1 -0
  191. package/src/components/invoices/send-email-dialog/locales/de.ts +18 -0
  192. package/src/components/invoices/send-email-dialog/locales/sl.ts +17 -0
  193. package/src/components/invoices/send-email-dialog/send-email-dialog.tsx +289 -0
  194. package/src/components/invoices/send-email-dialog.tsx +2 -0
  195. package/src/components/invoices/shared/index.ts +2 -0
  196. package/src/components/invoices/shared/scaled-document-preview.tsx +32 -0
  197. package/src/components/invoices/shared/use-a4-scaling.tsx +39 -0
  198. package/src/components/invoices/view/eslog-info-display.tsx +160 -0
  199. package/src/components/invoices/view/furs-info-display.tsx +213 -0
  200. package/src/components/items/create-item-form/create-item-form.tsx +155 -0
  201. package/src/components/items/create-item-form/locales/de.ts +14 -0
  202. package/src/components/items/create-item-form/locales/en.ts +9 -0
  203. package/src/components/items/create-item-form/locales/sl.ts +14 -0
  204. package/src/components/items/item-combobox.tsx +147 -0
  205. package/src/components/items/item-list-table/item-list-header.tsx +33 -0
  206. package/src/components/items/item-list-table/item-list-row-actions.tsx +48 -0
  207. package/src/components/items/item-list-table/item-list-row.tsx +32 -0
  208. package/src/components/items/item-list-table/item-list-table.tsx +76 -0
  209. package/src/components/items/item-list-table/locales/de.ts +10 -0
  210. package/src/components/items/item-list-table/locales/en.ts +10 -0
  211. package/src/components/items/item-list-table/locales/sl.ts +10 -0
  212. package/src/components/items/items.hooks.ts +63 -0
  213. package/src/components/loading-spinner.tsx +24 -0
  214. package/src/components/payments/create-payment-form/create-payment-form.tsx +222 -0
  215. package/src/components/payments/create-payment-form/locales/de.ts +20 -0
  216. package/src/components/payments/create-payment-form/locales/sl.ts +20 -0
  217. package/src/components/payments/edit-payment-form/edit-payment-form.tsx +230 -0
  218. package/src/components/payments/edit-payment-form/index.ts +1 -0
  219. package/src/components/payments/edit-payment-form/locales/de.ts +20 -0
  220. package/src/components/payments/edit-payment-form/locales/sl.ts +20 -0
  221. package/src/components/payments/index.ts +4 -0
  222. package/src/components/payments/list/index.ts +2 -0
  223. package/src/components/payments/list/list-row-actions.tsx +98 -0
  224. package/src/components/payments/list/list-table.tsx +186 -0
  225. package/src/components/payments/list/locales/de.ts +19 -0
  226. package/src/components/payments/list/locales/sl.ts +19 -0
  227. package/src/components/payments/payments.hooks.ts +15 -0
  228. package/src/components/request-logs/index.ts +3 -0
  229. package/src/components/request-logs/request-log-detail.tsx +242 -0
  230. package/src/components/request-logs/request-log-list-table.tsx +266 -0
  231. package/src/components/request-logs/request-logs-page.tsx +10 -0
  232. package/src/components/table/README.md +410 -0
  233. package/src/components/table/data-table.tsx +251 -0
  234. package/src/components/table/date-cell.tsx +35 -0
  235. package/src/components/table/filter-bar.tsx +114 -0
  236. package/src/components/table/filter-panel.tsx +407 -0
  237. package/src/components/table/hooks/use-table-fetch.ts +17 -0
  238. package/src/components/table/hooks/use-table-query.ts +36 -0
  239. package/src/components/table/hooks/use-table-state.ts +293 -0
  240. package/src/components/table/index.ts +35 -0
  241. package/src/components/table/search-input.tsx +85 -0
  242. package/src/components/table/sortable-header.tsx +56 -0
  243. package/src/components/table/table-empty-state.tsx +40 -0
  244. package/src/components/table/table-no-results.tsx +41 -0
  245. package/src/components/table/table-pagination.tsx +42 -0
  246. package/src/components/table/table-skeleton.tsx +54 -0
  247. package/src/components/table/types.ts +136 -0
  248. package/src/components/tax-reports/index.ts +1 -0
  249. package/src/components/tax-reports/kir-export-form.tsx +172 -0
  250. package/src/components/taxes/create-tax-form/create-tax-form.tsx +112 -0
  251. package/src/components/taxes/create-tax-form/locales/de.ts +8 -0
  252. package/src/components/taxes/create-tax-form/locales/en.ts +7 -0
  253. package/src/components/taxes/create-tax-form/locales/sl.ts +8 -0
  254. package/src/components/taxes/tax-list-table/locales/de.ts +11 -0
  255. package/src/components/taxes/tax-list-table/locales/en.ts +10 -0
  256. package/src/components/taxes/tax-list-table/locales/sl.ts +11 -0
  257. package/src/components/taxes/tax-list-table/tax-list-header.tsx +29 -0
  258. package/src/components/taxes/tax-list-table/tax-list-row-actions.tsx +43 -0
  259. package/src/components/taxes/tax-list-table/tax-list-row.tsx +46 -0
  260. package/src/components/taxes/tax-list-table/tax-list-table.tsx +59 -0
  261. package/src/components/taxes/taxes.hooks.ts +35 -0
  262. package/src/components/ui/alert-dialog.tsx +61 -0
  263. package/src/components/ui/alert.tsx +72 -0
  264. package/src/components/ui/badge.tsx +48 -0
  265. package/src/components/ui/breadcrumb.tsx +132 -0
  266. package/src/components/ui/button.tsx +61 -0
  267. package/src/components/ui/calendar.tsx +213 -0
  268. package/src/components/ui/card.tsx +94 -0
  269. package/src/components/ui/chart.tsx +380 -0
  270. package/src/components/ui/checkbox.tsx +27 -0
  271. package/src/components/ui/collapsible.tsx +56 -0
  272. package/src/components/ui/command.tsx +187 -0
  273. package/src/components/ui/dialog.tsx +187 -0
  274. package/src/components/ui/drawer.tsx +123 -0
  275. package/src/components/ui/dropdown-menu.tsx +291 -0
  276. package/src/components/ui/form.tsx +166 -0
  277. package/src/components/ui/input-group.tsx +149 -0
  278. package/src/components/ui/input.tsx +20 -0
  279. package/src/components/ui/label.tsx +18 -0
  280. package/src/components/ui/loading-spinner.tsx +16 -0
  281. package/src/components/ui/popover.tsx +108 -0
  282. package/src/components/ui/radio-group.tsx +37 -0
  283. package/src/components/ui/select.tsx +200 -0
  284. package/src/components/ui/separator.tsx +23 -0
  285. package/src/components/ui/sheet.tsx +145 -0
  286. package/src/components/ui/sidebar.tsx +771 -0
  287. package/src/components/ui/skeleton.tsx +13 -0
  288. package/src/components/ui/sonner.tsx +60 -0
  289. package/src/components/ui/spinner.tsx +10 -0
  290. package/src/components/ui/sticky-form-footer.tsx +55 -0
  291. package/src/components/ui/switch.tsx +30 -0
  292. package/src/components/ui/table.tsx +101 -0
  293. package/src/components/ui/tabs.tsx +80 -0
  294. package/src/components/ui/textarea.tsx +18 -0
  295. package/src/components/ui/tooltip.tsx +89 -0
  296. package/src/components/wl-subscription/index.ts +2 -0
  297. package/src/components/wl-subscription/locked-feature.tsx +173 -0
  298. package/src/components/wl-subscription/upgrade-modal.tsx +209 -0
  299. package/src/frontend.tsx +28 -0
  300. package/src/generate-schemas.ts +265 -0
  301. package/src/generated/schemas/advanceinvoice.ts +177 -0
  302. package/src/generated/schemas/creditnote.ts +187 -0
  303. package/src/generated/schemas/customer.ts +29 -0
  304. package/src/generated/schemas/entity.ts +252 -0
  305. package/src/generated/schemas/estimate.ts +159 -0
  306. package/src/generated/schemas/furssettings.ts +25 -0
  307. package/src/generated/schemas/index.ts +24 -0
  308. package/src/generated/schemas/invoice.ts +167 -0
  309. package/src/generated/schemas/item.ts +38 -0
  310. package/src/generated/schemas/payment.ts +44 -0
  311. package/src/generated/schemas/previewadvanceinvoice_body.ts +354 -0
  312. package/src/generated/schemas/previewestimate_body.ts +309 -0
  313. package/src/generated/schemas/registerfursmovablepremise_body.ts +22 -0
  314. package/src/generated/schemas/registerfursrealestatepremise_body.ts +32 -0
  315. package/src/generated/schemas/renderdocument_body.ts +594 -0
  316. package/src/generated/schemas/sendemail_body.ts +26 -0
  317. package/src/generated/schemas/startpdfexport_body.ts +20 -0
  318. package/src/generated/schemas/tax.ts +48 -0
  319. package/src/generated/schemas/uploadfile_body.ts +23 -0
  320. package/src/generated/schemas/uploadfurscertificate_body.ts +20 -0
  321. package/src/generated/schemas/userfurssettings.ts +19 -0
  322. package/src/hooks/create-resource-hooks.test.ts +483 -0
  323. package/src/hooks/create-resource-hooks.ts +300 -0
  324. package/src/hooks/use-debounce.ts +12 -0
  325. package/src/hooks/use-duplicate-document.ts +185 -0
  326. package/src/hooks/use-media-query.tsx +19 -0
  327. package/src/hooks/use-mobile.ts +39 -0
  328. package/src/hooks/use-next-document-number.ts +57 -0
  329. package/src/hooks/use-resource-mutation.ts +118 -0
  330. package/src/hooks/use-vies-check.ts +130 -0
  331. package/src/index.css +11 -0
  332. package/src/index.html +13 -0
  333. package/src/index.tsx +12 -0
  334. package/src/lib/auth.ts +4 -0
  335. package/src/lib/browser-cookies.ts +70 -0
  336. package/src/lib/constants.ts +287 -0
  337. package/src/lib/cookies.ts +36 -0
  338. package/src/lib/schemas/advance-invoice.ts +43 -0
  339. package/src/lib/schemas/credit-note.ts +32 -0
  340. package/src/lib/schemas/estimate.ts +31 -0
  341. package/src/lib/schemas/index.ts +18 -0
  342. package/src/lib/schemas/invoice.ts +43 -0
  343. package/src/lib/schemas/shared.ts +79 -0
  344. package/src/lib/translation.ts +38 -0
  345. package/src/lib/utils.ts +6 -0
  346. package/src/providers/entities-context.tsx +41 -0
  347. package/src/providers/entities-provider.tsx +201 -0
  348. package/src/providers/form-footer-context.tsx +72 -0
  349. package/src/providers/sdk-provider.tsx +164 -0
  350. package/src/providers/white-label-provider.tsx +91 -0
  351. package/src/providers/wl-subscription-provider.tsx +277 -0
  352. package/src/utils/string-helpers.ts +111 -0
@@ -0,0 +1,266 @@
1
+ "use client";
2
+
3
+ import { formatDistanceToNow } from "date-fns";
4
+ import { useCallback, useMemo } from "react";
5
+ import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/ui/components/ui/sheet";
6
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@/ui/components/ui/tooltip";
7
+ import { AUTH_COOKIES } from "@/ui/lib/auth";
8
+ import { getCookie } from "@/ui/lib/browser-cookies";
9
+ import { cn } from "@/ui/lib/utils";
10
+ import { DataTable } from "../table/data-table";
11
+ import type { Column, FilterConfig, ListTableProps, TableQueryParams, TableQueryResponse } from "../table/types";
12
+ import { RequestLogDetail } from "./request-log-detail";
13
+
14
+ // Request log response type (internal endpoint, not in SDK)
15
+ export interface RequestLogResponse {
16
+ id: string;
17
+ entity_id: string;
18
+ request_id: string;
19
+ method: string;
20
+ path: string;
21
+ res_status: string | null;
22
+ resource_type: string | null;
23
+ resource_id: string | null;
24
+ action: string | null;
25
+ req_body: Record<string, unknown> | null;
26
+ res_body: Record<string, unknown> | null;
27
+ headers: Record<string, unknown> | null;
28
+ created_at: string;
29
+ updated_at: string;
30
+ }
31
+
32
+ // Get API base URL from environment
33
+ const getApiBaseUrl = () => {
34
+ if (typeof import.meta !== "undefined" && import.meta.env) {
35
+ return import.meta.env.VITE_API_URL || import.meta.env.BUN_PUBLIC_API_URL || "";
36
+ }
37
+ return "";
38
+ };
39
+
40
+ export const REQUEST_LOGS_CACHE_KEY = "request-logs";
41
+
42
+ const METHOD_COLORS: Record<string, string> = {
43
+ GET: "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300",
44
+ POST: "bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300",
45
+ PATCH: "bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300",
46
+ PUT: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300",
47
+ DELETE: "bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300",
48
+ };
49
+
50
+ function StatusDot({ status }: { status: string | null }) {
51
+ const statusCode = status ? Number.parseInt(status, 10) : 0;
52
+ let color = "bg-gray-400";
53
+ if (statusCode >= 200 && statusCode < 300) {
54
+ color = "bg-green-500";
55
+ } else if (statusCode >= 400 && statusCode < 500) {
56
+ color = "bg-yellow-500";
57
+ } else if (statusCode >= 500) {
58
+ color = "bg-red-500";
59
+ }
60
+ return <span className={cn("inline-block h-2.5 w-2.5 rounded-full", color)} />;
61
+ }
62
+
63
+ function MethodBadge({ method }: { method: string }) {
64
+ return (
65
+ <span
66
+ className={cn(
67
+ "inline-flex items-center rounded px-1.5 py-0.5 font-medium font-mono text-xs",
68
+ METHOD_COLORS[method] || "bg-gray-100 text-gray-700",
69
+ )}
70
+ >
71
+ {method}
72
+ </span>
73
+ );
74
+ }
75
+
76
+ type RequestLogListTableProps = ListTableProps<RequestLogResponse> & {
77
+ /** Environment filter for account-level queries */
78
+ environment?: "live" | "sandbox";
79
+ /** Show entity column (for account-level view) */
80
+ showEntityColumn?: boolean;
81
+ /** Selected log for detail panel */
82
+ selectedLog?: RequestLogResponse | null;
83
+ /** Callback when a log is selected */
84
+ onSelectLog?: (log: RequestLogResponse | null) => void;
85
+ };
86
+
87
+ export function RequestLogListTable({
88
+ queryParams,
89
+ onChangeParams,
90
+ entityId,
91
+ environment,
92
+ showEntityColumn = false,
93
+ selectedLog,
94
+ onSelectLog,
95
+ }: RequestLogListTableProps) {
96
+ // Custom fetch function that handles both entity-scoped and account-scoped queries
97
+ // Don't use useTableFetch since we need special handling for environment
98
+ const handleFetch = useCallback(
99
+ async (params: TableQueryParams): Promise<TableQueryResponse<RequestLogResponse>> => {
100
+ const token = getCookie(AUTH_COOKIES.TOKEN);
101
+ if (!token) throw new Error("Not authenticated");
102
+
103
+ const queryParamsUrl = new URLSearchParams();
104
+
105
+ // Entity or environment filter
106
+ // For entity-scoped queries, use entity_id
107
+ // For account-scoped queries, use environment (defaults to "live")
108
+ if (entityId) {
109
+ queryParamsUrl.set("entity_id", entityId);
110
+ } else {
111
+ queryParamsUrl.set("environment", environment || "live");
112
+ }
113
+
114
+ // Pagination
115
+ if (params.limit) queryParamsUrl.set("limit", String(params.limit));
116
+ if (params.next_cursor) queryParamsUrl.set("next_cursor", params.next_cursor);
117
+ if (params.prev_cursor) queryParamsUrl.set("prev_cursor", params.prev_cursor);
118
+
119
+ // Search maps to path filter
120
+ if (params.search) queryParamsUrl.set("path", params.search);
121
+
122
+ // HTTP method filter
123
+ if (params.filter_method) queryParamsUrl.set("method", params.filter_method);
124
+
125
+ // HTTP status code filter
126
+ if (params.filter_http_status) queryParamsUrl.set("status", params.filter_http_status);
127
+
128
+ // Date filters
129
+ if (params.filter_date_from) queryParamsUrl.set("date_from", params.filter_date_from);
130
+ if (params.filter_date_to) queryParamsUrl.set("date_to", params.filter_date_to);
131
+
132
+ const apiBaseUrl = getApiBaseUrl();
133
+ const response = await fetch(`${apiBaseUrl}/request-logs?${queryParamsUrl.toString()}`, {
134
+ headers: {
135
+ Authorization: `Bearer ${token}`,
136
+ "Content-Type": "application/json",
137
+ },
138
+ });
139
+
140
+ if (!response.ok) {
141
+ throw new Error(`Failed to fetch request logs: ${response.status}`);
142
+ }
143
+
144
+ return (await response.json()) as TableQueryResponse<RequestLogResponse>;
145
+ },
146
+ [entityId, environment],
147
+ );
148
+
149
+ const columns: Column<RequestLogResponse>[] = useMemo(() => {
150
+ const cols: Column<RequestLogResponse>[] = [
151
+ {
152
+ id: "status_dot",
153
+ header: "",
154
+ className: "w-8",
155
+ cell: (log) => <StatusDot status={log.res_status} />,
156
+ },
157
+ {
158
+ id: "res_status",
159
+ header: "Status",
160
+ className: "w-16",
161
+ cell: (log) => <span className="font-mono text-muted-foreground text-sm">{log.res_status || "—"}</span>,
162
+ },
163
+ {
164
+ id: "method",
165
+ header: "Method",
166
+ className: "w-20",
167
+ cell: (log) => <MethodBadge method={log.method} />,
168
+ },
169
+ {
170
+ id: "path",
171
+ header: "Path",
172
+ cell: (log) => <span className="truncate font-mono text-sm">{log.path}</span>,
173
+ },
174
+ ];
175
+
176
+ if (showEntityColumn) {
177
+ cols.push({
178
+ id: "entity_id",
179
+ header: "Entity",
180
+ className: "hidden md:table-cell",
181
+ cell: (log) => (
182
+ <Tooltip>
183
+ <TooltipTrigger asChild>
184
+ <button
185
+ type="button"
186
+ className="cursor-pointer rounded bg-muted px-1.5 py-0.5 font-mono text-muted-foreground text-xs hover:bg-muted/80"
187
+ onClick={(e) => {
188
+ e.stopPropagation();
189
+ navigator.clipboard.writeText(log.entity_id);
190
+ }}
191
+ >
192
+ {log.entity_id}
193
+ </button>
194
+ </TooltipTrigger>
195
+ <TooltipContent>Copy</TooltipContent>
196
+ </Tooltip>
197
+ ),
198
+ });
199
+ } else {
200
+ cols.push({
201
+ id: "resource_id",
202
+ header: "Resource",
203
+ className: "hidden sm:table-cell",
204
+ cell: (log) => <span className="text-muted-foreground text-xs">{log.resource_id || "—"}</span>,
205
+ });
206
+ }
207
+
208
+ cols.push({
209
+ id: "created_at",
210
+ header: "Time",
211
+ align: "right",
212
+ sortable: true,
213
+ sortField: "created_at",
214
+ cell: (log) => (
215
+ <span className="text-muted-foreground text-xs">
216
+ {formatDistanceToNow(new Date(log.created_at), { addSuffix: true })}
217
+ </span>
218
+ ),
219
+ });
220
+
221
+ return cols;
222
+ }, [showEntityColumn]);
223
+
224
+ const filterConfig: FilterConfig = {
225
+ dateFields: [{ id: "created_at", label: "Date" }],
226
+ httpMethodFilter: true,
227
+ httpStatusCodeFilter: true,
228
+ };
229
+
230
+ const cacheKey = entityId
231
+ ? `${REQUEST_LOGS_CACHE_KEY}-${entityId}`
232
+ : `${REQUEST_LOGS_CACHE_KEY}-account-${environment || "live"}`;
233
+
234
+ return (
235
+ <>
236
+ <DataTable
237
+ columns={columns}
238
+ cacheKey={cacheKey}
239
+ resourceName="request log"
240
+ onFetch={handleFetch}
241
+ queryParams={queryParams}
242
+ onChangeParams={onChangeParams}
243
+ entityId={entityId}
244
+ filterConfig={filterConfig}
245
+ onRowClick={(log) => onSelectLog?.(log)}
246
+ defaultOrderBy="-created_at"
247
+ />
248
+
249
+ <Sheet open={!!selectedLog} onOpenChange={(open) => !open && onSelectLog?.(null)}>
250
+ <SheetContent className="w-full overflow-y-auto sm:max-w-2xl">
251
+ <SheetHeader>
252
+ <SheetTitle className="flex items-center gap-2">
253
+ {selectedLog && (
254
+ <>
255
+ <MethodBadge method={selectedLog.method} />
256
+ <span className="font-mono text-sm">{selectedLog.path}</span>
257
+ </>
258
+ )}
259
+ </SheetTitle>
260
+ </SheetHeader>
261
+ {selectedLog && <RequestLogDetail log={selectedLog} />}
262
+ </SheetContent>
263
+ </Sheet>
264
+ </>
265
+ );
266
+ }
@@ -0,0 +1,10 @@
1
+ "use client";
2
+
3
+ // Re-export from the new list table component
4
+ // Legacy component alias - deprecated, use RequestLogListTable instead
5
+ export {
6
+ REQUEST_LOGS_CACHE_KEY,
7
+ RequestLogListTable,
8
+ RequestLogListTable as RequestLogsPage,
9
+ type RequestLogResponse,
10
+ } from "./request-log-list-table";
@@ -0,0 +1,410 @@
1
+ # Table Component Library
2
+
3
+ A comprehensive, type-safe table system with built-in sorting, search, pagination, and loading states.
4
+
5
+ ## Features
6
+
7
+ - **Type-safe**: Full TypeScript support with generics
8
+ - **Flexible**: Use default rendering or custom row/header components
9
+ - **State Management**: Built-in state with optional external control
10
+ - **TanStack Query**: Seamless integration with query caching
11
+ - **Accessible**: ARIA labels and keyboard navigation
12
+ - **Responsive**: Mobile-friendly with proper breakpoints
13
+ - **Loading States**: Skeleton loaders and empty states
14
+ - **Cursor Pagination**: Efficient server-side pagination
15
+
16
+ ## Quick Start
17
+
18
+ ### Basic Usage (Simplified API)
19
+
20
+ The new API allows you to define columns with built-in cell renderers:
21
+
22
+ ```tsx
23
+ import { DataTable, FormattedDate } from "@space-invoices/ui";
24
+ import type { Invoice } from "@spaceinvoices/js-sdk";
25
+
26
+ function InvoiceTable() {
27
+ const { sdk } = useSDK();
28
+
29
+ return (
30
+ <DataTable<Invoice>
31
+ columns={[
32
+ {
33
+ id: "number",
34
+ header: "Invoice #",
35
+ sortable: true,
36
+ cell: (invoice) => (
37
+ <a href={`/invoices/${invoice.id}`} className="underline">
38
+ {invoice.number}
39
+ </a>
40
+ ),
41
+ },
42
+ {
43
+ id: "customer",
44
+ header: "Customer",
45
+ cell: (invoice) => invoice.customer?.name ?? "-",
46
+ },
47
+ {
48
+ id: "date",
49
+ header: "Date",
50
+ sortable: true,
51
+ cell: (invoice) => <FormattedDate date={invoice.date} />,
52
+ },
53
+ {
54
+ id: "total",
55
+ header: "Total",
56
+ align: "right",
57
+ sortable: true,
58
+ cell: (invoice) => `$${invoice.total}`,
59
+ },
60
+ ]}
61
+ cacheKey="invoices"
62
+ resourceName="invoice"
63
+ onFetch={(params) => sdk.invoices.getInvoices(params)}
64
+ />
65
+ );
66
+ }
67
+ ```
68
+
69
+ ### Advanced Usage (Custom Rendering)
70
+
71
+ For more control, use custom row and header renderers:
72
+
73
+ ```tsx
74
+ import { DataTable } from "@space-invoices/ui";
75
+ import InvoiceListHeader from "./invoice-list-header";
76
+ import InvoiceListRow from "./invoice-list-row";
77
+
78
+ function InvoiceTable() {
79
+ const { sdk } = useSDK();
80
+
81
+ return (
82
+ <DataTable<Invoice>
83
+ columns={[
84
+ { field: "number", header: "Number", sortable: true },
85
+ { field: "customer", header: "Customer" },
86
+ { field: "date", header: "Date", sortable: true },
87
+ { field: "total", header: "Total", sortable: true, align: "right" },
88
+ ]}
89
+ renderHeader={(props) => (
90
+ <InvoiceListHeader
91
+ orderBy={props.orderBy}
92
+ onSort={props.onSort}
93
+ />
94
+ )}
95
+ renderRow={(invoice) => (
96
+ <InvoiceListRow
97
+ key={invoice.id}
98
+ invoice={invoice}
99
+ onRowClick={(item) => navigate(`/invoices/${item.id}`)}
100
+ />
101
+ )}
102
+ cacheKey="invoices"
103
+ resourceName="invoice"
104
+ onFetch={(params) => sdk.invoices.getInvoices(params)}
105
+ />
106
+ );
107
+ }
108
+ ```
109
+
110
+ ## API Reference
111
+
112
+ ### DataTable Props
113
+
114
+ | Prop | Type | Required | Description |
115
+ |------|------|----------|-------------|
116
+ | `columns` | `Column<T>[]` | Yes | Column definitions |
117
+ | `cacheKey` | `string` | Yes | Unique key for react-query cache |
118
+ | `onFetch` | `(params) => Promise<Response>` | Yes | Data fetch function |
119
+ | `resourceName` | `string` | Yes | Resource name for empty states |
120
+ | `defaultOrderBy` | `string` | No | Default sort order (e.g., "-id") |
121
+ | `queryParams` | `TableQueryParams` | No | External query parameters |
122
+ | `onChangeParams` | `(params) => void` | No | Callback for param changes |
123
+ | `entityId` | `string` | No | Entity ID for multi-tenant filtering |
124
+ | `createNewLink` | `string` | No | Link for "Create New" button |
125
+ | `createNewTrigger` | `ReactNode` | No | Custom create action |
126
+ | `onRowClick` | `(item: T) => void` | No | Row click handler |
127
+ | `renderRow` | `(item: T) => ReactNode` | No | Custom row renderer |
128
+ | `renderHeader` | `(props) => ReactNode` | No | Custom header renderer |
129
+
130
+ ### Column Definition
131
+
132
+ ```typescript
133
+ type Column<T> = {
134
+ id: string; // Unique column identifier
135
+ header: ReactNode; // Header label or component
136
+ sortField?: string; // Field name for sorting (if different from id)
137
+ sortable?: boolean; // Enable sorting
138
+ align?: "left" | "center" | "right"; // Text alignment
139
+ cell?: (item: T) => ReactNode; // Cell renderer function
140
+ className?: string; // Optional CSS classes
141
+ };
142
+ ```
143
+
144
+ ## Hooks
145
+
146
+ ### useTableState
147
+
148
+ Manages table state internally with optional URL sync:
149
+
150
+ ```tsx
151
+ import { useTableState } from "@space-invoices/ui";
152
+
153
+ const { params, handleSort, handleSearch, handlePageChange } = useTableState({
154
+ initialParams: { order_by: "-created_at" },
155
+ defaultOrderBy: "-id",
156
+ onChangeParams: (params) => {
157
+ // Optional: sync with router or external state
158
+ navigate({ search: params });
159
+ },
160
+ });
161
+ ```
162
+
163
+ ### useTableQuery
164
+
165
+ TanStack Query wrapper for table data:
166
+
167
+ ```tsx
168
+ import { useTableQuery } from "@space-invoices/ui";
169
+
170
+ const { data, isFetching } = useTableQuery({
171
+ cacheKey: "customers",
172
+ fetchFn: (params) => sdk.customers.getCustomers(params),
173
+ params: { order_by: "-id", search: "acme" },
174
+ entityId: "entity-123",
175
+ });
176
+ ```
177
+
178
+ ### useTableFetch
179
+
180
+ Wraps fetch function to include entity ID:
181
+
182
+ ```tsx
183
+ import { useTableFetch } from "@space-invoices/ui";
184
+
185
+ const handleFetch = useTableFetch(
186
+ (params) => sdk.customers.getCustomers(params),
187
+ entityId
188
+ );
189
+ ```
190
+
191
+ ## Components
192
+
193
+ ### SearchInput
194
+
195
+ Search input with optional debouncing:
196
+
197
+ ```tsx
198
+ <SearchInput
199
+ initialValue=""
200
+ onSearch={(value) => console.log(value)}
201
+ placeholder="Search customers..."
202
+ debounceMs={300} // Optional debouncing
203
+ />
204
+ ```
205
+
206
+ ### SortableHeader
207
+
208
+ Sortable column header with visual indicators:
209
+
210
+ ```tsx
211
+ <SortableHeader
212
+ field="name"
213
+ currentOrder="-name"
214
+ onSort={(order) => console.log(order)}
215
+ align="left"
216
+ >
217
+ Customer Name
218
+ </SortableHeader>
219
+ ```
220
+
221
+ ### Pagination
222
+
223
+ Cursor-based pagination controls:
224
+
225
+ ```tsx
226
+ <Pagination
227
+ cursorBefore="prev-cursor"
228
+ cursorAfter="next-cursor"
229
+ onPageChange={({ before, after }) => console.log(before, after)}
230
+ />
231
+ ```
232
+
233
+ ### FormattedDate
234
+
235
+ Date formatting with error handling:
236
+
237
+ ```tsx
238
+ <FormattedDate
239
+ date="2024-01-15"
240
+ format={{
241
+ year: "numeric",
242
+ month: "short",
243
+ day: "numeric",
244
+ }}
245
+ />
246
+ ```
247
+
248
+ ## Usage Patterns
249
+
250
+ The table component supports two main usage patterns:
251
+
252
+ ### Simple Tables (Column-driven)
253
+
254
+ For straightforward tables, define columns with cell renderers:
255
+
256
+ ```tsx
257
+ <DataTable
258
+ columns={[
259
+ {
260
+ id: "name",
261
+ header: "Name",
262
+ sortable: true,
263
+ cell: (item) => item.name,
264
+ },
265
+ {
266
+ id: "email",
267
+ header: "Email",
268
+ cell: (item) => item.email,
269
+ },
270
+ ]}
271
+ cacheKey="users"
272
+ resourceName="user"
273
+ onFetch={(params) => sdk.users.getUsers(params)}
274
+ />
275
+ ```
276
+
277
+ ### Complex Tables (Custom renderers)
278
+
279
+ For advanced tables with complex row/header components:
280
+
281
+ ```tsx
282
+ <DataTable
283
+ columns={[
284
+ { id: "name", header: "Name", sortable: true },
285
+ { id: "email", header: "Email" },
286
+ ]}
287
+ renderRow={(item) => (
288
+ <CustomRow key={item.id} item={item} />
289
+ )}
290
+ renderHeader={(props) => (
291
+ <CustomHeader {...props} />
292
+ )}
293
+ cacheKey="users"
294
+ resourceName="user"
295
+ onFetch={(params) => sdk.users.getUsers(params)}
296
+ />
297
+ ```
298
+
299
+ ## Key Improvements
300
+
301
+ ### 1. Simplified State Management
302
+
303
+ **Before:**
304
+ - Manual `useState` for params
305
+ - Manual URL sync with `updateQueryParams`
306
+ - Complex callback chains
307
+
308
+ **After:**
309
+ - `useTableState` hook handles everything
310
+ - Automatic URL sync
311
+ - Single source of truth
312
+
313
+ ### 2. Cleaner Hooks
314
+
315
+ **Before:**
316
+ ```tsx
317
+ const { data, isFetching } = useTableQuery({
318
+ cacheKey,
319
+ fetchFn,
320
+ params,
321
+ entityId,
322
+ });
323
+
324
+ // Unnecessary isMounted ref logic
325
+ const isMounted = useRef(true);
326
+ ```
327
+
328
+ **After:**
329
+ ```tsx
330
+ // Simplified, no ref needed
331
+ const { data, isFetching } = useTableQuery({
332
+ cacheKey,
333
+ fetchFn,
334
+ params,
335
+ entityId,
336
+ });
337
+ ```
338
+
339
+ ### 3. Better Column API
340
+
341
+ **Before:**
342
+ - Column definitions not used for rendering
343
+ - Must implement custom row/header components
344
+
345
+ **After:**
346
+ - Column definitions drive rendering
347
+ - Optional custom renderers for flexibility
348
+ - Built-in cell renderers via `cell` prop
349
+
350
+ ### 4. Improved Type Safety
351
+
352
+ **Before:**
353
+ ```typescript
354
+ // Loose typing on columns
355
+ columns: { field: string; header: React.ReactNode }[]
356
+ ```
357
+
358
+ **After:**
359
+ ```typescript
360
+ // Strongly typed with generics
361
+ columns: Column<T>[]
362
+ // T is your data type (e.g., Invoice, Customer)
363
+ ```
364
+
365
+ ### 5. Better UX
366
+
367
+ **Before:**
368
+ - Basic search input
369
+ - Minimal empty states
370
+
371
+ **After:**
372
+ - Search with clear button
373
+ - Rich empty states with icons
374
+ - Better error handling in date formatter
375
+ - Improved accessibility
376
+
377
+ ## Best Practices
378
+
379
+ 1. **Use column definitions for simple tables**: Less code, easier to maintain
380
+ 2. **Use custom renderers for complex tables**: More control when needed
381
+ 3. **Define cache keys as constants**: Reuse across queries and mutations
382
+ 4. **Handle loading states**: The component handles this automatically
383
+ 5. **Provide meaningful resource names**: Used in empty states
384
+ 6. **Use FormattedDate**: Consistent date formatting with error handling
385
+ 7. **Enable sorting selectively**: Not all columns need sorting
386
+ 8. **Use proper TypeScript types**: Import from `@spaceinvoices/js-sdk`
387
+
388
+ ## Testing
389
+
390
+ All table components are fully tested:
391
+
392
+ ```bash
393
+ cd packages/ui
394
+ bun test test/components/table/
395
+ ```
396
+
397
+ ## Accessibility
398
+
399
+ - ARIA labels on interactive elements
400
+ - Keyboard navigation support
401
+ - Screen reader friendly
402
+ - Semantic HTML structure
403
+ - Proper focus management
404
+
405
+ ## Performance
406
+
407
+ - Efficient re-renders with proper memoization
408
+ - React Query caching (5-minute stale time)
409
+ - Cursor-based pagination (no offset issues)
410
+ - Skeleton loading (no layout shift)