@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,161 @@
1
+ import { useState } from "react";
2
+ import type { FieldValues, Path, PathValue, UseFormReturn } from "react-hook-form";
3
+
4
+ /**
5
+ * Customer data structure used in document forms.
6
+ * All fields are optional to support partial customer data.
7
+ */
8
+ export type CustomerData = {
9
+ name?: string | null;
10
+ address?: string | null;
11
+ address_2?: string | null;
12
+ post_code?: string | null;
13
+ city?: string | null;
14
+ state?: string | null;
15
+ country?: string | null;
16
+ tax_number?: string | null;
17
+ };
18
+
19
+ /**
20
+ * Form schema requirements for document customer handling.
21
+ * Documents must have customer_id and customer fields.
22
+ */
23
+ export type DocumentFormWithCustomer = FieldValues & {
24
+ customer_id?: string | null;
25
+ customer?: CustomerData | null;
26
+ };
27
+
28
+ /**
29
+ * Type-safe setValue wrapper for document forms.
30
+ */
31
+ function _setFormValue<TForm extends FieldValues>(
32
+ form: UseFormReturn<TForm>,
33
+ name: Path<TForm>,
34
+ value: PathValue<TForm, Path<TForm>>,
35
+ ) {
36
+ form.setValue(name, value);
37
+ }
38
+
39
+ /**
40
+ * Shared hook for managing customer selection and form state in document forms.
41
+ * Used by invoices, estimates, credit notes, and advance invoices.
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * const {
46
+ * originalCustomer,
47
+ * showCustomerForm,
48
+ * shouldFocusName,
49
+ * selectedCustomerId,
50
+ * handleCustomerSelect,
51
+ * handleCustomerClear,
52
+ * } = useDocumentCustomerForm(form);
53
+ * ```
54
+ */
55
+ export function useDocumentCustomerForm<TForm extends DocumentFormWithCustomer>(form: UseFormReturn<TForm>) {
56
+ // Initialize states based on form's default values (for duplication scenarios)
57
+ const formDefaults = form.formState.defaultValues;
58
+ const initialCustomerId = formDefaults?.customer_id as string | undefined;
59
+ const initialCustomer = formDefaults?.customer as CustomerData | undefined;
60
+ const hasInitialCustomer = !!(initialCustomerId || initialCustomer?.name);
61
+
62
+ const [originalCustomer, setOriginalCustomer] = useState<CustomerData | null>(
63
+ hasInitialCustomer && initialCustomer ? initialCustomer : null,
64
+ );
65
+ const [showCustomerForm, setShowCustomerForm] = useState(hasInitialCustomer);
66
+ const [shouldFocusName, setShouldFocusName] = useState(false);
67
+ const [selectedCustomerId, setSelectedCustomerId] = useState<string | undefined>(initialCustomerId);
68
+
69
+ // Type-safe setValue that works with the generic form type
70
+ const setValue = <K extends Path<TForm>>(name: K, value: PathValue<TForm, K>) => {
71
+ form.setValue(name, value);
72
+ };
73
+
74
+ const handleCustomerSelect = (customerId: string, customer: CustomerData) => {
75
+ const isNewCustomer = !customerId || customerId === "";
76
+
77
+ // Helper to convert empty/null to undefined for optional fields,
78
+ // but keep empty string for required fields (name) so form shows them
79
+ const toFormValue = (value: string | null | undefined, isRequired = false): string | undefined => {
80
+ if (value === null || value === undefined || value === "") {
81
+ return isRequired ? "" : undefined;
82
+ }
83
+ return value;
84
+ };
85
+
86
+ if (isNewCustomer) {
87
+ // New customer - clear customer_id and set customer data
88
+ setValue("customer_id" as Path<TForm>, undefined as PathValue<TForm, Path<TForm>>);
89
+ setValue(
90
+ "customer" as Path<TForm>,
91
+ {
92
+ name: toFormValue(customer.name, true),
93
+ address: toFormValue(customer.address),
94
+ address_2: toFormValue(customer.address_2),
95
+ post_code: toFormValue(customer.post_code),
96
+ city: toFormValue(customer.city),
97
+ state: toFormValue(customer.state),
98
+ country: toFormValue(customer.country),
99
+ tax_number: toFormValue(customer.tax_number),
100
+ } as PathValue<TForm, Path<TForm>>,
101
+ );
102
+ setOriginalCustomer(null);
103
+ setSelectedCustomerId(undefined);
104
+ setShouldFocusName(!customer.name);
105
+ } else {
106
+ // Existing customer - set customer_id and populate fields
107
+ setValue("customer_id" as Path<TForm>, customerId as PathValue<TForm, Path<TForm>>);
108
+
109
+ const customerData: CustomerData = {
110
+ name: toFormValue(customer.name, true),
111
+ address: toFormValue(customer.address),
112
+ address_2: toFormValue(customer.address_2),
113
+ post_code: toFormValue(customer.post_code),
114
+ city: toFormValue(customer.city),
115
+ state: toFormValue(customer.state),
116
+ country: toFormValue(customer.country),
117
+ tax_number: toFormValue(customer.tax_number),
118
+ };
119
+
120
+ setValue("customer" as Path<TForm>, customerData as PathValue<TForm, Path<TForm>>);
121
+ setOriginalCustomer(customerData);
122
+ setSelectedCustomerId(customerId);
123
+ setShouldFocusName(false);
124
+ }
125
+
126
+ setShowCustomerForm(true);
127
+ };
128
+
129
+ const handleCustomerClear = () => {
130
+ setValue("customer_id" as Path<TForm>, undefined as PathValue<TForm, Path<TForm>>);
131
+ // Clear customer object entirely - use undefined for optional fields
132
+ setValue(
133
+ "customer" as Path<TForm>,
134
+ {
135
+ name: "",
136
+ address: undefined,
137
+ address_2: undefined,
138
+ post_code: undefined,
139
+ city: undefined,
140
+ state: undefined,
141
+ country: undefined,
142
+ tax_number: undefined,
143
+ } as PathValue<TForm, Path<TForm>>,
144
+ );
145
+ setOriginalCustomer(null);
146
+ setSelectedCustomerId(undefined);
147
+ setShouldFocusName(false);
148
+ setShowCustomerForm(false);
149
+ };
150
+
151
+ return {
152
+ originalCustomer,
153
+ showCustomerForm,
154
+ shouldFocusName,
155
+ selectedCustomerId,
156
+ /** Initial customer name from form defaults (for duplication display) */
157
+ initialCustomerName: initialCustomer?.name ?? undefined,
158
+ handleCustomerSelect,
159
+ handleCustomerClear,
160
+ };
161
+ }
@@ -0,0 +1,13 @@
1
+ import type { CreateInvoiceRequest } from "@spaceinvoices/js-sdk";
2
+
3
+ type DocumentPreviewProps = {
4
+ data: Partial<CreateInvoiceRequest>;
5
+ };
6
+
7
+ export default function DocumentPreview({ data }: DocumentPreviewProps) {
8
+ return (
9
+ <div className="rounded-lg border p-4">
10
+ <pre>{JSON.stringify(data, null, 2)}</pre>
11
+ </div>
12
+ );
13
+ }
@@ -0,0 +1,146 @@
1
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import { AUTH_COOKIES } from "@/ui/lib/auth";
3
+ import { getCookie } from "@/ui/lib/browser-cookies";
4
+
5
+ // Document type union for API calls
6
+ export type DocumentType = "invoice" | "estimate" | "credit_note" | "advance_invoice";
7
+
8
+ // Cache key map for invalidation
9
+ const CACHE_KEYS: Record<DocumentType, string> = {
10
+ invoice: "invoices",
11
+ estimate: "estimates",
12
+ credit_note: "credit-notes",
13
+ advance_invoice: "advance-invoices",
14
+ };
15
+
16
+ /**
17
+ * Get API base URL from environment
18
+ */
19
+ function getApiBaseUrl(): string {
20
+ if (typeof window === "undefined") return "";
21
+ return (import.meta.env?.VITE_API_URL || import.meta.env?.BUN_PUBLIC_API_URL || "") as string;
22
+ }
23
+
24
+ /**
25
+ * Make authenticated API request
26
+ */
27
+ async function apiRequest<T>(path: string, options: RequestInit & { entityId: string }): Promise<T> {
28
+ const token = getCookie(AUTH_COOKIES.TOKEN);
29
+ const baseUrl = getApiBaseUrl();
30
+
31
+ const response = await fetch(`${baseUrl}${path}`, {
32
+ ...options,
33
+ headers: {
34
+ "Content-Type": "application/json",
35
+ Authorization: `Bearer ${token}`,
36
+ "x-entity-id": options.entityId,
37
+ ...options.headers,
38
+ },
39
+ });
40
+
41
+ if (!response.ok) {
42
+ const errorData = await response.json().catch(() => ({}));
43
+ throw new Error(errorData.message || `Request failed with status ${response.status}`);
44
+ }
45
+
46
+ // DELETE returns 204 No Content
47
+ if (response.status === 204) {
48
+ return undefined as T;
49
+ }
50
+
51
+ return response.json();
52
+ }
53
+
54
+ // ============================================================================
55
+ // Finalize Document Hook
56
+ // ============================================================================
57
+
58
+ type FinalizeDocumentOptions = {
59
+ entityId: string;
60
+ onSuccess?: (data: unknown) => void;
61
+ onError?: (error: Error) => void;
62
+ };
63
+
64
+ type FinalizeDocumentVariables = {
65
+ documentId: string;
66
+ documentType: DocumentType;
67
+ };
68
+
69
+ /**
70
+ * Hook to finalize a draft document
71
+ * Assigns a document number and runs fiscalization (if applicable)
72
+ */
73
+ export function useFinalizeDocument(options: FinalizeDocumentOptions) {
74
+ const queryClient = useQueryClient();
75
+
76
+ return useMutation({
77
+ mutationFn: async ({ documentId, documentType }: FinalizeDocumentVariables) => {
78
+ return apiRequest(`/documents/${documentId}/finalize?type=${documentType}`, {
79
+ method: "POST",
80
+ entityId: options.entityId,
81
+ });
82
+ },
83
+ onSuccess: (data, variables) => {
84
+ // Invalidate list cache
85
+ const cacheKey = CACHE_KEYS[variables.documentType];
86
+ queryClient.invalidateQueries({ queryKey: [cacheKey] });
87
+
88
+ // Invalidate document detail cache
89
+ queryClient.invalidateQueries({
90
+ queryKey: ["documents", variables.documentType, variables.documentId],
91
+ });
92
+
93
+ options.onSuccess?.(data);
94
+ },
95
+ onError: (error: Error) => {
96
+ options.onError?.(error);
97
+ },
98
+ });
99
+ }
100
+
101
+ // ============================================================================
102
+ // Delete Draft Document Hook
103
+ // ============================================================================
104
+
105
+ type DeleteDraftDocumentOptions = {
106
+ entityId: string;
107
+ onSuccess?: () => void;
108
+ onError?: (error: Error) => void;
109
+ };
110
+
111
+ type DeleteDraftDocumentVariables = {
112
+ documentId: string;
113
+ documentType: DocumentType;
114
+ };
115
+
116
+ /**
117
+ * Hook to delete a draft document
118
+ * Only draft documents can be deleted
119
+ */
120
+ export function useDeleteDraftDocument(options: DeleteDraftDocumentOptions) {
121
+ const queryClient = useQueryClient();
122
+
123
+ return useMutation({
124
+ mutationFn: async ({ documentId, documentType }: DeleteDraftDocumentVariables) => {
125
+ return apiRequest(`/documents/${documentId}?type=${documentType}`, {
126
+ method: "DELETE",
127
+ entityId: options.entityId,
128
+ });
129
+ },
130
+ onSuccess: (_, variables) => {
131
+ // Invalidate list cache
132
+ const cacheKey = CACHE_KEYS[variables.documentType];
133
+ queryClient.invalidateQueries({ queryKey: [cacheKey] });
134
+
135
+ // Remove document from detail cache
136
+ queryClient.removeQueries({
137
+ queryKey: ["documents", variables.documentType, variables.documentId],
138
+ });
139
+
140
+ options.onSuccess?.();
141
+ },
142
+ onError: (error: Error) => {
143
+ options.onError?.(error);
144
+ },
145
+ });
146
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Documents - Shared components for all document types (invoices, estimates, credit notes)
3
+ */
4
+
5
+ export { default as DocumentAddItemForm } from "./create/document-add-item-form";
6
+ export { default as DocumentAddItemTaxRateField } from "./create/document-add-item-tax-rate-field";
7
+ // Create form components
8
+ export { DocumentDetailsSection } from "./create/document-details-section";
9
+ export { DocumentItemsSection } from "./create/document-items-section";
10
+ export { DocumentRecipientSection } from "./create/document-recipient-section";
11
+ export { LiveInvoicePreview } from "./create/live-preview";
12
+ export { MarkAsPaidSection } from "./create/mark-as-paid-section";
13
+ // Shared utilities
14
+ export { prepareDocumentSubmission } from "./create/prepare-document-submission";
15
+ export { useDocumentCustomerForm } from "./create/use-document-customer-form";
16
+ // Preview components
17
+ export { default as DocumentPreview } from "./document-preview";
18
+ export { ScaledDocumentPreview } from "./shared/scaled-document-preview";
19
+ export { useA4Scaling } from "./shared/use-a4-scaling";
20
+ // Types
21
+ export * from "./types";
22
+ // View components
23
+ export { DocumentActionsBar, DocumentDetailsCard, DocumentPaymentsList } from "./view";
@@ -0,0 +1,172 @@
1
+ "use client";
2
+
3
+ import type { AdvanceInvoice, CreditNote, Estimate, Invoice } from "@spaceinvoices/js-sdk";
4
+ import { AlertCircle, FileText, Loader2 } from "lucide-react";
5
+ import { useEffect, useState } from "react";
6
+ import { cn } from "@/ui/lib/utils";
7
+ import { useEntitiesOptional } from "@/ui/providers/entities-context";
8
+ import { useSDK } from "@/ui/providers/sdk-provider";
9
+ import { ScaledDocumentPreview } from "./scaled-document-preview";
10
+ import { useA4Scaling } from "./use-a4-scaling";
11
+
12
+ type Document = Invoice | Estimate | CreditNote | AdvanceInvoice;
13
+
14
+ /**
15
+ * Get API path segment from shareable ID prefix
16
+ * Shareable IDs are prefixed with document type: inv_share_, est_share_, cre_share_, adv_share_
17
+ */
18
+ function getDocTypePathFromShareableId(shareableId: string): string {
19
+ if (shareableId.startsWith("inv_share_")) return "invoices";
20
+ if (shareableId.startsWith("est_share_")) return "estimates";
21
+ if (shareableId.startsWith("cre_share_")) return "credit-notes";
22
+ if (shareableId.startsWith("adv_share_")) return "advance-invoices";
23
+ // Fallback to invoices for backwards compatibility
24
+ return "invoices";
25
+ }
26
+
27
+ type DocumentPreviewDisplayProps = {
28
+ /** The document to display (invoice, estimate, credit note, or advance invoice) */
29
+ document: Document;
30
+ template?: "modern";
31
+ className?: string;
32
+ apiBaseUrl?: string;
33
+ /** Locale for document rendering (e.g., "en-US", "sl-SI"). Uses user's UI language. */
34
+ locale?: string;
35
+ /** Whether this is a public view (no auth required) */
36
+ isPublicView?: boolean;
37
+ /** Shareable ID for public view (required when isPublicView is true) */
38
+ shareableId?: string;
39
+ };
40
+
41
+ /**
42
+ * Document Preview Display Component
43
+ *
44
+ * Fetches and displays the HTML preview of a saved document.
45
+ * Works with any document type (invoice, estimate, credit note, advance invoice).
46
+ * Document type is auto-detected from the ID prefix.
47
+ */
48
+ export function DocumentPreviewDisplay({
49
+ document,
50
+ template,
51
+ className,
52
+ apiBaseUrl,
53
+ locale,
54
+ isPublicView = false,
55
+ shareableId,
56
+ }: DocumentPreviewDisplayProps) {
57
+ const [previewHtml, setPreviewHtml] = useState<string>("");
58
+ const [isLoading, setIsLoading] = useState(true);
59
+ const [error, setError] = useState<string | null>(null);
60
+ const entitiesContext = useEntitiesOptional();
61
+ const activeEntity = entitiesContext?.activeEntity;
62
+ const { sdk } = useSDK();
63
+
64
+ const { containerRef, contentRef, scale, contentHeight, A4_WIDTH_PX } = useA4Scaling(previewHtml);
65
+
66
+ useEffect(() => {
67
+ const fetchPreview = async () => {
68
+ // For public view, use per-type shareable HTML endpoint
69
+ if (isPublicView && shareableId && apiBaseUrl) {
70
+ setIsLoading(true);
71
+ setError(null);
72
+ try {
73
+ // Determine document type from shareable ID prefix
74
+ const docTypePath = getDocTypePathFromShareableId(shareableId);
75
+ const response = await fetch(
76
+ `${apiBaseUrl}/${docTypePath}/shareable/${shareableId}/html${locale ? `?locale=${locale}` : ""}`,
77
+ );
78
+ if (!response.ok) {
79
+ throw new Error("Failed to load preview");
80
+ }
81
+ const html = await response.text();
82
+ setPreviewHtml(html);
83
+ } catch (err) {
84
+ setError(err instanceof Error ? err.message : "Failed to load preview");
85
+ setPreviewHtml("");
86
+ } finally {
87
+ setIsLoading(false);
88
+ }
89
+ return;
90
+ }
91
+
92
+ // Authenticated view - require entity context and SDK
93
+ if (!document?.id || !activeEntity?.id || !sdk) {
94
+ return;
95
+ }
96
+
97
+ setIsLoading(true);
98
+ setError(null);
99
+
100
+ try {
101
+ // Fetch the rendered HTML by document ID using SDK wrapper
102
+ // Document type is auto-detected from ID prefix (inv_, est_, cre_, adv_)
103
+ const html = await sdk.invoices.renderHtml(document.id, { template, locale }, { entity_id: activeEntity.id });
104
+
105
+ setPreviewHtml(html);
106
+ setError(null);
107
+ } catch (err) {
108
+ setError(err instanceof Error ? err.message : "Failed to load preview");
109
+ setPreviewHtml("");
110
+ } finally {
111
+ setIsLoading(false);
112
+ }
113
+ };
114
+
115
+ fetchPreview();
116
+ }, [document?.id, activeEntity?.id, template, apiBaseUrl, locale, isPublicView, shareableId, sdk]);
117
+
118
+ return (
119
+ <div ref={containerRef} className={cn("relative h-full", className)}>
120
+ {/* Loading state */}
121
+ {isLoading && (
122
+ <div className="flex h-full items-center justify-center rounded-lg border bg-muted/50">
123
+ <div className="flex flex-col items-center gap-2">
124
+ <Loader2 className="h-8 w-8 animate-spin text-primary" />
125
+ <p className="text-muted-foreground text-sm">Loading preview...</p>
126
+ </div>
127
+ </div>
128
+ )}
129
+
130
+ {/* Error state */}
131
+ {error && !isLoading && (
132
+ <div className="flex h-full items-center justify-center rounded-lg border border-destructive/50 bg-destructive/10 p-8">
133
+ <div className="flex flex-col items-center gap-2 text-center">
134
+ <AlertCircle className="h-8 w-8 text-destructive" />
135
+ <p className="font-semibold text-destructive">Preview Error</p>
136
+ <p className="text-muted-foreground text-sm">{error}</p>
137
+ </div>
138
+ </div>
139
+ )}
140
+
141
+ {/* Empty state - no preview available */}
142
+ {!previewHtml && !error && !isLoading && (
143
+ <div className="flex h-full items-center justify-center rounded-lg border border-dashed bg-muted/30">
144
+ <div className="flex flex-col items-center gap-3 text-center">
145
+ <div className="rounded-full bg-muted p-4">
146
+ <FileText className="h-8 w-8 text-muted-foreground" />
147
+ </div>
148
+ <div>
149
+ <p className="font-medium text-muted-foreground">Document Preview</p>
150
+ <p className="text-muted-foreground/70 text-sm">Preview will appear here</p>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ )}
155
+
156
+ {/* Preview - Scoped HTML injection with A4 scaling */}
157
+ {previewHtml && !error && !isLoading && (
158
+ <ScaledDocumentPreview
159
+ htmlContent={previewHtml}
160
+ scale={scale}
161
+ contentHeight={contentHeight}
162
+ A4_WIDTH_PX={A4_WIDTH_PX}
163
+ contentRef={contentRef}
164
+ entityUpdatedAt={activeEntity?.updated_at ? new Date(activeEntity.updated_at) : null}
165
+ />
166
+ )}
167
+ </div>
168
+ );
169
+ }
170
+
171
+ /** @deprecated Use DocumentPreviewDisplay instead */
172
+ export const InvoicePreviewDisplay = DocumentPreviewDisplay;
@@ -0,0 +1,3 @@
1
+ export { DocumentPreviewDisplay, InvoicePreviewDisplay } from "./document-preview-display";
2
+ export { ScaledDocumentPreview } from "./scaled-document-preview";
3
+ export { useA4Scaling } from "./use-a4-scaling";
@@ -0,0 +1,70 @@
1
+ "use client";
2
+
3
+ import { type FC, useEffect, useRef, useState } from "react";
4
+
5
+ interface ScaledDocumentPreviewProps {
6
+ htmlContent: string;
7
+ scale: number;
8
+ contentHeight: number | null;
9
+ A4_WIDTH_PX: number;
10
+ contentRef: React.RefObject<HTMLDivElement | null>;
11
+ entityUpdatedAt?: Date | null;
12
+ }
13
+
14
+ /**
15
+ * Scaled Document Preview Component
16
+ *
17
+ * Renders HTML content in a Shadow DOM with A4 scaling applied using CSS transforms.
18
+ * Uses Shadow DOM to completely isolate template CSS from the parent page.
19
+ */
20
+ export const ScaledDocumentPreview: FC<ScaledDocumentPreviewProps> = ({ htmlContent, scale, A4_WIDTH_PX }) => {
21
+ const shadowHostRef = useRef<HTMLDivElement>(null);
22
+ const shadowRootRef = useRef<ShadowRoot | null>(null);
23
+ const [contentHeight, setContentHeight] = useState<number>(1123); // A4 height default
24
+
25
+ useEffect(() => {
26
+ const host = shadowHostRef.current;
27
+ if (!host) return;
28
+
29
+ // Create shadow root only once
30
+ if (!shadowRootRef.current) {
31
+ shadowRootRef.current = host.attachShadow({ mode: "open" });
32
+ }
33
+
34
+ const shadowRoot = shadowRootRef.current;
35
+ shadowRoot.innerHTML = htmlContent;
36
+
37
+ // Measure content height after render
38
+ const measureHeight = () => {
39
+ const firstChild = shadowRoot.firstElementChild as HTMLElement;
40
+ if (firstChild) {
41
+ setContentHeight(firstChild.scrollHeight || 1123);
42
+ }
43
+ };
44
+
45
+ setTimeout(measureHeight, 50);
46
+ }, [htmlContent]);
47
+
48
+ return (
49
+ <div className="rounded-lg border bg-neutral-100 p-4">
50
+ <div
51
+ style={{
52
+ width: A4_WIDTH_PX * scale,
53
+ height: contentHeight * scale,
54
+ margin: "0 auto",
55
+ overflow: "hidden",
56
+ }}
57
+ >
58
+ <div
59
+ ref={shadowHostRef}
60
+ style={{
61
+ width: A4_WIDTH_PX,
62
+ transform: `scale(${scale})`,
63
+ transformOrigin: "top left",
64
+ background: "white",
65
+ }}
66
+ />
67
+ </div>
68
+ </div>
69
+ );
70
+ };
@@ -0,0 +1,62 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+
3
+ /**
4
+ * Custom hook for A4 document scaling
5
+ * Handles responsive scaling of A4-sized documents to fit container width
6
+ *
7
+ * @returns Object containing scale, contentHeight, and refs for container and content
8
+ */
9
+ export function useA4Scaling(_htmlContent?: string) {
10
+ const containerRef = useRef<HTMLDivElement>(null);
11
+ const contentRef = useRef<HTMLDivElement>(null);
12
+ const [scale, setScale] = useState(1);
13
+ const [contentHeight, setContentHeight] = useState<number | null>(null);
14
+
15
+ // A4 width in pixels at 96 DPI (210mm)
16
+ const A4_WIDTH_PX = 794;
17
+
18
+ // Observe container width and calculate scale
19
+ useEffect(() => {
20
+ const container = containerRef.current;
21
+ if (!container) return;
22
+
23
+ const observer = new ResizeObserver((entries) => {
24
+ for (const entry of entries) {
25
+ const width = entry.contentRect.width;
26
+ // Subtract padding
27
+ const availableWidth = width - 32;
28
+ // Round to 2 decimal places and only update if significant change (>1%)
29
+ const newScale = Math.round((availableWidth / A4_WIDTH_PX) * 100) / 100;
30
+ setScale((prev) => (Math.abs(prev - newScale) > 0.01 ? newScale : prev));
31
+ }
32
+ });
33
+
34
+ observer.observe(container);
35
+ return () => observer.disconnect();
36
+ }, []);
37
+
38
+ // Observe content height for proper container sizing
39
+ useEffect(() => {
40
+ const content = contentRef.current;
41
+ if (!content) return;
42
+
43
+ const observer = new ResizeObserver((entries) => {
44
+ for (const entry of entries) {
45
+ const newHeight = Math.round(entry.contentRect.height);
46
+ // Only update if height changed by more than 5px to avoid jitter
47
+ setContentHeight((prev) => (prev === null || Math.abs(prev - newHeight) > 5 ? newHeight : prev));
48
+ }
49
+ });
50
+
51
+ observer.observe(content);
52
+ return () => observer.disconnect();
53
+ }, []);
54
+
55
+ return {
56
+ containerRef,
57
+ contentRef,
58
+ scale,
59
+ contentHeight,
60
+ A4_WIDTH_PX,
61
+ };
62
+ }