themelia-react-ui 2.0.3 → 2.0.4
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.
- package/CHANGELOG.md +50 -0
- package/README.md +65 -28
- package/dist/{accordion-Cjb6sx0G.js → accordion-96o0pN62.js} +5 -5
- package/dist/{accordion-Cjb6sx0G.js.map → accordion-96o0pN62.js.map} +1 -1
- package/dist/action-menu-CHgs8vmj.js +281 -0
- package/dist/action-menu-CHgs8vmj.js.map +1 -0
- package/dist/{affordance-geometry.generated-tF36bREk.js → affordance-geometry.generated-B8yGOr8-.js} +7 -1
- package/dist/affordance-geometry.generated-B8yGOr8-.js.map +1 -0
- package/dist/{ai-artifact-DvTwDVyL.js → ai-artifact-BZUGwBq6.js} +277 -276
- package/dist/ai-artifact-BZUGwBq6.js.map +1 -0
- package/dist/{app-shell-C4FhvCqF.js → app-shell-D63ATkIK.js} +84 -83
- package/dist/app-shell-D63ATkIK.js.map +1 -0
- package/dist/{attachment-BxDfL2mw.js → attachment-DkoqHPPl.js} +7 -7
- package/dist/{attachment-BxDfL2mw.js.map → attachment-DkoqHPPl.js.map} +1 -1
- package/dist/{auth-BGua8ouh.js → auth-D2FVZ1MH.js} +10 -10
- package/dist/{auth-BGua8ouh.js.map → auth-D2FVZ1MH.js.map} +1 -1
- package/dist/{avatar-DAPlDvhJ.js → avatar-DWO_HHhB.js} +5 -5
- package/dist/{avatar-DAPlDvhJ.js.map → avatar-DWO_HHhB.js.map} +1 -1
- package/dist/{badge-HpFqoqSY.js → badge-DErMA7ul.js} +4 -4
- package/dist/{badge-HpFqoqSY.js.map → badge-DErMA7ul.js.map} +1 -1
- package/dist/base/accordion.js +1 -1
- package/dist/base/action-menu.js +1 -1
- package/dist/base/attachment.js +1 -1
- package/dist/base/avatar.js +1 -1
- package/dist/base/badge.js +1 -1
- package/dist/base/batch-action-bar.js +1 -1
- package/dist/base/buttons.js +1 -1
- package/dist/base/cards.js +2 -2
- package/dist/base/carousel.js +3 -3
- package/dist/base/cascader.js +13 -13
- package/dist/base/chart.js +1 -1
- package/dist/base/choice-inputs.js +2 -2
- package/dist/base/code-block.js +1 -1
- package/dist/base/combobox.js +1 -1
- package/dist/base/command.js +1 -1
- package/dist/base/copyable.js +1 -1
- package/dist/base/date-pickers.js +2 -2
- package/dist/base/display.js +6 -6
- package/dist/base/feedback.js +3 -3
- package/dist/base/forms-numeric.js +1 -1
- package/dist/base/forms.js +3 -3
- package/dist/base/inline-edit.js +10 -10
- package/dist/base/item.js +1 -1
- package/dist/base/media-gallery.js +1 -1
- package/dist/base/metadata-list.js +1 -1
- package/dist/base/navigation.js +6 -6
- package/dist/base/popover-menu.js +1 -1
- package/dist/base/popover.js +1 -1
- package/dist/base/qr-code.js +1 -1
- package/dist/base/repeaters.js +9 -9
- package/dist/base/sheet.js +1 -1
- package/dist/base/sidebar.js +1 -1
- package/dist/base/signature-pad.js +6 -6
- package/dist/base/skeleton.js +1 -1
- package/dist/base/spinner.js +4 -4
- package/dist/base/structure.js +1 -1
- package/dist/base/table.js +1 -1
- package/dist/base/text-inputs.js +2 -2
- package/dist/base/toaster.js +3 -3
- package/dist/base/tree.js +1 -1
- package/dist/base/upload-queue.js +1 -1
- package/dist/base/upload.js +3 -3
- package/dist/base/value-inputs.js +1 -1
- package/dist/{batch-action-bar-DnUJ21NQ.js → batch-action-bar-CdfIkCsV.js} +4 -4
- package/dist/{batch-action-bar-DnUJ21NQ.js.map → batch-action-bar-CdfIkCsV.js.map} +1 -1
- package/dist/blocks/admin.js +20 -20
- package/dist/blocks/admin.js.map +1 -1
- package/dist/blocks/ai.js +15 -15
- package/dist/blocks/analytics/usage-card.js +1 -1
- package/dist/blocks/analytics.js +17 -17
- package/dist/blocks/analytics.js.map +1 -1
- package/dist/blocks/collaboration.js +1 -1
- package/dist/blocks/commerce/receipt.js +1 -1
- package/dist/blocks/commerce.js +570 -567
- package/dist/blocks/commerce.js.map +1 -1
- package/dist/blocks/data-display.js +2 -2
- package/dist/blocks/onboarding.js +5 -5
- package/dist/blocks/planning.js +11 -11
- package/dist/blocks/timelines.js +1 -1
- package/dist/{breadcrumbs-C-oqs1sQ.js → breadcrumbs-B-WFKecD.js} +4 -4
- package/dist/{breadcrumbs-C-oqs1sQ.js.map → breadcrumbs-B-WFKecD.js.map} +1 -1
- package/dist/{button-DVzspB_G.js → button-DkmK-z8E.js} +2 -2
- package/dist/{button-DVzspB_G.js.map → button-DkmK-z8E.js.map} +1 -1
- package/dist/{buttons-DPZekJFu.js → buttons-BbfzgczK.js} +8 -8
- package/dist/{buttons-DPZekJFu.js.map → buttons-BbfzgczK.js.map} +1 -1
- package/dist/{cards-BaBZR-AU.js → cards-4JTWqvVe.js} +10 -10
- package/dist/cards-4JTWqvVe.js.map +1 -0
- package/dist/{chart-ZOYyK2xG.js → chart-C19flDQb.js} +4 -4
- package/dist/{chart-ZOYyK2xG.js.map → chart-C19flDQb.js.map} +1 -1
- package/dist/{checkbox-AWIhNhmo.js → checkbox-CzYUTPeJ.js} +9 -5
- package/dist/checkbox-CzYUTPeJ.js.map +1 -0
- package/dist/{choice-inputs-CNHvARCJ.js → choice-inputs-JLtij2y4.js} +182 -176
- package/dist/choice-inputs-JLtij2y4.js.map +1 -0
- package/dist/{code-block-DeneBJR_.js → code-block-CGNaUtAE.js} +7 -7
- package/dist/{code-block-DeneBJR_.js.map → code-block-CGNaUtAE.js.map} +1 -1
- package/dist/{collapsible-C-_l4hpy.js → collapsible-Do4G0zcC.js} +3 -3
- package/dist/{collapsible-C-_l4hpy.js.map → collapsible-Do4G0zcC.js.map} +1 -1
- package/dist/{collection-content-By56pLog.js → collection-content-C0e8SsTm.js} +3 -3
- package/dist/{collection-content-By56pLog.js.map → collection-content-C0e8SsTm.js.map} +1 -1
- package/dist/{combobox-Dxo2fAYQ.js → combobox-CBJDs6w_.js} +12 -12
- package/dist/{combobox-Dxo2fAYQ.js.map → combobox-CBJDs6w_.js.map} +1 -1
- package/dist/{combobox-DaHESuiL.js → combobox-hbhSS18O.js} +7 -7
- package/dist/{combobox-DaHESuiL.js.map → combobox-hbhSS18O.js.map} +1 -1
- package/dist/{command-C25VMRII.js → command-D8NrBj-b.js} +7 -7
- package/dist/{command-C25VMRII.js.map → command-D8NrBj-b.js.map} +1 -1
- package/dist/{comments-DD0DUPwJ.js → comments-Di4w0H_S.js} +383 -382
- package/dist/comments-Di4w0H_S.js.map +1 -0
- package/dist/components/base/action-menu/action-menu.d.ts +2 -0
- package/dist/components/base/cards/card.recipes.d.ts +12 -5
- package/dist/components/base/cards/smart-card.types.d.ts +12 -7
- package/dist/components/base/choice-inputs/checkbox.d.ts +7 -2
- package/dist/components/base/choice-inputs/switch-card.d.ts +7 -0
- package/dist/components/base/choice-inputs/toggle-field.d.ts +7 -0
- package/dist/components/base/date-pickers/datepicker.types.d.ts +7 -0
- package/dist/components/base/forms/controlled-form-field.d.ts +6 -6
- package/dist/components/base/forms/form-field.d.ts +5 -3
- package/dist/components/base/forms/index.d.ts +1 -0
- package/dist/components/base/forms/server-errors.d.ts +16 -0
- package/dist/components/base/forms/workflow.d.ts +5 -2
- package/dist/components/base/metadata-list/metadata-list.d.ts +3 -1
- package/dist/components/base/navigation/page-header/page-header.d.ts +4 -2
- package/dist/components/base/structure/composition.recipes.d.ts +17 -0
- package/dist/components/base/text-inputs/hooks/use-form-field-state.d.ts +7 -2
- package/dist/components/base/text-inputs/input.d.ts +4 -2
- package/dist/components/base/text-inputs/textarea.d.ts +4 -2
- package/dist/components/base/value-inputs/date-time-input.d.ts +2 -1
- package/dist/components/base/value-inputs/slider.d.ts +5 -1
- package/dist/components/base/value-inputs/tags-input.d.ts +5 -1
- package/dist/components/typography/text.styles.d.ts +26 -1
- package/dist/composition.recipes-COdiFhxB.js +19 -0
- package/dist/composition.recipes-COdiFhxB.js.map +1 -0
- package/dist/containers-CRmHf7GP.js +104 -0
- package/dist/containers-CRmHf7GP.js.map +1 -0
- package/dist/{copyable-ClBuk_UK.js → copyable-CnGCZdX4.js} +4 -4
- package/dist/{copyable-ClBuk_UK.js.map → copyable-CnGCZdX4.js.map} +1 -1
- package/dist/{data-display-DT_khPl_.js → data-display-D4oujtx0.js} +239 -238
- package/dist/data-display-D4oujtx0.js.map +1 -0
- package/dist/{date-DCrsDTZc.js → date-Q7jnXxpk.js} +5 -5
- package/dist/{date-DCrsDTZc.js.map → date-Q7jnXxpk.js.map} +1 -1
- package/dist/{date-picker-B2K3ucWn.js → date-picker-zOt1oNss.js} +102 -90
- package/dist/date-picker-zOt1oNss.js.map +1 -0
- package/dist/date-pickers-cj8IXbDC.js +179 -0
- package/dist/date-pickers-cj8IXbDC.js.map +1 -0
- package/dist/{display-hAc1HjpI.js → display-BB4BL_-K.js} +8 -8
- package/dist/{display-hAc1HjpI.js.map → display-BB4BL_-K.js.map} +1 -1
- package/dist/{dropdown-menu-Cl4X7L5S.js → dropdown-menu-CjY8Vt7W.js} +8 -8
- package/dist/{dropdown-menu-Cl4X7L5S.js.map → dropdown-menu-CjY8Vt7W.js.map} +1 -1
- package/dist/features/activities.js +11 -11
- package/dist/features/ai-chat.js +9 -9
- package/dist/features/appointment-calendar.js +222 -220
- package/dist/features/appointment-calendar.js.map +1 -1
- package/dist/features/async-preview.js +8 -8
- package/dist/features/combobox.js +1 -1
- package/dist/features/comments.js +1 -1
- package/dist/features/data-view.js +10 -10
- package/dist/features/email-composer.js +275 -274
- package/dist/features/email-composer.js.map +1 -1
- package/dist/features/event-calendar.js +199 -198
- package/dist/features/event-calendar.js.map +1 -1
- package/dist/features/filters.js +1 -1
- package/dist/features/global-search.js +12 -12
- package/dist/features/kanban.js +4 -4
- package/dist/features/map.js +7 -7
- package/dist/features/media-library.js +623 -622
- package/dist/features/media-library.js.map +1 -1
- package/dist/features/mentions.js +1 -1
- package/dist/features/overlays.js +1 -1
- package/dist/features/products.js +973 -969
- package/dist/features/products.js.map +1 -1
- package/dist/features/questionnaire.js +12 -12
- package/dist/features/resource-assignment.js +8 -8
- package/dist/features/resource.js +113 -112
- package/dist/features/resource.js.map +1 -1
- package/dist/features/rich-text-editor.js +2 -2
- package/dist/features/schema-form.js +254 -253
- package/dist/features/schema-form.js.map +1 -1
- package/dist/features/suggestions.js +3 -3
- package/dist/features/sync.js +3 -3
- package/dist/features/table.js +1 -1
- package/dist/features/theme-tweaker.js +19 -19
- package/dist/features/theme-tweaker.js.map +1 -1
- package/dist/{feedback-BYIhtitP.js → feedback-DZNL5Lg1.js} +9 -9
- package/dist/{feedback-BYIhtitP.js.map → feedback-DZNL5Lg1.js.map} +1 -1
- package/dist/{field-affordance-recipes-DZhnIL_Y.js → field-affordance-recipes-DjwGx8eM.js} +2 -2
- package/dist/{field-affordance-recipes-DZhnIL_Y.js.map → field-affordance-recipes-DjwGx8eM.js.map} +1 -1
- package/dist/{filters-DJVAveHA.js → filters-BcT-uoFy.js} +19 -19
- package/dist/{filters-DJVAveHA.js.map → filters-BcT-uoFy.js.map} +1 -1
- package/dist/{form-field-CwH9y77i.js → form-field-CsgRmdit.js} +14 -8
- package/dist/form-field-CsgRmdit.js.map +1 -0
- package/dist/{form-sizing-BdP_Mf5p.js → form-sizing-NXgjkmBG.js} +3 -3
- package/dist/{form-sizing-BdP_Mf5p.js.map → form-sizing-NXgjkmBG.js.map} +1 -1
- package/dist/{forms-B9G_xwzi.js → forms-BV5m5aQF.js} +83 -65
- package/dist/forms-BV5m5aQF.js.map +1 -0
- package/dist/{forms-numeric-B_jraJaQ.js → forms-numeric-Cd33AKbD.js} +288 -288
- package/dist/forms-numeric-Cd33AKbD.js.map +1 -0
- package/dist/{header-BSfIEWbI.js → header-BA697l4a.js} +14 -14
- package/dist/{header-BSfIEWbI.js.map → header-BA697l4a.js.map} +1 -1
- package/dist/hooks/useFormErrors.d.ts +1 -1
- package/dist/{hooks-ClH9thn4.js → hooks-BQ-L7Uu2.js} +2 -2
- package/dist/{hooks-ClH9thn4.js.map → hooks-BQ-L7Uu2.js.map} +1 -1
- package/dist/{icon-badge-DYKIwvOV.js → icon-badge-COWbCCWu.js} +3 -3
- package/dist/{icon-badge-DYKIwvOV.js.map → icon-badge-COWbCCWu.js.map} +1 -1
- package/dist/index.js +2 -2
- package/dist/input-Cdw4k0fb.js +289 -0
- package/dist/input-Cdw4k0fb.js.map +1 -0
- package/dist/{input-CZw8iU99.js → input-DOPL5_QE.js} +3 -3
- package/dist/{input-CZw8iU99.js.map → input-DOPL5_QE.js.map} +1 -1
- package/dist/item-BvZ2DR_x.js +277 -0
- package/dist/item-BvZ2DR_x.js.map +1 -0
- package/dist/layout/app-shell.js +1 -1
- package/dist/layout/auth.js +1 -1
- package/dist/layout/containers.js +1 -1
- package/dist/layout/header.js +1 -1
- package/dist/layout/hooks.js +1 -1
- package/dist/layout/navigation.js +1 -1
- package/dist/layout/page.js +1 -1
- package/dist/layout/settings.js +1 -1
- package/dist/layout/sidebar.js +2 -2
- package/dist/layout/workspace.js +1 -1
- package/dist/layout.js +10 -10
- package/dist/lib/affordance-geometry.generated.d.ts +6 -0
- package/dist/lib/ui-provider/types.d.ts +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/{media-gallery-3zIVLMWH.js → media-gallery-CSl5W1Fy.js} +6 -6
- package/dist/{media-gallery-3zIVLMWH.js.map → media-gallery-CSl5W1Fy.js.map} +1 -1
- package/dist/{mentions-CGlEJ15c.js → mentions-RKOQaaIB.js} +8 -8
- package/dist/{mentions-CGlEJ15c.js.map → mentions-RKOQaaIB.js.map} +1 -1
- package/dist/{message-DZ_ruyUV.js → message-INU2xsxu.js} +4 -4
- package/dist/{message-DZ_ruyUV.js.map → message-INU2xsxu.js.map} +1 -1
- package/dist/metadata-list-qVV7AVxa.js +443 -0
- package/dist/metadata-list-qVV7AVxa.js.map +1 -0
- package/dist/{navigation-BzhHI3lo.js → navigation-BQNcg1jT.js} +8 -8
- package/dist/{navigation-BzhHI3lo.js.map → navigation-BQNcg1jT.js.map} +1 -1
- package/dist/{navigation-Bnotn4AQ.js → navigation-DLF3E1IC.js} +7 -7
- package/dist/{navigation-Bnotn4AQ.js.map → navigation-DLF3E1IC.js.map} +1 -1
- package/dist/{navigation-row-recipes-BFLCk_Aq.js → navigation-row-recipes-WZEs-hYo.js} +2 -2
- package/dist/{navigation-row-recipes-BFLCk_Aq.js.map → navigation-row-recipes-WZEs-hYo.js.map} +1 -1
- package/dist/{number-DbV1Dija.js → number-E_OiIfJz.js} +5 -5
- package/dist/{number-DbV1Dija.js.map → number-E_OiIfJz.js.map} +1 -1
- package/dist/{overflow-tab-bar-CWY7Nogn.js → overflow-tab-bar-CrbqMteP.js} +107 -105
- package/dist/overflow-tab-bar-CrbqMteP.js.map +1 -0
- package/dist/{overlays-tMkF-VAN.js → overlays-BlKynmde.js} +228 -227
- package/dist/overlays-BlKynmde.js.map +1 -0
- package/dist/page-CIwNUtGr.js +227 -0
- package/dist/page-CIwNUtGr.js.map +1 -0
- package/dist/page-header-commands-DFMlNYzN.js +128 -0
- package/dist/page-header-commands-DFMlNYzN.js.map +1 -0
- package/dist/{pagination-BL25hu2s.js → pagination-Ch8X_47u.js} +5 -5
- package/dist/{pagination-BL25hu2s.js.map → pagination-Ch8X_47u.js.map} +1 -1
- package/dist/{popover-menu-Cn9rUkdc.js → popover-menu-BYaCdFhf.js} +6 -6
- package/dist/{popover-menu-Cn9rUkdc.js.map → popover-menu-BYaCdFhf.js.map} +1 -1
- package/dist/{popover-gnTOhbFM.js → popover-uwzX2vo7.js} +4 -4
- package/dist/{popover-gnTOhbFM.js.map → popover-uwzX2vo7.js.map} +1 -1
- package/dist/{primitives-BFK08r9M.js → primitives-Du64iDbb.js} +6 -6
- package/dist/{primitives-BFK08r9M.js.map → primitives-Du64iDbb.js.map} +1 -1
- package/dist/primitives.js +4 -4
- package/dist/{progress-B1VnCT0K.js → progress-BvF2oCdV.js} +5 -5
- package/dist/{progress-B1VnCT0K.js.map → progress-BvF2oCdV.js.map} +1 -1
- package/dist/{receipt-BVSOH3Xo.js → receipt-D789Jwyy.js} +6 -6
- package/dist/{receipt-BVSOH3Xo.js.map → receipt-D789Jwyy.js.map} +1 -1
- package/dist/{rich-text-GOUyKQ-s.js → rich-text-CLJEf76s.js} +4 -4
- package/dist/{rich-text-GOUyKQ-s.js.map → rich-text-CLJEf76s.js.map} +1 -1
- package/dist/{rich-text-editor-cRLC-eBK.js → rich-text-editor-Cm1dpUpQ.js} +10 -10
- package/dist/{rich-text-editor-cRLC-eBK.js.map → rich-text-editor-Cm1dpUpQ.js.map} +1 -1
- package/dist/{rich-text-editor-toolbar-DfXg0-VC.js → rich-text-editor-toolbar-L__e4x47.js} +4 -4
- package/dist/{rich-text-editor-toolbar-DfXg0-VC.js.map → rich-text-editor-toolbar-L__e4x47.js.map} +1 -1
- package/dist/{scroll-area-DcI-M1UT.js → scroll-area-La5RXsEk.js} +2 -2
- package/dist/{scroll-area-DcI-M1UT.js.map → scroll-area-La5RXsEk.js.map} +1 -1
- package/dist/{separator-Bom7NsGO.js → separator-DAEV0w65.js} +2 -2
- package/dist/{separator-Bom7NsGO.js.map → separator-DAEV0w65.js.map} +1 -1
- package/dist/{separator-BDV-Y648.js → separator-GNfPG-90.js} +3 -3
- package/dist/{separator-BDV-Y648.js.map → separator-GNfPG-90.js.map} +1 -1
- package/dist/services/actions.js +1 -1
- package/dist/settings-BA2iboic.js +174 -0
- package/dist/settings-BA2iboic.js.map +1 -0
- package/dist/{sheet-ijlsjq-n.js → sheet-DoAxTb2J.js} +7 -7
- package/dist/{sheet-ijlsjq-n.js.map → sheet-DoAxTb2J.js.map} +1 -1
- package/dist/{sidebar-DkTO4VTO.js → sidebar-ByqKR_W5.js} +13 -13
- package/dist/{sidebar-DkTO4VTO.js.map → sidebar-ByqKR_W5.js.map} +1 -1
- package/dist/{sidebar-Db6gkh3U.js → sidebar-D2iSnGJ7.js} +11 -11
- package/dist/{sidebar-Db6gkh3U.js.map → sidebar-D2iSnGJ7.js.map} +1 -1
- package/dist/{skeleton-Byjnt7qr.js → skeleton-CUj8O5Bd.js} +3 -3
- package/dist/{skeleton-Byjnt7qr.js.map → skeleton-CUj8O5Bd.js.map} +1 -1
- package/dist/smart-card-CqkzQsK-.js +614 -0
- package/dist/smart-card-CqkzQsK-.js.map +1 -0
- package/dist/{structure-BmisBK-3.js → structure-C0V0_Fyf.js} +3 -3
- package/dist/{structure-BmisBK-3.js.map → structure-C0V0_Fyf.js.map} +1 -1
- package/dist/{structure.recipes-EPz60cU8.js → structure.recipes-BB4-n58V.js} +2 -2
- package/dist/{structure.recipes-EPz60cU8.js.map → structure.recipes-BB4-n58V.js.map} +1 -1
- package/dist/style.css +1 -1
- package/dist/{table-DsfzoUia.js → table-CpvnzJn9.js} +3 -3
- package/dist/{table-DsfzoUia.js.map → table-CpvnzJn9.js.map} +1 -1
- package/dist/{table-BweFy_P9.js → table-DH7rozUv.js} +19 -19
- package/dist/{table-BweFy_P9.js.map → table-DH7rozUv.js.map} +1 -1
- package/dist/{tabs-CDocW5D1.js → tabs-eqVsJZWO.js} +7 -7
- package/dist/{tabs-CDocW5D1.js.map → tabs-eqVsJZWO.js.map} +1 -1
- package/dist/{text-inputs-f9Ia-aCi.js → text-inputs-DXItiR0O.js} +101 -98
- package/dist/text-inputs-DXItiR0O.js.map +1 -0
- package/dist/{text.styles-g4He6UIW.js → text.styles-B64My8RZ.js} +25 -10
- package/dist/text.styles-B64My8RZ.js.map +1 -0
- package/dist/theme.css +152 -112
- package/dist/{timelines-CwM_9IhD.js → timelines-DmtztAY_.js} +10 -10
- package/dist/{timelines-CwM_9IhD.js.map → timelines-DmtztAY_.js.map} +1 -1
- package/dist/{tooltip-yiRJUFjp.js → tooltip-B4hjKtoV.js} +3 -3
- package/dist/{tooltip-yiRJUFjp.js.map → tooltip-B4hjKtoV.js.map} +1 -1
- package/dist/{tree-WnKQzwbq.js → tree-CV11PURP.js} +8 -8
- package/dist/{tree-WnKQzwbq.js.map → tree-CV11PURP.js.map} +1 -1
- package/dist/typography/rich-text.js +1 -1
- package/dist/typography-CSyJrh28.js +117 -0
- package/dist/typography-CSyJrh28.js.map +1 -0
- package/dist/typography.js +1 -1
- package/dist/{ui-provider-C_U0Xma4.js → ui-provider-CQCuCBcy.js} +3 -3
- package/dist/ui-provider-CQCuCBcy.js.map +1 -0
- package/dist/ui-provider.js +1 -1
- package/dist/{upload-CXyxmQBP.js → upload-C9SA0X-8.js} +11 -11
- package/dist/{upload-CXyxmQBP.js.map → upload-C9SA0X-8.js.map} +1 -1
- package/dist/{upload-queue-Boscubn9.js → upload-queue-DhQJiSRc.js} +9 -9
- package/dist/{upload-queue-Boscubn9.js.map → upload-queue-DhQJiSRc.js.map} +1 -1
- package/dist/{usage-card-BhO5eqkX.js → usage-card-DVTRDmGz.js} +134 -133
- package/dist/usage-card-DVTRDmGz.js.map +1 -0
- package/dist/{utils-BRhFDwuU.js → utils-CF3c95Io.js} +68 -5
- package/dist/utils-CF3c95Io.js.map +1 -0
- package/dist/value-inputs-C0o70p-U.js +980 -0
- package/dist/value-inputs-C0o70p-U.js.map +1 -0
- package/dist/{value-typography-BP8OG6h5.js → value-typography-ClupF0Gz.js} +2 -2
- package/dist/{value-typography-BP8OG6h5.js.map → value-typography-ClupF0Gz.js.map} +1 -1
- package/dist/workspace-9EFVXUyk.js +410 -0
- package/dist/workspace-9EFVXUyk.js.map +1 -0
- package/docs/consumer-skill/SKILL.md +14 -129
- package/docs/consumer-skill/examples/app-layout.tsx +22 -12
- package/docs/consumer-skill/examples/authentication.tsx +77 -0
- package/docs/consumer-skill/examples/edit.tsx +30 -38
- package/docs/consumer-skill/examples/manifest.json +51 -1
- package/docs/consumer-skill/examples/rhf-bindings.tsx +86 -0
- package/docs/consumer-skill/examples/rhf-edit-page.tsx +141 -0
- package/docs/consumer-skill/examples/rhf-overlay-form.tsx +92 -0
- package/docs/consumer-skill/examples/rhf-transport.tsx +28 -0
- package/docs/consumer-skill/references/agents-md.md +17 -7
- package/docs/consumer-skill/references/authentication.md +42 -0
- package/docs/consumer-skill/references/component-contracts.json +319824 -0
- package/docs/consumer-skill/references/composition-mapping.md +92 -0
- package/docs/consumer-skill/references/composition.md +43 -19
- package/docs/consumer-skill/references/design-contract.md +32 -0
- package/docs/consumer-skill/references/discovery.md +84 -7
- package/docs/consumer-skill/references/installation.md +40 -36
- package/docs/consumer-skill/references/integration.md +6 -2
- package/docs/consumer-skill/references/public-api.json +1519 -65
- package/docs/consumer-skill/references/review.md +9 -4
- package/docs/consumer-skill/references/rhf-workflows.md +86 -0
- package/docs/consumer-skill/references/screen-archetypes.md +22 -0
- package/docs/consumer-skill/references/theming.md +8 -9
- package/docs/consumer-skill/references/tokens.json +12 -3
- package/docs/consumer-skill/references/workflows.md +2 -1
- package/docs/consumer-skill/scripts/composition-schema.d.mts +2 -0
- package/docs/consumer-skill/scripts/composition-schema.mjs +32 -0
- package/docs/consumer-skill/scripts/composition.mjs +198 -0
- package/docs/consumer-skill/scripts/discovery.mjs +42 -10
- package/docs/consumer-skill/scripts/inspect-component.mjs +11 -0
- package/docs/consumer-skill/scripts/inspect-tokens.mjs +11 -0
- package/docs/consumer-skill/scripts/inspection.mjs +138 -0
- package/docs/consumer-skill/scripts/map-composition.mjs +12 -0
- package/docs/consumer-skill/scripts/setup-skills.mjs +76 -0
- package/docs/consumer-skills/themelia-components/SKILL.md +30 -0
- package/docs/consumer-skills/themelia-composition/SKILL.md +35 -0
- package/docs/consumer-skills/themelia-forms/SKILL.md +12 -0
- package/docs/consumer-skills/themelia-foundation/SKILL.md +14 -0
- package/docs/consumer-skills/themelia-tokenization/SKILL.md +35 -0
- package/package.json +4 -2
- package/templates/README.md +39 -0
- package/templates/admin-shell-template.tsx +382 -0
- package/templates/analytics-operations-template.tsx +376 -0
- package/templates/customers-template.tsx +509 -0
- package/templates/index.ts +29 -0
- package/templates/orders-template.tsx +1102 -0
- package/templates/products-template.tsx +834 -0
- package/templates/user-permissions-template.tsx +450 -0
- package/dist/action-menu-CllU52Mm.js +0 -280
- package/dist/action-menu-CllU52Mm.js.map +0 -1
- package/dist/affordance-geometry.generated-tF36bREk.js.map +0 -1
- package/dist/ai-artifact-DvTwDVyL.js.map +0 -1
- package/dist/app-shell-C4FhvCqF.js.map +0 -1
- package/dist/cards-BaBZR-AU.js.map +0 -1
- package/dist/checkbox-AWIhNhmo.js.map +0 -1
- package/dist/choice-inputs-CNHvARCJ.js.map +0 -1
- package/dist/comments-DD0DUPwJ.js.map +0 -1
- package/dist/containers-Mu1LMGV1.js +0 -103
- package/dist/containers-Mu1LMGV1.js.map +0 -1
- package/dist/data-display-DT_khPl_.js.map +0 -1
- package/dist/date-picker-B2K3ucWn.js.map +0 -1
- package/dist/date-pickers-Ds8gLaoP.js +0 -175
- package/dist/date-pickers-Ds8gLaoP.js.map +0 -1
- package/dist/form-field-CwH9y77i.js.map +0 -1
- package/dist/forms-B9G_xwzi.js.map +0 -1
- package/dist/forms-numeric-B_jraJaQ.js.map +0 -1
- package/dist/input-DQdEd0YD.js +0 -263
- package/dist/input-DQdEd0YD.js.map +0 -1
- package/dist/item-K5_ghvze.js +0 -275
- package/dist/item-K5_ghvze.js.map +0 -1
- package/dist/metadata-list-8N16ZMW_.js +0 -405
- package/dist/metadata-list-8N16ZMW_.js.map +0 -1
- package/dist/overflow-tab-bar-CWY7Nogn.js.map +0 -1
- package/dist/overlays-tMkF-VAN.js.map +0 -1
- package/dist/page-CL9YeH_V.js +0 -224
- package/dist/page-CL9YeH_V.js.map +0 -1
- package/dist/page-header-commands-DOb6WsfD.js +0 -126
- package/dist/page-header-commands-DOb6WsfD.js.map +0 -1
- package/dist/settings-ybnjVwbd.js +0 -171
- package/dist/settings-ybnjVwbd.js.map +0 -1
- package/dist/smart-card-DiC5aac3.js +0 -604
- package/dist/smart-card-DiC5aac3.js.map +0 -1
- package/dist/text-inputs-f9Ia-aCi.js.map +0 -1
- package/dist/text.styles-g4He6UIW.js.map +0 -1
- package/dist/typography-UTnveOEy.js +0 -117
- package/dist/typography-UTnveOEy.js.map +0 -1
- package/dist/ui-provider-C_U0Xma4.js.map +0 -1
- package/dist/usage-card-BhO5eqkX.js.map +0 -1
- package/dist/utils-BRhFDwuU.js.map +0 -1
- package/dist/value-inputs-TaX4OjNk.js +0 -963
- package/dist/value-inputs-TaX4OjNk.js.map +0 -1
- package/dist/workspace-B-OEm3Kf.js +0 -408
- package/dist/workspace-B-OEm3Kf.js.map +0 -1
|
@@ -3,7 +3,7 @@ import { Card } from 'themelia-react-ui/base/cards';
|
|
|
3
3
|
import { SidebarTrigger } from 'themelia-react-ui/base/sidebar';
|
|
4
4
|
import { AppLayout, type AppLayoutVariant } from 'themelia-react-ui/layout/app-shell';
|
|
5
5
|
import { HeaderUserMenu, type HeaderUserMenuProps } from 'themelia-react-ui/layout/header';
|
|
6
|
-
import { Page } from 'themelia-react-ui/layout/page';
|
|
6
|
+
import { Page, type PageProps } from 'themelia-react-ui/layout/page';
|
|
7
7
|
import { AppSidebar, LayoutSidebarProvider, SidebarNavigationUser, type AppSidebarProps } from 'themelia-react-ui/layout/sidebar';
|
|
8
8
|
import type { UIConfig } from 'themelia-react-ui/ui-provider';
|
|
9
9
|
import { Text } from 'themelia-react-ui/typography';
|
|
@@ -26,15 +26,13 @@ export interface WorkspaceLayoutProps {
|
|
|
26
26
|
workspaceSwitcher?: ReactNode;
|
|
27
27
|
user?: HeaderUserMenuProps['user'];
|
|
28
28
|
onLogout?: () => void;
|
|
29
|
-
onCreate: () => void;
|
|
30
|
-
narrow?: boolean;
|
|
31
29
|
children?: ReactNode;
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
/** The app accepts route/identity data and wires navigation/persistence callbacks. */
|
|
35
33
|
export function WorkspaceLayout({ variant = 'inset', sidebar, navigationGroups = {},
|
|
36
34
|
currentUrl, renderLink, workspaceLinks, workspaceSwitcher, user, onLogout,
|
|
37
|
-
|
|
35
|
+
children }: WorkspaceLayoutProps) {
|
|
38
36
|
const sidebarAccount = !sidebar && (variant === 'sidebar' || variant === 'inset');
|
|
39
37
|
const brand = <Text weight="semibold">Admin</Text>;
|
|
40
38
|
const account = user && !sidebarAccount ? <HeaderUserMenu user={user} onLogout={onLogout} /> : null;
|
|
@@ -42,18 +40,30 @@ export function WorkspaceLayout({ variant = 'inset', sidebar, navigationGroups =
|
|
|
42
40
|
currentUrl={currentUrl} renderLink={renderLink} navigationGroups={navigationGroups}
|
|
43
41
|
workspaceLinks={workspaceSwitcher ? undefined : workspaceLinks}
|
|
44
42
|
slots={{ headerEnd: workspaceSwitcher, footer: user && sidebarAccount ? <SidebarNavigationUser user={user} onLogout={onLogout} /> : undefined }} />;
|
|
45
|
-
const content = <Page narrow={narrow} gutter={variant === 'stacked' ? 'none' : undefined}
|
|
46
|
-
header={{ title: 'Workspace', description: 'Review activity and manage your records.',
|
|
47
|
-
primaryAction: { label: 'Create record', onClick: onCreate } }}>
|
|
48
|
-
{children ?? <Card title="Activity" footerText="Latest workspace activity"><Text>Application content goes here.</Text></Card>}
|
|
49
|
-
</Page>;
|
|
50
43
|
const trigger = <SidebarTrigger />;
|
|
51
44
|
return <LayoutSidebarProvider adaptTo="container" className="min-h-0">
|
|
52
45
|
{variant === 'stacked' ? <AppLayout variant="stacked" className="h-svh min-h-0" logo={brand}
|
|
53
|
-
slots={{headerEnd: account}}>{
|
|
46
|
+
slots={{headerEnd: account}}>{children}</AppLayout>
|
|
54
47
|
: variant === 'sidebar' || variant === 'inset' ? <AppLayout variant={variant} className="h-svh min-h-0"
|
|
55
|
-
sidebar={navigation} sidebarTrigger={trigger} slots={{ toolbarStart: brand, toolbarEnd: account }}>{
|
|
48
|
+
sidebar={navigation} sidebarTrigger={trigger} slots={{ toolbarStart: brand, toolbarEnd: account }}>{children}</AppLayout>
|
|
56
49
|
: <AppLayout variant={variant} className="h-svh min-h-0" logo={brand} sidebar={navigation}
|
|
57
|
-
sidebarTrigger={trigger} slots={{headerEnd: account}}>{
|
|
50
|
+
sidebarTrigger={trigger} slots={{headerEnd: account}}>{children}</AppLayout>}
|
|
58
51
|
</LayoutSidebarProvider>;
|
|
59
52
|
}
|
|
53
|
+
|
|
54
|
+
export interface WorkspaceExamplePageProps {
|
|
55
|
+
onCreate: () => void;
|
|
56
|
+
narrow?: boolean;
|
|
57
|
+
/** Use none when a stacked/contained parent already owns the inline gutter. */
|
|
58
|
+
gutter?: PageProps['gutter'];
|
|
59
|
+
children?: ReactNode;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Illustrative route content; mount this or another route Page inside the shell. */
|
|
63
|
+
export function WorkspaceExamplePage({ onCreate, narrow, gutter, children }: WorkspaceExamplePageProps) {
|
|
64
|
+
return <Page narrow={narrow} gutter={gutter}
|
|
65
|
+
header={{ title: 'Workspace', description: 'Review activity and manage your records.',
|
|
66
|
+
primaryAction: { label: 'Create record', onClick: onCreate } }}>
|
|
67
|
+
{children ?? <Card title="Activity" footerText="Latest workspace activity"><Text>Application content goes here.</Text></Card>}
|
|
68
|
+
</Page>;
|
|
69
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useId, useRef, useState, type FormEvent } from 'react';
|
|
2
|
+
import { Button } from 'themelia-react-ui/base/buttons';
|
|
3
|
+
import { Checkbox } from 'themelia-react-ui/base/choice-inputs';
|
|
4
|
+
import { FormField } from 'themelia-react-ui/base/forms';
|
|
5
|
+
import { Stack } from 'themelia-react-ui/base/structure';
|
|
6
|
+
import { Input, PasswordInput } from 'themelia-react-ui/base/text-inputs';
|
|
7
|
+
import { AuthFooterLinks, AuthForm, AuthFormRow, AuthShell, type AuthShellProps } from 'themelia-react-ui/layout/auth';
|
|
8
|
+
import { Text } from 'themelia-react-ui/typography';
|
|
9
|
+
|
|
10
|
+
export interface PasswordSignInValue {
|
|
11
|
+
email: string;
|
|
12
|
+
password: string;
|
|
13
|
+
rememberDevice: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PasswordSignInProps {
|
|
17
|
+
productName: string;
|
|
18
|
+
recoveryHref: string;
|
|
19
|
+
/** Optional public destination relevant to this sign-in flow. */
|
|
20
|
+
returnHref?: string;
|
|
21
|
+
renderLink?: AuthShellProps['renderLink'];
|
|
22
|
+
/** The app authenticates and navigates after success; reject on failure. */
|
|
23
|
+
onSignIn: (value: PasswordSignInValue) => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function PasswordSignIn({ productName, recoveryHref, returnHref, renderLink, onSignIn }: PasswordSignInProps) {
|
|
27
|
+
const id = useId();
|
|
28
|
+
const inFlight = useRef(false);
|
|
29
|
+
const [email, setEmail] = useState('');
|
|
30
|
+
const [password, setPassword] = useState('');
|
|
31
|
+
const [rememberDevice, setRememberDevice] = useState(false);
|
|
32
|
+
const [pending, setPending] = useState(false);
|
|
33
|
+
const [error, setError] = useState<string>();
|
|
34
|
+
|
|
35
|
+
async function submit(event: FormEvent<HTMLFormElement>) {
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
if (inFlight.current) return;
|
|
38
|
+
inFlight.current = true;
|
|
39
|
+
setError(undefined);
|
|
40
|
+
setPending(true);
|
|
41
|
+
try {
|
|
42
|
+
await onSignIn({ email, password, rememberDevice });
|
|
43
|
+
} catch {
|
|
44
|
+
setError('Sign-in failed. Check your details and try again.');
|
|
45
|
+
} finally {
|
|
46
|
+
inFlight.current = false;
|
|
47
|
+
setPending(false);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return <AuthShell brand={{ label: productName }} title="Sign in"
|
|
52
|
+
description="Use your account to continue." renderLink={renderLink}
|
|
53
|
+
footerLinks={returnHref ? [{ id: 'return', label: 'Return to application', href: returnHref }] : []}>
|
|
54
|
+
<AuthForm onSubmit={submit} aria-label="Password sign in" aria-busy={pending}>
|
|
55
|
+
<FormField label="Email" htmlFor={`${id}-email`} required>
|
|
56
|
+
<Input id={`${id}-email`} name="email" type="email" autoComplete="username"
|
|
57
|
+
required disabled={pending} value={email} onChange={event => setEmail(event.target.value)} />
|
|
58
|
+
</FormField>
|
|
59
|
+
<FormField label="Password" htmlFor={`${id}-password`} required>
|
|
60
|
+
<PasswordInput id={`${id}-password`} name="password" autoComplete="current-password"
|
|
61
|
+
required disabled={pending} value={password} onChange={event => setPassword(event.target.value)} />
|
|
62
|
+
</FormField>
|
|
63
|
+
<AuthFormRow>
|
|
64
|
+
<Stack render={<label htmlFor={`${id}-remember`} />} direction="horizontal" gap="md" wrap={false}>
|
|
65
|
+
<Checkbox id={`${id}-remember`} name="rememberDevice" checked={rememberDevice}
|
|
66
|
+
disabled={pending} onCheckedChange={checked => setRememberDevice(checked === true)} />
|
|
67
|
+
<Text tag="span">Remember this device</Text>
|
|
68
|
+
</Stack>
|
|
69
|
+
<AuthFooterLinks aria-label="Account recovery" renderLink={renderLink}
|
|
70
|
+
links={[{ id: 'recovery', label: 'Forgot password?', href: recoveryHref, disabled: pending }]} />
|
|
71
|
+
</AuthFormRow>
|
|
72
|
+
{error && <Text type="error" role="alert">{error}</Text>}
|
|
73
|
+
<Button tone="primary" type="submit" fullWidth loading={pending}
|
|
74
|
+
strings={{ loading: 'Signing in…' }}>Sign in</Button>
|
|
75
|
+
</AuthForm>
|
|
76
|
+
</AuthShell>;
|
|
77
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
import { useForm } from 'react-hook-form';
|
|
2
3
|
import { Button } from 'themelia-react-ui/base/buttons';
|
|
3
4
|
import { Card } from 'themelia-react-ui/base/cards';
|
|
4
|
-
import {
|
|
5
|
+
import { ControlledFormField, DirtyStateBanner, ErrorSummary, FormActionsBar, SubmitStateButton } from 'themelia-react-ui/base/forms';
|
|
5
6
|
import { Stack } from 'themelia-react-ui/base/structure';
|
|
6
7
|
import { Input } from 'themelia-react-ui/base/text-inputs';
|
|
7
8
|
import { Text } from 'themelia-react-ui/typography';
|
|
@@ -9,52 +10,43 @@ import { Text } from 'themelia-react-ui/typography';
|
|
|
9
10
|
export interface AccountEditValue { id: string; name: string; }
|
|
10
11
|
export interface AccountEditProps {
|
|
11
12
|
initial: AccountEditValue;
|
|
12
|
-
// Resolve with the authoritative saved value; reject if persistence failed.
|
|
13
13
|
onSave: (value: AccountEditValue) => Promise<AccountEditValue>;
|
|
14
14
|
onCancel: () => void;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/** Small embedded subject; full routes start from rhf-edit-page.tsx. */
|
|
18
18
|
export function AccountEdit({ initial, onSave, onCancel }: AccountEditProps) {
|
|
19
|
-
const
|
|
20
|
-
const input = useRef<HTMLInputElement>(null);
|
|
21
|
-
const inFlight = useRef(false);
|
|
22
|
-
const [name, setName] = useState(initial.name);
|
|
23
|
-
const [validation, setValidation] = useState<string>();
|
|
24
|
-
const [error, setError] = useState<string>();
|
|
25
|
-
const [pending, setPending] = useState(false);
|
|
19
|
+
const lock = useRef(false);
|
|
26
20
|
const [saved, setSaved] = useState(false);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
setValidation(undefined); inFlight.current = true; setPending(true);
|
|
21
|
+
const form = useForm<AccountEditValue>({ defaultValues: initial });
|
|
22
|
+
const { isDirty, isSubmitting, errors } = form.formState;
|
|
23
|
+
const submit = form.handleSubmit(async values => {
|
|
24
|
+
if (lock.current) return;
|
|
25
|
+
lock.current = true;
|
|
26
|
+
setSaved(false);
|
|
27
|
+
form.clearErrors('root.server');
|
|
36
28
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
form.reset(await onSave({ ...values, name: values.name.trim() }));
|
|
30
|
+
setSaved(true);
|
|
39
31
|
} catch {
|
|
40
|
-
setError('Saving failed. Your draft is kept; retry Save account.');
|
|
41
|
-
} finally {
|
|
42
|
-
}
|
|
32
|
+
form.setError('root.server', { type: 'transport', message: 'Saving failed. Your draft is kept; retry Save account.' });
|
|
33
|
+
} finally { lock.current = false; }
|
|
34
|
+
});
|
|
43
35
|
return <Card title="Edit account" surface="card">
|
|
44
|
-
<form onSubmit={submit} noValidate aria-busy={
|
|
36
|
+
<form onSubmit={submit} noValidate aria-busy={isSubmitting}>
|
|
45
37
|
<Stack gap="lg">
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
{saved && <Text role="status">Account saved.</Text>}
|
|
54
|
-
<
|
|
55
|
-
<Button
|
|
56
|
-
<
|
|
57
|
-
</
|
|
38
|
+
{isDirty && <DirtyStateBanner title="Unsaved changes" />}
|
|
39
|
+
{errors.root?.server?.message && <ErrorSummary title="Account could not be saved" errors={[errors.root.server.message]} />}
|
|
40
|
+
<ControlledFormField control={form.control} name="name" label="Account name" required
|
|
41
|
+
hint="At least two characters." rules={{ validate: value => value.trim().length >= 2 || 'Enter at least two characters.' }}>
|
|
42
|
+
{(field, _error, invalid) => <Input ref={field.ref} name={field.name} value={field.value}
|
|
43
|
+
onValueChange={value => field.onChange(value)} onBlur={field.onBlur} invalid={invalid} disabled={isSubmitting} />}
|
|
44
|
+
</ControlledFormField>
|
|
45
|
+
{saved && !isDirty && <Text role="status">Account saved.</Text>}
|
|
46
|
+
<FormActionsBar>
|
|
47
|
+
<Button type="button" disabled={isSubmitting} onClick={onCancel}>Cancel edit</Button>
|
|
48
|
+
<SubmitStateButton dirty={isDirty} disableWhenPristine submitting={isSubmitting}>Save account</SubmitStateButton>
|
|
49
|
+
</FormActionsBar>
|
|
58
50
|
</Stack>
|
|
59
51
|
</form>
|
|
60
52
|
</Card>;
|
|
@@ -105,7 +105,8 @@
|
|
|
105
105
|
{
|
|
106
106
|
"file": "app-layout.tsx",
|
|
107
107
|
"exports": [
|
|
108
|
-
"WorkspaceLayout"
|
|
108
|
+
"WorkspaceLayout",
|
|
109
|
+
"WorkspaceExamplePage"
|
|
109
110
|
],
|
|
110
111
|
"phase": "basic",
|
|
111
112
|
"optionalPeers": []
|
|
@@ -137,6 +138,55 @@
|
|
|
137
138
|
],
|
|
138
139
|
"phase": "basic",
|
|
139
140
|
"optionalPeers": []
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"file": "authentication.tsx",
|
|
144
|
+
"exports": [
|
|
145
|
+
"PasswordSignIn"
|
|
146
|
+
],
|
|
147
|
+
"phase": "optional",
|
|
148
|
+
"optionalPeers": [
|
|
149
|
+
"react-hook-form"
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"file": "rhf-edit-page.tsx",
|
|
154
|
+
"exports": [
|
|
155
|
+
"RHFEditPage"
|
|
156
|
+
],
|
|
157
|
+
"phase": "optional",
|
|
158
|
+
"optionalPeers": [
|
|
159
|
+
"react-hook-form"
|
|
160
|
+
]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"file": "rhf-overlay-form.tsx",
|
|
164
|
+
"exports": [
|
|
165
|
+
"RHFOverlayForm"
|
|
166
|
+
],
|
|
167
|
+
"phase": "optional",
|
|
168
|
+
"optionalPeers": [
|
|
169
|
+
"react-hook-form"
|
|
170
|
+
]
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"file": "rhf-bindings.tsx",
|
|
174
|
+
"exports": [
|
|
175
|
+
"TypedControlBindings"
|
|
176
|
+
],
|
|
177
|
+
"phase": "optional",
|
|
178
|
+
"optionalPeers": [
|
|
179
|
+
"react-day-picker",
|
|
180
|
+
"react-hook-form"
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"file": "rhf-transport.tsx",
|
|
185
|
+
"exports": [
|
|
186
|
+
"awaitCallbackSave"
|
|
187
|
+
],
|
|
188
|
+
"phase": "basic",
|
|
189
|
+
"optionalPeers": []
|
|
140
190
|
}
|
|
141
191
|
]
|
|
142
192
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useId } from 'react';
|
|
2
|
+
import { useForm } from 'react-hook-form';
|
|
3
|
+
import { ControlledFormField } from 'themelia-react-ui/base/forms';
|
|
4
|
+
import { Input, Textarea } from 'themelia-react-ui/base/text-inputs';
|
|
5
|
+
import { Checkbox, Select, Switch, SwitchCard, ToggleField } from 'themelia-react-ui/base/choice-inputs';
|
|
6
|
+
import { DatePicker } from 'themelia-react-ui/base/date-pickers';
|
|
7
|
+
import { ColorInput, DateTimeInput, PhoneInput, SliderField, TagsInput } from 'themelia-react-ui/base/value-inputs';
|
|
8
|
+
import { Stack } from 'themelia-react-ui/base/structure';
|
|
9
|
+
import { Text } from 'themelia-react-ui/typography';
|
|
10
|
+
|
|
11
|
+
export interface BindingValues {
|
|
12
|
+
title: string; description: string; category: string;
|
|
13
|
+
enabled: boolean; accepted: boolean; notifications: boolean; automation: boolean;
|
|
14
|
+
dueDate: Date | null; appointment: string; phone: string; tags: string[];
|
|
15
|
+
threshold: number; color: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Each binding follows the public control's actual value contract. */
|
|
19
|
+
export function TypedControlBindings() {
|
|
20
|
+
const id = useId();
|
|
21
|
+
const { control } = useForm<BindingValues>({ defaultValues: {
|
|
22
|
+
title: '', description: '', category: 'standard', enabled: false, accepted: false,
|
|
23
|
+
notifications: true, automation: false, dueDate: null, appointment: '', phone: '',
|
|
24
|
+
tags: [], threshold: 50, color: '#335577',
|
|
25
|
+
} });
|
|
26
|
+
return <Stack gap="xl">
|
|
27
|
+
<ControlledFormField control={control} name="title" label="Title">
|
|
28
|
+
{(field, _error, invalid) => <Input ref={field.ref} name={field.name}
|
|
29
|
+
value={field.value} onValueChange={value => field.onChange(value)} onBlur={field.onBlur} invalid={invalid} />}
|
|
30
|
+
</ControlledFormField>
|
|
31
|
+
<ControlledFormField control={control} name="description" label="Description">
|
|
32
|
+
{(field, _error, invalid) => <Textarea ref={field.ref} name={field.name}
|
|
33
|
+
value={field.value} onValueChange={value => field.onChange(value)} onBlur={field.onBlur} invalid={invalid} />}
|
|
34
|
+
</ControlledFormField>
|
|
35
|
+
<ControlledFormField control={control} name="category" label="Category">
|
|
36
|
+
{(field, _error, invalid) => <Select ref={field.ref} name={field.name}
|
|
37
|
+
value={field.value} onValueChange={value => field.onChange(value ?? '')} onBlur={field.onBlur}
|
|
38
|
+
invalid={invalid} options={[{ value: 'standard', label: 'Standard' }, { value: 'priority', label: 'Priority' }]} />}
|
|
39
|
+
</ControlledFormField>
|
|
40
|
+
<ControlledFormField control={control} name="enabled" label="Enabled">
|
|
41
|
+
{(field, _error, invalid) => <Switch ref={field.ref} name={field.name}
|
|
42
|
+
checked={field.value} onValueChange={checked => field.onChange(checked)} onBlur={field.onBlur} invalid={invalid} />}
|
|
43
|
+
</ControlledFormField>
|
|
44
|
+
<ControlledFormField control={control} name="accepted" label="Accept terms" rules={{ validate: value => value || 'Accept the terms.' }}>
|
|
45
|
+
{(field, _error, invalid) => <Checkbox ref={field.ref} name={field.name}
|
|
46
|
+
checked={field.value} onValueChange={checked => field.onChange(checked)} onBlur={field.onBlur} invalid={invalid} />}
|
|
47
|
+
</ControlledFormField>
|
|
48
|
+
<ControlledFormField control={control} name="notifications">
|
|
49
|
+
{(field, _error, invalid) => <ToggleField label="Email notifications" name={field.name}
|
|
50
|
+
value={field.value} controlRef={field.ref} onBlur={field.onBlur} onValueChange={checked => field.onChange(checked)}
|
|
51
|
+
invalid={invalid} />}
|
|
52
|
+
</ControlledFormField>
|
|
53
|
+
<ControlledFormField control={control} name="automation">
|
|
54
|
+
{(field, _error, invalid) => <SwitchCard label="Automatic processing" name={field.name}
|
|
55
|
+
value={field.value} controlRef={field.ref} onBlur={field.onBlur} onValueChange={checked => field.onChange(checked)}
|
|
56
|
+
invalid={invalid} />}
|
|
57
|
+
</ControlledFormField>
|
|
58
|
+
<ControlledFormField control={control} name="dueDate" label="Due date">
|
|
59
|
+
{(field, _error, invalid) => <DatePicker mode="single" value={field.value ?? undefined}
|
|
60
|
+
triggerRef={field.ref} onBlur={field.onBlur} onValueChange={output => field.onChange(output.date ?? null)} invalid={invalid} />}
|
|
61
|
+
</ControlledFormField>
|
|
62
|
+
<ControlledFormField control={control} name="appointment" label="Appointment">
|
|
63
|
+
{(field, _error, invalid) => <DateTimeInput value={field.value || undefined}
|
|
64
|
+
triggerRef={field.ref} onBlur={field.onBlur} onValueChange={value => field.onChange(value ?? '')} invalid={invalid} />}
|
|
65
|
+
</ControlledFormField>
|
|
66
|
+
<ControlledFormField control={control} name="phone" label="Phone">
|
|
67
|
+
{(field, _error, invalid) => <PhoneInput ref={field.ref} name={field.name}
|
|
68
|
+
value={field.value} onChange={event => field.onChange(event.target.value)} onBlur={field.onBlur} invalid={invalid} />}
|
|
69
|
+
</ControlledFormField>
|
|
70
|
+
<ControlledFormField control={control} name="tags" label="Tags">
|
|
71
|
+
{(field, _error, invalid) => <TagsInput<string> name={field.name} value={field.value}
|
|
72
|
+
inputRef={field.ref} onBlur={field.onBlur} onValueChange={value => field.onChange(value)} aria-invalid={invalid} />}
|
|
73
|
+
</ControlledFormField>
|
|
74
|
+
<ControlledFormField control={control} name="threshold" hint="Choose a value between 0 and 100." supportingTextId={`${id}-threshold-help`}>
|
|
75
|
+
{(field, _error, invalid) => <Stack gap="xs">
|
|
76
|
+
<Text tag="span" id={`${id}-threshold-label`} weight="medium">Threshold</Text>
|
|
77
|
+
<SliderField name={field.name} value={field.value} aria-labelledby={`${id}-threshold-label`} aria-describedby={`${id}-threshold-help`}
|
|
78
|
+
onValueChange={value => field.onChange(value)} inputRef={field.ref} onBlur={field.onBlur} invalid={invalid} showValue />
|
|
79
|
+
</Stack>}
|
|
80
|
+
</ControlledFormField>
|
|
81
|
+
<ControlledFormField control={control} name="color" label="Brand color">
|
|
82
|
+
{(field, _error, invalid) => <ColorInput ref={field.ref} name={field.name}
|
|
83
|
+
value={field.value} onValueChange={value => field.onChange(value)} onBlur={field.onBlur} invalid={invalid} />}
|
|
84
|
+
</ControlledFormField>
|
|
85
|
+
</Stack>;
|
|
86
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
2
|
+
import { useFieldArray, useForm, useWatch, type FieldErrors } from 'react-hook-form';
|
|
3
|
+
import { Button } from 'themelia-react-ui/base/buttons';
|
|
4
|
+
import { ControlledFormField, DirtyStateBanner, ErrorSummary, FormActionsBar, FormSection, SubmitStateButton } from 'themelia-react-ui/base/forms';
|
|
5
|
+
import { Select, ToggleField } from 'themelia-react-ui/base/choice-inputs';
|
|
6
|
+
import { Input, Textarea } from 'themelia-react-ui/base/text-inputs';
|
|
7
|
+
import { Stack } from 'themelia-react-ui/base/structure';
|
|
8
|
+
import { MetadataList } from 'themelia-react-ui/base/metadata-list';
|
|
9
|
+
import { TwoColumnLayout } from 'themelia-react-ui/layout/containers';
|
|
10
|
+
import { Page } from 'themelia-react-ui/layout/page';
|
|
11
|
+
import { Money } from 'themelia-react-ui/primitives';
|
|
12
|
+
import { Text } from 'themelia-react-ui/typography';
|
|
13
|
+
|
|
14
|
+
export interface OrderEditValues {
|
|
15
|
+
reference: string; email: string; notes: string; priority: string; notify: boolean;
|
|
16
|
+
lines: { product: string; quantity: number; priceMinor: number }[];
|
|
17
|
+
}
|
|
18
|
+
export type OrderSaveResult =
|
|
19
|
+
| { ok: true; values: OrderEditValues }
|
|
20
|
+
| { ok: false; errors: Partial<Record<'reference' | 'email' | 'notes' | 'lines', string>>; message?: string };
|
|
21
|
+
export interface RHFEditPageProps {
|
|
22
|
+
initial: OrderEditValues;
|
|
23
|
+
/** Await the complete request and return the accepted server representation. */
|
|
24
|
+
onSave: (values: OrderEditValues) => Promise<OrderSaveResult>;
|
|
25
|
+
onCancel: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function messages(errors: FieldErrors<OrderEditValues>): string[] {
|
|
29
|
+
const result: string[] = [];
|
|
30
|
+
for (const error of [errors.reference, errors.email, errors.notes, errors.priority, errors.lines, errors.lines?.root, errors.root?.server]) {
|
|
31
|
+
if (typeof error?.message === 'string') result.push(error.message);
|
|
32
|
+
}
|
|
33
|
+
for (const line of Array.isArray(errors.lines) ? errors.lines : []) for (const error of [line?.product, line?.quantity, line?.priceMinor]) {
|
|
34
|
+
if (typeof error?.message === 'string') result.push(error.message);
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Mount with key={record.id}; background revalidation must not reset a dirty draft. */
|
|
40
|
+
export function RHFEditPage({ initial, onSave, onCancel }: RHFEditPageProps) {
|
|
41
|
+
const id = useId();
|
|
42
|
+
const lock = useRef(false);
|
|
43
|
+
const pendingFocus = useRef<'reference' | 'email' | 'notes' | 'lines.0.product' | undefined>(undefined);
|
|
44
|
+
const [saved, setSaved] = useState(false);
|
|
45
|
+
const form = useForm<OrderEditValues>({ defaultValues: initial, mode: 'onBlur' });
|
|
46
|
+
const lines = useFieldArray({ control: form.control, name: 'lines', rules: { minLength: { value: 1, message: 'Keep at least one line item.' } } });
|
|
47
|
+
const draftLines = useWatch({ control: form.control, name: 'lines' });
|
|
48
|
+
const totalMinor = draftLines.reduce((sum, line) => sum + (line.quantity || 0) * (line.priceMinor || 0), 0);
|
|
49
|
+
const { isDirty, isSubmitting, errors } = form.formState;
|
|
50
|
+
const summary = messages(errors);
|
|
51
|
+
const { setFocus } = form;
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!isSubmitting && pendingFocus.current) {
|
|
54
|
+
setFocus(pendingFocus.current);
|
|
55
|
+
pendingFocus.current = undefined;
|
|
56
|
+
}
|
|
57
|
+
}, [isSubmitting, setFocus]);
|
|
58
|
+
const submit = form.handleSubmit(async values => {
|
|
59
|
+
if (lock.current) return;
|
|
60
|
+
lock.current = true;
|
|
61
|
+
setSaved(false);
|
|
62
|
+
pendingFocus.current = undefined;
|
|
63
|
+
form.clearErrors();
|
|
64
|
+
try {
|
|
65
|
+
const result = await onSave(values);
|
|
66
|
+
if (!result.ok) {
|
|
67
|
+
let focused = false;
|
|
68
|
+
for (const name of ['reference', 'email', 'notes', 'lines'] as const) {
|
|
69
|
+
const message = result.errors[name];
|
|
70
|
+
if (message) {
|
|
71
|
+
form.setError(name, { type: 'server', message });
|
|
72
|
+
if (!focused) pendingFocus.current = name === 'lines' ? (values.lines.length ? 'lines.0.product' : undefined) : name;
|
|
73
|
+
focused = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
form.setError('root.server', { type: 'server', message: result.message ?? 'Review the highlighted fields. Your draft is kept.' });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
form.reset(result.values);
|
|
80
|
+
setSaved(true);
|
|
81
|
+
} catch {
|
|
82
|
+
form.setError('root.server', { type: 'transport', message: 'Saving failed. Your draft is kept; retry Save order.' });
|
|
83
|
+
} finally { lock.current = false; }
|
|
84
|
+
});
|
|
85
|
+
return <Page
|
|
86
|
+
render={<form id={`${id}-form`} onSubmit={submit} noValidate aria-busy={isSubmitting} />}
|
|
87
|
+
header={{ title: 'Edit order', description: 'Update customer, fulfillment and line items.' }}>
|
|
88
|
+
{isDirty && <DirtyStateBanner title="Unsaved changes" description="Save to commit this draft, or discard to restore the last saved version." />}
|
|
89
|
+
{summary.length > 0 && <ErrorSummary title="Order could not be saved" errors={summary} />}
|
|
90
|
+
{saved && !isDirty && <Text role="status">Order saved.</Text>}
|
|
91
|
+
<TwoColumnLayout adaptTo="container" main={<>
|
|
92
|
+
<FormSection title="Customer" description="The customer receives order updates at this address.">
|
|
93
|
+
<ControlledFormField control={form.control} name="reference" label="Order reference" required rules={{ required: 'Enter an order reference.' }}>
|
|
94
|
+
{(field, _error, invalid) => <Input ref={field.ref} name={field.name} value={field.value} onValueChange={value => field.onChange(value)} onBlur={field.onBlur} invalid={invalid} disabled={isSubmitting} />}
|
|
95
|
+
</ControlledFormField>
|
|
96
|
+
<ControlledFormField control={form.control} name="email" label="Customer email" required rules={{ required: 'Enter an email.', pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: 'Enter a valid email.' } }}>
|
|
97
|
+
{(field, _error, invalid) => <Input type="email" ref={field.ref} name={field.name} value={field.value} onValueChange={value => field.onChange(value)} onBlur={field.onBlur} invalid={invalid} disabled={isSubmitting} />}
|
|
98
|
+
</ControlledFormField>
|
|
99
|
+
</FormSection>
|
|
100
|
+
<FormSection title="Line items" actions={<Button type="button" disabled={isSubmitting} onClick={() => lines.append({ product: '', quantity: 1, priceMinor: 0 })}>Add item</Button>}>
|
|
101
|
+
{lines.fields.map((line, index) => <Stack key={line.id} gap="md">
|
|
102
|
+
<ControlledFormField control={form.control} name={`lines.${index}.product`} label={`Item ${index + 1}`} rules={{ required: 'Enter a product.' }}>
|
|
103
|
+
{(field, _error, invalid) => <Input ref={field.ref} name={field.name} value={field.value} onValueChange={value => field.onChange(value)} onBlur={field.onBlur} invalid={invalid} disabled={isSubmitting} />}
|
|
104
|
+
</ControlledFormField>
|
|
105
|
+
<Stack direction={{ base: 'vertical', sm: 'horizontal' }} gap="md" align="end">
|
|
106
|
+
<ControlledFormField control={form.control} name={`lines.${index}.quantity`} label="Quantity" rules={{ validate: value => (Number.isFinite(value) && Number.isInteger(value) && value >= 1) || 'Enter a whole quantity of at least one.' }}>
|
|
107
|
+
{(field, _error, invalid) => <Input ref={field.ref} type="number" name={field.name} min={1} value={Number.isNaN(field.value) ? '' : String(field.value)} onChange={event => field.onChange(event.target.valueAsNumber)} onBlur={field.onBlur} invalid={invalid} disabled={isSubmitting} />}
|
|
108
|
+
</ControlledFormField>
|
|
109
|
+
<ControlledFormField control={form.control} name={`lines.${index}.priceMinor`} label="Unit price (cents)" rules={{ validate: value => (Number.isFinite(value) && Number.isInteger(value) && value >= 0) || 'Enter a whole price in cents of zero or more.' }}>
|
|
110
|
+
{(field, _error, invalid) => <Input ref={field.ref} type="number" name={field.name} min={0} value={Number.isNaN(field.value) ? '' : String(field.value)} onChange={event => field.onChange(event.target.valueAsNumber)} onBlur={field.onBlur} invalid={invalid} disabled={isSubmitting} />}
|
|
111
|
+
</ControlledFormField>
|
|
112
|
+
<Button type="button" disabled={isSubmitting || lines.fields.length < 2} onClick={() => lines.remove(index)}>Remove item {index + 1}</Button>
|
|
113
|
+
</Stack>
|
|
114
|
+
</Stack>)}
|
|
115
|
+
</FormSection>
|
|
116
|
+
<FormSection title="Internal notes">
|
|
117
|
+
<ControlledFormField control={form.control} name="notes" label="Notes" hint="Visible to your team only.">
|
|
118
|
+
{(field, _error, invalid) => <Textarea ref={field.ref} name={field.name} value={field.value} onValueChange={value => field.onChange(value)} onBlur={field.onBlur} invalid={invalid} disabled={isSubmitting} />}
|
|
119
|
+
</ControlledFormField>
|
|
120
|
+
</FormSection>
|
|
121
|
+
</>} aside={<>
|
|
122
|
+
<FormSection title="Fulfillment">
|
|
123
|
+
<ControlledFormField control={form.control} name="priority" label="Priority">
|
|
124
|
+
{(field, _error, invalid) => <Select ref={field.ref} name={field.name} value={field.value} onValueChange={value => field.onChange(value ?? '')} onBlur={field.onBlur} invalid={invalid} disabled={isSubmitting} options={[{ value: 'standard', label: 'Standard' }, { value: 'express', label: 'Express' }]} />}
|
|
125
|
+
</ControlledFormField>
|
|
126
|
+
<ControlledFormField control={form.control} name="notify">
|
|
127
|
+
{(field, _error, invalid) => <ToggleField label="Email customer on save" name={field.name} value={field.value} controlRef={field.ref} onBlur={field.onBlur} onValueChange={value => field.onChange(value)} invalid={invalid} disabled={isSubmitting} />}
|
|
128
|
+
</ControlledFormField>
|
|
129
|
+
</FormSection>
|
|
130
|
+
<FormSection title="Summary"><MetadataList items={[
|
|
131
|
+
{ id: 'items', label: 'Line items', value: lines.fields.length },
|
|
132
|
+
{ id: 'total', label: 'Draft total', value: <Money amount={totalMinor} currency="USD" unit="minor" /> },
|
|
133
|
+
]} /></FormSection>
|
|
134
|
+
</>} />
|
|
135
|
+
<FormActionsBar sticky leading={isDirty ? 'Changes are not saved yet.' : 'All changes saved.'}>
|
|
136
|
+
<Button type="button" disabled={isSubmitting} onClick={onCancel}>Cancel</Button>
|
|
137
|
+
<Button type="button" disabled={isSubmitting || !isDirty} onClick={() => { form.reset(); setSaved(false); }}>Discard changes</Button>
|
|
138
|
+
<SubmitStateButton dirty={isDirty} disableWhenPristine submitting={isSubmitting}>Save order</SubmitStateButton>
|
|
139
|
+
</FormActionsBar>
|
|
140
|
+
</Page>;
|
|
141
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useEffect, useId, useRef } from 'react';
|
|
2
|
+
import { useForm } from 'react-hook-form';
|
|
3
|
+
import { Button } from 'themelia-react-ui/base/buttons';
|
|
4
|
+
import { ControlledFormField, ErrorSummary } from 'themelia-react-ui/base/forms';
|
|
5
|
+
import { Input } from 'themelia-react-ui/base/text-inputs';
|
|
6
|
+
import { Select, ToggleField } from 'themelia-react-ui/base/choice-inputs';
|
|
7
|
+
import { Stack } from 'themelia-react-ui/base/structure';
|
|
8
|
+
import { ActionDrawer, useOverlayVisibility } from 'themelia-react-ui/features/overlays';
|
|
9
|
+
|
|
10
|
+
export interface CustomerDraft { name: string; email: string; segment: string; notify: boolean; }
|
|
11
|
+
export interface CustomerDraftRecord extends CustomerDraft { id: string; }
|
|
12
|
+
export type CustomerSaveResult =
|
|
13
|
+
| { ok: true; record: CustomerDraftRecord }
|
|
14
|
+
| { ok: false; errors: Partial<Record<'name' | 'email', string>>; message?: string };
|
|
15
|
+
export interface RHFOverlayFormProps {
|
|
16
|
+
record?: CustomerDraftRecord;
|
|
17
|
+
/** Resolve only when transport finishes; reject network failures. */
|
|
18
|
+
onSave: (draft: CustomerDraft, recordId?: string) => Promise<CustomerSaveResult>;
|
|
19
|
+
onSaved: (record: CustomerDraftRecord) => void;
|
|
20
|
+
}
|
|
21
|
+
const empty: CustomerDraft = { name: '', email: '', segment: 'standard', notify: true };
|
|
22
|
+
|
|
23
|
+
/** Mount with a record key if the selected customer changes while editing. */
|
|
24
|
+
export function RHFOverlayForm({ record, onSave, onSaved }: RHFOverlayFormProps) {
|
|
25
|
+
const id = useId();
|
|
26
|
+
const overlay = useOverlayVisibility();
|
|
27
|
+
const lock = useRef(false);
|
|
28
|
+
const pendingFocus = useRef<'name' | 'email' | undefined>(undefined);
|
|
29
|
+
const form = useForm<CustomerDraft>({ defaultValues: record ?? empty, mode: 'onBlur' });
|
|
30
|
+
const { isSubmitting, errors } = form.formState;
|
|
31
|
+
const { setFocus } = form;
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!isSubmitting && pendingFocus.current) {
|
|
34
|
+
setFocus(pendingFocus.current);
|
|
35
|
+
pendingFocus.current = undefined;
|
|
36
|
+
}
|
|
37
|
+
}, [isSubmitting, setFocus]);
|
|
38
|
+
const summary = [errors.name?.message, errors.email?.message, errors.root?.server?.message].filter((message): message is string => Boolean(message));
|
|
39
|
+
function open() {
|
|
40
|
+
// Snapshot on an intentional open; never reset from background record updates.
|
|
41
|
+
form.reset(record ? { name: record.name, email: record.email, segment: record.segment, notify: record.notify } : empty);
|
|
42
|
+
overlay.show();
|
|
43
|
+
}
|
|
44
|
+
const submit = form.handleSubmit(async values => {
|
|
45
|
+
if (lock.current) return;
|
|
46
|
+
lock.current = true;
|
|
47
|
+
pendingFocus.current = undefined;
|
|
48
|
+
form.clearErrors();
|
|
49
|
+
try {
|
|
50
|
+
const result = await onSave(values, record?.id);
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
let focused = false;
|
|
53
|
+
for (const name of ['name', 'email'] as const) if (result.errors[name]) {
|
|
54
|
+
form.setError(name, { type: 'server', message: result.errors[name] });
|
|
55
|
+
if (!focused) pendingFocus.current = name;
|
|
56
|
+
focused = true;
|
|
57
|
+
}
|
|
58
|
+
form.setError('root.server', { type: 'server', message: result.message ?? 'Review the highlighted fields. Your draft is kept.' });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
form.reset(result.record);
|
|
62
|
+
onSaved(result.record);
|
|
63
|
+
overlay.hide();
|
|
64
|
+
} catch {
|
|
65
|
+
form.setError('root.server', { type: 'transport', message: 'Customer could not be saved. Your draft is kept; retry Save customer.' });
|
|
66
|
+
} finally { lock.current = false; }
|
|
67
|
+
});
|
|
68
|
+
return <>
|
|
69
|
+
<Button onClick={open}>{record ? 'Edit customer' : 'Create customer'}</Button>
|
|
70
|
+
<ActionDrawer {...overlay.overlayProps} title={record ? 'Edit customer' : 'Create customer'}
|
|
71
|
+
description="Update contact details and communication preferences."
|
|
72
|
+
formId={`${id}-form`} loading={isSubmitting} strings={{ confirm: 'Save customer', loading: 'Saving customer…' }}>
|
|
73
|
+
<form id={`${id}-form`} onSubmit={submit} noValidate aria-busy={isSubmitting}>
|
|
74
|
+
<Stack gap="xl">
|
|
75
|
+
{summary.length > 0 && <ErrorSummary title="Customer could not be saved" errors={summary} />}
|
|
76
|
+
<ControlledFormField control={form.control} name="name" label="Customer name" required rules={{ required: 'Enter a customer name.', minLength: { value: 2, message: 'Enter at least two characters.' } }}>
|
|
77
|
+
{(field, _error, invalid) => <Input ref={field.ref} name={field.name} value={field.value} onValueChange={value => field.onChange(value)} onBlur={field.onBlur} invalid={invalid} disabled={isSubmitting} />}
|
|
78
|
+
</ControlledFormField>
|
|
79
|
+
<ControlledFormField control={form.control} name="email" label="Email" required rules={{ required: 'Enter an email.', pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: 'Enter a valid email.' } }}>
|
|
80
|
+
{(field, _error, invalid) => <Input type="email" ref={field.ref} name={field.name} value={field.value} onValueChange={value => field.onChange(value)} onBlur={field.onBlur} invalid={invalid} disabled={isSubmitting} />}
|
|
81
|
+
</ControlledFormField>
|
|
82
|
+
<ControlledFormField control={form.control} name="segment" label="Segment">
|
|
83
|
+
{(field, _error, invalid) => <Select ref={field.ref} name={field.name} value={field.value} onValueChange={value => field.onChange(value ?? '')} onBlur={field.onBlur} invalid={invalid} disabled={isSubmitting} options={[{ value: 'standard', label: 'Standard' }, { value: 'wholesale', label: 'Wholesale' }]} />}
|
|
84
|
+
</ControlledFormField>
|
|
85
|
+
<ControlledFormField control={form.control} name="notify">
|
|
86
|
+
{(field, _error, invalid) => <ToggleField label="Send account updates" name={field.name} value={field.value} controlRef={field.ref} onBlur={field.onBlur} onValueChange={value => field.onChange(value)} invalid={invalid} disabled={isSubmitting} />}
|
|
87
|
+
</ControlledFormField>
|
|
88
|
+
</Stack>
|
|
89
|
+
</form>
|
|
90
|
+
</ActionDrawer>
|
|
91
|
+
</>;
|
|
92
|
+
}
|