@ketvietlab/ketsuite 0.1.25 → 0.1.30
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/dist/cache.d.ts +23 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +71 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli-watch.d.ts +4 -0
- package/dist/cli-watch.d.ts.map +1 -0
- package/dist/cli-watch.js +52 -0
- package/dist/cli-watch.js.map +1 -0
- package/dist/cli.js +45 -17
- package/dist/cli.js.map +1 -1
- package/dist/deployment.d.ts +11 -0
- package/dist/deployment.d.ts.map +1 -1
- package/dist/deployment.js +49 -39
- package/dist/deployment.js.map +1 -1
- package/dist/deployments/commerce.d.ts +4 -0
- package/dist/deployments/commerce.d.ts.map +1 -0
- package/dist/deployments/commerce.js +34 -0
- package/dist/deployments/commerce.js.map +1 -0
- package/dist/deployments/common.d.ts +54 -0
- package/dist/deployments/common.d.ts.map +1 -0
- package/dist/deployments/common.js +112 -0
- package/dist/deployments/common.js.map +1 -0
- package/dist/deployments/hospitality.d.ts +4 -0
- package/dist/deployments/hospitality.d.ts.map +1 -0
- package/dist/deployments/hospitality.js +103 -0
- package/dist/deployments/hospitality.js.map +1 -0
- package/dist/deployments/office.d.ts +4 -0
- package/dist/deployments/office.d.ts.map +1 -0
- package/dist/deployments/office.js +31 -0
- package/dist/deployments/office.js.map +1 -0
- package/dist/development.d.ts +16 -0
- package/dist/development.d.ts.map +1 -1
- package/dist/development.js +31 -0
- package/dist/development.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/account_backend/index.js +364 -877
- package/dist/modules/account_backend/index.js.map +1 -1
- package/dist/modules/account_backend/screens/account-defaults.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/account-defaults.js +1 -1
- package/dist/modules/account_backend/screens/account-defaults.js.map +1 -1
- package/dist/modules/account_backend/screens/account-form.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/account-form.js +3 -1
- package/dist/modules/account_backend/screens/account-form.js.map +1 -1
- package/dist/modules/account_backend/screens/accounts-list.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/accounts-list.js +5 -15
- package/dist/modules/account_backend/screens/accounts-list.js.map +1 -1
- package/dist/modules/account_backend/screens/customer-invoice-form.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/customer-invoice-form.js +1 -1
- package/dist/modules/account_backend/screens/customer-invoice-form.js.map +1 -1
- package/dist/modules/account_backend/screens/customer-invoices-list.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/customer-invoices-list.js +5 -15
- package/dist/modules/account_backend/screens/customer-invoices-list.js.map +1 -1
- package/dist/modules/account_backend/screens/journal-entries-list.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/journal-entries-list.js +5 -15
- package/dist/modules/account_backend/screens/journal-entries-list.js.map +1 -1
- package/dist/modules/account_backend/screens/journal-entry-create.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/journal-entry-create.js +1 -1
- package/dist/modules/account_backend/screens/journal-entry-create.js.map +1 -1
- package/dist/modules/account_backend/screens/journal-form.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/journal-form.js +3 -3
- package/dist/modules/account_backend/screens/journal-form.js.map +1 -1
- package/dist/modules/account_backend/screens/journals-list.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/journals-list.js +5 -15
- package/dist/modules/account_backend/screens/journals-list.js.map +1 -1
- package/dist/modules/account_backend/screens/move-detail.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/move-detail.js +3 -1
- package/dist/modules/account_backend/screens/move-detail.js.map +1 -1
- package/dist/modules/account_backend/screens/opening-balances.d.ts +3 -1
- package/dist/modules/account_backend/screens/opening-balances.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/opening-balances.js +12 -6
- package/dist/modules/account_backend/screens/opening-balances.js.map +1 -1
- package/dist/modules/account_backend/screens/overview.js +4 -2
- package/dist/modules/account_backend/screens/overview.js.map +1 -1
- package/dist/modules/account_backend/screens/payment-form.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/payment-form.js +1 -1
- package/dist/modules/account_backend/screens/payment-form.js.map +1 -1
- package/dist/modules/account_backend/screens/payment-term-form.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/payment-term-form.js +4 -2
- package/dist/modules/account_backend/screens/payment-term-form.js.map +1 -1
- package/dist/modules/account_backend/screens/payment-terms-list.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/payment-terms-list.js +6 -17
- package/dist/modules/account_backend/screens/payment-terms-list.js.map +1 -1
- package/dist/modules/account_backend/screens/payments-list.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/payments-list.js +5 -15
- package/dist/modules/account_backend/screens/payments-list.js.map +1 -1
- package/dist/modules/account_backend/screens/period-closes.d.ts +3 -1
- package/dist/modules/account_backend/screens/period-closes.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/period-closes.js +40 -36
- package/dist/modules/account_backend/screens/period-closes.js.map +1 -1
- package/dist/modules/account_backend/screens/tax-form.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/tax-form.js +3 -1
- package/dist/modules/account_backend/screens/tax-form.js.map +1 -1
- package/dist/modules/account_backend/screens/taxes-list.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/taxes-list.js +5 -15
- package/dist/modules/account_backend/screens/taxes-list.js.map +1 -1
- package/dist/modules/account_backend/screens/vendor-bill-form.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/vendor-bill-form.js +1 -1
- package/dist/modules/account_backend/screens/vendor-bill-form.js.map +1 -1
- package/dist/modules/account_backend/screens/vendor-bills-list.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/vendor-bills-list.js +5 -15
- package/dist/modules/account_backend/screens/vendor-bills-list.js.map +1 -1
- package/dist/modules/account_backend/search-functions.d.ts +4 -0
- package/dist/modules/account_backend/search-functions.d.ts.map +1 -0
- package/dist/modules/account_backend/search-functions.js +5 -0
- package/dist/modules/account_backend/search-functions.js.map +1 -0
- package/dist/modules/account_backend/search.d.ts +774 -0
- package/dist/modules/account_backend/search.d.ts.map +1 -0
- package/dist/modules/account_backend/search.js +312 -0
- package/dist/modules/account_backend/search.js.map +1 -0
- package/dist/modules/account_partner_backend/screens/accounting-terms.d.ts.map +1 -1
- package/dist/modules/account_partner_backend/screens/accounting-terms.js +1 -1
- package/dist/modules/account_partner_backend/screens/accounting-terms.js.map +1 -1
- package/dist/modules/activity_backend/index.d.ts.map +1 -1
- package/dist/modules/activity_backend/index.js +0 -2
- package/dist/modules/activity_backend/index.js.map +1 -1
- package/dist/modules/activity_backend/screens/activities-list.d.ts.map +1 -1
- package/dist/modules/activity_backend/screens/activities-list.js +1 -1
- package/dist/modules/activity_backend/screens/activities-list.js.map +1 -1
- package/dist/modules/address_backend/index.js +2 -2
- package/dist/modules/address_backend/index.js.map +1 -1
- package/dist/modules/address_backend/routes.d.ts.map +1 -1
- package/dist/modules/address_backend/routes.js +32 -7
- package/dist/modules/address_backend/routes.js.map +1 -1
- package/dist/modules/address_backend/screens/catalogs-list.d.ts +3 -1
- package/dist/modules/address_backend/screens/catalogs-list.d.ts.map +1 -1
- package/dist/modules/address_backend/screens/catalogs-list.js +15 -9
- package/dist/modules/address_backend/screens/catalogs-list.js.map +1 -1
- package/dist/modules/address_backend/screens/country-browser.d.ts.map +1 -1
- package/dist/modules/address_backend/screens/country-browser.js +7 -15
- package/dist/modules/address_backend/screens/country-browser.js.map +1 -1
- package/dist/modules/address_backend/search-functions.d.ts +4 -0
- package/dist/modules/address_backend/search-functions.d.ts.map +1 -0
- package/dist/modules/address_backend/search-functions.js +7 -0
- package/dist/modules/address_backend/search-functions.js.map +1 -0
- package/dist/modules/address_backend/search.d.ts +38 -0
- package/dist/modules/address_backend/search.d.ts.map +1 -0
- package/dist/modules/address_backend/search.js +40 -0
- package/dist/modules/address_backend/search.js.map +1 -0
- package/dist/modules/attendance_backend/screens/credentials.d.ts.map +1 -1
- package/dist/modules/attendance_backend/screens/credentials.js +1 -1
- package/dist/modules/attendance_backend/screens/credentials.js.map +1 -1
- package/dist/modules/attendance_backend/screens/period.d.ts.map +1 -1
- package/dist/modules/attendance_backend/screens/period.js +1 -1
- package/dist/modules/attendance_backend/screens/period.js.map +1 -1
- package/dist/modules/backend/catalogue.d.ts.map +1 -1
- package/dist/modules/backend/catalogue.js +16 -13
- package/dist/modules/backend/catalogue.js.map +1 -1
- package/dist/modules/backend/collection-search.d.ts +16 -0
- package/dist/modules/backend/collection-search.d.ts.map +1 -0
- package/dist/modules/backend/collection-search.js +43 -0
- package/dist/modules/backend/collection-search.js.map +1 -0
- package/dist/modules/backend/current-company.d.ts +4 -0
- package/dist/modules/backend/current-company.d.ts.map +1 -0
- package/dist/modules/backend/current-company.js +27 -0
- package/dist/modules/backend/current-company.js.map +1 -0
- package/dist/modules/backend/design/HANDOFF.md +1 -1
- package/dist/modules/backend/design/auth.css +61 -0
- package/dist/modules/backend/design/brand/logo-dark.png +0 -0
- package/dist/modules/backend/design/brand/logo-light.png +0 -0
- package/dist/modules/backend/design/client/backend-shell.d.mts +2 -2
- package/dist/modules/backend/design/client/backend-shell.d.mts.map +1 -1
- package/dist/modules/backend/design/client/backend-shell.mjs +1 -1
- package/dist/modules/backend/design/client/backend-shell.mjs.map +4 -4
- package/dist/modules/backend/design/client/ket-table.d.mts +10 -0
- package/dist/modules/backend/design/client/ket-table.d.mts.map +1 -0
- package/dist/modules/backend/design/client/ket-table.mjs +2 -0
- package/dist/modules/backend/design/client/ket-table.mjs.map +7 -0
- package/dist/modules/backend/design/client/lightbox.d.mts +22 -0
- package/dist/modules/backend/design/client/lightbox.d.mts.map +1 -0
- package/dist/modules/backend/design/client/lightbox.mjs +2 -0
- package/dist/modules/backend/design/client/lightbox.mjs.map +7 -0
- package/dist/modules/backend/design/client/relation-select.d.mts +12 -3
- package/dist/modules/backend/design/client/relation-select.d.mts.map +1 -1
- package/dist/modules/backend/design/client/relation-select.mjs +1 -1
- package/dist/modules/backend/design/client/relation-select.mjs.map +4 -4
- package/dist/modules/backend/design/client/search-filter.d.mts +10 -0
- package/dist/modules/backend/design/client/search-filter.d.mts.map +1 -0
- package/dist/modules/backend/design/client/search-filter.mjs +2 -0
- package/dist/modules/backend/design/client/search-filter.mjs.map +7 -0
- package/dist/modules/backend/design/content.css +43 -5
- package/dist/modules/backend/design/forms.css +130 -229
- package/dist/modules/backend/design/foundation.css +0 -51
- package/dist/modules/backend/design/lists.css +79 -7
- package/dist/modules/backend/design/user-workflow.css +0 -20
- package/dist/modules/backend/global-search.d.ts +3 -0
- package/dist/modules/backend/global-search.d.ts.map +1 -0
- package/dist/modules/backend/global-search.js +82 -0
- package/dist/modules/backend/global-search.js.map +1 -0
- package/dist/modules/backend/index.d.ts +27 -0
- package/dist/modules/backend/index.d.ts.map +1 -1
- package/dist/modules/backend/index.js +24 -0
- package/dist/modules/backend/index.js.map +1 -1
- package/dist/modules/backend/islands.d.ts +44 -1
- package/dist/modules/backend/islands.d.ts.map +1 -1
- package/dist/modules/backend/islands.js +25 -1
- package/dist/modules/backend/islands.js.map +1 -1
- package/dist/modules/backend/joints.d.ts.map +1 -1
- package/dist/modules/backend/joints.js +12 -0
- package/dist/modules/backend/joints.js.map +1 -1
- package/dist/modules/backend/ket-table.d.ts +7 -0
- package/dist/modules/backend/ket-table.d.ts.map +1 -0
- package/dist/modules/backend/ket-table.js +2 -0
- package/dist/modules/backend/ket-table.js.map +1 -0
- package/dist/modules/backend/messages.d.ts.map +1 -1
- package/dist/modules/backend/messages.js +144 -0
- package/dist/modules/backend/messages.js.map +1 -1
- package/dist/modules/backend/optional-read.d.ts +4 -0
- package/dist/modules/backend/optional-read.d.ts.map +1 -0
- package/dist/modules/backend/optional-read.js +7 -0
- package/dist/modules/backend/optional-read.js.map +1 -0
- package/dist/modules/backend/paging.d.ts.map +1 -1
- package/dist/modules/backend/paging.js +2 -1
- package/dist/modules/backend/paging.js.map +1 -1
- package/dist/modules/backend/relation-select.d.ts +2 -2
- package/dist/modules/backend/relation-select.d.ts.map +1 -1
- package/dist/modules/backend/routes.d.ts.map +1 -1
- package/dist/modules/backend/routes.js +2 -0
- package/dist/modules/backend/routes.js.map +1 -1
- package/dist/modules/backend/row-list.d.ts +63 -0
- package/dist/modules/backend/row-list.d.ts.map +1 -0
- package/dist/modules/backend/row-list.js +252 -0
- package/dist/modules/backend/row-list.js.map +1 -0
- package/dist/modules/backend/screen.d.ts +3 -3
- package/dist/modules/backend/screen.d.ts.map +1 -1
- package/dist/modules/backend/screen.js +6 -7
- package/dist/modules/backend/screen.js.map +1 -1
- package/dist/modules/backend/search-filter-state.d.ts +42 -0
- package/dist/modules/backend/search-filter-state.d.ts.map +1 -0
- package/dist/modules/backend/search-filter-state.js +277 -0
- package/dist/modules/backend/search-filter-state.js.map +1 -0
- package/dist/modules/backend/search-filter.d.ts +77 -0
- package/dist/modules/backend/search-filter.d.ts.map +1 -0
- package/dist/modules/backend/search-filter.js +259 -0
- package/dist/modules/backend/search-filter.js.map +1 -0
- package/dist/modules/channel_api/core.d.ts +1 -0
- package/dist/modules/channel_api/core.d.ts.map +1 -1
- package/dist/modules/channel_api/core.js.map +1 -1
- package/dist/modules/channel_api/customer.d.ts.map +1 -1
- package/dist/modules/channel_api/customer.js +4 -5
- package/dist/modules/channel_api/customer.js.map +1 -1
- package/dist/modules/company_backend/index.d.ts.map +1 -1
- package/dist/modules/company_backend/index.js +2 -2
- package/dist/modules/company_backend/index.js.map +1 -1
- package/dist/modules/company_backend/routes.d.ts.map +1 -1
- package/dist/modules/company_backend/routes.js +33 -27
- package/dist/modules/company_backend/routes.js.map +1 -1
- package/dist/modules/company_backend/screens/branch-form.d.ts.map +1 -1
- package/dist/modules/company_backend/screens/branch-form.js +3 -1
- package/dist/modules/company_backend/screens/branch-form.js.map +1 -1
- package/dist/modules/company_backend/screens/companies-list.d.ts +3 -3
- package/dist/modules/company_backend/screens/companies-list.d.ts.map +1 -1
- package/dist/modules/company_backend/screens/companies-list.js +18 -18
- package/dist/modules/company_backend/screens/companies-list.js.map +1 -1
- package/dist/modules/company_backend/screens/company-form.d.ts.map +1 -1
- package/dist/modules/company_backend/screens/company-form.js +1 -3
- package/dist/modules/company_backend/screens/company-form.js.map +1 -1
- package/dist/modules/company_backend/screens/context.d.ts.map +1 -1
- package/dist/modules/company_backend/screens/context.js +1 -1
- package/dist/modules/company_backend/screens/context.js.map +1 -1
- package/dist/modules/company_backend/screens/hierarchy.d.ts.map +1 -1
- package/dist/modules/company_backend/screens/hierarchy.js +1 -2
- package/dist/modules/company_backend/screens/hierarchy.js.map +1 -1
- package/dist/modules/company_backend/search-functions.d.ts +4 -0
- package/dist/modules/company_backend/search-functions.d.ts.map +1 -0
- package/dist/modules/company_backend/search-functions.js +7 -0
- package/dist/modules/company_backend/search-functions.js.map +1 -0
- package/dist/modules/company_backend/search.d.ts +24 -0
- package/dist/modules/company_backend/search.d.ts.map +1 -0
- package/dist/modules/company_backend/search.js +24 -0
- package/dist/modules/company_backend/search.js.map +1 -0
- package/dist/modules/crm/functions/activities.js +7 -10
- package/dist/modules/crm/functions/activities.js.map +1 -1
- package/dist/modules/crm/functions/case-modal-context.d.ts +3 -0
- package/dist/modules/crm/functions/case-modal-context.d.ts.map +1 -0
- package/dist/modules/crm/functions/case-modal-context.js +156 -0
- package/dist/modules/crm/functions/case-modal-context.js.map +1 -0
- package/dist/modules/crm/functions/cases.d.ts.map +1 -1
- package/dist/modules/crm/functions/cases.js +35 -2
- package/dist/modules/crm/functions/cases.js.map +1 -1
- package/dist/modules/crm/functions/configuration.js +1 -1
- package/dist/modules/crm/functions/configuration.js.map +1 -1
- package/dist/modules/crm/functions/gamification.d.ts.map +1 -1
- package/dist/modules/crm/functions/gamification.js +11 -7
- package/dist/modules/crm/functions/gamification.js.map +1 -1
- package/dist/modules/crm/functions/index.d.ts.map +1 -1
- package/dist/modules/crm/functions/index.js +4 -0
- package/dist/modules/crm/functions/index.js.map +1 -1
- package/dist/modules/crm/functions/reporting.d.ts +3 -0
- package/dist/modules/crm/functions/reporting.d.ts.map +1 -0
- package/dist/modules/crm/functions/reporting.js +140 -0
- package/dist/modules/crm/functions/reporting.js.map +1 -0
- package/dist/modules/crm/functions/shared.d.ts +1 -0
- package/dist/modules/crm/functions/shared.d.ts.map +1 -1
- package/dist/modules/crm/functions/shared.js +17 -9
- package/dist/modules/crm/functions/shared.js.map +1 -1
- package/dist/modules/crm/messages.d.ts +8 -1
- package/dist/modules/crm/messages.d.ts.map +1 -1
- package/dist/modules/crm/messages.js +14 -0
- package/dist/modules/crm/messages.js.map +1 -1
- package/dist/modules/crm/models.d.ts.map +1 -1
- package/dist/modules/crm/models.js +7 -0
- package/dist/modules/crm/models.js.map +1 -1
- package/dist/modules/crm/operations.d.ts +4 -1
- package/dist/modules/crm/operations.d.ts.map +1 -1
- package/dist/modules/crm/operations.js +39 -7
- package/dist/modules/crm/operations.js.map +1 -1
- package/dist/modules/crm/reporting.d.ts +115 -0
- package/dist/modules/crm/reporting.d.ts.map +1 -0
- package/dist/modules/crm/reporting.js +241 -0
- package/dist/modules/crm/reporting.js.map +1 -0
- package/dist/modules/crm_backend/client/crm-case-modal.d.mts +302 -0
- package/dist/modules/crm_backend/client/crm-case-modal.d.mts.map +1 -0
- package/dist/modules/crm_backend/client/crm-case-modal.mjs +2 -0
- package/dist/modules/crm_backend/client/crm-case-modal.mjs.map +7 -0
- package/dist/modules/crm_backend/client/crm-configuration-modal.d.mts +55 -55
- package/dist/modules/crm_backend/client/crm-configuration-modal.d.mts.map +1 -1
- package/dist/modules/crm_backend/client/crm-configuration-modal.mjs +1 -1
- package/dist/modules/crm_backend/client/crm-configuration-modal.mjs.map +4 -4
- package/dist/modules/crm_backend/client/crm-kanban-view.mjs +1 -1
- package/dist/modules/crm_backend/client/crm-kanban-view.mjs.map +1 -1
- package/dist/modules/crm_backend/collection-rows.d.ts +6 -0
- package/dist/modules/crm_backend/collection-rows.d.ts.map +1 -0
- package/dist/modules/crm_backend/collection-rows.js +15 -0
- package/dist/modules/crm_backend/collection-rows.js.map +1 -0
- package/dist/modules/crm_backend/index.d.ts.map +1 -1
- package/dist/modules/crm_backend/index.js +2 -15
- package/dist/modules/crm_backend/index.js.map +1 -1
- package/dist/modules/crm_backend/islands.d.ts +1 -0
- package/dist/modules/crm_backend/islands.d.ts.map +1 -1
- package/dist/modules/crm_backend/islands.js +5 -0
- package/dist/modules/crm_backend/islands.js.map +1 -1
- package/dist/modules/crm_backend/messages.d.ts +34 -10
- package/dist/modules/crm_backend/messages.d.ts.map +1 -1
- package/dist/modules/crm_backend/messages.js +34 -10
- package/dist/modules/crm_backend/messages.js.map +1 -1
- package/dist/modules/crm_backend/modal/case-modal-view.d.ts +23 -0
- package/dist/modules/crm_backend/modal/case-modal-view.d.ts.map +1 -0
- package/dist/modules/crm_backend/modal/case-modal-view.js +640 -0
- package/dist/modules/crm_backend/modal/case-modal-view.js.map +1 -0
- package/dist/modules/crm_backend/routes.d.ts.map +1 -1
- package/dist/modules/crm_backend/routes.js +71 -15
- package/dist/modules/crm_backend/routes.js.map +1 -1
- package/dist/modules/crm_backend/screens/activity-planner.d.ts +1 -0
- package/dist/modules/crm_backend/screens/activity-planner.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/activity-planner.js +15 -11
- package/dist/modules/crm_backend/screens/activity-planner.js.map +1 -1
- package/dist/modules/crm_backend/screens/case-create.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/case-create.js +1 -1
- package/dist/modules/crm_backend/screens/case-create.js.map +1 -1
- package/dist/modules/crm_backend/screens/case-detail.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/case-detail.js +2 -2
- package/dist/modules/crm_backend/screens/case-detail.js.map +1 -1
- package/dist/modules/crm_backend/screens/cases-list.d.ts +1 -0
- package/dist/modules/crm_backend/screens/cases-list.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/cases-list.js +17 -26
- package/dist/modules/crm_backend/screens/cases-list.js.map +1 -1
- package/dist/modules/crm_backend/screens/configuration.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/configuration.js +18 -16
- package/dist/modules/crm_backend/screens/configuration.js.map +1 -1
- package/dist/modules/crm_backend/screens/leaderboard.d.ts +2 -0
- package/dist/modules/crm_backend/screens/leaderboard.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/leaderboard.js +12 -18
- package/dist/modules/crm_backend/screens/leaderboard.js.map +1 -1
- package/dist/modules/crm_backend/screens/pipeline.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/pipeline.js +1 -1
- package/dist/modules/crm_backend/screens/pipeline.js.map +1 -1
- package/dist/modules/flow/domain/command-check.d.ts +3 -0
- package/dist/modules/flow/domain/command-check.d.ts.map +1 -0
- package/dist/modules/flow/domain/command-check.js +9 -0
- package/dist/modules/flow/domain/command-check.js.map +1 -0
- package/dist/modules/flow/domain/command.d.ts +33 -0
- package/dist/modules/flow/domain/command.d.ts.map +1 -0
- package/dist/modules/flow/domain/command.js +35 -0
- package/dist/modules/flow/domain/command.js.map +1 -0
- package/dist/modules/flow/domain/issue-bulk.d.ts +50 -0
- package/dist/modules/flow/domain/issue-bulk.d.ts.map +1 -0
- package/dist/modules/flow/domain/issue-bulk.js +97 -0
- package/dist/modules/flow/domain/issue-bulk.js.map +1 -0
- package/dist/modules/flow/domain/issue-dependency.d.ts +19 -0
- package/dist/modules/flow/domain/issue-dependency.d.ts.map +1 -0
- package/dist/modules/flow/domain/issue-dependency.js +59 -0
- package/dist/modules/flow/domain/issue-dependency.js.map +1 -0
- package/dist/modules/flow/domain/issue-discussion.d.ts +52 -0
- package/dist/modules/flow/domain/issue-discussion.d.ts.map +1 -0
- package/dist/modules/flow/domain/issue-discussion.js +114 -0
- package/dist/modules/flow/domain/issue-discussion.js.map +1 -0
- package/dist/modules/flow/domain/issue-transfer.d.ts +62 -0
- package/dist/modules/flow/domain/issue-transfer.d.ts.map +1 -0
- package/dist/modules/flow/domain/issue-transfer.js +171 -0
- package/dist/modules/flow/domain/issue-transfer.js.map +1 -0
- package/dist/modules/flow/domain/issue-validation.d.ts +68 -0
- package/dist/modules/flow/domain/issue-validation.d.ts.map +1 -0
- package/dist/modules/flow/domain/issue-validation.js +136 -0
- package/dist/modules/flow/domain/issue-validation.js.map +1 -0
- package/dist/modules/flow/domain/issue-write.d.ts +118 -0
- package/dist/modules/flow/domain/issue-write.d.ts.map +1 -0
- package/dist/modules/flow/domain/issue-write.js +372 -0
- package/dist/modules/flow/domain/issue-write.js.map +1 -0
- package/dist/modules/flow/domain/sprint-lifecycle.d.ts +61 -0
- package/dist/modules/flow/domain/sprint-lifecycle.d.ts.map +1 -0
- package/dist/modules/flow/domain/sprint-lifecycle.js +145 -0
- package/dist/modules/flow/domain/sprint-lifecycle.js.map +1 -0
- package/dist/modules/flow/functions/board.d.ts +7 -0
- package/dist/modules/flow/functions/board.d.ts.map +1 -0
- package/dist/modules/flow/functions/board.js +59 -0
- package/dist/modules/flow/functions/board.js.map +1 -0
- package/dist/modules/flow/functions/column.d.ts +8 -0
- package/dist/modules/flow/functions/column.d.ts.map +1 -0
- package/dist/modules/flow/functions/column.js +64 -0
- package/dist/modules/flow/functions/column.js.map +1 -0
- package/dist/modules/flow/functions/effects.d.ts +21 -0
- package/dist/modules/flow/functions/effects.d.ts.map +1 -0
- package/dist/modules/flow/functions/effects.js +96 -0
- package/dist/modules/flow/functions/effects.js.map +1 -0
- package/dist/modules/flow/functions/entity-save.d.ts +4 -0
- package/dist/modules/flow/functions/entity-save.d.ts.map +1 -0
- package/dist/modules/flow/functions/entity-save.js +60 -0
- package/dist/modules/flow/functions/entity-save.js.map +1 -0
- package/dist/modules/flow/functions/epic.d.ts +26 -0
- package/dist/modules/flow/functions/epic.d.ts.map +1 -0
- package/dist/modules/flow/functions/epic.js +151 -0
- package/dist/modules/flow/functions/epic.js.map +1 -0
- package/dist/modules/flow/functions/field.d.ts +9 -0
- package/dist/modules/flow/functions/field.d.ts.map +1 -0
- package/dist/modules/flow/functions/field.js +117 -0
- package/dist/modules/flow/functions/field.js.map +1 -0
- package/dist/modules/flow/functions/index.d.ts +5 -0
- package/dist/modules/flow/functions/index.d.ts.map +1 -0
- package/dist/modules/flow/functions/index.js +42 -0
- package/dist/modules/flow/functions/index.js.map +1 -0
- package/dist/modules/flow/functions/issue-batch.d.ts +6 -0
- package/dist/modules/flow/functions/issue-batch.d.ts.map +1 -0
- package/dist/modules/flow/functions/issue-batch.js +149 -0
- package/dist/modules/flow/functions/issue-batch.js.map +1 -0
- package/dist/modules/flow/functions/issue-dependency.d.ts +8 -0
- package/dist/modules/flow/functions/issue-dependency.d.ts.map +1 -0
- package/dist/modules/flow/functions/issue-dependency.js +54 -0
- package/dist/modules/flow/functions/issue-dependency.js.map +1 -0
- package/dist/modules/flow/functions/issue-discussion.d.ts +6 -0
- package/dist/modules/flow/functions/issue-discussion.d.ts.map +1 -0
- package/dist/modules/flow/functions/issue-discussion.js +86 -0
- package/dist/modules/flow/functions/issue-discussion.js.map +1 -0
- package/dist/modules/flow/functions/issue-read.d.ts +18 -0
- package/dist/modules/flow/functions/issue-read.d.ts.map +1 -0
- package/dist/modules/flow/functions/issue-read.js +159 -0
- package/dist/modules/flow/functions/issue-read.js.map +1 -0
- package/dist/modules/flow/functions/issue-write.d.ts +9 -0
- package/dist/modules/flow/functions/issue-write.d.ts.map +1 -0
- package/dist/modules/flow/functions/issue-write.js +149 -0
- package/dist/modules/flow/functions/issue-write.js.map +1 -0
- package/dist/modules/flow/functions/issueType.d.ts +8 -0
- package/dist/modules/flow/functions/issueType.d.ts.map +1 -0
- package/dist/modules/flow/functions/issueType.js +66 -0
- package/dist/modules/flow/functions/issueType.js.map +1 -0
- package/dist/modules/flow/functions/page.d.ts +22 -0
- package/dist/modules/flow/functions/page.d.ts.map +1 -0
- package/dist/modules/flow/functions/page.js +202 -0
- package/dist/modules/flow/functions/page.js.map +1 -0
- package/dist/modules/flow/functions/project.d.ts +26 -0
- package/dist/modules/flow/functions/project.d.ts.map +1 -0
- package/dist/modules/flow/functions/project.js +381 -0
- package/dist/modules/flow/functions/project.js.map +1 -0
- package/dist/modules/flow/functions/sprint.d.ts +17 -0
- package/dist/modules/flow/functions/sprint.d.ts.map +1 -0
- package/dist/modules/flow/functions/sprint.js +173 -0
- package/dist/modules/flow/functions/sprint.js.map +1 -0
- package/dist/modules/flow/functions/tag.d.ts +14 -0
- package/dist/modules/flow/functions/tag.d.ts.map +1 -0
- package/dist/modules/flow/functions/tag.js +88 -0
- package/dist/modules/flow/functions/tag.js.map +1 -0
- package/dist/modules/flow/index.d.ts +8 -2
- package/dist/modules/flow/index.d.ts.map +1 -1
- package/dist/modules/flow/index.js +7 -2
- package/dist/modules/flow/index.js.map +1 -1
- package/dist/modules/flow/pages.d.ts +1 -1
- package/dist/modules/flow/pages.d.ts.map +1 -1
- package/dist/modules/flow/pages.js +1 -1
- package/dist/modules/flow/pages.js.map +1 -1
- package/dist/modules/flow/queries/calendar.d.ts +21 -0
- package/dist/modules/flow/queries/calendar.d.ts.map +1 -0
- package/dist/modules/flow/queries/calendar.js +31 -0
- package/dist/modules/flow/queries/calendar.js.map +1 -0
- package/dist/modules/flow/queries/issue-dependencies.d.ts +17 -0
- package/dist/modules/flow/queries/issue-dependencies.d.ts.map +1 -0
- package/dist/modules/flow/queries/issue-dependencies.js +46 -0
- package/dist/modules/flow/queries/issue-dependencies.js.map +1 -0
- package/dist/modules/flow/queries/issue-followers.d.ts +6 -0
- package/dist/modules/flow/queries/issue-followers.d.ts.map +1 -0
- package/dist/modules/flow/queries/issue-followers.js +18 -0
- package/dist/modules/flow/queries/issue-followers.js.map +1 -0
- package/dist/modules/flow/queries/issue-list.d.ts +153 -0
- package/dist/modules/flow/queries/issue-list.d.ts.map +1 -0
- package/dist/modules/flow/queries/issue-list.js +439 -0
- package/dist/modules/flow/queries/issue-list.js.map +1 -0
- package/dist/modules/flow/queries/options.d.ts +4 -0
- package/dist/modules/flow/queries/options.d.ts.map +1 -0
- package/dist/modules/flow/queries/options.js +13 -0
- package/dist/modules/flow/queries/options.js.map +1 -0
- package/dist/modules/flow/queries/progress.d.ts +24 -0
- package/dist/modules/flow/queries/progress.d.ts.map +1 -0
- package/dist/modules/flow/queries/progress.js +54 -0
- package/dist/modules/flow/queries/progress.js.map +1 -0
- package/dist/modules/flow/queries/project-list.d.ts +4 -0
- package/dist/modules/flow/queries/project-list.d.ts.map +1 -0
- package/dist/modules/flow/queries/project-list.js +19 -0
- package/dist/modules/flow/queries/project-list.js.map +1 -0
- package/dist/modules/flow_backend/index.js +2 -1
- package/dist/modules/flow_backend/index.js.map +1 -1
- package/dist/modules/flow_backend/messages.d.ts +1 -2
- package/dist/modules/flow_backend/messages.d.ts.map +1 -1
- package/dist/modules/flow_backend/messages.js +0 -2
- package/dist/modules/flow_backend/messages.js.map +1 -1
- package/dist/modules/flow_backend/routes.d.ts.map +1 -1
- package/dist/modules/flow_backend/routes.js +182 -127
- package/dist/modules/flow_backend/routes.js.map +1 -1
- package/dist/modules/flow_backend/screens/all-epics.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/all-epics.js +37 -45
- package/dist/modules/flow_backend/screens/all-epics.js.map +1 -1
- package/dist/modules/flow_backend/screens/all-pages.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/all-pages.js +43 -51
- package/dist/modules/flow_backend/screens/all-pages.js.map +1 -1
- package/dist/modules/flow_backend/screens/epic-detail.js +1 -1
- package/dist/modules/flow_backend/screens/epic-detail.js.map +1 -1
- package/dist/modules/flow_backend/screens/gantt.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/gantt.js +1 -1
- package/dist/modules/flow_backend/screens/gantt.js.map +1 -1
- package/dist/modules/flow_backend/screens/issue-detail.js +2 -1
- package/dist/modules/flow_backend/screens/issue-detail.js.map +1 -1
- package/dist/modules/flow_backend/screens/issues.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/issues.js +68 -82
- package/dist/modules/flow_backend/screens/issues.js.map +1 -1
- package/dist/modules/flow_backend/screens/map.js +1 -1
- package/dist/modules/flow_backend/screens/map.js.map +1 -1
- package/dist/modules/flow_backend/screens/my-work.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/my-work.js +63 -69
- package/dist/modules/flow_backend/screens/my-work.js.map +1 -1
- package/dist/modules/flow_backend/screens/page-detail.js +1 -1
- package/dist/modules/flow_backend/screens/page-detail.js.map +1 -1
- package/dist/modules/flow_backend/screens/project-create.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/project-create.js +1 -1
- package/dist/modules/flow_backend/screens/project-create.js.map +1 -1
- package/dist/modules/flow_backend/screens/project-pages.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/project-pages.js +1 -2
- package/dist/modules/flow_backend/screens/project-pages.js.map +1 -1
- package/dist/modules/flow_backend/screens/projects-list.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/projects-list.js +41 -64
- package/dist/modules/flow_backend/screens/projects-list.js.map +1 -1
- package/dist/modules/flow_backend/screens/sprints.d.ts +3 -1
- package/dist/modules/flow_backend/screens/sprints.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/sprints.js +77 -73
- package/dist/modules/flow_backend/screens/sprints.js.map +1 -1
- package/dist/modules/flow_backend/search-functions.d.ts +13 -0
- package/dist/modules/flow_backend/search-functions.d.ts.map +1 -0
- package/dist/modules/flow_backend/search-functions.js +40 -0
- package/dist/modules/flow_backend/search-functions.js.map +1 -0
- package/dist/modules/flow_backend/search.d.ts +48 -0
- package/dist/modules/flow_backend/search.d.ts.map +1 -0
- package/dist/modules/flow_backend/search.js +57 -0
- package/dist/modules/flow_backend/search.js.map +1 -0
- package/dist/modules/flow_staff_channel/channel-routes.js +1 -1
- package/dist/modules/flow_staff_channel/channel-routes.js.map +1 -1
- package/dist/modules/hospitality_billing/index.js +2 -1
- package/dist/modules/hospitality_billing/index.js.map +1 -1
- package/dist/modules/hospitality_billing/menus.js +2 -2
- package/dist/modules/hospitality_billing/menus.js.map +1 -1
- package/dist/modules/hospitality_billing/messages.js +2 -2
- package/dist/modules/hospitality_billing/messages.js.map +1 -1
- package/dist/modules/hospitality_billing/routes.d.ts.map +1 -1
- package/dist/modules/hospitality_billing/routes.js +39 -5
- package/dist/modules/hospitality_billing/routes.js.map +1 -1
- package/dist/modules/hospitality_billing/screens/billing.d.ts.map +1 -1
- package/dist/modules/hospitality_billing/screens/billing.js +21 -17
- package/dist/modules/hospitality_billing/screens/billing.js.map +1 -1
- package/dist/modules/hospitality_billing/screens/charge-rules.d.ts +4 -2
- package/dist/modules/hospitality_billing/screens/charge-rules.d.ts.map +1 -1
- package/dist/modules/hospitality_billing/screens/charge-rules.js +14 -9
- package/dist/modules/hospitality_billing/screens/charge-rules.js.map +1 -1
- package/dist/modules/hospitality_billing/screens/page-frame.d.ts +3 -2
- package/dist/modules/hospitality_billing/screens/page-frame.d.ts.map +1 -1
- package/dist/modules/hospitality_billing/screens/page-frame.js +2 -4
- package/dist/modules/hospitality_billing/screens/page-frame.js.map +1 -1
- package/dist/modules/hospitality_billing/screens/shared.d.ts +4 -4
- package/dist/modules/hospitality_billing/screens/shared.d.ts.map +1 -1
- package/dist/modules/hospitality_billing/screens/shared.js +2 -2
- package/dist/modules/hospitality_billing/screens/shared.js.map +1 -1
- package/dist/modules/hospitality_billing/search-functions.d.ts +4 -0
- package/dist/modules/hospitality_billing/search-functions.d.ts.map +1 -0
- package/dist/modules/hospitality_billing/search-functions.js +16 -0
- package/dist/modules/hospitality_billing/search-functions.js.map +1 -0
- package/dist/modules/hospitality_billing/search.d.ts +66 -0
- package/dist/modules/hospitality_billing/search.d.ts.map +1 -0
- package/dist/modules/hospitality_billing/search.js +92 -0
- package/dist/modules/hospitality_billing/search.js.map +1 -0
- package/dist/modules/hospitality_core/content.d.ts.map +1 -1
- package/dist/modules/hospitality_core/content.js +3 -0
- package/dist/modules/hospitality_core/content.js.map +1 -1
- package/dist/modules/hospitality_core/housekeeping.d.ts.map +1 -1
- package/dist/modules/hospitality_core/housekeeping.js +13 -4
- package/dist/modules/hospitality_core/housekeeping.js.map +1 -1
- package/dist/modules/hospitality_core/index.d.ts.map +1 -1
- package/dist/modules/hospitality_core/index.js +2 -0
- package/dist/modules/hospitality_core/index.js.map +1 -1
- package/dist/modules/hospitality_core/menus.d.ts.map +1 -1
- package/dist/modules/hospitality_core/menus.js +27 -30
- package/dist/modules/hospitality_core/menus.js.map +1 -1
- package/dist/modules/hospitality_core/messages.d.ts.map +1 -1
- package/dist/modules/hospitality_core/messages.js +6 -2
- package/dist/modules/hospitality_core/messages.js.map +1 -1
- package/dist/modules/hospitality_core/routes.d.ts.map +1 -1
- package/dist/modules/hospitality_core/routes.js +135 -47
- package/dist/modules/hospitality_core/routes.js.map +1 -1
- package/dist/modules/hospitality_core/screens/amenities.d.ts +4 -2
- package/dist/modules/hospitality_core/screens/amenities.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/amenities.js +9 -5
- package/dist/modules/hospitality_core/screens/amenities.js.map +1 -1
- package/dist/modules/hospitality_core/screens/check-out-prep.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/check-out-prep.js +1 -1
- package/dist/modules/hospitality_core/screens/check-out-prep.js.map +1 -1
- package/dist/modules/hospitality_core/screens/cleaning-tasks.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/cleaning-tasks.js +45 -30
- package/dist/modules/hospitality_core/screens/cleaning-tasks.js.map +1 -1
- package/dist/modules/hospitality_core/screens/folios.d.ts +4 -2
- package/dist/modules/hospitality_core/screens/folios.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/folios.js +10 -4
- package/dist/modules/hospitality_core/screens/folios.js.map +1 -1
- package/dist/modules/hospitality_core/screens/front-desk.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/front-desk.js +40 -40
- package/dist/modules/hospitality_core/screens/front-desk.js.map +1 -1
- package/dist/modules/hospitality_core/screens/page-frame.d.ts +4 -3
- package/dist/modules/hospitality_core/screens/page-frame.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/page-frame.js +4 -7
- package/dist/modules/hospitality_core/screens/page-frame.js.map +1 -1
- package/dist/modules/hospitality_core/screens/policies.d.ts +4 -2
- package/dist/modules/hospitality_core/screens/policies.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/policies.js +9 -5
- package/dist/modules/hospitality_core/screens/policies.js.map +1 -1
- package/dist/modules/hospitality_core/screens/properties.d.ts +4 -2
- package/dist/modules/hospitality_core/screens/properties.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/properties.js +49 -42
- package/dist/modules/hospitality_core/screens/properties.js.map +1 -1
- package/dist/modules/hospitality_core/screens/rate-plans.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/rate-plans.js +19 -15
- package/dist/modules/hospitality_core/screens/rate-plans.js.map +1 -1
- package/dist/modules/hospitality_core/screens/reservations.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/reservations.js +26 -19
- package/dist/modules/hospitality_core/screens/reservations.js.map +1 -1
- package/dist/modules/hospitality_core/screens/room-types.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/room-types.js +23 -19
- package/dist/modules/hospitality_core/screens/room-types.js.map +1 -1
- package/dist/modules/hospitality_core/screens/rooms.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/rooms.js +26 -19
- package/dist/modules/hospitality_core/screens/rooms.js.map +1 -1
- package/dist/modules/hospitality_core/screens/shared.d.ts +4 -4
- package/dist/modules/hospitality_core/screens/shared.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/shared.js +2 -2
- package/dist/modules/hospitality_core/screens/shared.js.map +1 -1
- package/dist/modules/hospitality_core/screens/stays.d.ts +4 -2
- package/dist/modules/hospitality_core/screens/stays.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/stays.js +10 -4
- package/dist/modules/hospitality_core/screens/stays.js.map +1 -1
- package/dist/modules/hospitality_core/screens/tape-chart.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/tape-chart.js +3 -12
- package/dist/modules/hospitality_core/screens/tape-chart.js.map +1 -1
- package/dist/modules/hospitality_core/search-functions.d.ts +10 -0
- package/dist/modules/hospitality_core/search-functions.d.ts.map +1 -0
- package/dist/modules/hospitality_core/search-functions.js +17 -0
- package/dist/modules/hospitality_core/search-functions.js.map +1 -0
- package/dist/modules/hospitality_core/search.d.ts +218 -0
- package/dist/modules/hospitality_core/search.d.ts.map +1 -0
- package/dist/modules/hospitality_core/search.js +201 -0
- package/dist/modules/hospitality_core/search.js.map +1 -0
- package/dist/modules/hr_backend/index.d.ts.map +1 -1
- package/dist/modules/hr_backend/index.js +2 -0
- package/dist/modules/hr_backend/index.js.map +1 -1
- package/dist/modules/hr_backend/routes.d.ts.map +1 -1
- package/dist/modules/hr_backend/routes.js +55 -71
- package/dist/modules/hr_backend/routes.js.map +1 -1
- package/dist/modules/hr_backend/screens/employee-form.d.ts.map +1 -1
- package/dist/modules/hr_backend/screens/employee-form.js +1 -1
- package/dist/modules/hr_backend/screens/employee-form.js.map +1 -1
- package/dist/modules/hr_backend/screens/employees-list.d.ts +3 -1
- package/dist/modules/hr_backend/screens/employees-list.d.ts.map +1 -1
- package/dist/modules/hr_backend/screens/employees-list.js +14 -12
- package/dist/modules/hr_backend/screens/employees-list.js.map +1 -1
- package/dist/modules/hr_backend/screens/leaves-list.d.ts +3 -1
- package/dist/modules/hr_backend/screens/leaves-list.d.ts.map +1 -1
- package/dist/modules/hr_backend/screens/leaves-list.js +11 -10
- package/dist/modules/hr_backend/screens/leaves-list.js.map +1 -1
- package/dist/modules/hr_backend/search-functions.d.ts +4 -0
- package/dist/modules/hr_backend/search-functions.d.ts.map +1 -0
- package/dist/modules/hr_backend/search-functions.js +8 -0
- package/dist/modules/hr_backend/search-functions.js.map +1 -0
- package/dist/modules/hr_backend/search.d.ts +73 -0
- package/dist/modules/hr_backend/search.d.ts.map +1 -0
- package/dist/modules/hr_backend/search.js +67 -0
- package/dist/modules/hr_backend/search.js.map +1 -0
- package/dist/modules/livedoc/islands.d.ts +10 -0
- package/dist/modules/livedoc/islands.d.ts.map +1 -1
- package/dist/modules/loyalty_backend/index.js +176 -215
- package/dist/modules/loyalty_backend/index.js.map +1 -1
- package/dist/modules/loyalty_backend/messages.d.ts +0 -10
- package/dist/modules/loyalty_backend/messages.d.ts.map +1 -1
- package/dist/modules/loyalty_backend/messages.js +0 -10
- package/dist/modules/loyalty_backend/messages.js.map +1 -1
- package/dist/modules/loyalty_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/loyalty_backend/screens/index.js +321 -326
- package/dist/modules/loyalty_backend/screens/index.js.map +1 -1
- package/dist/modules/loyalty_backend/screens/page-frame.d.ts +7 -5
- package/dist/modules/loyalty_backend/screens/page-frame.d.ts.map +1 -1
- package/dist/modules/loyalty_backend/screens/page-frame.js +3 -3
- package/dist/modules/loyalty_backend/screens/page-frame.js.map +1 -1
- package/dist/modules/loyalty_backend/search-functions.d.ts +4 -0
- package/dist/modules/loyalty_backend/search-functions.d.ts.map +1 -0
- package/dist/modules/loyalty_backend/search-functions.js +11 -0
- package/dist/modules/loyalty_backend/search-functions.js.map +1 -0
- package/dist/modules/loyalty_backend/search.d.ts +33 -0
- package/dist/modules/loyalty_backend/search.d.ts.map +1 -0
- package/dist/modules/loyalty_backend/search.js +85 -0
- package/dist/modules/loyalty_backend/search.js.map +1 -0
- package/dist/modules/mail_backend/index.d.ts.map +1 -1
- package/dist/modules/mail_backend/index.js +0 -2
- package/dist/modules/mail_backend/index.js.map +1 -1
- package/dist/modules/mail_backend/screens/inbox-list.d.ts.map +1 -1
- package/dist/modules/mail_backend/screens/inbox-list.js +1 -1
- package/dist/modules/mail_backend/screens/inbox-list.js.map +1 -1
- package/dist/modules/mail_inbound_backend/index.d.ts.map +1 -1
- package/dist/modules/mail_inbound_backend/index.js +0 -2
- package/dist/modules/mail_inbound_backend/index.js.map +1 -1
- package/dist/modules/mail_inbound_backend/screens/inbound-list.d.ts.map +1 -1
- package/dist/modules/mail_inbound_backend/screens/inbound-list.js +1 -1
- package/dist/modules/mail_inbound_backend/screens/inbound-list.js.map +1 -1
- package/dist/modules/mail_transport_backend/index.d.ts.map +1 -1
- package/dist/modules/mail_transport_backend/index.js +0 -2
- package/dist/modules/mail_transport_backend/index.js.map +1 -1
- package/dist/modules/mail_transport_backend/screens/outbox-list.d.ts.map +1 -1
- package/dist/modules/mail_transport_backend/screens/outbox-list.js +1 -1
- package/dist/modules/mail_transport_backend/screens/outbox-list.js.map +1 -1
- package/dist/modules/manufacturing_backend/index.d.ts.map +1 -1
- package/dist/modules/manufacturing_backend/index.js +14 -0
- package/dist/modules/manufacturing_backend/index.js.map +1 -1
- package/dist/modules/manufacturing_backend/routes.d.ts.map +1 -1
- package/dist/modules/manufacturing_backend/routes.js +74 -20
- package/dist/modules/manufacturing_backend/routes.js.map +1 -1
- package/dist/modules/manufacturing_backend/screens/boms-list.d.ts +3 -1
- package/dist/modules/manufacturing_backend/screens/boms-list.d.ts.map +1 -1
- package/dist/modules/manufacturing_backend/screens/boms-list.js +12 -11
- package/dist/modules/manufacturing_backend/screens/boms-list.js.map +1 -1
- package/dist/modules/manufacturing_backend/screens/order-execution.d.ts.map +1 -1
- package/dist/modules/manufacturing_backend/screens/order-execution.js +1 -1
- package/dist/modules/manufacturing_backend/screens/order-execution.js.map +1 -1
- package/dist/modules/manufacturing_backend/screens/orders-list.d.ts +3 -1
- package/dist/modules/manufacturing_backend/screens/orders-list.d.ts.map +1 -1
- package/dist/modules/manufacturing_backend/screens/orders-list.js +13 -12
- package/dist/modules/manufacturing_backend/screens/orders-list.js.map +1 -1
- package/dist/modules/manufacturing_backend/screens/work-centers-list.d.ts +3 -1
- package/dist/modules/manufacturing_backend/screens/work-centers-list.d.ts.map +1 -1
- package/dist/modules/manufacturing_backend/screens/work-centers-list.js +13 -12
- package/dist/modules/manufacturing_backend/screens/work-centers-list.js.map +1 -1
- package/dist/modules/manufacturing_backend/search-functions.d.ts +4 -0
- package/dist/modules/manufacturing_backend/search-functions.d.ts.map +1 -0
- package/dist/modules/manufacturing_backend/search-functions.js +9 -0
- package/dist/modules/manufacturing_backend/search-functions.js.map +1 -0
- package/dist/modules/manufacturing_backend/search.d.ts +99 -0
- package/dist/modules/manufacturing_backend/search.d.ts.map +1 -0
- package/dist/modules/manufacturing_backend/search.js +75 -0
- package/dist/modules/manufacturing_backend/search.js.map +1 -0
- package/dist/modules/oauth_backend/index.d.ts.map +1 -1
- package/dist/modules/oauth_backend/index.js +2 -0
- package/dist/modules/oauth_backend/index.js.map +1 -1
- package/dist/modules/oauth_backend/messages.d.ts +0 -4
- package/dist/modules/oauth_backend/messages.d.ts.map +1 -1
- package/dist/modules/oauth_backend/messages.js +0 -4
- package/dist/modules/oauth_backend/messages.js.map +1 -1
- package/dist/modules/oauth_backend/routes.d.ts.map +1 -1
- package/dist/modules/oauth_backend/routes.js +33 -2
- package/dist/modules/oauth_backend/routes.js.map +1 -1
- package/dist/modules/oauth_backend/screens/index.d.ts +7 -3
- package/dist/modules/oauth_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/oauth_backend/screens/index.js +114 -104
- package/dist/modules/oauth_backend/screens/index.js.map +1 -1
- package/dist/modules/oauth_backend/search-functions.d.ts +4 -0
- package/dist/modules/oauth_backend/search-functions.d.ts.map +1 -0
- package/dist/modules/oauth_backend/search-functions.js +8 -0
- package/dist/modules/oauth_backend/search-functions.js.map +1 -0
- package/dist/modules/oauth_backend/search.d.ts +68 -0
- package/dist/modules/oauth_backend/search.d.ts.map +1 -0
- package/dist/modules/oauth_backend/search.js +69 -0
- package/dist/modules/oauth_backend/search.js.map +1 -0
- package/dist/modules/partner/functions.d.ts.map +1 -1
- package/dist/modules/partner/functions.js +121 -23
- package/dist/modules/partner/functions.js.map +1 -1
- package/dist/modules/partner_backend/functions.d.ts +13 -0
- package/dist/modules/partner_backend/functions.d.ts.map +1 -0
- package/dist/modules/partner_backend/functions.js +56 -0
- package/dist/modules/partner_backend/functions.js.map +1 -0
- package/dist/modules/partner_backend/index.d.ts.map +1 -1
- package/dist/modules/partner_backend/index.js +28 -6
- package/dist/modules/partner_backend/index.js.map +1 -1
- package/dist/modules/partner_backend/routes.d.ts.map +1 -1
- package/dist/modules/partner_backend/routes.js +269 -86
- package/dist/modules/partner_backend/routes.js.map +1 -1
- package/dist/modules/partner_backend/screens/form.d.ts.map +1 -1
- package/dist/modules/partner_backend/screens/form.js +3 -1
- package/dist/modules/partner_backend/screens/form.js.map +1 -1
- package/dist/modules/partner_backend/screens/index.d.ts +1 -1
- package/dist/modules/partner_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/partner_backend/screens/list.d.ts +3 -4
- package/dist/modules/partner_backend/screens/list.d.ts.map +1 -1
- package/dist/modules/partner_backend/screens/list.js +13 -92
- package/dist/modules/partner_backend/screens/list.js.map +1 -1
- package/dist/modules/partner_backend/screens/new.d.ts.map +1 -1
- package/dist/modules/partner_backend/screens/new.js +1 -1
- package/dist/modules/partner_backend/screens/new.js.map +1 -1
- package/dist/modules/partner_backend/screens/types.d.ts +0 -11
- package/dist/modules/partner_backend/screens/types.d.ts.map +1 -1
- package/dist/modules/pos_backend/index.js +34 -9
- package/dist/modules/pos_backend/index.js.map +1 -1
- package/dist/modules/pos_backend/screens.d.ts +4 -2
- package/dist/modules/pos_backend/screens.d.ts.map +1 -1
- package/dist/modules/pos_backend/screens.js +105 -96
- package/dist/modules/pos_backend/screens.js.map +1 -1
- package/dist/modules/pos_backend/search-functions.d.ts +4 -0
- package/dist/modules/pos_backend/search-functions.d.ts.map +1 -0
- package/dist/modules/pos_backend/search-functions.js +7 -0
- package/dist/modules/pos_backend/search-functions.js.map +1 -0
- package/dist/modules/pos_backend/search.d.ts +41 -0
- package/dist/modules/pos_backend/search.d.ts.map +1 -0
- package/dist/modules/pos_backend/search.js +33 -0
- package/dist/modules/pos_backend/search.js.map +1 -0
- package/dist/modules/pricing_backend/index.js +31 -4
- package/dist/modules/pricing_backend/index.js.map +1 -1
- package/dist/modules/pricing_backend/screens/pricelist-detail.d.ts.map +1 -1
- package/dist/modules/pricing_backend/screens/pricelist-detail.js +1 -1
- package/dist/modules/pricing_backend/screens/pricelist-detail.js.map +1 -1
- package/dist/modules/pricing_backend/screens/pricelists-list.d.ts +3 -1
- package/dist/modules/pricing_backend/screens/pricelists-list.d.ts.map +1 -1
- package/dist/modules/pricing_backend/screens/pricelists-list.js +14 -12
- package/dist/modules/pricing_backend/screens/pricelists-list.js.map +1 -1
- package/dist/modules/pricing_backend/search-functions.d.ts +4 -0
- package/dist/modules/pricing_backend/search-functions.d.ts.map +1 -0
- package/dist/modules/pricing_backend/search-functions.js +11 -0
- package/dist/modules/pricing_backend/search-functions.js.map +1 -0
- package/dist/modules/pricing_backend/search.d.ts +41 -0
- package/dist/modules/pricing_backend/search.d.ts.map +1 -0
- package/dist/modules/pricing_backend/search.js +34 -0
- package/dist/modules/pricing_backend/search.js.map +1 -0
- package/dist/modules/product/attribute-draft.d.ts +3 -0
- package/dist/modules/product/attribute-draft.d.ts.map +1 -0
- package/dist/modules/product/attribute-draft.js +126 -0
- package/dist/modules/product/attribute-draft.js.map +1 -0
- package/dist/modules/product/attribute-modal-context.d.ts +4 -0
- package/dist/modules/product/attribute-modal-context.d.ts.map +1 -0
- package/dist/modules/product/attribute-modal-context.js +57 -0
- package/dist/modules/product/attribute-modal-context.js.map +1 -0
- package/dist/modules/product/attribute-modal-labels.d.ts +3 -0
- package/dist/modules/product/attribute-modal-labels.d.ts.map +1 -0
- package/dist/modules/product/attribute-modal-labels.js +30 -0
- package/dist/modules/product/attribute-modal-labels.js.map +1 -0
- package/dist/modules/product/functions.d.ts.map +1 -1
- package/dist/modules/product/functions.js +7 -1
- package/dist/modules/product/functions.js.map +1 -1
- package/dist/modules/product/messages.d.ts.map +1 -1
- package/dist/modules/product/messages.js +24 -0
- package/dist/modules/product/messages.js.map +1 -1
- package/dist/modules/product/models.d.ts.map +1 -1
- package/dist/modules/product/models.js +14 -2
- package/dist/modules/product/models.js.map +1 -1
- package/dist/modules/product/product-modal-context.d.ts.map +1 -1
- package/dist/modules/product/product-modal-context.js +20 -30
- package/dist/modules/product/product-modal-context.js.map +1 -1
- package/dist/modules/product/record-images.d.ts +12 -0
- package/dist/modules/product/record-images.d.ts.map +1 -0
- package/dist/modules/product/record-images.js +37 -0
- package/dist/modules/product/record-images.js.map +1 -0
- package/dist/modules/product/search.d.ts +3 -0
- package/dist/modules/product/search.d.ts.map +1 -1
- package/dist/modules/product/search.js +4 -0
- package/dist/modules/product/search.js.map +1 -1
- package/dist/modules/product/variant-setup.d.ts +46 -0
- package/dist/modules/product/variant-setup.d.ts.map +1 -0
- package/dist/modules/product/variant-setup.js +444 -0
- package/dist/modules/product/variant-setup.js.map +1 -0
- package/dist/modules/product_backend/attribute-search-state.d.ts +22 -0
- package/dist/modules/product_backend/attribute-search-state.d.ts.map +1 -0
- package/dist/modules/product_backend/attribute-search-state.js +62 -0
- package/dist/modules/product_backend/attribute-search-state.js.map +1 -0
- package/dist/modules/product_backend/attributes-search.d.ts +10 -0
- package/dist/modules/product_backend/attributes-search.d.ts.map +1 -0
- package/dist/modules/product_backend/attributes-search.js +67 -0
- package/dist/modules/product_backend/attributes-search.js.map +1 -0
- package/dist/modules/product_backend/client/attribute-modal.d.mts +62 -0
- package/dist/modules/product_backend/client/attribute-modal.d.mts.map +1 -0
- package/dist/modules/product_backend/client/attribute-modal.mjs +2 -0
- package/dist/modules/product_backend/client/attribute-modal.mjs.map +7 -0
- package/dist/modules/product_backend/client/product-modal.d.mts +45 -44
- package/dist/modules/product_backend/client/product-modal.d.mts.map +1 -1
- package/dist/modules/product_backend/client/product-modal.mjs +1 -1
- package/dist/modules/product_backend/client/product-modal.mjs.map +4 -4
- package/dist/modules/product_backend/client/product.css +262 -0
- package/dist/modules/product_backend/client/variant-editor.d.mts +16 -0
- package/dist/modules/product_backend/client/variant-editor.d.mts.map +1 -0
- package/dist/modules/product_backend/client/variant-editor.mjs +2 -0
- package/dist/modules/product_backend/client/variant-editor.mjs.map +7 -0
- package/dist/modules/product_backend/functions.d.ts.map +1 -0
- package/dist/modules/product_backend/functions.js +29 -0
- package/dist/modules/product_backend/functions.js.map +1 -0
- package/dist/modules/product_backend/index.d.ts.map +1 -1
- package/dist/modules/product_backend/index.js +227 -5
- package/dist/modules/product_backend/index.js.map +1 -1
- package/dist/modules/product_backend/islands.d.ts +20 -0
- package/dist/modules/product_backend/islands.d.ts.map +1 -1
- package/dist/modules/product_backend/islands.js +25 -0
- package/dist/modules/product_backend/islands.js.map +1 -1
- package/dist/modules/product_backend/menus.d.ts.map +1 -1
- package/dist/modules/product_backend/menus.js +4 -5
- package/dist/modules/product_backend/menus.js.map +1 -1
- package/dist/modules/product_backend/modal/attribute-modal-view.d.ts +25 -0
- package/dist/modules/product_backend/modal/attribute-modal-view.d.ts.map +1 -0
- package/dist/modules/product_backend/modal/attribute-modal-view.js +180 -0
- package/dist/modules/product_backend/modal/attribute-modal-view.js.map +1 -0
- package/dist/modules/product_backend/modal/product-modal-view.d.ts +25 -6
- package/dist/modules/product_backend/modal/product-modal-view.d.ts.map +1 -1
- package/dist/modules/product_backend/modal/product-modal-view.js +334 -251
- package/dist/modules/product_backend/modal/product-modal-view.js.map +1 -1
- package/dist/modules/product_backend/routes.d.ts.map +1 -1
- package/dist/modules/product_backend/routes.js +159 -200
- package/dist/modules/product_backend/routes.js.map +1 -1
- package/dist/modules/product_backend/screens/attributes.d.ts +18 -4
- package/dist/modules/product_backend/screens/attributes.d.ts.map +1 -1
- package/dist/modules/product_backend/screens/attributes.js +71 -63
- package/dist/modules/product_backend/screens/attributes.js.map +1 -1
- package/dist/modules/product_backend/screens/detail.d.ts.map +1 -1
- package/dist/modules/product_backend/screens/detail.js +3 -2
- package/dist/modules/product_backend/screens/detail.js.map +1 -1
- package/dist/modules/product_backend/screens/list.d.ts +13 -7
- package/dist/modules/product_backend/screens/list.d.ts.map +1 -1
- package/dist/modules/product_backend/screens/list.js +38 -57
- package/dist/modules/product_backend/screens/list.js.map +1 -1
- package/dist/modules/product_backend/screens/variant.d.ts.map +1 -1
- package/dist/modules/product_backend/screens/variant.js +3 -2
- package/dist/modules/product_backend/screens/variant.js.map +1 -1
- package/dist/modules/product_media/functions.d.ts.map +1 -1
- package/dist/modules/product_media/functions.js +2 -0
- package/dist/modules/product_media/functions.js.map +1 -1
- package/dist/modules/purchase_backend/client/purchase-order-modal.d.mts +185 -0
- package/dist/modules/purchase_backend/client/purchase-order-modal.d.mts.map +1 -0
- package/dist/modules/purchase_backend/client/purchase-order-modal.mjs +2 -0
- package/dist/modules/purchase_backend/client/purchase-order-modal.mjs.map +7 -0
- package/dist/modules/purchase_backend/client/vendor-pricelist-modal.d.mts +52 -0
- package/dist/modules/purchase_backend/client/vendor-pricelist-modal.d.mts.map +1 -0
- package/dist/modules/purchase_backend/client/vendor-pricelist-modal.mjs +2 -0
- package/dist/modules/purchase_backend/client/vendor-pricelist-modal.mjs.map +7 -0
- package/dist/modules/purchase_backend/index.js +192 -215
- package/dist/modules/purchase_backend/index.js.map +1 -1
- package/dist/modules/purchase_backend/modal/context.d.ts +4 -0
- package/dist/modules/purchase_backend/modal/context.d.ts.map +1 -0
- package/dist/modules/purchase_backend/modal/context.js +94 -0
- package/dist/modules/purchase_backend/modal/context.js.map +1 -0
- package/dist/modules/purchase_backend/modal/order-modal-view.d.ts +18 -0
- package/dist/modules/purchase_backend/modal/order-modal-view.d.ts.map +1 -0
- package/dist/modules/purchase_backend/modal/order-modal-view.js +290 -0
- package/dist/modules/purchase_backend/modal/order-modal-view.js.map +1 -0
- package/dist/modules/purchase_backend/modal/pricelist-context.d.ts +3 -0
- package/dist/modules/purchase_backend/modal/pricelist-context.d.ts.map +1 -0
- package/dist/modules/purchase_backend/modal/pricelist-context.js +36 -0
- package/dist/modules/purchase_backend/modal/pricelist-context.js.map +1 -0
- package/dist/modules/purchase_backend/modal/pricelist-view.d.ts +12 -0
- package/dist/modules/purchase_backend/modal/pricelist-view.d.ts.map +1 -0
- package/dist/modules/purchase_backend/modal/pricelist-view.js +78 -0
- package/dist/modules/purchase_backend/modal/pricelist-view.js.map +1 -0
- package/dist/modules/purchase_backend/screens/order-detail.d.ts.map +1 -1
- package/dist/modules/purchase_backend/screens/order-detail.js +4 -2
- package/dist/modules/purchase_backend/screens/order-detail.js.map +1 -1
- package/dist/modules/purchase_backend/screens/order-list-shared.js +2 -2
- package/dist/modules/purchase_backend/screens/order-list-shared.js.map +1 -1
- package/dist/modules/purchase_backend/screens/overview.d.ts +5 -0
- package/dist/modules/purchase_backend/screens/overview.d.ts.map +1 -1
- package/dist/modules/purchase_backend/screens/overview.js +27 -6
- package/dist/modules/purchase_backend/screens/overview.js.map +1 -1
- package/dist/modules/purchase_backend/screens/purchase-orders-list.d.ts.map +1 -1
- package/dist/modules/purchase_backend/screens/purchase-orders-list.js +11 -13
- package/dist/modules/purchase_backend/screens/purchase-orders-list.js.map +1 -1
- package/dist/modules/purchase_backend/screens/rfq-create.d.ts.map +1 -1
- package/dist/modules/purchase_backend/screens/rfq-create.js +1 -1
- package/dist/modules/purchase_backend/screens/rfq-create.js.map +1 -1
- package/dist/modules/purchase_backend/screens/rfqs-list.d.ts +1 -1
- package/dist/modules/purchase_backend/screens/rfqs-list.d.ts.map +1 -1
- package/dist/modules/purchase_backend/screens/rfqs-list.js +10 -15
- package/dist/modules/purchase_backend/screens/rfqs-list.js.map +1 -1
- package/dist/modules/purchase_backend/screens/vendor-pricelist-create.d.ts.map +1 -1
- package/dist/modules/purchase_backend/screens/vendor-pricelist-create.js +1 -1
- package/dist/modules/purchase_backend/screens/vendor-pricelist-create.js.map +1 -1
- package/dist/modules/purchase_backend/screens/vendor-pricelists-list.d.ts +2 -1
- package/dist/modules/purchase_backend/screens/vendor-pricelists-list.d.ts.map +1 -1
- package/dist/modules/purchase_backend/screens/vendor-pricelists-list.js +11 -21
- package/dist/modules/purchase_backend/screens/vendor-pricelists-list.js.map +1 -1
- package/dist/modules/purchase_backend/search-functions.d.ts +4 -0
- package/dist/modules/purchase_backend/search-functions.d.ts.map +1 -0
- package/dist/modules/purchase_backend/search-functions.js +9 -0
- package/dist/modules/purchase_backend/search-functions.js.map +1 -0
- package/dist/modules/purchase_backend/search.d.ts +128 -0
- package/dist/modules/purchase_backend/search.d.ts.map +1 -0
- package/dist/modules/purchase_backend/search.js +106 -0
- package/dist/modules/purchase_backend/search.js.map +1 -0
- package/dist/modules/sale/functions/line-edit.d.ts +4 -0
- package/dist/modules/sale/functions/line-edit.d.ts.map +1 -0
- package/dist/modules/sale/functions/line-edit.js +120 -0
- package/dist/modules/sale/functions/line-edit.js.map +1 -0
- package/dist/modules/sale/index.d.ts.map +1 -1
- package/dist/modules/sale/index.js +16 -1
- package/dist/modules/sale/index.js.map +1 -1
- package/dist/modules/sale_backend/client/invoicing-policy-modal.d.mts +53 -0
- package/dist/modules/sale_backend/client/invoicing-policy-modal.d.mts.map +1 -0
- package/dist/modules/sale_backend/client/invoicing-policy-modal.mjs +2 -0
- package/dist/modules/sale_backend/client/invoicing-policy-modal.mjs.map +7 -0
- package/dist/modules/sale_backend/client/sale-order-modal.d.mts +161 -0
- package/dist/modules/sale_backend/client/sale-order-modal.d.mts.map +1 -0
- package/dist/modules/sale_backend/client/sale-order-modal.mjs +2 -0
- package/dist/modules/sale_backend/client/sale-order-modal.mjs.map +7 -0
- package/dist/modules/sale_backend/index.js +160 -48
- package/dist/modules/sale_backend/index.js.map +1 -1
- package/dist/modules/sale_backend/modal/context.d.ts +4 -0
- package/dist/modules/sale_backend/modal/context.d.ts.map +1 -0
- package/dist/modules/sale_backend/modal/context.js +94 -0
- package/dist/modules/sale_backend/modal/context.js.map +1 -0
- package/dist/modules/sale_backend/modal/order-modal-view.d.ts +18 -0
- package/dist/modules/sale_backend/modal/order-modal-view.d.ts.map +1 -0
- package/dist/modules/sale_backend/modal/order-modal-view.js +289 -0
- package/dist/modules/sale_backend/modal/order-modal-view.js.map +1 -0
- package/dist/modules/sale_backend/modal/policy-context.d.ts +3 -0
- package/dist/modules/sale_backend/modal/policy-context.d.ts.map +1 -0
- package/dist/modules/sale_backend/modal/policy-context.js +26 -0
- package/dist/modules/sale_backend/modal/policy-context.js.map +1 -0
- package/dist/modules/sale_backend/modal/policy-view.d.ts +11 -0
- package/dist/modules/sale_backend/modal/policy-view.d.ts.map +1 -0
- package/dist/modules/sale_backend/modal/policy-view.js +70 -0
- package/dist/modules/sale_backend/modal/policy-view.js.map +1 -0
- package/dist/modules/sale_backend/order-collection.d.ts +7 -0
- package/dist/modules/sale_backend/order-collection.d.ts.map +1 -0
- package/dist/modules/sale_backend/order-collection.js +27 -0
- package/dist/modules/sale_backend/order-collection.js.map +1 -0
- package/dist/modules/sale_backend/screens/invoicing-policies-list.d.ts +1 -1
- package/dist/modules/sale_backend/screens/invoicing-policies-list.d.ts.map +1 -1
- package/dist/modules/sale_backend/screens/invoicing-policies-list.js +10 -14
- package/dist/modules/sale_backend/screens/invoicing-policies-list.js.map +1 -1
- package/dist/modules/sale_backend/screens/invoicing-policy-create.d.ts.map +1 -1
- package/dist/modules/sale_backend/screens/invoicing-policy-create.js +1 -1
- package/dist/modules/sale_backend/screens/invoicing-policy-create.js.map +1 -1
- package/dist/modules/sale_backend/screens/order-detail.d.ts.map +1 -1
- package/dist/modules/sale_backend/screens/order-detail.js +3 -1
- package/dist/modules/sale_backend/screens/order-detail.js.map +1 -1
- package/dist/modules/sale_backend/screens/overview.d.ts +3 -0
- package/dist/modules/sale_backend/screens/overview.d.ts.map +1 -1
- package/dist/modules/sale_backend/screens/overview.js +24 -16
- package/dist/modules/sale_backend/screens/overview.js.map +1 -1
- package/dist/modules/sale_backend/screens/quotation-create.d.ts.map +1 -1
- package/dist/modules/sale_backend/screens/quotation-create.js +1 -1
- package/dist/modules/sale_backend/screens/quotation-create.js.map +1 -1
- package/dist/modules/sale_backend/screens/quotations-list.d.ts +1 -1
- package/dist/modules/sale_backend/screens/quotations-list.d.ts.map +1 -1
- package/dist/modules/sale_backend/screens/quotations-list.js +12 -23
- package/dist/modules/sale_backend/screens/quotations-list.js.map +1 -1
- package/dist/modules/sale_backend/screens/sales-orders-list.d.ts.map +1 -1
- package/dist/modules/sale_backend/screens/sales-orders-list.js +11 -20
- package/dist/modules/sale_backend/screens/sales-orders-list.js.map +1 -1
- package/dist/modules/sale_backend/search-functions.d.ts +4 -0
- package/dist/modules/sale_backend/search-functions.d.ts.map +1 -0
- package/dist/modules/sale_backend/search-functions.js +9 -0
- package/dist/modules/sale_backend/search-functions.js.map +1 -0
- package/dist/modules/sale_backend/search.d.ts +146 -0
- package/dist/modules/sale_backend/search.d.ts.map +1 -0
- package/dist/modules/sale_backend/search.js +112 -0
- package/dist/modules/sale_backend/search.js.map +1 -0
- package/dist/modules/stock/functions/inventory-preview.d.ts +4 -0
- package/dist/modules/stock/functions/inventory-preview.d.ts.map +1 -0
- package/dist/modules/stock/functions/inventory-preview.js +68 -0
- package/dist/modules/stock/functions/inventory-preview.js.map +1 -0
- package/dist/modules/stock/index.d.ts.map +1 -1
- package/dist/modules/stock/index.js +14 -1
- package/dist/modules/stock/index.js.map +1 -1
- package/dist/modules/stock_backend/client/inventory-count-modal.d.mts +60 -0
- package/dist/modules/stock_backend/client/inventory-count-modal.d.mts.map +1 -0
- package/dist/modules/stock_backend/client/inventory-count-modal.mjs +2 -0
- package/dist/modules/stock_backend/client/inventory-count-modal.mjs.map +7 -0
- package/dist/modules/stock_backend/client/stock-configuration-modal.d.mts +105 -0
- package/dist/modules/stock_backend/client/stock-configuration-modal.d.mts.map +1 -0
- package/dist/modules/stock_backend/client/stock-configuration-modal.mjs +2 -0
- package/dist/modules/stock_backend/client/stock-configuration-modal.mjs.map +7 -0
- package/dist/modules/stock_backend/client/stock-transfer-modal.d.mts +130 -0
- package/dist/modules/stock_backend/client/stock-transfer-modal.d.mts.map +1 -0
- package/dist/modules/stock_backend/client/stock-transfer-modal.mjs +2 -0
- package/dist/modules/stock_backend/client/stock-transfer-modal.mjs.map +7 -0
- package/dist/modules/stock_backend/functions/index.d.ts +10 -0
- package/dist/modules/stock_backend/functions/index.d.ts.map +1 -0
- package/dist/modules/stock_backend/functions/index.js +20 -0
- package/dist/modules/stock_backend/functions/index.js.map +1 -0
- package/dist/modules/stock_backend/functions.d.ts +2 -0
- package/dist/modules/stock_backend/functions.d.ts.map +1 -0
- package/dist/modules/stock_backend/functions.js +2 -0
- package/dist/modules/stock_backend/functions.js.map +1 -0
- package/dist/modules/stock_backend/index.js +85 -10
- package/dist/modules/stock_backend/index.js.map +1 -1
- package/dist/modules/stock_backend/islands.d.ts +8 -0
- package/dist/modules/stock_backend/islands.d.ts.map +1 -1
- package/dist/modules/stock_backend/islands.js +41 -0
- package/dist/modules/stock_backend/islands.js.map +1 -1
- package/dist/modules/stock_backend/modal/configuration-context.d.ts +3 -0
- package/dist/modules/stock_backend/modal/configuration-context.d.ts.map +1 -0
- package/dist/modules/stock_backend/modal/configuration-context.js +65 -0
- package/dist/modules/stock_backend/modal/configuration-context.js.map +1 -0
- package/dist/modules/stock_backend/modal/configuration-view.d.ts +21 -0
- package/dist/modules/stock_backend/modal/configuration-view.d.ts.map +1 -0
- package/dist/modules/stock_backend/modal/configuration-view.js +201 -0
- package/dist/modules/stock_backend/modal/configuration-view.js.map +1 -0
- package/dist/modules/stock_backend/modal/configuration.d.ts +20 -0
- package/dist/modules/stock_backend/modal/configuration.d.ts.map +1 -0
- package/dist/modules/stock_backend/modal/configuration.js +117 -0
- package/dist/modules/stock_backend/modal/configuration.js.map +1 -0
- package/dist/modules/stock_backend/modal/inventory-context.d.ts +3 -0
- package/dist/modules/stock_backend/modal/inventory-context.d.ts.map +1 -0
- package/dist/modules/stock_backend/modal/inventory-context.js +37 -0
- package/dist/modules/stock_backend/modal/inventory-context.js.map +1 -0
- package/dist/modules/stock_backend/modal/inventory-view.d.ts +14 -0
- package/dist/modules/stock_backend/modal/inventory-view.d.ts.map +1 -0
- package/dist/modules/stock_backend/modal/inventory-view.js +142 -0
- package/dist/modules/stock_backend/modal/inventory-view.js.map +1 -0
- package/dist/modules/stock_backend/modal/transfer-context.d.ts +3 -0
- package/dist/modules/stock_backend/modal/transfer-context.d.ts.map +1 -0
- package/dist/modules/stock_backend/modal/transfer-context.js +54 -0
- package/dist/modules/stock_backend/modal/transfer-context.js.map +1 -0
- package/dist/modules/stock_backend/modal/transfer-view.d.ts +15 -0
- package/dist/modules/stock_backend/modal/transfer-view.d.ts.map +1 -0
- package/dist/modules/stock_backend/modal/transfer-view.js +252 -0
- package/dist/modules/stock_backend/modal/transfer-view.js.map +1 -0
- package/dist/modules/stock_backend/routes.d.ts.map +1 -1
- package/dist/modules/stock_backend/routes.js +274 -82
- package/dist/modules/stock_backend/routes.js.map +1 -1
- package/dist/modules/stock_backend/screens/forecast.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/forecast.js +5 -32
- package/dist/modules/stock_backend/screens/forecast.js.map +1 -1
- package/dist/modules/stock_backend/screens/inventory.d.ts +3 -1
- package/dist/modules/stock_backend/screens/inventory.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/inventory.js +27 -74
- package/dist/modules/stock_backend/screens/inventory.js.map +1 -1
- package/dist/modules/stock_backend/screens/location-create.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/location-create.js +1 -1
- package/dist/modules/stock_backend/screens/location-create.js.map +1 -1
- package/dist/modules/stock_backend/screens/locations-list.d.ts +1 -1
- package/dist/modules/stock_backend/screens/locations-list.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/locations-list.js +11 -22
- package/dist/modules/stock_backend/screens/locations-list.js.map +1 -1
- package/dist/modules/stock_backend/screens/lot-create.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/lot-create.js +1 -1
- package/dist/modules/stock_backend/screens/lot-create.js.map +1 -1
- package/dist/modules/stock_backend/screens/lot-detail.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/lot-detail.js +3 -2
- package/dist/modules/stock_backend/screens/lot-detail.js.map +1 -1
- package/dist/modules/stock_backend/screens/lots-list.d.ts +1 -1
- package/dist/modules/stock_backend/screens/lots-list.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/lots-list.js +11 -13
- package/dist/modules/stock_backend/screens/lots-list.js.map +1 -1
- package/dist/modules/stock_backend/screens/overview.d.ts +9 -0
- package/dist/modules/stock_backend/screens/overview.d.ts.map +1 -0
- package/dist/modules/stock_backend/screens/overview.js +38 -0
- package/dist/modules/stock_backend/screens/overview.js.map +1 -0
- package/dist/modules/stock_backend/screens/picking-type-create.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/picking-type-create.js +1 -1
- package/dist/modules/stock_backend/screens/picking-type-create.js.map +1 -1
- package/dist/modules/stock_backend/screens/picking-types-list.d.ts +1 -1
- package/dist/modules/stock_backend/screens/picking-types-list.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/picking-types-list.js +11 -22
- package/dist/modules/stock_backend/screens/picking-types-list.js.map +1 -1
- package/dist/modules/stock_backend/screens/replenishment-create.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/replenishment-create.js +1 -1
- package/dist/modules/stock_backend/screens/replenishment-create.js.map +1 -1
- package/dist/modules/stock_backend/screens/replenishment-list.d.ts +2 -2
- package/dist/modules/stock_backend/screens/replenishment-list.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/replenishment-list.js +12 -23
- package/dist/modules/stock_backend/screens/replenishment-list.js.map +1 -1
- package/dist/modules/stock_backend/screens/stock-route-create.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/stock-route-create.js +1 -1
- package/dist/modules/stock_backend/screens/stock-route-create.js.map +1 -1
- package/dist/modules/stock_backend/screens/stock-route-detail.js +2 -1
- package/dist/modules/stock_backend/screens/stock-route-detail.js.map +1 -1
- package/dist/modules/stock_backend/screens/stock-routes-list.d.ts +1 -1
- package/dist/modules/stock_backend/screens/stock-routes-list.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/stock-routes-list.js +12 -23
- package/dist/modules/stock_backend/screens/stock-routes-list.js.map +1 -1
- package/dist/modules/stock_backend/screens/transfer-create.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/transfer-create.js +1 -1
- package/dist/modules/stock_backend/screens/transfer-create.js.map +1 -1
- package/dist/modules/stock_backend/screens/transfer-detail.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/transfer-detail.js +3 -2
- package/dist/modules/stock_backend/screens/transfer-detail.js.map +1 -1
- package/dist/modules/stock_backend/screens/transfers-list.d.ts +1 -1
- package/dist/modules/stock_backend/screens/transfers-list.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/transfers-list.js +12 -23
- package/dist/modules/stock_backend/screens/transfers-list.js.map +1 -1
- package/dist/modules/stock_backend/screens/warehouse-create.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/warehouse-create.js +1 -1
- package/dist/modules/stock_backend/screens/warehouse-create.js.map +1 -1
- package/dist/modules/stock_backend/screens/warehouses-list.d.ts +1 -1
- package/dist/modules/stock_backend/screens/warehouses-list.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/warehouses-list.js +11 -22
- package/dist/modules/stock_backend/screens/warehouses-list.js.map +1 -1
- package/dist/modules/stock_backend/search.d.ts +286 -0
- package/dist/modules/stock_backend/search.d.ts.map +1 -0
- package/dist/modules/stock_backend/search.js +371 -0
- package/dist/modules/stock_backend/search.js.map +1 -0
- package/dist/modules/storage/functions.d.ts.map +1 -1
- package/dist/modules/storage/functions.js +28 -5
- package/dist/modules/storage/functions.js.map +1 -1
- package/dist/modules/storage/jobs.d.ts.map +1 -1
- package/dist/modules/storage/jobs.js +120 -2
- package/dist/modules/storage/jobs.js.map +1 -1
- package/dist/modules/storage/models.d.ts.map +1 -1
- package/dist/modules/storage/models.js +21 -0
- package/dist/modules/storage/models.js.map +1 -1
- package/dist/modules/storage/policy.d.ts +27 -0
- package/dist/modules/storage/policy.d.ts.map +1 -1
- package/dist/modules/storage/policy.js +14 -0
- package/dist/modules/storage/policy.js.map +1 -1
- package/dist/modules/storage/routes.d.ts +1 -0
- package/dist/modules/storage/routes.d.ts.map +1 -1
- package/dist/modules/storage/routes.js +36 -10
- package/dist/modules/storage/routes.js.map +1 -1
- package/dist/modules/user/access-denial.d.ts +3 -0
- package/dist/modules/user/access-denial.d.ts.map +1 -0
- package/dist/modules/user/access-denial.js +34 -0
- package/dist/modules/user/access-denial.js.map +1 -0
- package/dist/modules/user/access-policy.d.ts +16 -0
- package/dist/modules/user/access-policy.d.ts.map +1 -0
- package/dist/modules/user/access-policy.js +455 -0
- package/dist/modules/user/access-policy.js.map +1 -0
- package/dist/modules/user/access-surfaces.d.ts +26 -0
- package/dist/modules/user/access-surfaces.d.ts.map +1 -0
- package/dist/modules/user/access-surfaces.js +76 -0
- package/dist/modules/user/access-surfaces.js.map +1 -0
- package/dist/modules/user/authorization.d.ts +12 -1
- package/dist/modules/user/authorization.d.ts.map +1 -1
- package/dist/modules/user/authorization.js +342 -36
- package/dist/modules/user/authorization.js.map +1 -1
- package/dist/modules/user/functions.d.ts.map +1 -1
- package/dist/modules/user/functions.js +26 -3
- package/dist/modules/user/functions.js.map +1 -1
- package/dist/modules/user/index.d.ts.map +1 -1
- package/dist/modules/user/index.js +13 -1
- package/dist/modules/user/index.js.map +1 -1
- package/dist/modules/user/modal-labels.d.ts +3 -0
- package/dist/modules/user/modal-labels.d.ts.map +1 -0
- package/dist/modules/user/modal-labels.js +30 -0
- package/dist/modules/user/modal-labels.js.map +1 -0
- package/dist/modules/user/modal-messages.d.ts +4 -0
- package/dist/modules/user/modal-messages.d.ts.map +1 -0
- package/dist/modules/user/modal-messages.js +15 -0
- package/dist/modules/user/modal-messages.js.map +1 -0
- package/dist/modules/user/models.d.ts.map +1 -1
- package/dist/modules/user/models.js +48 -0
- package/dist/modules/user/models.js.map +1 -1
- package/dist/modules/user/permission-areas.d.ts +20 -0
- package/dist/modules/user/permission-areas.d.ts.map +1 -0
- package/dist/modules/user/permission-areas.js +89 -0
- package/dist/modules/user/permission-areas.js.map +1 -0
- package/dist/modules/user/policy-jobs.d.ts +4 -0
- package/dist/modules/user/policy-jobs.d.ts.map +1 -0
- package/dist/modules/user/policy-jobs.js +15 -0
- package/dist/modules/user/policy-jobs.js.map +1 -0
- package/dist/modules/user/role-modal-context.d.ts +3 -0
- package/dist/modules/user/role-modal-context.d.ts.map +1 -0
- package/dist/modules/user/role-modal-context.js +264 -0
- package/dist/modules/user/role-modal-context.js.map +1 -0
- package/dist/modules/user/roles.d.ts.map +1 -1
- package/dist/modules/user/roles.js +2 -0
- package/dist/modules/user/roles.js.map +1 -1
- package/dist/modules/user/user-modal-context.d.ts +5 -1
- package/dist/modules/user/user-modal-context.d.ts.map +1 -1
- package/dist/modules/user/user-modal-context.js +227 -31
- package/dist/modules/user/user-modal-context.js.map +1 -1
- package/dist/modules/user_backend/access-options.d.ts +25 -0
- package/dist/modules/user_backend/access-options.d.ts.map +1 -0
- package/dist/modules/user_backend/access-options.js +38 -0
- package/dist/modules/user_backend/access-options.js.map +1 -0
- package/dist/modules/user_backend/access-policy-routes.d.ts +3 -0
- package/dist/modules/user_backend/access-policy-routes.d.ts.map +1 -0
- package/dist/modules/user_backend/access-policy-routes.js +76 -0
- package/dist/modules/user_backend/access-policy-routes.js.map +1 -0
- package/dist/modules/user_backend/client/access-policy-modal.d.mts +123 -0
- package/dist/modules/user_backend/client/access-policy-modal.d.mts.map +1 -0
- package/dist/modules/user_backend/client/access-policy-modal.mjs +2 -0
- package/dist/modules/user_backend/client/access-policy-modal.mjs.map +7 -0
- package/dist/modules/user_backend/client/role-modal.d.mts +103 -0
- package/dist/modules/user_backend/client/role-modal.d.mts.map +1 -0
- package/dist/modules/user_backend/client/role-modal.mjs +2 -0
- package/dist/modules/user_backend/client/role-modal.mjs.map +7 -0
- package/dist/modules/user_backend/client/user-modal.d.mts +160 -7
- package/dist/modules/user_backend/client/user-modal.d.mts.map +1 -1
- package/dist/modules/user_backend/client/user-modal.mjs +1 -1
- package/dist/modules/user_backend/client/user-modal.mjs.map +4 -4
- package/dist/modules/user_backend/index.d.ts +2 -2
- package/dist/modules/user_backend/index.d.ts.map +1 -1
- package/dist/modules/user_backend/index.js +4 -2
- package/dist/modules/user_backend/index.js.map +1 -1
- package/dist/modules/user_backend/islands.d.ts +14 -1
- package/dist/modules/user_backend/islands.d.ts.map +1 -1
- package/dist/modules/user_backend/islands.js +11 -3
- package/dist/modules/user_backend/islands.js.map +1 -1
- package/dist/modules/user_backend/managed-role-routes.d.ts +3 -0
- package/dist/modules/user_backend/managed-role-routes.d.ts.map +1 -0
- package/dist/modules/user_backend/managed-role-routes.js +44 -0
- package/dist/modules/user_backend/managed-role-routes.js.map +1 -0
- package/dist/modules/user_backend/menus.d.ts.map +1 -1
- package/dist/modules/user_backend/menus.js +9 -7
- package/dist/modules/user_backend/menus.js.map +1 -1
- package/dist/modules/user_backend/messages.d.ts +508 -6
- package/dist/modules/user_backend/messages.d.ts.map +1 -1
- package/dist/modules/user_backend/messages.js +546 -44
- package/dist/modules/user_backend/messages.js.map +1 -1
- package/dist/modules/user_backend/modal/access-policy-modal-view.d.ts +43 -0
- package/dist/modules/user_backend/modal/access-policy-modal-view.d.ts.map +1 -0
- package/dist/modules/user_backend/modal/access-policy-modal-view.js +384 -0
- package/dist/modules/user_backend/modal/access-policy-modal-view.js.map +1 -0
- package/dist/modules/user_backend/modal/access-surfaces.d.ts +51 -0
- package/dist/modules/user_backend/modal/access-surfaces.d.ts.map +1 -0
- package/dist/modules/user_backend/modal/access-surfaces.js +138 -0
- package/dist/modules/user_backend/modal/access-surfaces.js.map +1 -0
- package/dist/modules/user_backend/modal/role-modal-view.d.ts +48 -0
- package/dist/modules/user_backend/modal/role-modal-view.d.ts.map +1 -0
- package/dist/modules/user_backend/modal/role-modal-view.js +488 -0
- package/dist/modules/user_backend/modal/role-modal-view.js.map +1 -0
- package/dist/modules/user_backend/modal/user-modal-view.d.ts +45 -0
- package/dist/modules/user_backend/modal/user-modal-view.d.ts.map +1 -1
- package/dist/modules/user_backend/modal/user-modal-view.js +1317 -71
- package/dist/modules/user_backend/modal/user-modal-view.js.map +1 -1
- package/dist/modules/user_backend/routes.d.ts.map +1 -1
- package/dist/modules/user_backend/routes.js +109 -571
- package/dist/modules/user_backend/routes.js.map +1 -1
- package/dist/modules/user_backend/screens/access-policies-list.d.ts +31 -0
- package/dist/modules/user_backend/screens/access-policies-list.d.ts.map +1 -0
- package/dist/modules/user_backend/screens/access-policies-list.js +54 -0
- package/dist/modules/user_backend/screens/access-policies-list.js.map +1 -0
- package/dist/modules/user_backend/screens/index.d.ts +1 -3
- package/dist/modules/user_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/user_backend/screens/index.js +2 -3
- package/dist/modules/user_backend/screens/index.js.map +1 -1
- package/dist/modules/user_backend/screens/profile-form.d.ts.map +1 -1
- package/dist/modules/user_backend/screens/profile-form.js +1 -1
- package/dist/modules/user_backend/screens/profile-form.js.map +1 -1
- package/dist/modules/user_backend/screens/roles-list.d.ts +9 -3
- package/dist/modules/user_backend/screens/roles-list.d.ts.map +1 -1
- package/dist/modules/user_backend/screens/roles-list.js +15 -23
- package/dist/modules/user_backend/screens/roles-list.js.map +1 -1
- package/dist/modules/user_backend/screens/users-list.d.ts +7 -5
- package/dist/modules/user_backend/screens/users-list.d.ts.map +1 -1
- package/dist/modules/user_backend/screens/users-list.js +29 -21
- package/dist/modules/user_backend/screens/users-list.js.map +1 -1
- package/dist/modules/user_backend/search-functions.d.ts +4 -0
- package/dist/modules/user_backend/search-functions.d.ts.map +1 -0
- package/dist/modules/user_backend/search-functions.js +13 -0
- package/dist/modules/user_backend/search-functions.js.map +1 -0
- package/dist/modules/user_backend/search.d.ts +128 -0
- package/dist/modules/user_backend/search.d.ts.map +1 -0
- package/dist/modules/user_backend/search.js +131 -0
- package/dist/modules/user_backend/search.js.map +1 -0
- package/dist/modules/website/cms.d.ts.map +1 -1
- package/dist/modules/website/cms.js +17 -7
- package/dist/modules/website/cms.js.map +1 -1
- package/dist/modules/website/customer.d.ts +61 -1
- package/dist/modules/website/customer.d.ts.map +1 -1
- package/dist/modules/website/customer.js +197 -15
- package/dist/modules/website/customer.js.map +1 -1
- package/dist/modules/website/index.d.ts.map +1 -1
- package/dist/modules/website/index.js +16 -2
- package/dist/modules/website/index.js.map +1 -1
- package/dist/modules/website/models.d.ts.map +1 -1
- package/dist/modules/website/models.js +15 -0
- package/dist/modules/website/models.js.map +1 -1
- package/dist/modules/website/publication.d.ts.map +1 -1
- package/dist/modules/website/publication.js +11 -5
- package/dist/modules/website/publication.js.map +1 -1
- package/dist/modules/website_backend/collection.d.ts +12 -0
- package/dist/modules/website_backend/collection.d.ts.map +1 -0
- package/dist/modules/website_backend/collection.js +17 -0
- package/dist/modules/website_backend/collection.js.map +1 -0
- package/dist/modules/website_backend/functions.d.ts +10 -0
- package/dist/modules/website_backend/functions.d.ts.map +1 -0
- package/dist/modules/website_backend/functions.js +37 -0
- package/dist/modules/website_backend/functions.js.map +1 -0
- package/dist/modules/website_backend/index.js +2 -0
- package/dist/modules/website_backend/index.js.map +1 -1
- package/dist/modules/website_backend/routes.d.ts.map +1 -1
- package/dist/modules/website_backend/routes.js +124 -29
- package/dist/modules/website_backend/routes.js.map +1 -1
- package/dist/modules/website_backend/screens/index.d.ts +4 -8
- package/dist/modules/website_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/website_backend/screens/index.js +685 -666
- package/dist/modules/website_backend/screens/index.js.map +1 -1
- package/dist/modules/website_backend/screens/page-frame.d.ts +7 -5
- package/dist/modules/website_backend/screens/page-frame.d.ts.map +1 -1
- package/dist/modules/website_backend/screens/page-frame.js +3 -3
- package/dist/modules/website_backend/screens/page-frame.js.map +1 -1
- package/dist/modules/website_backend/search.d.ts +125 -0
- package/dist/modules/website_backend/search.d.ts.map +1 -0
- package/dist/modules/website_backend/search.js +135 -0
- package/dist/modules/website_backend/search.js.map +1 -0
- package/dist/permission-catalogue.d.ts.map +1 -1
- package/dist/permission-catalogue.js +180 -24
- package/dist/permission-catalogue.js.map +1 -1
- package/dist/phone.d.ts +32 -0
- package/dist/phone.d.ts.map +1 -0
- package/dist/phone.js +101 -0
- package/dist/phone.js.map +1 -0
- package/dist/role-templates.d.ts +79 -0
- package/dist/role-templates.d.ts.map +1 -0
- package/dist/role-templates.js +212 -0
- package/dist/role-templates.js.map +1 -0
- package/dist/scaffold/index.d.ts +1 -1
- package/dist/scaffold/index.d.ts.map +1 -1
- package/dist/scaffold/index.js +10 -5
- package/dist/scaffold/index.js.map +1 -1
- package/dist/scaffold/templates/README.md.tmpl +2 -1
- package/dist/scaffold/templates/ket.workspace.mjs.tmpl +4 -1
- package/dist/scaffold/templates/package.json.tmpl +2 -2
- package/dist/ui/actions.d.ts +2 -0
- package/dist/ui/actions.d.ts.map +1 -1
- package/dist/ui/actions.js +1 -1
- package/dist/ui/actions.js.map +1 -1
- package/dist/ui/chrome.d.ts +19 -6
- package/dist/ui/chrome.d.ts.map +1 -1
- package/dist/ui/chrome.js +12 -7
- package/dist/ui/chrome.js.map +1 -1
- package/dist/ui/client/bulk-selection.d.ts +7 -0
- package/dist/ui/client/bulk-selection.d.ts.map +1 -0
- package/dist/ui/client/bulk-selection.js +67 -0
- package/dist/ui/client/bulk-selection.js.map +1 -0
- package/dist/ui/client/ket-table-view.d.ts +3 -0
- package/dist/ui/client/ket-table-view.d.ts.map +1 -0
- package/dist/ui/client/ket-table-view.js +27 -0
- package/dist/ui/client/ket-table-view.js.map +1 -0
- package/dist/ui/client/live-doc-shell.d.ts +66 -2
- package/dist/ui/client/live-doc-shell.d.ts.map +1 -1
- package/dist/ui/client/live-doc-shell.js +83 -6
- package/dist/ui/client/live-doc-shell.js.map +1 -1
- package/dist/ui/client/live-doc-tail.d.ts +4 -0
- package/dist/ui/client/live-doc-tail.d.ts.map +1 -0
- package/dist/ui/client/live-doc-tail.js +21 -0
- package/dist/ui/client/live-doc-tail.js.map +1 -0
- package/dist/ui/client/live-doc-view.d.ts +47 -1
- package/dist/ui/client/live-doc-view.d.ts.map +1 -1
- package/dist/ui/client/live-doc-view.js +318 -5
- package/dist/ui/client/live-doc-view.js.map +1 -1
- package/dist/ui/client/live-doc.css +81 -0
- package/dist/ui/client/live-doc.d.mts +11 -4
- package/dist/ui/client/live-doc.d.mts.map +1 -1
- package/dist/ui/client/live-doc.mjs +14 -10
- package/dist/ui/client/live-doc.mjs.map +4 -4
- package/dist/ui/client/record-modal-form.d.ts +34 -0
- package/dist/ui/client/record-modal-form.d.ts.map +1 -1
- package/dist/ui/client/record-modal-form.js +17 -1
- package/dist/ui/client/record-modal-form.js.map +1 -1
- package/dist/ui/client/record-modal.d.ts +56 -6
- package/dist/ui/client/record-modal.d.ts.map +1 -1
- package/dist/ui/client/record-modal.js +235 -39
- package/dist/ui/client/record-modal.js.map +1 -1
- package/dist/ui/client/search-filter-view.d.ts +2 -0
- package/dist/ui/client/search-filter-view.d.ts.map +1 -0
- package/dist/ui/client/search-filter-view.js +6 -0
- package/dist/ui/client/search-filter-view.js.map +1 -0
- package/dist/ui/client/table-selection-view.d.ts.map +1 -1
- package/dist/ui/client/table-selection-view.js +33 -16
- package/dist/ui/client/table-selection-view.js.map +1 -1
- package/dist/ui/client/user-modal-client.d.ts +6 -0
- package/dist/ui/client/user-modal-client.d.ts.map +1 -0
- package/dist/ui/client/user-modal-client.js +4 -0
- package/dist/ui/client/user-modal-client.js.map +1 -0
- package/dist/ui/client/user-workflow.d.ts.map +1 -1
- package/dist/ui/client/user-workflow.js +4 -3
- package/dist/ui/client/user-workflow.js.map +1 -1
- package/dist/ui/client/variant-attribute-create.d.ts +19 -0
- package/dist/ui/client/variant-attribute-create.d.ts.map +1 -0
- package/dist/ui/client/variant-attribute-create.js +177 -0
- package/dist/ui/client/variant-attribute-create.js.map +1 -0
- package/dist/ui/client/variant-editor-view.d.ts +72 -0
- package/dist/ui/client/variant-editor-view.d.ts.map +1 -0
- package/dist/ui/client/variant-editor-view.js +943 -0
- package/dist/ui/client/variant-editor-view.js.map +1 -0
- package/dist/ui/collection-state.d.ts +11 -0
- package/dist/ui/collection-state.d.ts.map +1 -0
- package/dist/ui/collection-state.js +45 -0
- package/dist/ui/collection-state.js.map +1 -0
- package/dist/ui/collection.d.ts +23 -0
- package/dist/ui/collection.d.ts.map +1 -0
- package/dist/ui/collection.js +118 -0
- package/dist/ui/collection.js.map +1 -0
- package/dist/ui/form.d.ts +1 -1
- package/dist/ui/form.d.ts.map +1 -1
- package/dist/ui/form.js +1 -1
- package/dist/ui/form.js.map +1 -1
- package/dist/ui/index.d.ts +3 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +3 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/layout.d.ts +8 -9
- package/dist/ui/layout.d.ts.map +1 -1
- package/dist/ui/layout.js +27 -12
- package/dist/ui/layout.js.map +1 -1
- package/dist/ui/list-page.d.ts +4 -2
- package/dist/ui/list-page.d.ts.map +1 -1
- package/dist/ui/list-page.js +29 -4
- package/dist/ui/list-page.js.map +1 -1
- package/dist/ui/nav.d.ts +2 -3
- package/dist/ui/nav.d.ts.map +1 -1
- package/dist/ui/nav.js +4 -8
- package/dist/ui/nav.js.map +1 -1
- package/dist/ui/navigation.d.ts +1 -0
- package/dist/ui/navigation.d.ts.map +1 -1
- package/dist/ui/navigation.js +3 -3
- package/dist/ui/navigation.js.map +1 -1
- package/dist/ui/table.d.ts +2 -0
- package/dist/ui/table.d.ts.map +1 -1
- package/dist/ui/table.js +24 -0
- package/dist/ui/table.js.map +1 -1
- package/dist/ui/user-workflow.d.ts +2 -1
- package/dist/ui/user-workflow.d.ts.map +1 -1
- package/dist/ui/user-workflow.js +44 -29
- package/dist/ui/user-workflow.js.map +1 -1
- package/package.json +14 -4
- package/dist/modules/flow/functions.d.ts.map +0 -1
- package/dist/modules/flow/functions.js +0 -1876
- package/dist/modules/flow/functions.js.map +0 -1
- package/dist/modules/flow/operations.d.ts +0 -414
- package/dist/modules/flow/operations.d.ts.map +0 -1
- package/dist/modules/flow/operations.js +0 -1671
- package/dist/modules/flow/operations.js.map +0 -1
- package/dist/modules/user_backend/access-routes.d.ts +0 -31
- package/dist/modules/user_backend/access-routes.d.ts.map +0 -1
- package/dist/modules/user_backend/access-routes.js +0 -447
- package/dist/modules/user_backend/access-routes.js.map +0 -1
- package/dist/modules/user_backend/screens/presets-form.d.ts +0 -16
- package/dist/modules/user_backend/screens/presets-form.d.ts.map +0 -1
- package/dist/modules/user_backend/screens/presets-form.js +0 -37
- package/dist/modules/user_backend/screens/presets-form.js.map +0 -1
- package/dist/modules/user_backend/screens/role-form.d.ts +0 -21
- package/dist/modules/user_backend/screens/role-form.d.ts.map +0 -1
- package/dist/modules/user_backend/screens/role-form.js +0 -93
- package/dist/modules/user_backend/screens/role-form.js.map +0 -1
- package/dist/modules/user_backend/screens/user-form.d.ts +0 -56
- package/dist/modules/user_backend/screens/user-form.d.ts.map +0 -1
- package/dist/modules/user_backend/screens/user-form.js +0 -202
- package/dist/modules/user_backend/screens/user-form.js.map +0 -1
- package/dist/ui/client/relation-select-view.d.ts +0 -77
- package/dist/ui/client/relation-select-view.d.ts.map +0 -1
- package/dist/ui/client/relation-select-view.js +0 -216
- package/dist/ui/client/relation-select-view.js.map +0 -1
- /package/dist/modules/{flow → product_backend}/functions.d.ts +0 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../ui/client/variant-editor-view.tsx", "../../../../../design-system/src/primitives/actions/index.tsx", "../../../../../design-system/src/primitives/status/index.tsx", "../../../../../design-system/src/primitives/feedback/index.tsx", "../../../../../design-system/src/primitives/field/index.tsx", "../../../../../design-system/src/layouts/layout/index.tsx", "../../../../../design-system/src/patterns/modal-sheet/index.tsx", "../../../../../design-system/src/layouts/page-identity/index.tsx", "../../../../../design-system/src/layouts/shell/index.tsx", "../../../../../design-system/src/primitives/field/native-control.tsx", "../../../../../design-system/src/primitives/navigation/index.tsx", "../../../../../design-system/src/primitives/progress/index.tsx", "../../../../../design-system/src/interactions/menu/index.tsx", "../../../../../design-system/src/interactions/popover/index.tsx", "../../../../../design-system/src/interactions/tooltip/index.tsx", "../../../../../design-system/src/interactions/dialog/index.tsx", "../../../../../design-system/src/interactions/toast/index.tsx", "../../../../../design-system/src/interactions/spinner/index.tsx", "../../../../../design-system/src/interactions/skeleton/index.tsx", "../../../../../design-system/src/interactions/relation-select/index.tsx", "../../../../../design-system/src/interactions/lightbox/index.tsx", "../../../../../design-system/src/interactions/search-filter/index.tsx", "../../../../../design-system/src/interactions/ket-table/index.tsx", "../../../../../design-system/src/record/formatted-values/index.tsx", "../../../../../design-system/src/runtime/index.js", "../../../../../design-system/src/forms/shared.tsx", "../../../../../design-system/src/forms/scalar-fields/index.tsx", "../../../../../design-system/src/forms/combobox/index.tsx", "../../../../../design-system/src/forms/date-time/index.tsx", "../../../../../design-system/src/forms/upload/index.tsx", "../../../../../design-system/src/forms/relation-picker/index.tsx", "../../../../../design-system/src/data-operations/list-controls/index.tsx", "../../../../../design-system/src/data-operations/inline-edit/index.tsx", "../../../../../design-system/src/data-display/resource-list/index.tsx", "../../../../../design-system/src/patterns/list-chrome/index.tsx", "../../../../../design-system/src/data-display/data-grid/index.tsx", "../../../../../design-system/src/data-display/tree/index.tsx", "../../../../../design-system/src/data-display/matrix/index.tsx", "../../../../../design-system/src/data-display/bar-chart/index.tsx", "../../../../../design-system/src/data-operations/timeframe-filter/index.tsx", "../../../../../design-system/src/record/display/index.tsx", "../../../../../design-system/src/record/composition/index.tsx", "../../../../../design-system/src/record/activity/index.tsx", "../../../../../design-system/src/record/media/index.tsx", "../../../../../design-system/src/layouts/app-navigation/index.tsx", "../../../../../design-system/src/patterns/data-table/index.tsx", "../../../../../design-system/src/patterns/list-page/index.tsx", "../../../../../design-system/src/patterns/record-page/index.tsx", "../../../../../design-system/src/patterns/form-page/index.tsx", "../../../../../design-system/src/patterns/workspace-page/index.tsx", "../../../../../design-system/src/patterns/dashboard-page/index.tsx", "../../../../../design-system/src/patterns/board-page/index.tsx", "../../../../../design-system/src/patterns/pipeline/index.tsx", "../../../../../design-system/src/patterns/record-form/index.tsx", "../../../../../design-system/src/interactions/reorder-list/index.tsx", "../../../../../design-system/src/contract/index.ts", "../../../ui/icons.ts", "../../../ui/client/record-modal.tsx", "../../../ui/record-modal.tsx", "../../../ui/client/record-modal-form.tsx", "../../../ui/client/variant-attribute-create.tsx"],
|
|
4
|
+
"sourcesContent": ["// The attributes-and-variants editor, as an island inside the product template modal.\n//\n// One editable copy of the template's setup lives here \u2014 attribute lines with their\n// values and price extras on top, variant rows below \u2014 and nothing reaches the\n// server until \"Save\", which sends the whole copy to `product.saveVariantSetup` in\n// one call. The record modal's own views are render-pure and re-read their context\n// after every command; a setup that is half-edited across two dozen controls needs\n// state that survives a keystroke, which is what an island is for.\n//\n// The tab reads as two steps: pick attributes and tick the values this product is\n// sold in, then turn the missing combinations into variant rows and fill them in.\n// Every control is a design-system one; the island listens once on its root and\n// routes by control name (`values|\u2026`, `extra|\u2026`, `combo|\u2026`, `field|\u2026`) or by the\n// `variantAction` button value, so no control needs its own handler.\n//\n// It lives in the kit because it writes markup (tools/ui-audit.ts); the product\n// module only supplies the setup, the labels and whether the viewer may save.\n\nimport { each, effect, signal } from '@ketvietlab/ketjs-view'\nimport type {\n IslandController,\n IslandElement,\n IslandProps,\n JSXChild,\n TemplateResult,\n} from '@ketvietlab/ketjs-view'\nimport {\n Badge,\n Button,\n CheckboxGroup,\n createLightbox,\n createRelationSelectView,\n Disclosure,\n EmptyState,\n DropZone,\n Field,\n IconButton,\n LightboxThumb,\n MoneyField,\n Notice,\n Section,\n Select,\n TextField,\n} from '@ketvietlab/design-system'\nimport type { LightboxLabels, RelationSelectLabels } from '@ketvietlab/design-system'\nimport { icon } from '../icons.ts'\nimport { callRecordFunction } from './record-modal.tsx'\nimport { createVariantAttributeForm } from './variant-attribute-create.tsx'\n\nexport type VariantEditorValue = { valueId: string; name: string; priceExtra: string }\nexport type VariantEditorLine = {\n attributeId: string\n name: string\n createVariant: string\n displayType: string\n values: VariantEditorValue[]\n}\nexport type VariantEditorAttribute = {\n attributeId: string\n name: string\n createVariant: string\n values: Array<{ valueId: string; name: string }>\n}\nexport type VariantEditorVariant = {\n id: string\n valueIds: Record<string, string>\n defaultCode: string | null\n barcode: string | null\n weight: string\n volume: string\n active: boolean\n /** Primary first. */\n images: VariantEditorImage[]\n}\nexport type VariantEditorImage = {\n mediaId: string\n attachmentId: string\n alt: string | null\n primary: boolean\n}\nexport type VariantEditorSetup = {\n templateId: string\n listPrice: string\n lines: VariantEditorLine[]\n catalogue: VariantEditorAttribute[]\n variants: VariantEditorVariant[]\n}\n\nexport type VariantEditorProps = {\n id: string\n /** The record kind announced in `ket:records-changed` after a save. */\n kind: string\n setup: VariantEditorSetup\n editable: boolean\n /** Separate permission for creating shared catalogue attributes. */\n createAttribute?: boolean\n saveFunction: string\n /** Every label, already translated; `{name}` params are interpolated here. */\n labels: Record<string, string>\n relationLabels: RelationSelectLabels\n /** The image viewer's own labels. */\n lightboxLabels: LightboxLabels\n /** Whether the viewer may add or replace, and remove, a variant's image. */\n media: { upload: boolean; remove: boolean }\n}\n\ntype Row = VariantEditorVariant & { key: string; open: boolean }\ntype RowField = 'defaultCode' | 'barcode' | 'weight' | 'volume'\n\n/** The name every action button shares; its value says which action and on what. */\nconst ACTION = 'variantAction'\n\nconst clone = <T,>(value: T): T => JSON.parse(JSON.stringify(value)) as T\n\nconst label = (labels: Record<string, string>, key: string, params: Record<string, unknown> = {}): string =>\n (labels[key] ?? key).replace(/\\{(\\w+)\\}/g, (_, name: string) => String(params[name] ?? `{${name}}`))\n\n/** The stored file at a rendition size; the original is served until the render job made it. */\nconst imageUrl = (attachmentId: string, size: 'thumb' | 'medium' | 'large'): string =>\n `/files/${encodeURIComponent(attachmentId)}?size=${size}`\n\nconst IMAGE_TYPES = 'image/avif,image/gif,image/jpeg,image/png,image/webp'\n\nconst money = (amount: number): string => (Number.isFinite(amount) ? amount.toLocaleString('vi-VN') : '\u2014')\n\nconst decimal = (value: string): number => {\n const amount = Number(value)\n return Number.isFinite(amount) ? amount : 0\n}\n\nconst isDecimal = (value: string): boolean => /^-?\\d+(\\.\\d+)?$/.test(value.trim())\n\nconst signed = (value: string): string => {\n if (!isDecimal(value)) return value.trim() || '?'\n const amount = decimal(value)\n return `${amount > 0 ? '+' : ''}${money(amount)}`\n}\n\n/** How many combinations the \"missing\" notice names before summing up the rest. */\nconst PREVIEW = 3\n\nlet rowSequence = 0\nconst toRows = (variants: VariantEditorVariant[]): Row[] =>\n variants.map((variant) => ({ ...variant, valueIds: { ...variant.valueIds }, key: variant.id, open: false }))\n\nexport function createVariantEditorView(props: VariantEditorProps): IslandController {\n const labels = props.labels\n const t = (key: string, params?: Record<string, unknown>) => label(labels, key, params)\n const editable = props.editable\n\n let saved = clone(props.setup)\n const listPrice = signal(saved.listPrice)\n const catalogue = signal(saved.catalogue)\n const lines = signal<VariantEditorLine[]>(clone(saved.lines))\n const rows = signal<Row[]>(toRows(saved.variants))\n /** Attribute lines whose price-extra disclosure the user opened. */\n const openExtras = signal<string[]>([])\n const saving = signal(false)\n const problem = signal<string | null>(null)\n const rowIssues = signal<Record<string, string>>({})\n const notice = signal<string | null>(null)\n const dirty = signal(false)\n /** The row whose image is uploading or being removed, and the one a file is dragged over. */\n const imageBusy = signal<string | null>(null)\n const dragOver = signal<string | null>(null)\n const viewer = createLightbox(props.lightboxLabels, props.id)\n let host: HTMLElement | null = null\n let disposed = false\n\n const touch = (): void => {\n dirty.set(true)\n notice.set(null)\n }\n const setLines = (next: VariantEditorLine[]): void => {\n lines.set(next)\n touch()\n }\n const setRows = (next: Row[]): void => {\n rows.set(next)\n touch()\n }\n const updateRow = (key: string, patch: Partial<Row>): void =>\n setRows(rows().map((row) => (row.key === key ? { ...row, ...patch } : row)))\n const rowOf = (key: string): Row | undefined => rows().find((row) => row.key === key)\n\n /** Moves focus once the render that follows a change has landed. */\n const focusSoon = (selector: string, fallback?: string): void => {\n setTimeout(() => {\n if (disposed || !host) return\n const target =\n host.querySelector<HTMLElement>(selector) ??\n (fallback ? host.querySelector<HTMLElement>(fallback) : null)\n target?.focus()\n }, 0)\n }\n const pickerTrigger = `[id=\"${props.id}-attribute-picker\"] button`\n const actionButton = (value: string): string => `button[name=\"${ACTION}\"][value=\"${value}\"]`\n\n // \u2500\u2500 Derived \u2500\u2500\n const variantLines = (): VariantEditorLine[] =>\n lines().filter((line) => line.createVariant !== 'no_variant' && line.values.length > 0)\n\n const combinationOf = (row: Row): string | null => {\n const picked: string[] = []\n for (const line of variantLines()) {\n const valueId = row.valueIds[line.attributeId]\n if (!valueId || !line.values.some((value) => value.valueId === valueId)) return null\n picked.push(valueId)\n }\n return picked.join(',')\n }\n\n /** Rows sharing a combination, by row key \u2192 the other row's position (1-based). */\n const duplicates = (): Map<string, number> => {\n const seen = new Map<string, number>()\n const clash = new Map<string, number>()\n rows().forEach((row, index) => {\n if (!row.active) return\n const combination = combinationOf(row)\n if (combination == null) return\n const first = seen.get(combination)\n if (first === undefined) seen.set(combination, index)\n else {\n clash.set(row.key, first + 1)\n clash.set(rows()[first]!.key, index + 1)\n }\n })\n return clash\n }\n\n const incomplete = (): Set<string> =>\n new Set(\n rows()\n .filter((row) => row.active && combinationOf(row) == null)\n .map((row) => row.key),\n )\n\n const extraOf = (row: Row): Array<{ line: VariantEditorLine; value: VariantEditorValue }> =>\n variantLines().flatMap((line) => {\n const value = line.values.find((entry) => entry.valueId === row.valueIds[line.attributeId])\n return value ? [{ line, value }] : []\n })\n\n const priceOf = (row: Row): number =>\n extraOf(row).reduce((total, { value }) => total + decimal(value.priceExtra), decimal(listPrice()))\n\n const missingCombinations = (): Array<Record<string, string>> => {\n const groups = variantLines()\n if (!groups.length) return []\n const all = groups.reduce<Array<Record<string, string>>>(\n (acc, line) =>\n acc.flatMap((prefix) =>\n line.values.map((value) => ({ ...prefix, [line.attributeId]: value.valueId })),\n ),\n [{}],\n )\n const taken = new Set(\n rows()\n .filter((row) => row.active)\n .map(combinationOf)\n .filter((key): key is string => key != null),\n )\n return all.filter((valueIds) => !taken.has(groups.map((line) => valueIds[line.attributeId]).join(',')))\n }\n\n /** A combination as it reads in a row title: \"\u0110\u1ECF / 50 ml\", or what is still to choose. */\n const combinationLabel = (valueIds: Record<string, string>): string =>\n variantLines().length\n ? variantLines()\n .map(\n (line) =>\n line.values.find((value) => value.valueId === valueIds[line.attributeId])?.name ??\n t('choose', { name: line.name }),\n )\n .join(' / ')\n : t('defaultVariant')\n\n const usageOf = (attributeId: string, valueId: string): number =>\n rows().filter((row) => row.active && row.valueIds[attributeId] === valueId).length\n\n const invalidExtras = (): boolean =>\n lines().some((line) => line.values.some((value) => !isDecimal(value.priceExtra)))\n\n const rowInvalid = (row: Row): boolean => !isDecimal(row.weight) || !isDecimal(row.volume)\n const invalidRows = (): boolean => rows().some(rowInvalid)\n\n const blocked = (): boolean =>\n !editable ||\n saving() ||\n attributeForm.open() ||\n !dirty() ||\n duplicates().size > 0 ||\n incomplete().size > 0 ||\n invalidExtras() ||\n invalidRows()\n\n /** Every value the line could hold: the catalogue's, then any the line keeps that it no longer lists. */\n const optionsOf = (line: VariantEditorLine): Array<{ valueId: string; name: string }> => {\n const listed = catalogue().find((entry) => entry.attributeId === line.attributeId)?.values ?? []\n return [\n ...listed,\n ...line.values.filter((value) => !listed.some((entry) => entry.valueId === value.valueId)),\n ]\n }\n\n const firstValueOf = (attributeId: string): string => `input[name=\"values|${attributeId}[]\"]`\n\n const attributeForm = createVariantAttributeForm({\n id: props.id,\n kind: props.kind,\n enabled: editable && props.createAttribute === true,\n launcher: false,\n returnFocus: () => host?.querySelector<HTMLElement>(pickerTrigger)?.focus(),\n disabled: saving,\n t,\n added: (attribute) => {\n catalogue.set([...catalogue(), attribute])\n // Reset discards the template draft, not the newly saved shared attribute.\n saved.catalogue = [...saved.catalogue, attribute]\n setLines([\n ...lines(),\n {\n attributeId: attribute.attributeId,\n name: attribute.name,\n createVariant: attribute.createVariant,\n displayType: 'select',\n values: attribute.values.map((value) => ({ ...value, priceExtra: '0' })),\n },\n ])\n // After the form hands focus back to the picker: the next step is this line's values.\n focusSoon(firstValueOf(attribute.attributeId))\n },\n })\n\n // \u2500\u2500 Attribute actions \u2500\u2500\n const addLine = (attributeId: string): void => {\n const attribute = catalogue().find((entry) => entry.attributeId === attributeId)\n if (!attribute) return\n setLines([\n ...lines(),\n {\n attributeId,\n name: attribute.name,\n createVariant: attribute.createVariant,\n displayType: 'select',\n values: [],\n },\n ])\n focusSoon(firstValueOf(attributeId), pickerTrigger)\n }\n\n const attributePicker = createRelationSelectView(\n {\n id: `${props.id}-attribute-picker`,\n config: {\n name: 'addAttributeId',\n ariaLabel: t('addAttribute'),\n options: [],\n labels: { ...props.relationLabels, choose: t('addAttribute'), create: t('createAttribute') },\n },\n },\n {\n options: () =>\n catalogue()\n .filter((attribute) => !lines().some((line) => line.attributeId === attribute.attributeId))\n .map((attribute) => ({\n value: attribute.attributeId,\n label: attribute.name,\n description: attribute.values.map((value) => value.name).join(' \u00B7 '),\n })),\n disabled: () => !editable || saving() || attributeForm.open(),\n resetAfterSelect: true,\n onSelect: (option) => addLine(option.value),\n ...(editable && props.createAttribute\n ? { onCreate: (query: string) => attributeForm.start(query) }\n : {}),\n },\n )\n\n const removeLine = (attributeId: string): void => {\n setLines(lines().filter((line) => line.attributeId !== attributeId))\n setRows(\n rows().map((row) => {\n const valueIds = { ...row.valueIds }\n delete valueIds[attributeId]\n return { ...row, valueIds }\n }),\n )\n openExtras.set(openExtras().filter((id) => id !== attributeId))\n focusSoon(pickerTrigger)\n }\n\n /** Ticks values on a line, in the catalogue's order; a value ticked again gets its saved extra back. */\n const selectValues = (attributeId: string, valueIds: string[]): void => {\n const line = lines().find((entry) => entry.attributeId === attributeId)\n if (!line) return\n const options = optionsOf(line)\n const adding = valueIds.filter((valueId) => !line.values.some((value) => value.valueId === valueId))\n if (!adding.length) return\n const savedLine = saved.lines.find((entry) => entry.attributeId === attributeId)\n const added = adding.flatMap((valueId) => {\n const option = options.find((entry) => entry.valueId === valueId)\n if (!option) return []\n const priceExtra = savedLine?.values.find((value) => value.valueId === valueId)?.priceExtra ?? '0'\n return [{ valueId, name: option.name, priceExtra }]\n })\n const order = (valueId: string) => options.findIndex((entry) => entry.valueId === valueId)\n setLines(\n lines().map((entry) =>\n entry.attributeId === attributeId\n ? {\n ...entry,\n values: [...entry.values, ...added].sort((a, b) => order(a.valueId) - order(b.valueId)),\n }\n : entry,\n ),\n )\n }\n\n const removeValue = (attributeId: string, valueId: string): void =>\n setLines(\n lines().map((line) =>\n line.attributeId === attributeId\n ? { ...line, values: line.values.filter((value) => value.valueId !== valueId) }\n : line,\n ),\n )\n\n const setExtra = (attributeId: string, valueId: string, priceExtra: string): void =>\n setLines(\n lines().map((line) =>\n line.attributeId === attributeId\n ? {\n ...line,\n values: line.values.map((value) =>\n value.valueId === valueId ? { ...value, priceExtra } : value,\n ),\n }\n : line,\n ),\n )\n\n // \u2500\u2500 Variant actions \u2500\u2500\n const newRow = (valueIds: Record<string, string>, open: boolean): Row => ({\n id: '',\n key: `new-${++rowSequence}`,\n valueIds,\n defaultCode: null,\n barcode: null,\n weight: '0',\n volume: '0',\n active: true,\n images: [],\n open,\n })\n\n const addRow = (): void => {\n const row = newRow({}, true)\n setRows([...rows(), row])\n focusSoon(`[name^=\"combo|${row.key}|\"], [name=\"field|${row.key}|defaultCode\"]`)\n }\n\n const generate = (): void => {\n const created = missingCombinations().map((ids) => newRow(ids, false))\n if (!created.length) return\n setRows([...rows(), ...created])\n focusSoon(actionButton(`toggleRow|${created[0]!.key}`))\n }\n\n const toggleRow = (key: string): void => {\n rows.set(rows().map((row) => (row.key === key ? { ...row, open: !row.open } : row)))\n }\n\n const removeRow = (key: string): void => {\n const row = rowOf(key)\n if (!row) return\n // An unsaved row simply goes away; a saved one is archived on save, since it\n // may already be on documents this editor cannot see.\n if (row.id) {\n updateRow(key, { active: !row.active })\n return\n }\n const index = rows().indexOf(row)\n const next = rows()[index + 1] ?? rows()[index - 1]\n setRows(rows().filter((entry) => entry.key !== key))\n focusSoon(next ? actionButton(`toggleRow|${next.key}`) : actionButton('addRow'), actionButton('generate'))\n }\n\n const setAllActive = (active: boolean): void => {\n setRows(\n rows()\n .filter((row) => row.id || active)\n .map((row) => ({ ...row, active })),\n )\n focusSoon(actionButton(active ? 'archiveAll' : 'activateAll'), actionButton('addRow'))\n }\n\n // \u2500\u2500 Images \u2500\u2500\n // An image is not part of the setup Save sends: it is stored and linked the moment\n // it is chosen, like the template's own main image, so it needs a saved variant.\n\n /** Replace a row's images here and in the saved copy, so Reset does not bring the old ones back. */\n const setImages = (row: Row, images: VariantEditorImage[]): void => {\n rows.set(rows().map((entry) => (entry.key === row.key ? { ...entry, images } : entry)))\n saved = {\n ...saved,\n variants: saved.variants.map((variant) => (variant.id === row.id ? { ...variant, images } : variant)),\n }\n }\n\n const uploadImage = async (row: Row, file: File): Promise<void> => {\n const replacing = row.images.length > 0\n if (!row.id || !props.media.upload || (replacing && !props.media.remove)) return\n if (imageBusy() || !file.type.startsWith('image/')) return\n imageBusy.set(row.key)\n problem.set(null)\n try {\n const body = new FormData()\n body.append('resModel', 'product.Product')\n body.append('resId', row.id)\n body.append('resField', 'media')\n body.append('public', 'false')\n body.append('file', file, file.name)\n const response = await fetch('/files', { method: 'POST', credentials: 'same-origin', body })\n const stored = (await response.json().catch(() => null)) as { id?: unknown } | null\n if (!response.ok || typeof stored?.id !== 'string') throw new Error(t('imageFailed'))\n const attached = await callRecordFunction('product_media.attachMedia', {\n id: stored.id,\n attachmentId: stored.id,\n productId: row.id,\n alt: file.name,\n primary: true,\n })\n if (!attached.ok) throw new Error(attached.message ?? attached.issues[0]?.message ?? t('imageFailed'))\n const previous = row.images.find((image) => image.primary)\n if (previous) await callRecordFunction('product_media.removeMedia', { id: previous.mediaId })\n setImages(row, [\n { mediaId: stored.id, attachmentId: stored.id, alt: file.name, primary: true },\n ...row.images.filter((image) => image !== previous),\n ])\n document.dispatchEvent(\n new CustomEvent('ket:records-changed', { detail: { kind: props.kind, ids: [saved.templateId] } }),\n )\n } catch (error) {\n problem.set(error instanceof Error ? error.message : t('imageFailed'))\n } finally {\n imageBusy.set(null)\n }\n }\n\n const removeImage = async (row: Row): Promise<void> => {\n const current = row.images[0]\n if (!current || !props.media.remove || imageBusy()) return\n if (!globalThis.confirm(t('imageRemoveConfirm'))) return\n imageBusy.set(row.key)\n problem.set(null)\n const result = await callRecordFunction('product_media.removeMedia', { id: current.mediaId }).catch(\n () => null,\n )\n imageBusy.set(null)\n if (!result?.ok) {\n problem.set(result && !result.ok ? (result.message ?? t('saveFailed')) : t('saveFailed'))\n return\n }\n const rest = row.images.slice(1)\n // The gallery promotes the next image to primary; mirror that here.\n setImages(\n row,\n rest.map((image, index) => ({ ...image, primary: index === 0 })),\n )\n }\n\n const openImages = (row: Row, trigger: EventTarget | null): void =>\n viewer.open(\n row.images.map((image) => ({\n src: imageUrl(image.attachmentId, 'large'),\n thumbnail: imageUrl(image.attachmentId, 'thumb'),\n alt: image.alt || t('image'),\n caption: image.alt,\n })),\n 0,\n trigger,\n )\n\n const reset = (): void => {\n listPrice.set(saved.listPrice)\n catalogue.set(saved.catalogue)\n lines.set(clone(saved.lines))\n rows.set(toRows(saved.variants))\n rowIssues.set({})\n problem.set(null)\n dirty.set(false)\n }\n\n const save = async (): Promise<void> => {\n if (blocked()) return\n saving.set(true)\n problem.set(null)\n rowIssues.set({})\n const snapshot = rows()\n const result = await callRecordFunction<{\n setup?: VariantEditorSetup\n created?: number\n archived?: number\n }>(props.saveFunction, {\n templateId: saved.templateId,\n lines: lines().map((line) => ({\n attributeId: line.attributeId,\n values: line.values.map((value) => ({ valueId: value.valueId, priceExtra: value.priceExtra.trim() })),\n })),\n variants: snapshot.map((row) => ({\n id: row.id || null,\n valueIds: row.valueIds,\n defaultCode: row.defaultCode,\n barcode: row.barcode,\n weight: row.weight,\n volume: row.volume,\n active: row.active,\n })),\n }).catch(() => null)\n saving.set(false)\n if (!result) {\n problem.set(t('saveFailed'))\n return\n }\n if (!result.ok) {\n const byRow: Record<string, string> = {}\n for (const issue of result.issues) {\n const match = /^variants\\.(\\d+)(?:\\.(\\w+))?/.exec(issue.field ?? '')\n const row = match ? snapshot[Number(match[1])] : undefined\n if (row) byRow[row.key] = issue.message ?? issue.code\n }\n rowIssues.set(byRow)\n problem.set(result.message ?? result.issues[0]?.message ?? t('saveFailed'))\n if (Object.keys(byRow).length)\n rows.set(rows().map((row) => (byRow[row.key] ? { ...row, open: true } : row)))\n return\n }\n const next = result.value.setup\n if (next) saved = clone(next)\n reset()\n notice.set(t('saved', { created: result.value.created ?? 0, archived: result.value.archived ?? 0 }))\n document.dispatchEvent(\n new CustomEvent('ket:records-changed', { detail: { kind: props.kind, ids: [saved.templateId] } }),\n )\n }\n\n // \u2500\u2500 Events \u2500\u2500\n // Buttons: one name, the action in the value. Inputs: the name says what they edit.\n const onClick = (event: Event): void => {\n const button = event.target instanceof Element ? event.target.closest('button') : null\n if (!button || button.name !== ACTION || button.disabled || !editableAction(button.value)) return\n const [action, key = ''] = button.value.split('|')\n if (action === 'toggleRow') toggleRow(key)\n else if (action === 'selectAll') {\n const line = lines().find((entry) => entry.attributeId === key)\n if (line)\n selectValues(\n key,\n optionsOf(line).map((value) => value.valueId),\n )\n focusSoon(firstValueOf(key))\n } else if (action === 'removeLine') removeLine(key)\n else if (action === 'removeRow') removeRow(key)\n else if (action === 'generate') generate()\n else if (action === 'addRow') addRow()\n else if (action === 'activateAll') setAllActive(true)\n else if (action === 'archiveAll') setAllActive(false)\n else if (action === 'removeImage') {\n const row = rowOf(key)\n if (row) void removeImage(row)\n } else if (action === 'reset') {\n reset()\n focusSoon(pickerTrigger)\n }\n }\n /** Opening and closing a row is reading, not editing; everything else needs the permission. */\n const editableAction = (value: string): boolean => value.startsWith('toggleRow|') || (editable && !saving())\n\n const onChange = (event: Event): void => {\n const control = event.target\n if (!editable || saving()) return\n if (control instanceof HTMLInputElement && control.type === 'checkbox') {\n const match = /^values\\|(.+)\\[\\]$/.exec(control.name)\n if (!match) return\n if (control.checked) selectValues(match[1]!, [control.value])\n else removeValue(match[1]!, control.value)\n } else if (control instanceof HTMLSelectElement) {\n const [kind, key, attributeId] = control.name.split('|')\n const row = kind === 'combo' && key ? rowOf(key) : undefined\n if (row && attributeId)\n updateRow(row.key, { valueIds: { ...row.valueIds, [attributeId]: control.value } })\n } else if (control instanceof HTMLInputElement && control.type === 'file') {\n const [kind, key] = control.name.split('|')\n const row = kind === 'image' && key ? rowOf(key) : undefined\n const file = control.files?.[0]\n control.value = ''\n if (row && file) void uploadImage(row, file)\n }\n }\n\n const onInput = (event: Event): void => {\n const control = event.target\n if (!editable || saving() || !(control instanceof HTMLInputElement)) return\n const [kind, key, name] = control.name.split('|')\n if (!key || !name) return\n if (kind === 'extra') setExtra(key, name, control.value)\n else if (kind === 'field' && ['defaultCode', 'barcode', 'weight', 'volume'].includes(name)) {\n const decimalField = name === 'weight' || name === 'volume'\n updateRow(key, { [name]: decimalField ? control.value : control.value || null } as Partial<Row>)\n }\n }\n\n /** The value a managed control should show, or undefined for controls this island does not own. */\n const expectedValue = (name: string): string | undefined => {\n const [kind, key, name2] = name.split('|')\n if (!key || !name2) return undefined\n if (kind === 'extra')\n return lines()\n .find((line) => line.attributeId === key)\n ?.values.find((value) => value.valueId === name2)?.priceExtra\n const row = rowOf(key)\n if (!row) return undefined\n if (kind === 'combo') return row.valueIds[name2] ?? ''\n if (kind === 'field') return String(row[name2 as RowField] ?? '')\n return undefined\n }\n\n // The renderer writes attributes, which a control stops following once the user has\n // touched it. After Reset, a failed save or an unticked value, bring the live state\n // back in line with the draft \u2014 except the text the user is typing in right now.\n const syncControls = (): void => {\n if (!host) return\n for (const input of host.querySelectorAll<HTMLInputElement>('input[type=\"checkbox\"][name^=\"values|\"]')) {\n const attributeId = input.name.slice('values|'.length, -2)\n const want =\n lines()\n .find((line) => line.attributeId === attributeId)\n ?.values.some((value) => value.valueId === input.value) ?? false\n if (input.checked !== want) input.checked = want\n }\n for (const control of host.querySelectorAll<HTMLInputElement | HTMLSelectElement>(\n 'input[name^=\"extra|\"], input[name^=\"field|\"], select[name^=\"combo|\"]',\n )) {\n if (control === document.activeElement && control instanceof HTMLInputElement) continue\n const want = expectedValue(control.name)\n if (want !== undefined && control.value !== want) control.value = want\n }\n }\n\n // \u2500\u2500 Views \u2500\u2500\n const extrasView = (line: VariantEditorLine): JSXChild => {\n const set = line.values.filter((value) => !isDecimal(value.priceExtra) || decimal(value.priceExtra) !== 0)\n const invalid = line.values.some((value) => !isDecimal(value.priceExtra))\n return (\n <div data-ui=\"variant-editor-extras\" data-attribute={line.attributeId}>\n {Disclosure({\n summary: set.length\n ? t('priceExtrasSet', {\n list: set.map((value) => `${value.name} ${signed(value.priceExtra)}`).join(', '),\n })\n : t('priceExtrasNone'),\n open: invalid || openExtras().includes(line.attributeId),\n body: (\n <div data-ui=\"variant-editor-extra-fields\">\n {each(\n line.values,\n (value) => value.valueId,\n (value) =>\n MoneyField({\n id: `${props.id}-extra-${line.attributeId}-${value.valueId}`,\n name: `extra|${line.attributeId}|${value.valueId}`,\n label: value.name,\n value: value.priceExtra,\n step: 'any',\n disabled: !editable || saving(),\n help: t('appliesTo', { count: usageOf(line.attributeId, value.valueId) }),\n error: isDecimal(value.priceExtra) ? null : t('invalidNumber'),\n }),\n )}\n </div>\n ),\n })}\n </div>\n )\n }\n\n const lineView = (line: VariantEditorLine): TemplateResult => {\n const options = optionsOf(line)\n const shown = editable ? options : line.values\n const selected = new Set(line.values.map((value) => value.valueId))\n const noVariant = line.createVariant === 'no_variant'\n return (\n <li data-ui=\"variant-editor-line\" data-empty={String(!line.values.length)}>\n <div data-ui=\"variant-editor-line-main\">\n {CheckboxGroup({\n id: `${props.id}-values-${line.attributeId}`,\n name: `values|${line.attributeId}`,\n label: line.name,\n span: 'full',\n disabled: !editable || saving(),\n options: shown.map((value) => ({\n value: value.valueId,\n label: value.name,\n checked: selected.has(value.valueId),\n })),\n help: noVariant\n ? t('noVariantHint')\n : line.values.length\n ? t('valuesSelected', { count: line.values.length, total: options.length })\n : t('valuesNone'),\n })}\n <div data-ui=\"variant-editor-line-actions\">\n {noVariant ? Badge({ label: t('noVariant'), tone: 'neutral' }) : null}\n {editable && selected.size < options.length\n ? Button({\n name: ACTION,\n value: `selectAll|${line.attributeId}`,\n label: t('selectAll'),\n variant: 'tertiary',\n size: 'compact',\n disabled: saving(),\n })\n : null}\n {editable\n ? IconButton({\n name: ACTION,\n value: `removeLine|${line.attributeId}`,\n label: t('removeAttributeNamed', { name: line.name }),\n icon: icon('x'),\n size: 'compact',\n disabled: saving(),\n })\n : null}\n </div>\n </div>\n {line.values.length ? extrasView(line) : null}\n </li>\n )\n }\n\n const attributesView = (): TemplateResult =>\n Section({\n eyebrow: t('stepAttributes'),\n title: t('attributesTitle'),\n description: t('attributesHint'),\n actions: editable ? (\n <div data-ui=\"variant-editor-picker\" id={`${props.id}-attribute-picker`}>\n {attributePicker.view()}\n </div>\n ) : undefined,\n body: (\n <div data-ui=\"variant-editor-body\">\n {lines().length ? (\n <ul data-ui=\"variant-editor-lines\">\n {each(\n lines(),\n (line) => line.attributeId,\n (line) => lineView(line),\n )}\n </ul>\n ) : (\n EmptyState({\n title: t('attributesEmpty'),\n message: editable ? t('attributesEmptyHint') : '',\n })\n )}\n {attributeForm.view()}\n </div>\n ),\n })\n\n const rowThumb = (row: Row, size: 'small' | 'large'): JSXChild => {\n const image = row.images[0]\n return image ? (\n LightboxThumb({\n item: {\n src: imageUrl(image.attachmentId, 'large'),\n thumbnail: imageUrl(image.attachmentId, 'thumb'),\n alt: image.alt || t('image'),\n },\n labels: props.lightboxLabels,\n size,\n onOpen: (trigger) => openImages(row, trigger),\n })\n ) : (\n <span data-ui=\"lightbox-empty\" data-size={size}>\n {size === 'small' ? '' : t('image')}\n </span>\n )\n }\n\n const imageBlock = (row: Row, bodyId: string): JSXChild => {\n // Replacing attaches then removes, so a row that already has an image needs both.\n const canUpload = editable && props.media.upload && (!row.images.length || props.media.remove)\n const busy = imageBusy() === row.key\n return (\n <div\n role=\"group\"\n aria-label={t('image')}\n data-ui=\"variant-editor-image\"\n data-drag={String(dragOver() === row.key)}\n data-busy={String(busy)}\n onDragOver={(event: DragEvent) => {\n if (!canUpload || !row.id) return\n event.preventDefault()\n dragOver.set(row.key)\n }}\n onDragLeave={(event: DragEvent) => {\n const related = event.relatedTarget\n if (related instanceof Node && (event.currentTarget as Element).contains(related)) return\n dragOver.set(null)\n }}\n onDrop={(event: DragEvent) => {\n if (!canUpload || !row.id) return\n event.preventDefault()\n dragOver.set(null)\n const file = event.dataTransfer?.files?.[0]\n if (file) void uploadImage(row, file)\n }}\n >\n {rowThumb(row, 'large')}\n <div data-ui=\"variant-editor-image-actions\">\n {canUpload && row.id\n ? DropZone({\n id: `${bodyId}-image`,\n name: `image|${row.key}`,\n label: row.images.length ? t('replaceImage') : t('uploadImage'),\n accept: IMAGE_TYPES,\n disabled: busy || saving(),\n status: t('dropImage'),\n span: 'full',\n })\n : null}\n {canUpload && !row.id ? <small data-ui=\"variant-editor-hint\">{t('imageSaveFirst')}</small> : null}\n {editable && props.media.remove && row.images.length\n ? Button({\n name: ACTION,\n value: `removeImage|${row.key}`,\n label: t('removeImage'),\n variant: 'tertiary',\n size: 'compact',\n disabled: busy || saving(),\n })\n : null}\n </div>\n </div>\n )\n }\n\n const rowField = (row: Row, bodyId: string, field: RowField): JSXChild => {\n const value = String(row[field] ?? '')\n const common = {\n id: `${bodyId}-${field}`,\n name: `field|${row.key}|${field}`,\n label: t(field),\n value,\n disabled: !editable || saving(),\n }\n return field === 'weight' || field === 'volume'\n ? Field({\n ...common,\n type: 'decimal',\n step: 'any',\n error: isDecimal(value) ? null : t('invalidNumber'),\n })\n : TextField(common)\n }\n\n const rowStatus = (row: Row, clash: Map<string, number>, open: Set<string>): JSXChild => {\n if (!row.active) return Badge({ label: t('archived'), tone: 'neutral' })\n if (clash.has(row.key))\n return Badge({ label: t('duplicate', { row: clash.get(row.key) }), tone: 'danger' })\n if (rowInvalid(row)) return Badge({ label: t('rowInvalid'), tone: 'danger' })\n if (open.has(row.key)) return Badge({ label: t('incomplete'), tone: 'warning' })\n if (!row.id) return Badge({ label: t('new'), tone: 'info' })\n return null\n }\n\n const rowView = (\n row: Row,\n index: number,\n clash: Map<string, number>,\n open: Set<string>,\n ): TemplateResult => {\n const bodyId = `${props.id}-row-${row.key}`\n // Every row repeats the same short action labels; the combination names them.\n const titleId = `${bodyId}-title`\n const breakdown = extraOf(row).filter(({ value }) => decimal(value.priceExtra) !== 0)\n const issue = rowIssues()[row.key]\n return (\n <article\n data-ui=\"variant-editor-row\"\n role=\"listitem\"\n data-active={String(row.active)}\n data-open={String(row.open)}\n data-invalid={String(clash.has(row.key) || Boolean(issue) || rowInvalid(row))}\n >\n <div data-ui=\"variant-editor-row-head\">\n <span data-ui=\"variant-editor-row-number\">{`#${index + 1}`}</span>\n {rowThumb(row, 'small')}\n <div data-ui=\"variant-editor-row-title\">\n <strong id={titleId} data-incomplete={String(open.has(row.key))}>\n {combinationLabel(row.valueIds)}\n </strong>\n {row.defaultCode ? <small>{row.defaultCode}</small> : null}\n </div>\n <div data-ui=\"variant-editor-row-meta\">\n {rowStatus(row, clash, open)}\n <span data-ui=\"variant-editor-row-price\">{money(priceOf(row))}</span>\n </div>\n <div data-ui=\"variant-editor-row-actions\">\n {Button({\n name: ACTION,\n value: `toggleRow|${row.key}`,\n label: row.open ? t('collapse') : editable ? t('edit') : t('details'),\n variant: 'tertiary',\n size: 'compact',\n expanded: row.open,\n controls: row.open ? bodyId : null,\n describedBy: titleId,\n })}\n {editable\n ? Button({\n name: ACTION,\n value: `removeRow|${row.key}`,\n label: !row.id ? t('remove') : row.active ? t('archive') : t('restore'),\n variant: 'tertiary',\n size: 'compact',\n disabled: saving(),\n describedBy: titleId,\n })\n : null}\n </div>\n </div>\n {issue ? <p data-ui=\"variant-editor-row-issue\">{issue}</p> : null}\n {row.open ? (\n <div data-ui=\"variant-editor-row-body\" id={bodyId}>\n {imageBlock(row, bodyId)}\n <div data-ui=\"variant-editor-fields\">\n {each(\n variantLines(),\n (line) => line.attributeId,\n (line) =>\n Select({\n id: `${bodyId}-${line.attributeId}`,\n name: `combo|${row.key}|${line.attributeId}`,\n label: line.name,\n value: row.valueIds[line.attributeId] ?? '',\n disabled: !editable || !row.active || saving(),\n options: [\n { value: '', label: t('choose', { name: line.name }) },\n ...line.values.map((value) => ({ value: value.valueId, label: value.name })),\n ],\n }),\n )}\n {rowField(row, bodyId, 'defaultCode')}\n {rowField(row, bodyId, 'barcode')}\n {rowField(row, bodyId, 'weight')}\n {rowField(row, bodyId, 'volume')}\n </div>\n <p data-ui=\"variant-editor-breakdown\">\n {[\n `${t('listPrice')} ${money(decimal(listPrice()))}`,\n ...breakdown.map(\n ({ line, value }) => `${line.name} ${value.name} +${money(decimal(value.priceExtra))}`,\n ),\n ].join(' \u00B7 ')}\n {` = ${money(priceOf(row))}`}\n </p>\n </div>\n ) : null}\n </article>\n )\n }\n\n const missingView = (missing: Array<Record<string, string>>): JSXChild => {\n const names = missing.slice(0, PREVIEW).map(combinationLabel)\n const rest = missing.length - names.length\n return Notice({\n tone: 'info',\n title: t('missing', { count: missing.length }),\n message: rest > 0 ? `${names.join(', ')} ${t('missingMore', { count: rest })}` : names.join(', '),\n actions: Button({\n name: ACTION,\n value: 'generate',\n label: t('generate', { count: missing.length }),\n variant: 'primary',\n size: 'compact',\n leading: icon('plus'),\n disabled: saving(),\n }),\n })\n }\n\n const variantsView = (): TemplateResult => {\n const clash = duplicates()\n const open = incomplete()\n const missing = editable ? missingCombinations() : []\n const activeCount = rows().filter((row) => row.active).length\n const toolbar = editable && (variantLines().length > 0 || rows().length > 0)\n return Section({\n eyebrow: t('stepVariants'),\n title: t('variantsTitle'),\n description: rows().length ? t('variantsCount', { active: activeCount, total: rows().length }) : null,\n actions: toolbar ? (\n <div data-ui=\"variant-editor-toolbar\">\n {Button({\n name: ACTION,\n value: 'addRow',\n label: t('addVariant'),\n variant: 'secondary',\n size: 'compact',\n leading: icon('plus'),\n disabled: saving(),\n })}\n {rows().some((row) => row.id && !row.active)\n ? Button({\n name: ACTION,\n value: 'activateAll',\n label: t('activateAll'),\n variant: 'tertiary',\n size: 'compact',\n disabled: saving(),\n })\n : null}\n {activeCount\n ? Button({\n name: ACTION,\n value: 'archiveAll',\n label: t('archiveAll'),\n variant: 'tertiary',\n size: 'compact',\n disabled: saving(),\n })\n : null}\n </div>\n ) : undefined,\n body: (\n <div data-ui=\"variant-editor-body\">\n {missing.length ? missingView(missing) : null}\n {rows().length ? (\n <div data-ui=\"variant-editor-rows\" role=\"list\">\n {each(\n rows(),\n (row) => row.key,\n (row) => rowView(row, rows().indexOf(row), clash, open),\n )}\n </div>\n ) : missing.length ? null : (\n EmptyState({\n title: t('variantsEmpty'),\n message: variantLines().length ? t('variantsEmptyHint') : t('variantsNeedAttributes'),\n })\n )}\n </div>\n ),\n })\n }\n\n const footerView = (): JSXChild => {\n const clash = duplicates().size\n const archiving = rows().filter(\n (row) => row.id && !row.active && saved.variants.find((v) => v.id === row.id)?.active,\n ).length\n const creating = rows().filter((row) => !row.id && row.active).length\n const parts = [\n creating ? t('summaryCreate', { count: creating }) : '',\n archiving ? t('summaryArchive', { count: archiving }) : '',\n ].filter(Boolean)\n const invalid = invalidExtras() || invalidRows()\n return (\n <footer data-ui=\"variant-editor-footer\">\n <p\n data-ui=\"variant-editor-summary\"\n data-tone={clash || invalid ? 'danger' : 'neutral'}\n aria-live=\"polite\"\n >\n {saving()\n ? t('saving')\n : clash\n ? t('summaryDuplicate')\n : invalid\n ? t('summaryInvalid')\n : (notice() ?? (dirty() ? parts.join(' \u00B7 ') || t('summaryChanged') : ''))}\n </p>\n {editable\n ? Button({\n name: ACTION,\n value: 'reset',\n label: t('reset'),\n variant: 'secondary',\n size: 'compact',\n disabled: !dirty() || saving(),\n })\n : null}\n </footer>\n )\n }\n\n // Save lives in the modal footer, outside this island: it submits this empty form,\n // and the island keeps that button's disabled and busy state in step with its own.\n const saveForm = variantEditorSaveForm(props.id)\n const saveButton = (): HTMLButtonElement | null =>\n document.querySelector<HTMLButtonElement>(`button[form=\"${saveForm}\"]`)\n\n return {\n view: () => (\n // biome-ignore lint/a11y/noStaticElementInteractions: delegates events from the native controls inside, which keep their own keyboard behaviour.\n // biome-ignore lint/a11y/useKeyWithClickEvents: every click it routes comes from a native button, which already handles Enter and Space.\n <div\n data-ui=\"variant-editor\"\n id={props.id}\n aria-busy={saving() ? 'true' : null}\n onClick={onClick}\n onChange={onChange}\n onInput={onInput}\n >\n {editable ? null : Notice({ title: t('readOnly'), message: '' })}\n {problem() ? Notice({ title: t('saveFailed'), message: problem() ?? '', tone: 'danger' }) : null}\n {attributesView()}\n {variantsView()}\n {footerView()}\n <form id={saveForm} data-record-dirty={dirty() ? 'true' : null} hidden />\n {viewer.layer()}\n </div>\n ),\n mount: ({ root, lifetime }) => {\n host = root as unknown as HTMLElement\n lifetime.addEventListener('abort', () => {\n disposed = true\n host = null\n })\n attributeForm.attach(host, lifetime)\n const pickerRoot = host.querySelector<HTMLElement>(`[id=\"${props.id}-attribute-picker\"]`)\n if (pickerRoot) attributePicker.mount?.({ root: pickerRoot as unknown as IslandElement, lifetime })\n lifetime.addEventListener('abort', () => attributePicker.dispose?.())\n viewer.attach(lifetime)\n // A price-extra disclosure stays as the user left it across renders.\n host.addEventListener(\n 'toggle',\n (event) => {\n const details = event.target\n if (!(details instanceof HTMLDetailsElement)) return\n const attributeId = details.closest<HTMLElement>('[data-ui=\"variant-editor-extras\"]')?.dataset\n .attribute\n if (!attributeId) return\n const others = openExtras().filter((id) => id !== attributeId)\n openExtras.set(details.open ? [...others, attributeId] : others)\n },\n { signal: lifetime, capture: true },\n )\n // Capture phase, so the record modal's own submit handler never sees this form.\n document.addEventListener(\n 'submit',\n (event) => {\n if (!(event.target instanceof HTMLFormElement) || event.target.id !== saveForm) return\n event.preventDefault()\n event.stopImmediatePropagation()\n if (!blocked()) void save()\n },\n { signal: lifetime, capture: true },\n )\n const stopSave = effect(() => {\n const button = saveButton()\n if (!button) return\n button.disabled = blocked()\n if (saving()) button.setAttribute('aria-busy', 'true')\n else button.removeAttribute('aria-busy')\n })\n const stopSync = effect(() => {\n lines()\n rows()\n queueMicrotask(syncControls)\n })\n lifetime.addEventListener('abort', () => {\n stopSave()\n stopSync()\n })\n },\n }\n}\n\n/** The id of the form a Save button outside the island submits to save this editor. */\nexport const variantEditorSaveForm = (id: string): string => `${id}-save`\n\nexport const variantEditor = (props: IslandProps): IslandController =>\n createVariantEditorView(props as unknown as VariantEditorProps)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['action', 'action-leading', 'action-label', 'action-spinner', 'action-group'] as const\n\nexport type ActionVariant = 'primary' | 'secondary' | 'tertiary' | 'destructive'\nexport type ActionSize = 'compact' | 'default' | 'prominent'\n\ntype ActionBase = {\n label: string\n variant?: ActionVariant\n size?: ActionSize\n leading?: JSXChild\n disabled?: boolean\n loading?: boolean\n describedBy?: string | null\n}\n\nexport type ButtonProps = ActionBase & {\n type?: 'button' | 'submit' | 'reset'\n name?: string | null\n value?: string | null\n form?: string | null\n /** For a button that shows or hides a region: whether that region is open now. */\n expanded?: boolean\n /** The id of the region an `expanded` button shows or hides. */\n controls?: string | null\n}\n\nexport type LinkButtonProps = ActionBase & { href: string }\nexport type IconButtonProps = Omit<ButtonProps, 'leading'> & {\n icon: JSXChild\n pressed?: boolean\n}\n\nconst ActionContent = (props: ActionBase & { iconOnly?: boolean }): TemplateResult => (\n <>\n {props.loading === true && <span data-ui=\"action-spinner\" aria-hidden=\"true\" />}\n {props.loading !== true && props.leading !== undefined && (\n <span data-ui=\"action-leading\" aria-hidden=\"true\">\n {props.leading}\n </span>\n )}\n {!props.iconOnly && <span data-ui=\"action-label\">{props.label}</span>}\n </>\n)\n\nexport const Button = (props: ButtonProps): TemplateResult => (\n <button\n data-ui=\"action\"\n data-variant={props.variant ?? 'secondary'}\n data-size={props.size ?? 'default'}\n type={props.type ?? 'button'}\n name={props.name ?? null}\n value={props.value ?? null}\n form={props.form ?? null}\n disabled={props.disabled === true || props.loading === true}\n aria-busy={props.loading === true ? 'true' : null}\n aria-describedby={props.describedBy ?? null}\n aria-expanded={props.expanded === undefined ? null : String(props.expanded)}\n aria-controls={props.controls ?? null}\n >\n <ActionContent {...props} />\n </button>\n)\n\n/** An icon-only action with a visible tooltip and accessible name. */\nexport const IconButton = (props: IconButtonProps): TemplateResult => (\n <button\n data-ui=\"action\"\n data-icon-only=\"true\"\n data-variant={props.variant ?? 'tertiary'}\n data-size={props.size ?? 'default'}\n type={props.type ?? 'button'}\n name={props.name ?? null}\n value={props.value ?? null}\n form={props.form ?? null}\n disabled={props.disabled === true || props.loading === true}\n aria-label={props.label}\n aria-pressed={props.pressed === undefined ? null : String(props.pressed)}\n aria-busy={props.loading === true ? 'true' : null}\n aria-describedby={props.describedBy ?? null}\n aria-expanded={props.expanded === undefined ? null : String(props.expanded)}\n aria-controls={props.controls ?? null}\n title={props.label}\n >\n <ActionContent {...props} leading={props.icon} iconOnly />\n </button>\n)\n\nexport const LinkButton = (props: LinkButtonProps): TemplateResult =>\n props.disabled || props.loading ? (\n <button\n data-ui=\"action\"\n data-variant={props.variant ?? 'secondary'}\n data-size={props.size ?? 'default'}\n type=\"button\"\n disabled\n aria-busy={props.loading === true ? 'true' : null}\n aria-describedby={props.describedBy ?? null}\n >\n <ActionContent {...props} />\n </button>\n ) : (\n <a\n data-ui=\"action\"\n data-variant={props.variant ?? 'secondary'}\n data-size={props.size ?? 'default'}\n href={props.href}\n aria-describedby={props.describedBy ?? null}\n >\n <ActionContent {...props} />\n </a>\n )\n\nexport const ActionGroup = (props: {\n actions: readonly JSXChild[]\n label?: string | null\n}): TemplateResult => (\n <div data-ui=\"action-group\" role=\"group\" aria-label={props.label ?? null}>\n {each(\n props.actions,\n (_, index) => index,\n (action) => (\n <>{action}</>\n ),\n )}\n </div>\n)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'badge',\n 'tag',\n 'tag-remove',\n 'count-badge',\n 'avatar',\n 'code',\n 'text',\n 'media-label',\n 'media-label-image',\n 'media-label-copy',\n] as const\n\n/** Plain information; use badges only when a state or exception needs emphasis. */\nexport const Text = (props: { children: string; tone?: 'default' | 'muted' }): TemplateResult => (\n <span data-ui=\"text\" data-tone={props.tone ?? 'default'}>\n {props.children}\n </span>\n)\n\n/** A readable row name with optional media. Empty collections do not pay for an unused image column. */\nexport const MediaLabel = (props: {\n label: string\n src?: string\n reserveImage?: boolean\n}): TemplateResult => (\n <span data-ui=\"media-label\">\n {props.src || props.reserveImage ? (\n <span data-ui=\"media-label-image\" aria-hidden=\"true\">\n {props.src ? <img src={props.src} alt=\"\" loading=\"lazy\" decoding=\"async\" /> : null}\n </span>\n ) : null}\n <span data-ui=\"media-label-copy\">{props.label}</span>\n </span>\n)\n\nexport type Tone = 'neutral' | 'info' | 'positive' | 'warning' | 'danger'\n\nexport const Badge = (props: { label: string; tone?: Tone; value?: string }): TemplateResult => (\n <span\n data-ui=\"badge\"\n data-pattern=\"badge\"\n data-tone={props.tone ?? 'neutral'}\n data-value={props.value ?? ''}\n >\n {props.label}\n </span>\n)\n\nexport const Tag = (props: {\n label: string\n removeHref?: string | null\n removeLabel?: string | null\n}): TemplateResult => (\n <span data-ui=\"tag\">\n {props.label}\n {!!props.removeHref && (\n <a\n data-ui=\"tag-remove\"\n href={props.removeHref}\n aria-label={props.removeLabel ?? `Remove ${props.label}`}\n >\n \u00D7\n </a>\n )}\n </span>\n)\n\nexport const CountBadge = (props: { count: number; label: string }): TemplateResult => (\n <span data-ui=\"count-badge\" role=\"status\" aria-label={props.label}>\n {String(props.count)}\n </span>\n)\n\nexport const initials = (name: string): string => {\n const parts = name.trim().split(/\\s+/).filter(Boolean)\n if (!parts.length) return '?'\n const first = parts.length > 1 ? parts[parts.length - 1]! : parts[0]!\n const second = parts.length > 2 ? parts[parts.length - 2]! : ''\n return (second.slice(0, 1) + first.slice(0, 1)).toLocaleUpperCase('vi')\n}\n\nexport const Avatar = (props: { name: string; size?: 'small' | 'default' | 'large' }): TemplateResult => (\n <span\n data-ui=\"avatar\"\n data-pattern=\"avatar\"\n data-size={props.size ?? 'default'}\n title={props.name}\n aria-hidden=\"true\"\n >\n {initials(props.name)}\n </span>\n)\n\nexport const Code = (props: { value: string; context?: string | null }): TemplateResult => (\n <code data-ui=\"code\" data-context={props.context ?? null}>\n {props.value}\n </code>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'notice',\n 'notice-mark',\n 'notice-copy',\n 'notice-title',\n 'notice-message',\n 'notice-actions',\n 'empty',\n 'empty-mark',\n 'empty-title',\n 'empty-message',\n 'empty-actions',\n 'loading',\n 'loading-label',\n] as const\n\nexport type NoticeTone = 'info' | 'positive' | 'warning' | 'danger'\n\n// Lucide (ISC) glyphs, vendored per tone: info, circle-check, triangle-alert, circle-alert.\nconst NOTICE_ICONS: Record<NoticeTone, () => TemplateResult> = {\n info: () => (\n <>\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"M12 16v-4\" />\n <path d=\"M12 8h.01\" />\n </>\n ),\n positive: () => (\n <>\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"m9 12 2 2 4-4\" />\n </>\n ),\n warning: () => (\n <>\n <path d=\"m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\" />\n <path d=\"M12 9v4\" />\n <path d=\"M12 17h.01\" />\n </>\n ),\n danger: () => (\n <>\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"M12 8v4\" />\n <path d=\"M12 16h.01\" />\n </>\n ),\n}\n\nexport const Notice = (props: {\n title: string\n message: string\n tone?: NoticeTone\n actions?: JSXChild\n}): TemplateResult => (\n <aside\n data-ui=\"notice\"\n data-pattern=\"notice\"\n data-tone={props.tone ?? 'info'}\n role={props.tone === 'danger' ? 'alert' : 'status'}\n >\n <span data-ui=\"notice-mark\" aria-hidden=\"true\">\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n {NOTICE_ICONS[props.tone ?? 'info']()}\n </svg>\n </span>\n <div data-ui=\"notice-copy\">\n <p data-ui=\"notice-title\">{props.title}</p>\n <p data-ui=\"notice-message\">{props.message}</p>\n </div>\n {props.actions !== undefined && <div data-ui=\"notice-actions\">{props.actions}</div>}\n </aside>\n)\n\nexport const EmptyState = (props: { title: string; message: string; actions?: JSXChild }): TemplateResult => (\n <div data-ui=\"empty\" role=\"status\">\n <span data-ui=\"empty-mark\" aria-hidden=\"true\">\n \u25C7\n </span>\n <p data-ui=\"empty-title\">{props.title}</p>\n <p data-ui=\"empty-message\">{props.message}</p>\n {props.actions !== undefined && <div data-ui=\"empty-actions\">{props.actions}</div>}\n </div>\n)\n\nexport const LoadingState = (props: { label: string; lines?: number }): TemplateResult => (\n <div data-ui=\"loading\" role=\"status\" aria-live=\"polite\">\n <span data-ui=\"loading-label\">{props.label}</span>\n <div data-ui=\"skeleton\" aria-hidden=\"true\">\n {each(\n Array.from({ length: Math.max(1, props.lines ?? 3) }),\n (_, index) => index,\n (_, index) => (\n <span data-ui=\"skeleton-line\" data-line={String(index + 1)} />\n ),\n )}\n </div>\n </div>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Disclosure } from '../../layouts/index.tsx'\nimport { NativeFieldControl } from './native-control.tsx'\n\nexport const HOOKS = [\n 'field',\n 'field-label',\n 'field-required',\n 'field-control',\n 'field-options',\n 'field-option',\n 'field-option-input',\n 'field-help',\n 'field-error',\n] as const\n\nexport type FieldOption = {\n value: string\n label: string\n name?: string\n checked?: boolean\n disabled?: boolean\n}\nexport type FieldProps = {\n /** Nested fields for a collapsible group inside a larger record form. */\n fields?: readonly FieldProps[]\n open?: boolean\n\n id: string\n name: string\n label: string\n /** A trusted, progressively enhanced control such as a relation selector. */\n control?: JSXChild\n type?:\n | 'text'\n | 'email'\n | 'tel'\n | 'number'\n | 'decimal'\n | 'password'\n | 'time'\n | 'color'\n | 'date'\n | 'datetime-local'\n | 'month'\n | 'week'\n | 'select'\n | 'textarea'\n | 'checkbox'\n | 'checkbox-group'\n | 'radio'\n value?: string | number | boolean | null\n /** Embedded controls share the frame of a compound field. */\n labelHidden?: boolean\n /** Keep the selected value accessible while showing only the select chevron. */\n selectionHidden?: boolean\n appearance?: 'default' | 'embedded'\n placeholder?: string | null\n required?: boolean\n disabled?: boolean\n readOnly?: boolean\n min?: string | number\n max?: string | number\n help?: string | null\n error?: string | null\n options?: readonly FieldOption[]\n /**\n * How checkbox-group and radio options flow. `horizontal` (default) wraps them on a line; `vertical` lists\n * one option per line under the shared label, for choices whose labels should scan as a column.\n */\n optionsOrientation?: 'horizontal' | 'vertical'\n span?: 'half' | 'full'\n step?: string | null\n autocomplete?: string | null\n}\n\nconst hasError = (props: FieldProps): boolean => !!props.error || (props.fields?.some(hasError) ?? false)\n\nexport const Field = (props: FieldProps): TemplateResult => {\n const helpId = props.help ? `${props.id}-help` : null\n const errorId = props.error ? `${props.id}-error` : null\n const describedBy = [helpId, errorId].filter(Boolean).join(' ') || null\n const label = (\n <span data-ui=\"field-label\">\n {props.label}\n {props.required && (\n <span data-ui=\"field-required\" aria-hidden=\"true\">\n {' *'}\n </span>\n )}\n </span>\n )\n\n if (props.fields)\n return (\n <div data-ui=\"field\" data-kind=\"group\" data-span=\"full\" data-invalid={String(!!props.error)}>\n <Disclosure\n summary={props.label}\n open={props.open || hasError(props)}\n body={\n <div data-ui=\"form-grid\">\n {each(\n props.fields.map((field) => ({\n ...field,\n disabled: props.disabled || field.disabled,\n readOnly: props.readOnly || field.readOnly,\n })),\n (field) => field.id,\n (field) => (\n <Field {...field} />\n ),\n )}\n </div>\n }\n />\n {!!props.help && (\n <small data-ui=\"field-help\" id={helpId ?? undefined}>\n {props.help}\n </small>\n )}\n {!!props.error && (\n <small data-ui=\"field-error\" id={errorId ?? undefined}>\n {props.error}\n </small>\n )}\n </div>\n )\n\n if (props.type === 'checkbox-group')\n return (\n <div\n data-ui=\"field\"\n data-kind=\"checkbox-group\"\n data-span={props.span ?? 'half'}\n data-invalid={String(!!props.error)}\n >\n <span data-ui=\"field-label\" id={`${props.id}-label`}>\n {props.label}\n {props.required && (\n <span data-ui=\"field-required\" aria-hidden=\"true\">\n {' *'}\n </span>\n )}\n </span>\n <div\n data-ui=\"field-options\"\n data-orientation={props.optionsOrientation === 'vertical' ? 'vertical' : null}\n role=\"group\"\n aria-labelledby={`${props.id}-label`}\n >\n {each(\n props.options ?? [],\n (option) => option.name ?? option.value,\n (option) => (\n <label data-ui=\"field-option\">\n <input\n data-ui=\"field-option-input\"\n type=\"checkbox\"\n name={option.name ?? `${props.name}[]`}\n value={option.value}\n checked={option.checked === true}\n disabled={props.disabled === true || option.disabled === true}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={describedBy}\n autocomplete=\"off\"\n />\n <span>{option.label}</span>\n </label>\n ),\n )}\n </div>\n {!!props.help && (\n <small data-ui=\"field-help\" id={helpId ?? undefined}>\n {props.help}\n </small>\n )}\n {!!props.error && (\n <small data-ui=\"field-error\" id={errorId ?? undefined}>\n {props.error}\n </small>\n )}\n </div>\n )\n\n if (props.type === 'radio')\n return (\n <div\n data-ui=\"field\"\n data-kind=\"radio\"\n data-span={props.span ?? 'half'}\n data-invalid={String(!!props.error)}\n >\n <span data-ui=\"field-label\" id={`${props.id}-label`}>\n {props.label}\n {props.required && (\n <span data-ui=\"field-required\" aria-hidden=\"true\">\n {' *'}\n </span>\n )}\n </span>\n <div\n data-ui=\"field-options\"\n data-orientation={props.optionsOrientation === 'vertical' ? 'vertical' : null}\n role=\"radiogroup\"\n aria-labelledby={`${props.id}-label`}\n >\n {each(\n props.options ?? [],\n (option) => option.value,\n (option) => (\n <label data-ui=\"field-option\">\n <input\n data-ui=\"field-option-input\"\n type=\"radio\"\n name={props.name}\n value={option.value}\n checked={String(props.value ?? '') === option.value}\n required={props.required === true}\n disabled={props.disabled === true || option.disabled === true}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={describedBy}\n autocomplete=\"off\"\n />\n <span>{option.label}</span>\n </label>\n ),\n )}\n </div>\n {!!props.help && (\n <small data-ui=\"field-help\" id={helpId ?? undefined}>\n {props.help}\n </small>\n )}\n {!!props.error && (\n <small data-ui=\"field-error\" id={errorId ?? undefined}>\n {props.error}\n </small>\n )}\n </div>\n )\n\n return (\n <label\n data-ui=\"field\"\n data-kind={props.type ?? 'text'}\n data-label-hidden={props.labelHidden ? 'true' : null}\n data-selection-hidden={props.selectionHidden ? 'true' : null}\n data-span={props.span ?? 'half'}\n data-invalid={String(!!props.error)}\n for={props.id}\n >\n {props.type === 'checkbox' && NativeFieldControl(props, describedBy)}\n {label}\n {props.type !== 'checkbox' && NativeFieldControl(props, describedBy)}\n {!!props.help && (\n <small data-ui=\"field-help\" id={helpId ?? undefined}>\n {props.help}\n </small>\n )}\n {!!props.error && (\n <small data-ui=\"field-error\" id={errorId ?? undefined}>\n {props.error}\n </small>\n )}\n </label>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport type { Tone } from '../../primitives/status.tsx'\n\nexport const HOOKS = [\n 'stack',\n 'inline',\n 'grid',\n 'card-grid',\n 'surface',\n 'surface-head',\n 'surface-heading',\n 'surface-title',\n 'surface-description',\n 'surface-actions',\n 'disclosure',\n 'disclosure-summary',\n 'disclosure-body',\n 'section',\n 'section-head',\n 'section-eyebrow',\n 'section-title',\n 'section-description',\n 'section-actions',\n 'section-body',\n 'content-card',\n 'card-media',\n 'card-head',\n 'card-leading',\n 'card-heading',\n 'card-eyebrow',\n 'card-title',\n 'card-summary',\n 'card-status',\n 'card-body',\n 'card-meta',\n 'card-actions',\n 'kanban',\n 'kanban-card',\n 'kanban-media',\n 'kanban-title',\n 'kanban-meta',\n 'kanban-note',\n 'kanban-actions',\n 'metric',\n 'metric-icon',\n 'metric-label',\n 'metric-value',\n 'metric-detail',\n] as const\n\nexport { AppShell, Page, PageHeader, RecordCanvas, RecordSection } from '../shell.tsx'\n\nconst Items = (props: { items: readonly JSXChild[] }): TemplateResult => (\n <>\n {each(\n props.items,\n (_, index) => index,\n // biome-ignore lint/complexity/noUselessFragments: `each` requires a TemplateResult callback.\n (item) => (typeof item === 'string' || typeof item === 'number' ? <span>{item}</span> : <>{item}</>),\n )}\n </>\n)\n\n/**\n * `divided` draws a hairline between items, the peer-section divider of LAYOUT.md\n * L3 (a form with several groups, a list of sections in a modal). It never frames\n * an item.\n */\nexport const Stack = (props: {\n items: readonly JSXChild[]\n gap?: 'compact' | 'default' | 'loose'\n divided?: boolean\n}): TemplateResult => (\n <div\n data-ui=\"stack\"\n data-pattern=\"stack\"\n data-gap={props.gap ?? 'default'}\n data-divided={props.divided === true ? 'true' : null}\n >\n <Items items={props.items} />\n </div>\n)\n\nexport const Inline = (props: { items: readonly JSXChild[] }): TemplateResult => (\n <div data-ui=\"inline\">\n <Items items={props.items} />\n </div>\n)\n\n/**\n * Fixed columns. `align=\"stretch\"` gives every block in a row the row's height,\n * so side-by-side dashboard cards end on one line instead of leaving a hole\n * under the shorter one.\n */\nexport const Grid = (props: {\n items: readonly JSXChild[]\n columns?: 2 | 3 | 4\n align?: 'start' | 'stretch'\n}): TemplateResult => (\n <div\n data-ui=\"grid\"\n data-columns={String(props.columns ?? 3)}\n data-align={props.align === 'stretch' ? 'stretch' : null}\n >\n <Items items={props.items} />\n </div>\n)\n\nexport type CardGridProps<T> = {\n items: readonly T[]\n id: (item: T) => unknown\n card: (item: T) => TemplateResult\n minimum?: 'compact' | 'default' | 'wide'\n}\n\n/** An adaptive card collection; unlike `Grid`, column count follows available width. */\nexport const CardGrid = <T,>(props: CardGridProps<T>): TemplateResult => (\n <div data-ui=\"card-grid\" data-minimum={props.minimum === 'default' ? null : props.minimum}>\n {each(props.items, props.id, (item) => props.card(item))}\n </div>\n)\n\nexport const Surface = (props: {\n body: JSXChild\n title?: string\n /** One line under the title. A dashboard block keeps its heading, context and content in one card. */\n description?: string | null\n actions?: JSXChild\n tone?: 'default' | 'subtle' | 'raised'\n padding?: 'none' | 'compact' | 'default'\n}): TemplateResult => (\n <div\n data-ui=\"surface\"\n data-tone={props.tone ?? 'default'}\n data-padding={props.padding ?? 'default'}\n data-has-heading={props.title ? 'true' : null}\n >\n {props.title && (\n <header data-ui=\"surface-head\">\n {props.description ? (\n <div data-ui=\"surface-heading\">\n <h2 data-ui=\"surface-title\">{props.title}</h2>\n <p data-ui=\"surface-description\">{props.description}</p>\n </div>\n ) : (\n <h2 data-ui=\"surface-title\">{props.title}</h2>\n )}\n {props.actions !== undefined && <div data-ui=\"surface-actions\">{props.actions}</div>}\n </header>\n )}\n {props.body}\n </div>\n)\n\n/** Native, keyboard-accessible progressive disclosure for dense secondary detail. */\nexport const Disclosure = (props: { summary: string; body: JSXChild; open?: boolean }): TemplateResult => (\n <details data-ui=\"disclosure\" open={props.open === true ? true : undefined}>\n <summary data-ui=\"disclosure-summary\">{props.summary}</summary>\n <div data-ui=\"disclosure-body\">{props.body}</div>\n </details>\n)\n\nexport const Section = (props: {\n title: string\n body: JSXChild\n description?: string | null\n eyebrow?: string | null\n actions?: JSXChild\n}): TemplateResult => (\n <section data-ui=\"section\">\n <header data-ui=\"section-head\">\n <div>\n {!!props.eyebrow && <p data-ui=\"section-eyebrow\">{props.eyebrow}</p>}\n <h2 data-ui=\"section-title\">{props.title}</h2>\n {!!props.description && <p data-ui=\"section-description\">{props.description}</p>}\n </div>\n {props.actions !== undefined && <div data-ui=\"section-actions\">{props.actions}</div>}\n </header>\n <div data-ui=\"section-body\">{props.body}</div>\n </section>\n)\n\nexport type ContentCardProps = {\n title: string\n eyebrow?: string | null\n summary?: string | null\n leading?: JSXChild\n media?: JSXChild\n status?: JSXChild\n body?: JSXChild\n meta?: JSXChild\n actions?: JSXChild\n href?: string | null\n selected?: boolean\n tone?: 'default' | 'subtle' | 'raised'\n padding?: 'compact' | 'default'\n}\n\nexport const ContentCard = (props: ContentCardProps): TemplateResult => (\n <article\n data-ui=\"content-card\"\n data-interactive={String(!!props.href)}\n data-selected={String(props.selected === true)}\n data-tone={props.tone ?? 'default'}\n data-padding={props.padding ?? 'default'}\n >\n {props.media !== undefined && <div data-ui=\"card-media\">{props.media}</div>}\n <header data-ui=\"card-head\">\n {props.leading !== undefined && (\n <span data-ui=\"card-leading\" aria-hidden=\"true\">\n {props.leading}\n </span>\n )}\n <div data-ui=\"card-heading\">\n {!!props.eyebrow && <p data-ui=\"card-eyebrow\">{props.eyebrow}</p>}\n <h3 data-ui=\"card-title\">{props.href ? <a href={props.href}>{props.title}</a> : props.title}</h3>\n {!!props.summary && <p data-ui=\"card-summary\">{props.summary}</p>}\n </div>\n {props.status !== undefined && <div data-ui=\"card-status\">{props.status}</div>}\n </header>\n {props.body !== undefined && <div data-ui=\"card-body\">{props.body}</div>}\n {props.meta !== undefined && <div data-ui=\"card-meta\">{props.meta}</div>}\n {props.actions !== undefined && <div data-ui=\"card-actions\">{props.actions}</div>}\n </article>\n)\n\nexport type KanbanCardProps = {\n id: string\n title: string\n href?: string | null\n media?: JSXChild\n meta?: JSXChild\n note?: string | null\n actions?: JSXChild\n selected?: boolean\n}\n\nexport const KanbanCard = (props: KanbanCardProps): TemplateResult => (\n <article\n data-ui=\"kanban-card\"\n data-key={props.id}\n data-interactive={String(!!props.href)}\n data-selected={String(props.selected === true)}\n >\n {props.media !== undefined && <div data-ui=\"kanban-media\">{props.media}</div>}\n <h3 data-ui=\"kanban-title\">{props.href ? <a href={props.href}>{props.title}</a> : props.title}</h3>\n {props.meta !== undefined && <div data-ui=\"kanban-meta\">{props.meta}</div>}\n {!!props.note && <p data-ui=\"kanban-note\">{props.note}</p>}\n {props.actions !== undefined && <div data-ui=\"kanban-actions\">{props.actions}</div>}\n </article>\n)\n\nexport type KanbanGridProps<T> = {\n rows: readonly T[]\n id: (row: T) => unknown\n card: (row: T) => TemplateResult\n}\n\nexport const KanbanGrid = <T,>(props: KanbanGridProps<T>): TemplateResult => (\n <div data-ui=\"kanban\">{each(props.rows, props.id, (row) => props.card(row))}</div>\n)\n\n/**\n * One number, with the context that makes it mean something.\n *\n * `tone` colours a dot beside the label and nothing else. The number itself stays\n * in the page's ink: a KPI row is read by comparing figures, and four differently\n * coloured figures are compared by colour instead. The dot is drawn only when a\n * tone is passed, so a metric that has no state to report does not grow a grey\n * bullet that means nothing.\n *\n * `href` makes the whole card the link rather than the label, because the card is\n * what the pointer is already over. There is nothing else inside a metric to\n * click, which is exactly why `ContentCard` does the opposite.\n */\nexport const Metric = (props: {\n label: string\n value: string | number\n detail?: string | null\n tone?: Tone\n /**\n * A glyph naming the quantity, in a tile beside it. Decorative: the label is\n * the name of the number, so this is hidden from assistive technology.\n */\n icon?: JSXChild\n href?: string | null\n}): TemplateResult => {\n const attributes = {\n 'data-ui': 'metric',\n 'data-tone': props.tone ?? 'neutral',\n // One marker to a card, never two. The dot reports state for a metric that\n // has no glyph; where there is a glyph, the glyph is already the thing the\n // eye lands on, and a coloured bullet beside the label competes with it.\n 'data-dot': String(props.tone !== undefined && props.icon === undefined),\n }\n const body = (\n <>\n {props.icon !== undefined && (\n <span data-ui=\"metric-icon\" aria-hidden=\"true\">\n {props.icon}\n </span>\n )}\n <p data-ui=\"metric-label\">{props.label}</p>\n <p data-ui=\"metric-value\">{String(props.value)}</p>\n {!!props.detail && <p data-ui=\"metric-detail\">{props.detail}</p>}\n </>\n )\n return props.href ? (\n <a {...attributes} data-interactive=\"true\" href={props.href}>\n {body}\n </a>\n ) : (\n <article {...attributes} data-interactive=\"false\">\n {body}\n </article>\n )\n}\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'modal-layer',\n 'modal-backdrop',\n 'modal-sheet',\n 'modal-head',\n 'modal-heading',\n 'modal-title-row',\n 'modal-title',\n 'modal-description',\n 'modal-close',\n 'modal-body',\n 'modal-actions',\n] as const\n\nexport const ModalSheet = (props: {\n id: string\n title: string\n body: JSXChild\n /** Required for `overlay`: a route modal closes by navigating. Ignored in `client` mode. */\n closeHref?: string\n closeLabel: string\n description?: string | null\n /**\n * What state the thing in this dialog is in, beside its title \u2014 a badge, not a\n * sentence. It belongs to the heading, so it stays put when the body scrolls\n * and is read out with the title rather than after it.\n */\n status?: JSXChild\n actions?: JSXChild\n presentation?: 'sheet' | 'dialog'\n size?: 'default' | 'large'\n /**\n * `content` (default) lets a dialog grow with its body up to the viewport cap. `fixed`\n * holds the dialog at that cap so switching between tabs of different heights does not\n * resize it; the body scrolls inside while the head stays put.\n */\n height?: 'content' | 'fixed'\n /**\n * Overrides `fixed`'s height (default: the viewport cap, `100dvh` minus a margin \u2014\n * tall on most screens). A CSS length or `calc()`/`min()` expression, for a dialog\n * whose own content is shorter than the viewport and would otherwise sit in a mostly\n * empty frame. Set inline with `!important`, not as a plain CSS rule: a legacy\n * admin stylesheet (`packages/ketsuite/src/modules/backend/design/forms.css`)\n * still targets these same `data-ui` hooks at equal specificity and would\n * otherwise win the cascade by loading later. Ignored when `height` isn't `fixed`.\n */\n fixedHeight?: string\n /**\n * `overlay` is URL-owned: its close controls are links the route runtime\n * follows. `client` belongs to a modal island that opens and closes it in the\n * browser: close controls are buttons, and it carries no route-modal marker, so\n * no navigation layer ever treats closing it as a page change.\n */\n mode?: 'overlay' | 'embedded' | 'client'\n /** A native dialog ancestor can own the accessible dialog role and name. */\n dialogSemantics?: 'self' | 'parent'\n unsavedPrompt?: string | null\n}): TemplateResult => {\n const client = props.mode === 'client'\n const embedded = props.mode === 'embedded'\n return (\n <div\n data-ui=\"modal-layer\"\n data-mode={props.mode ?? 'overlay'}\n data-route-modal={embedded || client ? null : 'true'}\n data-client-modal={client ? 'true' : null}\n data-presentation={props.presentation ?? 'sheet'}\n data-unsaved-prompt={props.unsavedPrompt ?? null}\n >\n {client ? (\n <button data-ui=\"modal-backdrop\" type=\"button\" aria-hidden=\"true\" tabIndex={-1}>\n <span>{props.closeLabel}</span>\n </button>\n ) : (\n <a data-ui=\"modal-backdrop\" href={props.closeHref ?? '#'} aria-label={props.closeLabel}>\n <span>{props.closeLabel}</span>\n </a>\n )}\n <section\n id={props.id}\n data-ui=\"modal-sheet\"\n data-size={props.size ?? 'default'}\n data-height={props.height === 'fixed' ? 'fixed' : null}\n style={props.height === 'fixed' && props.fixedHeight ? `height: ${props.fixedHeight} !important` : ''}\n {...(props.dialogSemantics === 'parent'\n ? {}\n : { role: 'dialog', 'aria-modal': embedded ? 'false' : 'true' })}\n aria-labelledby={props.dialogSemantics === 'parent' ? null : `${props.id}-title`}\n aria-describedby={\n props.dialogSemantics !== 'parent' && props.description ? `${props.id}-description` : null\n }\n tabindex={embedded ? null : '-1'}\n >\n <header data-ui=\"modal-head\">\n <div data-ui=\"modal-heading\">\n <div data-ui=\"modal-title-row\">\n <h2 data-ui=\"modal-title\" id={`${props.id}-title`}>\n {props.title}\n </h2>\n {props.status !== undefined && props.status}\n </div>\n {!!props.description && (\n <p data-ui=\"modal-description\" id={`${props.id}-description`}>\n {props.description}\n </p>\n )}\n </div>\n {client ? (\n <button\n data-ui=\"modal-close\"\n type=\"button\"\n aria-label={props.closeLabel}\n title={props.closeLabel}\n >\n \u00D7\n </button>\n ) : (\n <a\n data-ui=\"modal-close\"\n href={props.closeHref ?? '#'}\n aria-label={props.closeLabel}\n title={props.closeLabel}\n >\n \u00D7\n </a>\n )}\n </header>\n <div data-ui=\"modal-body\">{props.body}</div>\n {props.actions !== undefined && <footer data-ui=\"modal-actions\">{props.actions}</footer>}\n </section>\n </div>\n )\n}\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport type PageIdentityKind =\n | 'page'\n | 'list-page'\n | 'record-page'\n | 'form-page'\n | 'workspace-page'\n | 'dashboard-page'\n | 'board-page'\n\nexport type PageIdentityProps = {\n context?: JSXChild\n eyebrow?: string | null\n title: string\n status?: JSXChild\n actions?: JSXChild\n /** Retain action content while removing an empty or conditional identity slot from layout. */\n actionsHidden?: boolean\n meta?: JSXChild\n}\n\n/**\n * The single identity band shared by every full-page component and recipe.\n *\n * A page identity has no description: every page header keeps one height, so\n * switching screens never moves the controls and content below it. Explain a\n * section inside the section, or put guidance in the empty state.\n */\nexport const pageIdentityContent = (kind: PageIdentityKind, props: PageIdentityProps): TemplateResult => (\n <>\n <div data-ui={`${kind}-heading`} data-kv-page-identity=\"heading\">\n {!!props.eyebrow && (\n <p data-ui={`${kind}-eyebrow`} data-kv-page-identity=\"eyebrow\">\n {props.eyebrow}\n </p>\n )}\n <div data-ui={`${kind}-title-row`} data-kv-page-identity=\"title-row\">\n <h1 data-ui={`${kind}-title`} data-kv-page-identity=\"title\">\n {props.title}\n </h1>\n {props.actions !== undefined && (\n <div\n data-ui={`${kind}-actions`}\n data-kv-page-identity=\"actions\"\n hidden={props.actionsHidden === true}\n >\n {props.actions}\n </div>\n )}\n </div>\n {props.status !== undefined && (\n <div data-ui={`${kind}-subline`} data-kv-page-identity=\"subline\">\n <span data-ui={`${kind}-status`} data-kv-page-identity=\"status\">\n {props.status}\n </span>\n </div>\n )}\n </div>\n {props.meta !== undefined && (\n <div data-ui={`${kind}-meta`} data-kv-page-identity=\"meta\">\n {props.meta}\n </div>\n )}\n </>\n)\n\nexport const pageIdentity = (kind: PageIdentityKind, props: PageIdentityProps): TemplateResult => (\n <>\n {props.context != null && (\n <div data-ui={`${kind}-context`} data-kv-page-identity=\"context\">\n {props.context}\n </div>\n )}\n <header data-ui={`${kind}-header`} data-kv-page-identity=\"header\">\n {pageIdentityContent(kind, props)}\n </header>\n </>\n)\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { ModalSheet } from '../../patterns/modal-sheet/index.tsx'\nimport { pageIdentity, pageIdentityContent, type PageIdentityProps } from '../page-identity/index.tsx'\n\nexport const HOOKS = [\n 'app-shell',\n 'app-shell-topbar',\n 'app-topbar',\n 'app-location-bar',\n 'app-location-context',\n 'app-brand',\n 'app-topbar-brand',\n 'app-topbar-context',\n 'global-search-trigger',\n 'global-search-trigger-label',\n 'global-search-shortcut',\n 'global-search-dialog',\n 'global-search',\n 'global-search-input',\n 'global-search-submit',\n 'app-sidebar',\n 'app-main',\n 'app-right-rail',\n 'page',\n 'page-context',\n 'page-header',\n 'page-heading',\n 'page-eyebrow',\n 'page-title-row',\n 'page-title',\n 'page-subline',\n 'page-status',\n 'page-actions',\n 'page-meta',\n 'page-body',\n 'record-canvas',\n 'record-content',\n 'record-section',\n 'record-section-title',\n] as const\n\nexport const AppShell = (props: {\n topbar?: JSXChild\n location?: JSXChild\n sidebar: JSXChild\n main: JSXChild\n rightRail?: JSXChild\n mode?: 'viewport' | 'embedded'\n}): TemplateResult => (\n <div\n data-ui=\"app-shell\"\n data-has-right-rail={String(props.rightRail !== undefined)}\n data-has-location={props.location !== undefined ? 'true' : null}\n data-has-topbar={props.topbar !== undefined ? 'true' : null}\n data-mode={props.mode ?? 'viewport'}\n >\n {props.topbar !== undefined && <div data-ui=\"app-shell-topbar\">{props.topbar}</div>}\n <aside data-ui=\"app-sidebar\">{props.sidebar}</aside>\n {props.mode === 'embedded' ? (\n <div data-ui=\"app-main\">\n {props.location}\n {props.main}\n </div>\n ) : (\n <main data-ui=\"app-main\">\n {props.location}\n {props.main}\n </main>\n )}\n {props.rightRail !== undefined && <aside data-ui=\"app-right-rail\">{props.rightRail}</aside>}\n </div>\n)\n\nconst SearchIcon = (): TemplateResult => (\n <svg\n viewBox=\"0 0 24 24\"\n width=\"18\"\n height=\"18\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n aria-hidden=\"true\"\n >\n <circle cx=\"10.5\" cy=\"10.5\" r=\"6.5\" />\n <path d=\"m16 16 4.5 4.5\" />\n </svg>\n)\n\nexport const AppBrand = (props: {\n label: string\n href: string\n image?: string\n darkImage?: string\n imageFit?: 'contain' | 'cover'\n}): TemplateResult => (\n <a data-ui=\"app-brand\" data-image-fit={props.imageFit} href={props.href} aria-label={props.label}>\n {props.image ? (\n <img src={props.image} alt={props.label} data-appearance={props.darkImage ? 'light' : undefined} />\n ) : (\n props.label\n )}\n {props.darkImage && <img src={props.darkImage} alt={props.label} data-appearance=\"dark\" />}\n </a>\n)\n\n/** Application identity and a compact launcher for a native GET search dialog. */\nexport const AppTopbar = (props: {\n brand?: { label: string; href: string; image?: string }\n /** A location strip stays inside the main column, beside the sidebar identity. */\n location?: JSXChild\n navigation?: JSXChild\n context?: JSXChild\n tools?: JSXChild\n search: {\n id?: string\n action: string\n label: string\n triggerLabel?: string\n closeLabel?: string\n placeholder: string\n submitLabel: string\n query?: string\n locale?: string\n }\n}): TemplateResult => {\n const id = props.search.id ?? 'app-global-search'\n const href = `${props.search.action}${props.search.action.includes('?') ? '&' : '?'}${props.search.locale ? `lang=${encodeURIComponent(props.search.locale)}&` : ''}q=${encodeURIComponent(props.search.query ?? '')}`\n const form = (\n <form\n data-ui=\"global-search\"\n role=\"search\"\n aria-label={props.search.label}\n action={props.search.action}\n method=\"get\"\n >\n {props.search.locale ? <input type=\"hidden\" name=\"lang\" value={props.search.locale} /> : null}\n <input\n data-ui=\"global-search-input\"\n type=\"search\"\n name=\"q\"\n value={props.search.query ?? ''}\n placeholder={props.search.placeholder}\n aria-label={props.search.label}\n autocomplete=\"off\"\n maxlength={200}\n />\n <button\n data-ui=\"global-search-submit\"\n type=\"submit\"\n aria-label={props.search.submitLabel}\n title={props.search.submitLabel}\n >\n <SearchIcon />\n </button>\n </form>\n )\n return (\n <>\n <header data-ui={props.location !== undefined ? 'app-location-bar' : 'app-topbar'}>\n {props.navigation}\n {props.brand && (\n <a data-ui=\"app-topbar-brand\" href={props.brand.href} aria-label={props.brand.label}>\n {props.brand.image ? <img src={props.brand.image} alt={props.brand.label} /> : props.brand.label}\n </a>\n )}\n {props.location !== undefined && <div data-ui=\"app-location-context\">{props.location}</div>}\n {props.context != null && <div data-ui=\"app-topbar-context\">{props.context}</div>}\n <a\n data-ui=\"global-search-trigger\"\n href={href}\n aria-label={props.search.label}\n aria-haspopup=\"dialog\"\n aria-controls={id}\n aria-expanded=\"false\"\n title={props.search.label}\n >\n <SearchIcon />\n <span data-ui=\"global-search-trigger-label\">{props.search.triggerLabel ?? props.search.label}</span>\n <kbd data-ui=\"global-search-shortcut\" hidden>\n Ctrl K\n </kbd>\n </a>\n {props.tools}\n </header>\n <dialog data-ui=\"global-search-dialog\" id={id} aria-labelledby={`${id}-sheet-title`}>\n {form}\n <template>\n <ModalSheet\n id={`${id}-sheet`}\n title={props.search.label}\n closeLabel={props.search.closeLabel ?? 'Close'}\n mode=\"client\"\n presentation=\"dialog\"\n dialogSemantics=\"parent\"\n body={null}\n />\n </template>\n </dialog>\n <noscript>{form}</noscript>\n </>\n )\n}\n\nexport type PageHeaderProps = Omit<PageIdentityProps, 'context'>\n\nexport const PageHeader = (props: PageHeaderProps): TemplateResult => (\n <header data-ui=\"page-header\" data-kv-page-identity=\"header\">\n {pageIdentityContent('page', props)}\n </header>\n)\n\nexport type PageProps = PageIdentityProps & {\n body: JSXChild\n}\n\nexport const Page = (props: PageProps): TemplateResult => {\n const { body, ...identity } = props\n return (\n <section data-ui=\"page\">\n {pageIdentity('page', identity)}\n <div data-ui=\"page-body\">{body}</div>\n </section>\n )\n}\n\nexport const RecordCanvas = (props: { body: JSXChild }): TemplateResult => (\n <div data-ui=\"record-canvas\">\n <div data-ui=\"record-content\">{props.body}</div>\n </div>\n)\n\nexport const RecordSection = (props: { body: JSXChild; title?: string | null }): TemplateResult => (\n <section data-ui=\"record-section\">\n {!!props.title && <h2 data-ui=\"record-section-title\">{props.title}</h2>}\n {props.body}\n </section>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport type { FieldProps } from './index.tsx'\n\nexport const NativeFieldControl = (props: FieldProps, describedBy: string | null): TemplateResult => {\n if (props.control !== undefined) return <>{props.control}</>\n if (props.type === 'textarea')\n return (\n <textarea\n data-ui=\"field-control\"\n data-appearance={props.appearance === 'embedded' ? 'embedded' : null}\n id={props.id}\n name={props.name}\n placeholder={props.placeholder ?? null}\n required={props.required === true}\n disabled={props.disabled === true}\n readonly={props.readOnly === true}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={describedBy}\n >\n {String(props.value ?? '')}\n </textarea>\n )\n if (props.type === 'select')\n return (\n <select\n data-ui=\"field-control\"\n data-appearance={props.appearance === 'embedded' ? 'embedded' : null}\n id={props.id}\n name={props.name}\n required={props.required === true}\n disabled={props.disabled === true}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={describedBy}\n >\n {each(\n props.options ?? [],\n (option) => option.value,\n (option) => (\n <option\n value={option.value}\n selected={String(props.value ?? '') === option.value}\n disabled={option.disabled === true}\n >\n {option.label}\n </option>\n ),\n )}\n </select>\n )\n return (\n <input\n data-ui=\"field-control\"\n data-appearance={props.appearance === 'embedded' ? 'embedded' : null}\n id={props.id}\n type={props.type === 'decimal' ? 'number' : (props.type ?? 'text')}\n name={props.name}\n value={props.type === 'checkbox' ? '1' : String(props.value ?? '')}\n checked={props.type === 'checkbox' && (props.value === true || props.value === '1')}\n placeholder={props.placeholder ?? null}\n required={props.required === true}\n disabled={props.disabled === true}\n readonly={props.readOnly === true}\n min={props.min}\n max={props.max}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={describedBy}\n autocomplete={props.autocomplete ?? 'off'}\n step={props.type === 'decimal' ? (props.step ?? 'any') : (props.step ?? null)}\n />\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'breadcrumbs',\n 'breadcrumb',\n 'breadcrumb-overflow',\n 'breadcrumb-overflow-trigger',\n 'breadcrumb-overflow-list',\n 'nav-list',\n 'nav-item',\n 'nav-item-leading',\n 'nav-item-label',\n 'nav-item-count',\n 'tabs',\n 'tab',\n 'tab-count',\n 'tabbed-view',\n 'tabbed-view-context',\n 'tab-panel',\n] as const\n\nexport type BreadcrumbItem = {\n id?: string\n label: string\n href?: string\n}\n\ntype BreadcrumbOverflow = {\n id: '__breadcrumb-overflow'\n kind: 'overflow'\n items: readonly BreadcrumbItem[]\n}\n\nexport type BreadcrumbsProps = {\n label: string\n items: readonly BreadcrumbItem[]\n /** Keep the first and current locations visible; put intermediate ancestors in a native disclosure. */\n maxItems?: number\n /** Accessible label for the collapsed ancestor trigger, supplied in the application's locale. */\n overflowLabel?: string\n}\n\nexport const Breadcrumbs = (props: BreadcrumbsProps): TemplateResult => {\n const limit = Math.max(3, Math.floor(props.maxItems ?? props.items.length))\n const overflow = props.items.length > limit\n const omitted = overflow ? props.items.slice(1, props.items.length - (limit - 2)) : []\n const visible: readonly (BreadcrumbItem | BreadcrumbOverflow)[] = overflow\n ? [\n props.items[0]!,\n { id: '__breadcrumb-overflow', kind: 'overflow', items: omitted },\n ...props.items.slice(props.items.length - (limit - 2)),\n ]\n : props.items\n\n return (\n <nav data-ui=\"breadcrumbs\" data-pattern=\"breadcrumbs\" aria-label={props.label}>\n <ol>\n {each(\n visible,\n (item, index) => item.id ?? `${index}:${'label' in item ? item.label : 'overflow'}`,\n (item, index) =>\n 'kind' in item ? (\n <li data-ui=\"breadcrumb-overflow\">\n <details>\n <summary\n data-ui=\"breadcrumb-overflow-trigger\"\n aria-label={props.overflowLabel ?? props.label}\n >\n \u2026\n </summary>\n <ol data-ui=\"breadcrumb-overflow-list\">\n {each(\n item.items,\n (ancestor, ancestorIndex) => ancestor.id ?? `${ancestorIndex}:${ancestor.label}`,\n (ancestor) => (\n <li>\n {ancestor.href ? (\n <a href={ancestor.href}>{ancestor.label}</a>\n ) : (\n <span>{ancestor.label}</span>\n )}\n </li>\n ),\n )}\n </ol>\n </details>\n </li>\n ) : (\n <li data-ui=\"breadcrumb\">\n {item.href !== undefined && index < visible.length - 1 ? (\n <a href={item.href}>{item.label}</a>\n ) : (\n <span aria-current={index === visible.length - 1 ? 'page' : null}>{item.label}</span>\n )}\n </li>\n ),\n )}\n </ol>\n </nav>\n )\n}\n\nexport type NavItemProps = {\n label: string\n href: string\n leading?: JSXChild\n count?: number | string\n active?: boolean\n}\n\nexport const NavItem = (props: NavItemProps): TemplateResult => (\n <a\n data-ui=\"nav-item\"\n data-active={props.active === true ? 'true' : null}\n href={props.href}\n aria-current={props.active === true ? 'page' : null}\n >\n {props.leading !== undefined && (\n <span data-ui=\"nav-item-leading\" aria-hidden=\"true\">\n {props.leading}\n </span>\n )}\n <span data-ui=\"nav-item-label\">{props.label}</span>\n {props.count !== undefined && <span data-ui=\"nav-item-count\">{String(props.count)}</span>}\n </a>\n)\n\nexport const NavList = (props: { label: string; items: readonly NavItemProps[] }): TemplateResult => (\n <nav data-ui=\"nav-list\" aria-label={props.label}>\n {each(\n props.items,\n (item) => `${item.href}:${item.label}`,\n (item) => (\n <NavItem {...item} />\n ),\n )}\n </nav>\n)\n\nexport type TabItem = {\n id: string\n label: string\n href: string\n /** A number, or a bounded label such as \"10.000+\" when the exact total is not counted. */\n count?: number | string\n active?: boolean\n}\n\nexport type TabProps = TabItem & {\n /** DOM id used to associate this navigation item with the active panel. */\n elementId?: string\n /** Id of the panel this navigation item opens. */\n controls?: string\n}\n\nexport const Tab = (props: TabProps): TemplateResult => (\n <a\n data-ui=\"tab\"\n data-active={props.active === true ? 'true' : null}\n id={props.elementId}\n href={props.href}\n aria-current={props.active === true ? 'page' : null}\n aria-controls={props.controls}\n >\n {props.label}\n {props.count !== undefined && <span data-ui=\"tab-count\">{String(props.count)}</span>}\n </a>\n)\n\nexport type TabsProps = {\n /** Stable id used to associate the active tab with a TabPanel. */\n id?: string\n label: string\n items: readonly TabItem[]\n extension?: JSXChild\n /** Panel controlled by these route-navigation tabs. */\n panelId?: string\n}\n\nconst tabElementId = (tabsId: string, itemId: string): string =>\n `${tabsId}-${itemId.replace(/[^a-zA-Z0-9_-]+/gu, '-')}-tab`\n\nexport const Tabs = (props: TabsProps): TemplateResult => (\n <nav data-ui=\"tabs\" data-pattern=\"tabs\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <Tab\n {...item}\n elementId={props.id ? tabElementId(props.id, item.id) : undefined}\n controls={props.panelId}\n />\n ),\n )}\n {props.extension}\n </nav>\n)\n\nexport type TabPanelProps = {\n id?: string\n labelledBy?: string\n body: JSXChild\n}\n\n/**\n * Content associated with route-navigation tabs. It owns scrolling and vertical\n * rhythm, but deliberately adds no left or right padding.\n */\nexport const TabPanel = (props: TabPanelProps): TemplateResult => (\n <section data-ui=\"tab-panel\" id={props.id} aria-labelledby={props.labelledBy} tabindex=\"-1\">\n {props.body}\n </section>\n)\n\nexport type TabbedViewProps = {\n id: string\n label: string\n items: readonly TabItem[]\n body: JSXChild\n /** Stable content above the tab bar, such as a record summary or issues. */\n context?: JSXChild\n extension?: JSXChild\n}\n\n/** A complete tabbed region with one stable navigation bar and one scrolling panel. */\nexport const TabbedView = (props: TabbedViewProps): TemplateResult => {\n const tabsId = `${props.id}-tabs`\n const panelId = `${props.id}-panel`\n const active = props.items.find((item) => item.active === true)\n return (\n <div data-ui=\"tabbed-view\">\n {props.context !== undefined && props.context !== '' ? (\n <div data-ui=\"tabbed-view-context\">{props.context}</div>\n ) : (\n ''\n )}\n <Tabs\n id={tabsId}\n label={props.label}\n items={props.items}\n extension={props.extension}\n panelId={panelId}\n />\n <TabPanel\n id={panelId}\n labelledBy={active ? tabElementId(tabsId, active.id) : undefined}\n body={props.body}\n />\n </div>\n )\n}\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'progress',\n 'progress-label',\n 'progress-value',\n 'progress-track',\n 'progress-bar',\n] as const\n\nexport type ProgressTone = 'primary' | 'positive' | 'warning' | 'danger'\n\nexport const Progress = (props: {\n value: number\n label: string\n tone?: ProgressTone\n showValue?: boolean\n}): TemplateResult => {\n const value = Number.isFinite(props.value) ? Math.max(0, Math.min(100, props.value)) : 0\n return (\n <div data-ui=\"progress\" data-tone={props.tone ?? 'primary'}>\n {(props.showValue ?? true) && (\n <div data-ui=\"progress-label\">\n <span>{props.label}</span>\n <span data-ui=\"progress-value\">{`${value}%`}</span>\n </div>\n )}\n <div\n data-ui=\"progress-track\"\n role=\"progressbar\"\n aria-label={props.label}\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n aria-valuenow={String(value)}\n >\n <span data-ui=\"progress-bar\" style={`width: ${value}%`} />\n </div>\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'menu',\n 'menu-trigger',\n 'menu-trigger-count',\n 'menu-panel',\n 'menu-search',\n 'menu-search-input',\n 'menu-label',\n 'menu-separator',\n 'menu-item',\n 'menu-item-leading',\n 'menu-item-copy',\n 'menu-item-check',\n 'menu-item-shortcut',\n] as const\n\nexport type MenuItem = {\n id: string\n kind?: 'item'\n label: string\n href?: string\n name?: string\n value?: string\n form?: string\n leading?: JSXChild\n description?: string\n disabled?: boolean\n destructive?: boolean\n /** Turns the command into a menuitemcheckbox while preserving server-owned state. */\n checked?: boolean\n /** Visible keyboard hint; the browser interaction remains owned by the application. */\n shortcut?: string\n}\n\nexport type MenuLabel = { id: string; kind: 'label'; label: string }\nexport type MenuSeparator = { id: string; kind: 'separator' }\nexport type MenuEntry = MenuItem | MenuLabel | MenuSeparator\n\n/**\n * A search field at the top of the panel, for a menu that picks from more choices\n * than fit at once \u2014 people, tags. It is a GET form: the server narrows the items\n * and renders the menu open again, so the list and what is picked stay server-owned.\n */\nexport type MenuSearch = {\n action: string\n name: string\n value?: string\n label: string\n placeholder?: string\n /** Query parameters the search keeps, so narrowing the list does not drop a filter. */\n hidden?: Readonly<Record<string, string>>\n}\n\nexport type MenuProps = {\n id: string\n label: string\n items: readonly MenuEntry[]\n /** Replaces the label inside the trigger \u2014 an icon, say. The label still names it. */\n trigger?: JSXChild\n open?: boolean\n align?: 'start' | 'end'\n /** `compact` sits in a row of list facets at their height. */\n size?: 'default' | 'compact'\n /** How many choices are in effect, shown on the trigger. Zero shows nothing. */\n count?: number\n search?: MenuSearch\n /**\n * `bottom` (default) opens below the trigger. `top` opens above it, for a\n * trigger near the bottom of a container that clips overflow \u2014 a fixed\n * modal footer, say \u2014 where the panel would otherwise render past the\n * container's edge and be cut off rather than merely overlapping content.\n */\n placement?: 'top' | 'bottom'\n}\n\nexport const Menu = (props: MenuProps): TemplateResult => (\n <details\n data-ui=\"menu\"\n data-align={props.align ?? 'start'}\n data-size={props.size === 'compact' ? 'compact' : null}\n data-active={props.count ? 'true' : null}\n data-placement={props.placement ?? 'bottom'}\n open={props.open === true ? true : undefined}\n >\n {/* `details` exposes the open state natively; the runtime mirrors it into aria-expanded on toggle. */}\n <summary\n data-ui=\"menu-trigger\"\n aria-haspopup=\"menu\"\n aria-controls={`${props.id}-panel`}\n // A trigger that is only an icon still needs a name, and a pointer needs a hint.\n aria-label={props.trigger !== undefined ? props.label : undefined}\n title={props.trigger !== undefined ? props.label : undefined}\n >\n {props.trigger ?? props.label}\n {!!props.count && <span data-ui=\"menu-trigger-count\">{String(props.count)}</span>}\n </summary>\n <div data-ui=\"menu-panel\" id={`${props.id}-panel`} role=\"menu\" aria-label={props.label}>\n {props.search && (\n <form data-ui=\"menu-search\" role=\"search\" method=\"get\" action={props.search.action}>\n {each(\n Object.entries(props.search.hidden ?? {}),\n ([name]) => name,\n ([name, value]) => (\n <input type=\"hidden\" name={name} value={value} />\n ),\n )}\n <input\n data-ui=\"menu-search-input\"\n type=\"search\"\n name={props.search.name}\n value={props.search.value ?? ''}\n placeholder={props.search.placeholder ?? props.search.label}\n aria-label={props.search.label}\n autocomplete=\"off\"\n />\n </form>\n )}\n {each(\n props.items,\n (item) => item.id,\n (item) => {\n if (item.kind === 'separator') return <hr data-ui=\"menu-separator\" />\n if (item.kind === 'label')\n return (\n <span data-ui=\"menu-label\" role=\"presentation\">\n {item.label}\n </span>\n )\n const role = item.checked === undefined ? 'menuitem' : 'menuitemcheckbox'\n const content = (\n <>\n {item.checked !== undefined && (\n <span data-ui=\"menu-item-check\" aria-hidden=\"true\">\n {item.checked ? '\u2713' : ''}\n </span>\n )}\n {item.leading !== undefined && (\n <span data-ui=\"menu-item-leading\" aria-hidden=\"true\">\n {item.leading}\n </span>\n )}\n <span data-ui=\"menu-item-copy\">\n <span>{item.label}</span>\n {item.description && <small>{item.description}</small>}\n </span>\n {item.shortcut && <kbd data-ui=\"menu-item-shortcut\">{item.shortcut}</kbd>}\n </>\n )\n if (item.disabled)\n return (\n // biome-ignore lint/a11y/useAriaPropsSupportedByRole: `role` is menuitemcheckbox whenever aria-checked is set; otherwise aria-checked is null and not rendered.\n <span\n data-ui=\"menu-item\"\n role={role}\n aria-checked={item.checked === undefined ? null : String(item.checked)}\n aria-disabled=\"true\"\n tabIndex=\"-1\"\n >\n {content}\n </span>\n )\n if (item.href)\n return (\n // biome-ignore lint/a11y/useAriaPropsSupportedByRole: `role` is menuitemcheckbox whenever aria-checked is set; otherwise aria-checked is null and not rendered.\n <a\n data-ui=\"menu-item\"\n data-destructive={item.destructive ? 'true' : null}\n role={role}\n aria-checked={item.checked === undefined ? null : String(item.checked)}\n href={item.href}\n >\n {content}\n </a>\n )\n return (\n // biome-ignore lint/a11y/useAriaPropsSupportedByRole: `role` is menuitemcheckbox whenever aria-checked is set; otherwise aria-checked is null and not rendered.\n <button\n data-ui=\"menu-item\"\n data-destructive={item.destructive ? 'true' : null}\n role={role}\n aria-checked={item.checked === undefined ? null : String(item.checked)}\n type=\"submit\"\n name={item.name ?? 'intent'}\n value={item.value ?? item.id}\n form={item.form ?? null}\n >\n {content}\n </button>\n )\n },\n )}\n </div>\n </details>\n)\n\nexport const ActionMenu = (props: Omit<MenuProps, 'trigger'> & { triggerLabel?: string }): TemplateResult => (\n <Menu\n {...props}\n trigger={<span>{props.triggerLabel ?? props.label} \u00B7\u00B7\u00B7</span>}\n align={props.align ?? 'end'}\n />\n)\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['popover', 'popover-trigger', 'popover-panel', 'popover-close'] as const\n\nexport type PopoverProps = {\n id: string\n label: string\n trigger: JSXChild\n body: JSXChild\n open: boolean\n openHref: string\n closeHref: string\n closeLabel: string\n placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'\n}\n\nexport const Popover = (props: PopoverProps): TemplateResult => (\n <span data-ui=\"popover\" data-open={String(props.open)} data-placement={props.placement ?? 'bottom-start'}>\n <a\n data-ui=\"popover-trigger\"\n href={props.open ? props.closeHref : props.openHref}\n aria-haspopup=\"dialog\"\n aria-expanded={String(props.open)}\n aria-controls={`${props.id}-panel`}\n >\n {props.trigger}\n </a>\n {props.open && (\n <aside\n data-ui=\"popover-panel\"\n id={`${props.id}-panel`}\n role=\"dialog\"\n aria-label={props.label}\n tabindex=\"-1\"\n >\n <a data-ui=\"popover-close\" href={props.closeHref} aria-label={props.closeLabel}>\n \u00D7\n </a>\n {props.body}\n </aside>\n )}\n </span>\n)\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['tooltip', 'tooltip-trigger', 'tooltip-content'] as const\n\nexport const Tooltip = (props: {\n id: string\n text: string\n trigger: JSXChild\n placement?: 'top' | 'right' | 'bottom' | 'left'\n}): TemplateResult => (\n <span data-ui=\"tooltip\" data-placement={props.placement ?? 'top'}>\n <span data-ui=\"tooltip-trigger\" aria-describedby={props.id}>\n {props.trigger}\n </span>\n <span data-ui=\"tooltip-content\" id={props.id} role=\"tooltip\">\n {props.text}\n </span>\n </span>\n)\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { ActionGroup, Button, LinkButton } from '../../primitives/actions/index.tsx'\nimport type { ActionVariant } from '../../primitives/actions/index.tsx'\nimport { ModalSheet } from '../../patterns/modal-sheet/index.tsx'\n\nexport const HOOKS = ['dialog', 'confirm-dialog-message'] as const\n\nexport type DialogProps = {\n id: string\n title: string\n body: JSXChild\n closeHref: string\n closeLabel: string\n description?: string\n actions?: JSXChild\n mode?: 'overlay' | 'embedded'\n unsavedPrompt?: string\n}\n\nexport const Dialog = (props: DialogProps): TemplateResult => (\n <div data-ui=\"dialog\">\n <ModalSheet {...props} presentation=\"dialog\" />\n </div>\n)\n\nexport type ConfirmDialogProps = Omit<DialogProps, 'body' | 'actions'> & {\n message: string\n confirmLabel: string\n confirmName?: string\n confirmValue?: string\n confirmForm?: string\n confirmVariant?: ActionVariant\n}\n\nexport const ConfirmDialog = (props: ConfirmDialogProps): TemplateResult => (\n <Dialog\n {...props}\n body={<p data-ui=\"confirm-dialog-message\">{props.message}</p>}\n actions={\n <ActionGroup\n label={props.title}\n actions={[\n <LinkButton label={props.closeLabel} href={props.closeHref} />,\n <Button\n label={props.confirmLabel}\n variant={props.confirmVariant ?? 'destructive'}\n type=\"submit\"\n name={props.confirmName ?? 'intent'}\n value={props.confirmValue ?? 'confirm'}\n form={props.confirmForm ?? null}\n />,\n ]}\n />\n }\n />\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport type { Tone } from '../../primitives/status/index.tsx'\n\nexport const HOOKS = [\n 'toast-region',\n 'toast',\n 'toast-copy',\n 'toast-title',\n 'toast-message',\n 'toast-actions',\n] as const\n\nexport type ToastProps = {\n id: string\n title: string\n message?: string\n tone?: Tone\n actions?: JSXChild\n}\n\nexport const Toast = (props: ToastProps): TemplateResult => (\n <article\n data-ui=\"toast\"\n data-tone={props.tone ?? 'neutral'}\n role={props.tone === 'danger' ? 'alert' : 'status'}\n >\n <div data-ui=\"toast-copy\">\n <strong data-ui=\"toast-title\">{props.title}</strong>\n {props.message && <p data-ui=\"toast-message\">{props.message}</p>}\n </div>\n {props.actions !== undefined && <div data-ui=\"toast-actions\">{props.actions}</div>}\n </article>\n)\n\nexport const ToastRegion = (props: { label: string; toasts: readonly ToastProps[] }): TemplateResult => (\n <section data-ui=\"toast-region\" aria-label={props.label} aria-live=\"polite\" aria-relevant=\"additions text\">\n {each(\n props.toasts,\n (toast) => toast.id,\n (toast) => (\n <Toast {...toast} />\n ),\n )}\n </section>\n)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['spinner', 'visually-hidden'] as const\n\nexport const Spinner = (props: { label: string; size?: 'small' | 'default' | 'large' }): TemplateResult => (\n <span data-ui=\"spinner\" data-size={props.size ?? 'default'} role=\"status\">\n <span data-ui=\"visually-hidden\">{props.label}</span>\n </span>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['skeleton', 'skeleton-line'] as const\n\nexport const Skeleton = (props: { label: string; lines?: number }): TemplateResult => (\n <div data-ui=\"skeleton\" role=\"status\" aria-label={props.label}>\n {each(\n Array.from({ length: Math.max(1, props.lines ?? 3) }),\n (_, index) => index,\n (_, index) => (\n <span data-ui=\"skeleton-line\" data-line={String(index + 1)} />\n ),\n )}\n </div>\n)\n", "// Unlike its neighbors here, this is a ketjs-view *island*: it owns reactive state\n// (`signal`) and fetches its own data (`/_ket/fn/...`, a ketjs runtime contract, not\n// an application-specific one) rather than being a static, server-rendered control\n// wired up by the shared `attachDesignSystemInteractions` delegator. A consuming app\n// mounts it through its own island runtime (see `defineIsland` in `@ketvietlab/ketjs-view`)\n// and supplies the `manager` config that names which functions to call.\n\nimport { each, effect, signal } from '@ketvietlab/ketjs-view'\nimport type { IslandController, IslandProps, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'relation-select',\n 'relation-native',\n 'relation-trigger',\n 'relation-value',\n 'relation-caret',\n 'relation-chips',\n 'relation-chip',\n 'relation-chip-clear',\n 'relation-menu',\n 'relation-search',\n 'relation-options',\n 'relation-option',\n 'relation-footer',\n 'relation-empty',\n 'relation-editor',\n 'relation-dialog-toolbar',\n 'relation-search-field',\n 'relation-search-icon',\n 'relation-dialog-list',\n 'relation-dialog-row',\n 'relation-dialog-main',\n 'relation-dialog-meta',\n 'relation-dialog-actions',\n] as const\n\nexport type RelationOption = { value: string; label: string; description?: string | null }\n\nexport type RelationEditorField = {\n name: string\n label: string\n type?: 'text' | 'email' | 'tel' | 'select'\n required?: boolean\n options?: RelationOption[]\n}\n\nexport type RelationManager = {\n listFunction: string\n listInput?: Record<string, unknown>\n searchParam?: string\n limitParam?: string\n limit?: number\n idField?: string\n labelField?: string\n descriptionField?: string\n saveFunction?: string\n saveDefaults?: Record<string, unknown>\n removeFunction?: string\n removeDefaults?: Record<string, unknown>\n fields?: RelationEditorField[]\n excludeIds?: string[]\n}\n\nexport type RelationSelectLabels = {\n choose: string\n search: string\n more: string\n noRecords: string\n loading: string\n loadError: string\n dialogTitle: string\n close: string\n select: string\n create: string\n edit: string\n save: string\n cancel: string\n remove: string\n confirmRemove: string\n retry: string\n clear: string\n chosen: string\n}\n\nexport type RelationSelectConfig = {\n name: string\n ariaLabel: string\n value?: string | null\n /**\n * Several records under one field name, joined by a comma on the way out.\n *\n * A repeated form key would not survive `readForm`, which builds a\n * `Record<string, string>` and keeps only the last value \u2014 so a multi-valued\n * field posts one comma-separated string, which is also what the routes that\n * take value lists already parse.\n */\n multiple?: boolean\n values?: string[]\n options: RelationOption[]\n required?: boolean\n disabled?: boolean\n labels: RelationSelectLabels\n manager?: RelationManager\n}\n\n/** Client-only composition hooks; keep callbacks out of serialized island props. */\nexport type RelationSelectCallbacks = {\n options?: () => RelationOption[]\n disabled?: () => boolean\n onSelect?: (option: RelationOption) => void\n onCreate?: (query: string) => void\n /** An add-to-list picker returns to its placeholder after each selection. */\n resetAfterSelect?: boolean\n}\n\ntype RelationRow = Record<string, unknown>\ntype RelationEditor = { id: string; row: RelationRow }\ntype RelationSelectIslandProps = IslandProps & { id: string; config: RelationSelectConfig }\ntype ApiPayload = {\n ok?: boolean\n value?: unknown\n message?: unknown\n errors?: Array<{ message?: unknown; code?: unknown }>\n}\n\nconst array = <Value,>(value: unknown): Value[] => (Array.isArray(value) ? (value as Value[]) : [])\nconst string = (value: unknown): string => (value == null ? '' : String(value))\n\nconst callApi = async (name: string, input: unknown, requestSignal?: AbortSignal): Promise<unknown> => {\n const response = await fetch(`/_ket/fn/${encodeURIComponent(name)}`, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(input),\n signal: requestSignal,\n })\n const payload = (await response.json()) as ApiPayload\n if (!response.ok || payload.ok === false) {\n const domainError = array<{ message?: unknown; code?: unknown }>(payload.errors)[0]\n throw new Error(\n string(domainError?.message ?? domainError?.code ?? payload.message ?? `HTTP ${response.status}`),\n )\n }\n return payload.value\n}\n\nexport function createRelationSelectView(\n props: RelationSelectIslandProps,\n callbacks: RelationSelectCallbacks = {},\n): IslandController {\n const { id: islandId, config } = props\n const labels = config.labels\n const manager = config.manager\n // One representation for both modes: an array of chosen ids. Single-select is\n // the array of length 0 or 1, so downstream code only branches where the UI\n // genuinely differs \u2014 the trigger and the chips.\n const multiple = config.multiple === true\n const chosen = signal(\n multiple\n ? array<unknown>(config.values).map(string).filter(Boolean)\n : [string(config.value)].filter(Boolean),\n )\n const selected = (): string => chosen()[0] ?? ''\n const isChosen = (value: unknown): boolean => chosen().includes(string(value))\n const options = signal<RelationOption[]>(array<RelationOption>(config.options))\n const availableOptions = (): RelationOption[] => callbacks.options?.() ?? options()\n const disabled = (): boolean => callbacks.disabled?.() ?? config.disabled === true\n const open = signal(false)\n const dialog = signal(false)\n const query = signal('')\n const rows = signal<RelationRow[]>([])\n const loading = signal(false)\n const error = signal('')\n const editor = signal<RelationEditor | null>(null)\n const pendingRemove = signal('')\n let activeRequest: AbortController | null = null\n let searchTimer: ReturnType<typeof setTimeout> | undefined\n let disposed = false\n\n const rowId = (row: RelationRow): string => string(row[manager?.idField ?? 'id'])\n const rowLabel = (row: RelationRow): string =>\n string(row[manager?.labelField ?? 'name']) || labels.noRecords\n const rowDescription = (row: RelationRow): string => string(row[manager?.descriptionField ?? 'ref'])\n const selectedLabel = (): string =>\n options().find((entry) => string(entry.value) === selected())?.label ?? labels.choose\n\n const filteredOptions = (): RelationOption[] => {\n const needle = query().trim().toLocaleLowerCase()\n const held = needle\n ? availableOptions().filter((entry) =>\n `${string(entry.label)} ${string(entry.description)}`.toLocaleLowerCase().includes(needle),\n )\n : availableOptions()\n return callbacks.options ? held : held.slice(0, 7)\n }\n\n const choose = (value: unknown, label: unknown, description: unknown = ''): void => {\n if (disabled()) return\n const id = string(value)\n if (!options().some((entry) => string(entry.value) === id))\n options.set([...options(), { value: id, label: string(label), description: string(description) }])\n if (multiple) {\n chosen.set(isChosen(id) ? chosen().filter((held) => held !== id) : [...chosen(), id])\n callbacks.onSelect?.({ value: id, label: string(label), description: string(description) })\n return\n }\n chosen.set(id ? [id] : [])\n open.set(false)\n dialog.set(false)\n query.set('')\n editor.set(null)\n pendingRemove.set('')\n if (callbacks.resetAfterSelect) chosen.set([])\n callbacks.onSelect?.({ value: id, label: string(label), description: string(description) })\n }\n\n const unchoose = (value: unknown): void => {\n chosen.set(chosen().filter((held) => held !== string(value)))\n }\n\n const labelOf = (value: unknown): string =>\n options().find((entry) => string(entry.value) === string(value))?.label ?? string(value)\n\n const loadRows = async (): Promise<void> => {\n if (!manager?.listFunction) return\n activeRequest?.abort()\n const request = new AbortController()\n activeRequest = request\n loading.set(true)\n error.set('')\n try {\n const input: Record<string, unknown> = { ...(manager.listInput ?? {}) }\n input[manager.searchParam ?? 'search'] = query().trim()\n input[manager.limitParam ?? 'limit'] = manager.limit ?? 80\n const value = await callApi(manager.listFunction, input, request.signal)\n const excluded = new Set(array<unknown>(manager.excludeIds).map(string))\n rows.set(array<RelationRow>(value).filter((row) => !excluded.has(rowId(row))))\n } catch (caught) {\n if (disposed || (caught instanceof Error && caught.name === 'AbortError')) return\n error.set(caught instanceof Error ? caught.message : labels.loadError)\n } finally {\n if (activeRequest === request) loading.set(false)\n }\n }\n\n const openDialog = async (): Promise<void> => {\n open.set(false)\n dialog.set(true)\n editor.set(null)\n pendingRemove.set('')\n await loadRows()\n }\n\n const closeDialog = (): void => {\n dialog.set(false)\n editor.set(null)\n pendingRemove.set('')\n error.set('')\n }\n\n const searchManager = (event: Event): void => {\n if (!(event.currentTarget instanceof HTMLInputElement)) return\n query.set(event.currentTarget.value)\n clearTimeout(searchTimer)\n searchTimer = setTimeout(loadRows, 180)\n }\n\n const save = async (event: Event): Promise<void> => {\n event.preventDefault()\n if (!manager?.saveFunction || !(event.currentTarget instanceof HTMLFormElement)) return\n const editing = editor()\n const id = editing?.id || crypto.randomUUID()\n const payload: Record<string, unknown> = { ...(manager.saveDefaults ?? {}), id }\n for (const [name, value] of new FormData(event.currentTarget).entries()) payload[name] = string(value)\n loading.set(true)\n error.set('')\n try {\n await callApi(manager.saveFunction, payload)\n const label = string(payload[manager.labelField ?? 'name'])\n if (!editing?.id) {\n choose(id, label)\n if (multiple) {\n editor.set(null)\n await loadRows()\n }\n return\n }\n options.set(\n options().map((entry) =>\n string(entry.value) === id ? { ...entry, label: label || entry.label } : entry,\n ),\n )\n editor.set(null)\n await loadRows()\n } catch (caught) {\n error.set(caught instanceof Error ? caught.message : labels.loadError)\n } finally {\n loading.set(false)\n }\n }\n\n const remove = async (row: RelationRow): Promise<void> => {\n const id = rowId(row)\n if (pendingRemove() !== id) {\n pendingRemove.set(id)\n return\n }\n if (!manager?.removeFunction) return\n loading.set(true)\n error.set('')\n try {\n await callApi(manager.removeFunction, { ...(manager.removeDefaults ?? {}), id })\n unchoose(id)\n options.set(options().filter((entry) => string(entry.value) !== id))\n pendingRemove.set('')\n await loadRows()\n } catch (caught) {\n error.set(caught instanceof Error ? caught.message : labels.loadError)\n } finally {\n loading.set(false)\n }\n }\n\n const editorField = (field: RelationEditorField): TemplateResult => {\n const held = editor()?.row ?? manager?.saveDefaults ?? {}\n const value = string(held[field.name])\n const fieldId = `${islandId}-relation-editor-${field.name}`\n return (\n <label data-ui=\"form-field\" data-span=\"full\" htmlFor={fieldId}>\n <span data-ui=\"form-label\">\n {field.label}\n {field.required ? (\n <span data-ui=\"form-required\" aria-hidden=\"true\">\n {' '}\n *\n </span>\n ) : null}\n </span>\n {field.type === 'select' ? (\n <select data-ui=\"form-control\" id={fieldId} name={field.name} required={field.required === true}>\n {each(\n array<RelationOption>(field.options),\n (entry) => entry.value,\n (entry) => (\n <option value={entry.value} selected={string(entry.value) === value}>\n {entry.label}\n </option>\n ),\n )}\n </select>\n ) : (\n <input\n data-ui=\"form-control\"\n id={fieldId}\n type={field.type ?? 'text'}\n name={field.name}\n value={value}\n autocomplete=\"off\"\n required={field.required === true}\n />\n )}\n </label>\n )\n }\n\n const editorForm = (): TemplateResult => (\n <form data-ui=\"relation-editor\" onSubmit={save}>\n <div data-ui=\"form-grid\">\n {each(array<RelationEditorField>(manager?.fields), (field) => field.name, editorField)}\n </div>\n <div data-ui=\"form-actions\">\n <div data-ui=\"action-group\">\n <button data-ui=\"action\" data-variant=\"primary\" type=\"submit\" disabled={loading()}>\n {labels.save}\n </button>\n <button data-ui=\"action\" data-variant=\"tertiary\" type=\"button\" onClick={() => editor.set(null)}>\n {labels.cancel}\n </button>\n </div>\n </div>\n </form>\n )\n\n const managerRow = (row: RelationRow): TemplateResult => {\n const id = rowId(row)\n const description = rowDescription(row)\n return (\n <li data-ui=\"relation-dialog-row\" data-selected={String(isChosen(id))}>\n <button\n data-ui=\"relation-dialog-main\"\n type=\"button\"\n aria-pressed={multiple ? String(isChosen(id)) : null}\n onClick={() => choose(id, rowLabel(row), description)}\n >\n <strong>{rowLabel(row)}</strong>\n {description ? <small data-ui=\"relation-dialog-meta\">{description}</small> : null}\n </button>\n <div data-ui=\"relation-dialog-actions\">\n <button\n data-ui=\"action\"\n data-size=\"compact\"\n data-variant={isChosen(id) ? 'primary' : 'secondary'}\n type=\"button\"\n onClick={() => choose(id, rowLabel(row), description)}\n >\n {isChosen(id) ? labels.chosen : labels.select}\n </button>\n {manager?.saveFunction ? (\n <button\n data-ui=\"action\"\n data-size=\"compact\"\n data-variant=\"tertiary\"\n type=\"button\"\n onClick={() => editor.set({ id, row })}\n >\n {labels.edit}\n </button>\n ) : null}\n {manager?.removeFunction ? (\n <button\n data-ui=\"action\"\n data-size=\"compact\"\n data-variant=\"destructive\"\n type=\"button\"\n onClick={() => remove(row)}\n >\n {pendingRemove() === id ? labels.confirmRemove : labels.remove}\n </button>\n ) : null}\n </div>\n </li>\n )\n }\n\n const dialogView = (): TemplateResult => (\n <div data-ui=\"modal-layer\" data-presentation=\"dialog\">\n <button data-ui=\"modal-backdrop\" type=\"button\" aria-label={labels.close} onClick={closeDialog}>\n <span>{labels.close}</span>\n </button>\n <section\n data-ui=\"modal-sheet\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={`${islandId}-relation-title`}\n >\n <header data-ui=\"modal-head\">\n <h2 data-ui=\"modal-title\" id={`${islandId}-relation-title`}>\n {labels.dialogTitle}\n </h2>\n <button\n data-ui=\"modal-close\"\n type=\"button\"\n aria-label={labels.close}\n title={labels.close}\n onClick={closeDialog}\n >\n \u00D7\n </button>\n </header>\n <div data-ui=\"modal-body\">\n <div data-ui=\"relation-dialog-toolbar\">\n <div data-ui=\"relation-search-field\">\n <span data-ui=\"relation-search-icon\" aria-hidden=\"true\">\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <circle cx=\"11\" cy=\"11\" r=\"8\" />\n <path d=\"m21 21-4.3-4.3\" />\n </svg>\n </span>\n <input\n data-ui=\"field-control\"\n type=\"search\"\n value={query()}\n autocomplete=\"off\"\n placeholder={labels.search}\n aria-label={labels.search}\n onInput={searchManager}\n />\n </div>\n {manager?.saveFunction ? (\n <button\n data-ui=\"action\"\n data-variant=\"primary\"\n type=\"button\"\n onClick={() => editor.set({ id: '', row: manager.saveDefaults ?? {} })}\n >\n {labels.create}\n </button>\n ) : null}\n </div>\n {error() ? (\n <aside data-ui=\"notice\" data-tone=\"danger\" role=\"alert\">\n <div data-ui=\"notice-copy\">\n <p data-ui=\"notice-title\">{labels.loadError}</p>\n <p data-ui=\"notice-message\">{error()}</p>\n </div>\n <button data-ui=\"action\" data-variant=\"secondary\" type=\"button\" onClick={loadRows}>\n {labels.retry}\n </button>\n </aside>\n ) : null}\n {editor() ? editorForm() : null}\n {loading() ? (\n <p data-ui=\"relation-empty\" role=\"status\">\n {labels.loading}\n </p>\n ) : null}\n {!loading() && !editor() ? (\n rows().length ? (\n <ul data-ui=\"relation-dialog-list\">{each(rows(), rowId, managerRow)}</ul>\n ) : (\n <p data-ui=\"relation-empty\">{labels.noRecords}</p>\n )\n ) : null}\n </div>\n </section>\n </div>\n )\n\n const handleKeydown = (event: Event): void => {\n if (!(event instanceof KeyboardEvent) || event.key !== 'Escape') return\n if (!open() && !dialog()) return\n event.preventDefault()\n event.stopPropagation()\n const trigger = (event.currentTarget as HTMLElement | null)?.querySelector<HTMLElement>(\n '[data-ui=\"relation-trigger\"]',\n )\n open.set(false)\n closeDialog()\n trigger?.focus()\n }\n\n const handleInvalid = (event: Event): void => {\n event.preventDefault()\n open.set(true)\n if (event.currentTarget instanceof HTMLSelectElement)\n (event.currentTarget.nextElementSibling as HTMLElement | null)?.focus()\n }\n\n return {\n view: () => (\n <div data-ui=\"relation-select\" role=\"group\" aria-label={config.ariaLabel} onKeydown={handleKeydown}>\n {/* biome-ignore lint/a11y/noAriaHiddenOnFocusable: the off-tab native control preserves form submission and constraint validation while the adjacent trigger is the accessible interactive control. */}\n <select\n data-ui=\"relation-native\"\n name={config.name}\n required={config.required === true}\n disabled={disabled()}\n tabindex=\"-1\"\n aria-hidden=\"true\"\n onInvalid={handleInvalid}\n >\n <option value=\"\" selected={!chosen().length} />\n {multiple ? (\n chosen().length ? (\n <option value={chosen().join(',')} selected>\n {chosen().map(labelOf).join(', ')}\n </option>\n ) : null\n ) : (\n each(\n options().filter((entry) => string(entry.value)),\n (entry) => entry.value,\n (entry) => (\n <option value={entry.value} selected={string(entry.value) === selected()}>\n {entry.label}\n </option>\n ),\n )\n )}\n </select>\n <button\n data-ui=\"relation-trigger\"\n data-empty={String(!chosen().length)}\n type=\"button\"\n aria-haspopup=\"listbox\"\n aria-label={config.ariaLabel}\n aria-expanded={String(open())}\n aria-controls={`${islandId}-relation-menu`}\n disabled={disabled()}\n onClick={() => open.set(!open())}\n >\n <span data-ui=\"relation-value\">{multiple ? labels.choose : selectedLabel()}</span>\n <span data-ui=\"relation-caret\" aria-hidden=\"true\">\n \u25BE\n </span>\n </button>\n {multiple && chosen().length ? (\n <ul data-ui=\"relation-chips\" aria-label={labels.chosen}>\n {each(\n chosen(),\n (value) => value,\n (value) => (\n <li data-ui=\"relation-chip\">\n <span>{labelOf(value)}</span>\n <button\n data-ui=\"relation-chip-clear\"\n type=\"button\"\n aria-label={`${labels.clear}: ${labelOf(value)}`}\n title={labels.clear}\n disabled={disabled()}\n onClick={() => unchoose(value)}\n >\n \u00D7\n </button>\n </li>\n ),\n )}\n </ul>\n ) : null}\n {open() ? (\n <div data-ui=\"relation-menu\" id={`${islandId}-relation-menu`}>\n <input\n data-ui=\"relation-search\"\n type=\"search\"\n value={query()}\n autocomplete=\"off\"\n placeholder={labels.search}\n aria-label={labels.search}\n onInput={(event) => {\n if (event.currentTarget instanceof HTMLInputElement) query.set(event.currentTarget.value)\n }}\n />\n <div data-ui=\"relation-options\" role=\"listbox\">\n {filteredOptions().length ? (\n each(\n filteredOptions(),\n (entry) => entry.value,\n (entry) => (\n <button\n data-ui=\"relation-option\"\n type=\"button\"\n role=\"option\"\n data-selected={String(isChosen(entry.value))}\n aria-selected={String(isChosen(entry.value))}\n onClick={() => choose(entry.value, entry.label, entry.description)}\n >\n <span>{entry.label}</span>\n {entry.description ? <small>{entry.description}</small> : null}\n </button>\n ),\n )\n ) : (\n <p data-ui=\"relation-empty\">{labels.noRecords}</p>\n )}\n </div>\n {manager || !callbacks.options || callbacks.onCreate ? (\n <footer data-ui=\"relation-footer\">\n {manager || !callbacks.options ? (\n <button data-ui=\"action\" data-variant=\"tertiary\" type=\"button\" onClick={openDialog}>\n {labels.more}\n </button>\n ) : null}\n {callbacks.onCreate ? (\n <button\n data-ui=\"action\"\n data-variant=\"tertiary\"\n type=\"button\"\n disabled={disabled()}\n onClick={() => {\n if (disabled()) return\n const name = query().trim()\n open.set(false)\n query.set('')\n callbacks.onCreate?.(name)\n }}\n >\n {labels.create}\n </button>\n ) : null}\n </footer>\n ) : null}\n </div>\n ) : null}\n {dialog() ? dialogView() : null}\n </div>\n ),\n // The hidden native select stands in for a real one \u2014 including firing\n // `change` for whatever the host page already listens for there (a state\n // recomputed off another field's selection, say). Skip the effect's own\n // eager first run: nothing has changed yet, only settled onto its initial value.\n // The dispatch itself waits a microtask: firing it synchronously, still\n // inside the signal write that is choosing this very value, let a listener\n // that reacts by re-rendering (and so disposing and re-mounting this same\n // island) re-enter this instance's own `choose()` while it was still on the\n // stack \u2014 deferring runs it only once the choice has fully settled.\n mount: ({ root, lifetime }) => {\n let first = true\n const stop = effect(() => {\n chosen()\n if (first) {\n first = false\n return\n }\n queueMicrotask(() => {\n if (disposed) return\n const native = [...root.querySelectorAll('[data-ui=\"relation-native\"]')][0]\n if (native instanceof HTMLSelectElement)\n native.dispatchEvent(new Event('change', { bubbles: true }))\n })\n })\n lifetime.addEventListener('abort', stop)\n // The inline dropdown isn't a <details> \u2014 nothing in the shared\n // `attachDesignSystemInteractions` outside-click delegator sees it, so it\n // closes itself the same way every other popup does. The dialog already\n // closes on its own backdrop click.\n document.addEventListener(\n 'click',\n (event) => {\n if (!open()) return\n const target = event.target\n if (target instanceof Node && (root as unknown as Node).contains(target)) return\n open.set(false)\n },\n { signal: lifetime },\n )\n },\n dispose: () => {\n disposed = true\n clearTimeout(searchTimer)\n activeRequest?.abort()\n },\n }\n}\n\nexport const relationSelect = (props: IslandProps): IslandController =>\n createRelationSelectView(props as RelationSelectIslandProps)\n", "// A Fancybox-style image viewer, as a ketjs-view island: a thumbnail (or a row of\n// them) that opens a full-screen stage over several images, with zoom (buttons,\n// wheel, keyboard, tap), pan (drag), pinch-to-zoom (two fingers), previous/next\n// (buttons, arrow keys) and a counter.\n//\n// Like relation-select, it owns reactive state rather than being a static control\n// wired by the shared delegator \u2014 zoom level, pan offset and the open item all\n// change on every pointer move. A consuming app mounts it through its own island\n// runtime; everything it needs (the images, the labels) arrives as JSON props.\n\nimport { each, signal } from '@ketvietlab/ketjs-view'\nimport type { IslandController, IslandProps, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'lightbox',\n 'lightbox-thumbs',\n 'lightbox-thumb',\n 'lightbox-empty',\n 'lightbox-layer',\n 'lightbox-backdrop',\n 'lightbox-toolbar',\n 'lightbox-counter',\n 'lightbox-tool',\n 'lightbox-stage',\n 'lightbox-image',\n 'lightbox-nav',\n 'lightbox-caption',\n] as const\n\nexport type LightboxItem = {\n src: string\n alt: string\n /** A smaller rendition for the trigger; the full `src` is used when omitted. */\n thumbnail?: string | null\n caption?: string | null\n}\n\nexport type LightboxLabels = {\n /** The trigger's accessible name, `{alt}` interpolated. */\n open: string\n close: string\n previous: string\n next: string\n zoomIn: string\n zoomOut: string\n /** `{index}` and `{total}` interpolated. */\n counter: string\n /** Shown in place of a thumbnail when there are no items. */\n empty?: string\n}\n\nexport type LightboxConfig = {\n items: readonly LightboxItem[]\n labels: LightboxLabels\n /** `first` shows one thumbnail that opens the whole set; `all` shows one per item. */\n thumbnails?: 'first' | 'all'\n size?: 'small' | 'medium' | 'large'\n}\n\nexport type LightboxIslandProps = { id: string; config: LightboxConfig }\n\nconst MIN_ZOOM = 1\nconst MAX_ZOOM = 4\nconst ZOOM_STEP = 0.5\n\nconst fill = (template: string, params: Record<string, unknown>): string =>\n template.replace(/\\{(\\w+)\\}/g, (_, name: string) => String(params[name] ?? `{${name}}`))\n\nconst clamp = (value: number, min: number, max: number): number => Math.min(max, Math.max(min, value))\n\n/**\n * The viewer without a trigger of its own: whatever renders thumbnails (a record's\n * image field, the rows of a variant editor) calls `open` with the set to show and\n * places `layer()` anywhere in its own view. `attach` wires the keyboard once, for\n * the lifetime of the island that owns it.\n */\nexport type LightboxController = {\n open: (items: readonly LightboxItem[], at: number, trigger: EventTarget | null) => void\n close: () => void\n isOpen: () => boolean\n layer: () => TemplateResult | null\n attach: (lifetime: AbortSignal) => void\n}\n\nexport function createLightbox(labels: LightboxLabels, id = 'lightbox'): LightboxController {\n const items = signal<readonly LightboxItem[]>([])\n const open = signal(false)\n const index = signal(0)\n const zoom = signal(MIN_ZOOM)\n const pan = signal({ x: 0, y: 0 })\n let returnFocus: HTMLElement | null = null\n // Every pointer down on the stage, so two fingers read as a pinch and one as a pan.\n const pointers = new Map<number, { x: number; y: number }>()\n let gesture: {\n start: { x: number; y: number }\n origin: { x: number; y: number }\n zoom: number\n distance: number | null\n moved: boolean\n } | null = null\n\n const resetView = (): void => {\n zoom.set(MIN_ZOOM)\n pan.set({ x: 0, y: 0 })\n }\n\n const show = (set: readonly LightboxItem[], at: number, trigger: EventTarget | null): void => {\n if (!set.length) return\n returnFocus = trigger instanceof HTMLElement ? trigger : null\n items.set(set)\n index.set(clamp(at, 0, set.length - 1))\n resetView()\n open.set(true)\n queueMicrotask(() => {\n if (typeof document === 'undefined') return\n document.getElementById(`${id}-lightbox-close`)?.focus()\n })\n }\n\n const hide = (): void => {\n if (!open()) return\n open.set(false)\n resetView()\n pointers.clear()\n gesture = null\n returnFocus?.focus()\n returnFocus = null\n }\n\n const step = (delta: number): void => {\n const total = items().length\n if (total < 2) return\n index.set((index() + delta + total) % total)\n resetView()\n }\n\n const zoomTo = (next: number): void => {\n const value = clamp(next, MIN_ZOOM, MAX_ZOOM)\n zoom.set(value)\n // Back at fit size there is nothing to pan across.\n if (value === MIN_ZOOM) pan.set({ x: 0, y: 0 })\n }\n\n const centerOf = (): { x: number; y: number } => {\n const points = [...pointers.values()]\n if (!points.length) return { x: 0, y: 0 }\n return {\n x: points.reduce((sum, point) => sum + point.x, 0) / points.length,\n y: points.reduce((sum, point) => sum + point.y, 0) / points.length,\n }\n }\n\n const distanceOf = (): number => {\n const [a, b] = [...pointers.values()]\n return a && b ? Math.hypot(a.x - b.x, a.y - b.y) : 0\n }\n\n const endPointer = (event: PointerEvent, completed: boolean): void => {\n // At fit size a horizontal swipe with one finger moves to the neighbouring image.\n if (completed && gesture && pointers.size === 1 && zoom() === MIN_ZOOM && gesture.distance === null) {\n const dx = event.clientX - gesture.start.x\n if (Math.abs(dx) > 60 && Math.abs(dx) > Math.abs(event.clientY - gesture.start.y)) step(dx < 0 ? 1 : -1)\n }\n pointers.delete(event.pointerId)\n ;(event.currentTarget as Element | null)?.releasePointerCapture?.(event.pointerId)\n if (!pointers.size) {\n // Keep `moved` until the click that follows this pointerup has been read.\n const finished = gesture\n if (finished && completed) setTimeout(() => (gesture === finished ? (gesture = null) : null), 0)\n else gesture = null\n return\n }\n // One finger lifted from a pinch: carry on as a pan from where things are now.\n if (gesture) gesture = { ...gesture, start: centerOf(), origin: pan(), zoom: zoom(), distance: null }\n }\n\n const layer = (): TemplateResult | null => {\n const set = items()\n const item = set[index()]\n if (!open() || !item) return null\n const scale = zoom()\n const offset = pan()\n const many = set.length > 1\n return (\n <div\n data-ui=\"lightbox-layer\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={item.alt}\n id={`${id}-lightbox`}\n >\n <div data-ui=\"lightbox-backdrop\" aria-hidden=\"true\" onClick={hide} />\n <div data-ui=\"lightbox-toolbar\">\n <span data-ui=\"lightbox-counter\" aria-live=\"polite\">\n {many ? fill(labels.counter, { index: index() + 1, total: set.length }) : ''}\n </span>\n <button\n type=\"button\"\n data-ui=\"lightbox-tool\"\n data-tool=\"zoom-out\"\n aria-label={labels.zoomOut}\n title={labels.zoomOut}\n disabled={scale <= MIN_ZOOM}\n onClick={() => zoomTo(scale - ZOOM_STEP)}\n >\n \u2212\n </button>\n <button\n type=\"button\"\n data-ui=\"lightbox-tool\"\n data-tool=\"zoom-in\"\n aria-label={labels.zoomIn}\n title={labels.zoomIn}\n disabled={scale >= MAX_ZOOM}\n onClick={() => zoomTo(scale + ZOOM_STEP)}\n >\n +\n </button>\n <button\n type=\"button\"\n data-ui=\"lightbox-tool\"\n data-tool=\"close\"\n id={`${id}-lightbox-close`}\n aria-label={labels.close}\n title={labels.close}\n onClick={hide}\n >\n \u00D7\n </button>\n </div>\n {/* biome-ignore lint/a11y/noStaticElementInteractions: pointer gestures on the picture; the keyboard equivalents (+, -, arrows, Escape) are bound on the document while open */}\n <div\n role=\"presentation\"\n data-ui=\"lightbox-stage\"\n data-zoomed={String(scale > MIN_ZOOM)}\n onClick={(event) => {\n // A click on the empty stage around the picture closes, like the backdrop.\n if (event.target === event.currentTarget && !gesture?.moved) hide()\n }}\n onWheel={(event: WheelEvent) => {\n event.preventDefault()\n zoomTo(zoom() * (event.deltaY < 0 ? 1.15 : 1 / 1.15))\n }}\n onPointerDown={(event: PointerEvent) => {\n pointers.set(event.pointerId, { x: event.clientX, y: event.clientY })\n ;(event.currentTarget as Element).setPointerCapture?.(event.pointerId)\n gesture = {\n start: centerOf(),\n origin: pan(),\n zoom: zoom(),\n distance: pointers.size > 1 ? distanceOf() : null,\n moved: gesture?.moved === true && pointers.size > 1,\n }\n }}\n onPointerMove={(event: PointerEvent) => {\n if (!gesture || !pointers.has(event.pointerId)) return\n pointers.set(event.pointerId, { x: event.clientX, y: event.clientY })\n const center = centerOf()\n const dx = center.x - gesture.start.x\n const dy = center.y - gesture.start.y\n if (Math.abs(dx) + Math.abs(dy) > 4) gesture.moved = true\n if (pointers.size > 1 && gesture.distance) {\n // Pinch: scale by how far the fingers spread since they both came down.\n zoomTo(gesture.zoom * (distanceOf() / gesture.distance))\n gesture.moved = true\n }\n if (zoom() > MIN_ZOOM) pan.set({ x: gesture.origin.x + dx, y: gesture.origin.y + dy })\n }}\n onPointerUp={(event: PointerEvent) => endPointer(event, true)}\n onPointerCancel={(event: PointerEvent) => endPointer(event, false)}\n >\n {/* biome-ignore lint/a11y/useKeyWithClickEvents: + and - zoom from the keyboard while the viewer is open */}\n <img\n data-ui=\"lightbox-image\"\n src={item.src}\n alt={item.alt}\n draggable=\"false\"\n style={`transform: translate(${offset.x}px, ${offset.y}px) scale(${scale})`}\n onClick={() => {\n // A tap (not the end of a pan or pinch) toggles between fit and 2\u00D7.\n if (!gesture?.moved) zoomTo(zoom() > MIN_ZOOM ? MIN_ZOOM : 2)\n }}\n />\n </div>\n {many ? (\n <button\n type=\"button\"\n data-ui=\"lightbox-nav\"\n data-direction=\"previous\"\n aria-label={labels.previous}\n title={labels.previous}\n onClick={() => step(-1)}\n >\n \u2039\n </button>\n ) : null}\n {many ? (\n <button\n type=\"button\"\n data-ui=\"lightbox-nav\"\n data-direction=\"next\"\n aria-label={labels.next}\n title={labels.next}\n onClick={() => step(1)}\n >\n \u203A\n </button>\n ) : null}\n {item.caption ? <p data-ui=\"lightbox-caption\">{item.caption}</p> : null}\n </div>\n )\n }\n\n const attach = (lifetime: AbortSignal): void => {\n // Capture phase: an enclosing record modal also closes on Escape, and the\n // viewer on top of it must be the only layer that answers.\n document.addEventListener(\n 'keydown',\n (event) => {\n if (!open()) return\n if (event.key === 'Escape') hide()\n else if (event.key === 'ArrowLeft') step(-1)\n else if (event.key === 'ArrowRight') step(1)\n else if (event.key === '+' || event.key === '=') zoomTo(zoom() + ZOOM_STEP)\n else if (event.key === '-') zoomTo(zoom() - ZOOM_STEP)\n else return\n event.preventDefault()\n event.stopImmediatePropagation()\n },\n { signal: lifetime, capture: true },\n )\n lifetime.addEventListener('abort', () => {\n open.set(false)\n pointers.clear()\n gesture = null\n })\n }\n\n return { open: show, close: hide, isOpen: () => open(), layer, attach }\n}\n\n/** A thumbnail \u2014 or one per item \u2014 that opens the whole set in the viewer. */\nexport function createLightboxView(props: LightboxIslandProps): IslandController {\n const { id, config } = props\n const labels = config.labels\n const items = config.items\n const viewer = createLightbox(labels, id)\n\n const thumbs = (): TemplateResult => {\n const shown = config.thumbnails === 'all' ? items : items.slice(0, 1)\n if (!shown.length)\n return (\n <span data-ui=\"lightbox-empty\" data-size={config.size ?? 'medium'}>\n {labels.empty ?? ''}\n </span>\n )\n return (\n <div data-ui=\"lightbox-thumbs\">\n {each(\n [...shown],\n (item) => item.src,\n (item) =>\n LightboxThumb({\n item,\n labels,\n size: config.size,\n onOpen: (trigger) => viewer.open(items, items.indexOf(item), trigger),\n }),\n )}\n </div>\n )\n }\n\n return {\n view: () => (\n <div data-ui=\"lightbox\" id={id}>\n {thumbs()}\n {viewer.layer()}\n </div>\n ),\n mount: ({ lifetime }) => viewer.attach(lifetime),\n }\n}\n\n/** One thumbnail button, for a view that drives its own `createLightbox` controller. */\nexport const LightboxThumb = (props: {\n item: LightboxItem\n labels: Pick<LightboxLabels, 'open'>\n size?: 'small' | 'medium' | 'large'\n onOpen: (trigger: EventTarget | null) => void\n}): TemplateResult => (\n <button\n type=\"button\"\n data-ui=\"lightbox-thumb\"\n data-size={props.size ?? 'medium'}\n aria-label={fill(props.labels.open, { alt: props.item.alt })}\n aria-haspopup=\"dialog\"\n onClick={(event) => props.onOpen(event.currentTarget)}\n >\n <img src={props.item.thumbnail ?? props.item.src} alt=\"\" loading=\"lazy\" decoding=\"async\" />\n </button>\n)\n\nexport const lightbox = (props: IslandProps): IslandController =>\n createLightboxView(props as unknown as LightboxIslandProps)\n", "import { ModalSheet } from '../../patterns/modal-sheet/index.tsx'\nimport { each, signal } from '@ketvietlab/ketjs-view'\nimport type { IslandController, IslandElement, IslandProps, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'search-filter',\n 'search-filter-bar',\n 'search-filter-section-toggle',\n 'search-filter-sheet',\n 'search-filter-clear',\n 'search-filter-icon',\n 'search-filter-field',\n 'search-filter-input',\n 'search-filter-facets',\n 'search-filter-facet',\n 'search-filter-facet-remove',\n 'search-filter-suggestions',\n 'search-filter-suggestion',\n 'search-filter-toggle',\n 'search-filter-columns',\n 'search-filter-column',\n 'search-filter-column-title',\n 'search-filter-empty',\n 'search-filter-grouping',\n 'search-filter-grouping-head',\n 'search-filter-grouping-list',\n 'search-filter-grouping-item',\n 'search-filter-grouping-order',\n 'search-filter-grouping-label',\n 'search-filter-grouping-actions',\n 'search-filter-grouping-action',\n 'search-filter-grouping-clear',\n 'search-filter-grouping-add-label',\n 'custom-filter',\n 'custom-filter-row',\n 'custom-filter-add',\n 'custom-group-by',\n 'favorite-list',\n 'favorite-item',\n 'favorite-item-default',\n 'favorite-item-remove',\n 'favorite-save',\n 'favorite-save-toggle',\n 'favorite-save-row',\n] as const\n\n/**\n * The same 7 field types and their operator vocabulary as\n * `packages/ketjs/src/data/list-search.ts`'s `ListFieldType`/`defaultOperators` \u2014\n * redeclared here because design-system cannot depend on `@ketvietlab/ketjs`.\n */\nexport type SearchFilterFieldType =\n | 'text'\n | 'number'\n | 'boolean'\n | 'selection'\n | 'reference'\n | 'date'\n | 'datetime'\nexport type SearchFilterOperator =\n | 'contains'\n | 'notContains'\n | 'equals'\n | 'notEquals'\n | 'startsWith'\n | 'gt'\n | 'gte'\n | 'lt'\n | 'lte'\n | 'between'\n | 'anyOf'\n | 'isTrue'\n | 'isFalse'\n | 'isSet'\n | 'isNotSet'\n\n/** The density of the search bar; `compact` uses the small control-height token. */\nexport type SearchFilterSize = 'default' | 'compact'\n\nconst defaultOperators: Record<SearchFilterFieldType, readonly SearchFilterOperator[]> = {\n text: ['contains', 'notContains', 'equals', 'notEquals', 'startsWith', 'isSet', 'isNotSet'],\n number: ['equals', 'notEquals', 'gt', 'gte', 'lt', 'lte', 'between', 'isSet', 'isNotSet'],\n boolean: ['isTrue', 'isFalse'],\n selection: ['equals', 'notEquals', 'anyOf', 'isSet', 'isNotSet'],\n reference: ['equals', 'notEquals', 'anyOf', 'isSet', 'isNotSet'],\n date: ['equals', 'notEquals', 'gt', 'gte', 'lt', 'lte', 'between', 'isSet', 'isNotSet'],\n datetime: ['equals', 'notEquals', 'gt', 'gte', 'lt', 'lte', 'between', 'isSet', 'isNotSet'],\n}\n\nconst operatorLabels: Record<SearchFilterOperator, string> = {\n contains: 'contains',\n notContains: 'does not contain',\n equals: 'is',\n notEquals: 'is not',\n startsWith: 'starts with',\n gt: 'is greater than',\n gte: 'is greater than or equal to',\n lt: 'is less than',\n lte: 'is less than or equal to',\n between: 'is between',\n anyOf: 'is any of',\n isTrue: 'is true',\n isFalse: 'is false',\n isSet: 'is set',\n isNotSet: 'is not set',\n}\n\n/** An operator that stands on its own and takes no value (\"is set\", \"is true\", \u2026). */\nconst isValuelessOperator = (operator: SearchFilterOperator): boolean =>\n operator === 'isTrue' || operator === 'isFalse' || operator === 'isSet' || operator === 'isNotSet'\n\nexport type CustomFilterField = {\n value: string\n label: string\n type: SearchFilterFieldType\n /** Screen-owned choices, including permission-checked reference lookups. */\n choices?: readonly { value: string; label: string }[]\n}\n\n/**\n * The rule a free-text \"Search <field> for: \u2026\" suggestion stands for, or null\n * when the text cannot be one. Every suggestion must be a filter the server\n * accepts, so a field whose values free text cannot name \u2014 a choice, a record,\n * a date \u2014 is left to the custom-filter editor instead of being offered here.\n */\nconst suggestedRule = (field: CustomFilterField, text: string): CustomFilterRule | null => {\n if (field.type === 'text') return { field: field.value, operator: 'contains', value: text }\n // Whole numbers only: `1.000` is a thousand to a Vietnamese reader and one to `Number`.\n if (field.type === 'number' && /^-?\\d+$/.test(text))\n return { field: field.value, operator: 'equals', value: text }\n return null\n}\n\n/** A removable chip in the search field. `type` only drives its colour token. */\nexport type SearchFacet = { id: string; type: 'field' | 'filter' | 'groupBy' | 'favorite'; label: string }\n\nexport type SearchFilterOption = {\n id: string\n label: string\n active: boolean\n /** Options sharing a `group` are OR'd together, set off from the next group by a divider. */\n group?: string\n options?: SearchFilterOption[]\n}\n\nexport type SearchGroupByOption = {\n id: string\n label: string\n active: boolean\n group?: string\n options?: SearchGroupByOption[]\n}\n\nexport type SearchFavorite = { id: string; label: string; isDefault: boolean; active: boolean }\nexport type SearchFilterCustomRule = {\n id: string\n field: string\n operator: SearchFilterOperator\n value: string\n label: string\n}\n\nexport type SearchFilterManager = {\n applyFunction: string\n bodyId: string\n /** Static, screen-owned input kept with every apply request (for example the active locale). */\n applyInput?: Record<string, unknown>\n saveFavoriteFunction?: string\n deleteFavoriteFunction?: string\n setDefaultFavoriteFunction?: string\n}\n\n/** A shell handles this event synchronously and supplies its navigation promise. */\nexport type SearchFilterNavigateDetail = {\n id: string\n href: string\n signal: AbortSignal\n respondWith(result: Promise<void>): void\n}\n\nexport type SearchFilterLabels = {\n searchLabel: string\n searchPlaceholder: string\n /** Accessible name for the combined mobile Filters/Group By/Favorites sheet. */\n toggleLabel: string\n filters: string\n groupBy: string\n groupByApplied?: string\n groupByAdd?: string\n groupByClear?: string\n groupByMoveEarlier?: string\n groupByMoveLater?: string\n favorites: string\n /** The field-less suggestion: `${searchGenericLabel}: \"query\"`. */\n searchGenericLabel: string\n /** The per-field suggestion: `${searchFieldPrefix} <field> ${searchFieldPreposition}: \"query\"`. */\n searchFieldPrefix: string\n searchFieldPreposition: string\n customFilterField: string\n customFilterOperator: string\n customFilterValue: string\n customFilterAdd: string\n customGroupByPlaceholder: string\n saveSearch: string\n favoriteName: string\n favoriteDefault: string\n favoriteSaveAction: string\n favoriteCancel?: string\n favoriteError?: string\n favoriteRemove: string\n favoriteSetDefault: string\n noFavorites: string\n clear: string\n applyError: string\n retry: string\n clearFilters?: string\n close?: string\n valueFrom?: string\n valueTo?: string\n operatorLabels?: Partial<Record<SearchFilterOperator, string>>\n}\n\nexport type SearchFilterConfig = {\n name: string\n /** Disable unsupported editors while retaining the same search and preset-facet interaction. */\n capabilities?: { groupBy?: boolean; favorites?: boolean; customFilters?: boolean }\n /** Keeps the standard interaction while reducing the search bar's visual density. */\n size?: SearchFilterSize\n query?: string\n /** Native link to a screen-owned favorite form, when one exists. */\n favoriteHref?: string\n facets: SearchFacet[]\n filters: SearchFilterOption[]\n groupBy: SearchGroupByOption[]\n /** Maximum simultaneous grouping levels supported by the consumer. */\n maxGroupBy?: number\n favorites: SearchFavorite[]\n customFilterFields: CustomFilterField[]\n /** Rules already encoded in the server-rendered list state. */\n customFilters?: SearchFilterCustomRule[]\n labels: SearchFilterLabels\n manager?: SearchFilterManager\n}\n\n/** Shared by SSR and interactive chips; values remain data and choices supply display labels. */\nexport const searchFilterRuleLabel = (options: {\n fieldLabel: string\n operator: SearchFilterOperator\n value?: unknown\n choices?: CustomFilterField['choices']\n operatorLabels?: SearchFilterLabels['operatorLabels']\n}): string => {\n const operator = options.operatorLabels?.[options.operator] ?? operatorLabels[options.operator]\n const prefix = `${options.fieldLabel} ${operator}`\n if (isValuelessOperator(options.operator) || options.value == null || options.value === '') return prefix\n const values = Array.isArray(options.value)\n ? options.value\n : options.operator === 'anyOf' || options.operator === 'between'\n ? String(options.value)\n .split(',')\n .map((part) => part.trim())\n : [options.value]\n const value = values\n .map((part) => options.choices?.find((choice) => choice.value === String(part))?.label ?? String(part))\n .join(', ')\n return `${prefix}: ${value}`\n}\n\ntype SearchFilterIslandProps = IslandProps & { id: string; config: SearchFilterConfig }\ntype ApiPayload = { ok?: boolean; value?: unknown; message?: unknown; errors?: Array<{ message?: unknown }> }\ntype CustomFilterRule = { field: string; operator: SearchFilterOperator; value: string }\ntype Draft = { facets: SearchFacet[]; rules: Record<string, CustomFilterRule> }\ntype RetryDraft = Draft & { consumed: string }\n\nconst string = (value: unknown): string => (value == null ? '' : String(value))\n\n/**\n * A function call that failed. `retryable` when it never reached a verdict \u2014 the\n * network dropped or the server faulted \u2014 so the same request may yet succeed;\n * a refusal (4xx) would only be refused again.\n */\nclass ApiError extends Error {\n readonly retryable: boolean\n constructor(message: string, retryable: boolean) {\n super(message)\n this.retryable = retryable\n }\n}\n\nconst callApi = async (name: string, input: unknown): Promise<unknown> => {\n let response: Response\n try {\n response = await fetch(`/_ket/fn/${encodeURIComponent(name)}`, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(input),\n })\n } catch (caught) {\n throw new ApiError(caught instanceof Error ? caught.message : 'network error', true)\n }\n // A proxy in front of a faulting server answers with HTML, not the JSON envelope.\n const payload = (await response.json().catch(() => ({}))) as ApiPayload\n if (!response.ok || payload.ok === false) {\n const domainError = payload.errors?.[0]\n throw new ApiError(\n string(domainError?.message ?? payload.message ?? `HTTP ${response.status}`),\n response.status >= 500,\n )\n }\n return payload.value\n}\n\nconst flatten = <Option extends { id: string; label: string; options?: Option[] }>(\n options: readonly Option[],\n): Option[] => options.flatMap((option) => (option.options?.length ? flatten(option.options) : [option]))\n\nexport function createSearchFilterView(props: SearchFilterIslandProps): IslandController {\n const { config } = props\n const labels = {\n clearFilters: 'Clear filters',\n close: 'Close',\n favoriteCancel: 'Cancel',\n favoriteError: 'Could not save changes to saved searches',\n valueFrom: 'From',\n valueTo: 'To',\n groupByApplied: config.labels.groupBy,\n groupByAdd: config.labels.customGroupByPlaceholder,\n groupByClear: config.labels.clear,\n groupByMoveEarlier: 'Move earlier',\n groupByMoveLater: 'Move later',\n ...config.labels,\n }\n const operatorLabel = (operator: SearchFilterOperator): string =>\n labels.operatorLabels?.[operator] ?? operatorLabels[operator]\n const capabilities = config.capabilities ?? {}\n const manager = config.manager\n const hasGrouping = capabilities.groupBy !== false && config.groupBy.length > 0\n const groupByOptions = flatten(config.groupBy)\n const fieldSuggestions = (text: string): CustomFilterField[] =>\n config.customFilterFields.filter((field) => suggestedRule(field, text))\n\n const facets = signal<SearchFacet[]>(config.facets)\n const favorites = signal<SearchFavorite[]>(config.favorites)\n const hasFavorites = (): boolean =>\n capabilities.favorites !== false &&\n Boolean(favorites().length || manager?.saveFavoriteFunction || config.favoriteHref)\n const customFilterRules = signal<Record<string, CustomFilterRule>>(\n Object.fromEntries(\n (config.customFilters ?? []).map((rule) => [\n rule.id,\n { field: rule.field, operator: rule.operator, value: rule.value },\n ]),\n ),\n )\n const query = signal(config.query ?? '')\n const suggestionsOpen = signal(false)\n const customFilterField = signal('')\n const customFilterOperator = signal('')\n const customFilterValue = signal('')\n const menuOpen = signal(false)\n const panel = signal<'filter' | 'groupBy' | 'favorite'>('filter')\n const sheetOpen = signal(false)\n const savingFavorite = signal(false)\n const customFilterEnd = signal('')\n let sheetOpener: HTMLElement | null = null\n let menuOpener: HTMLElement | null = null\n const focusControl = (selector: string): void => {\n const control = [...(mountedRoot?.querySelectorAll(selector) ?? [])][0]\n if (typeof HTMLElement !== 'undefined' && control instanceof HTMLElement) control.focus()\n }\n const closeFavoriteForm = (): void => {\n savingFavorite.set(false)\n queueMicrotask(() => {\n if (menuOpen() || sheetOpen()) focusControl('[data-ui=\"favorite-save-toggle\"]')\n else menuOpener?.focus()\n })\n }\n const closeMenu = (): void => {\n savingFavorite.set(false)\n menuOpen.set(false)\n }\n const dismissDisclosure = (event: KeyboardEvent): boolean => {\n const disclosure =\n event.target instanceof Element ? event.target.closest('details[data-ui=\"disclosure\"][open]') : null\n if (!(disclosure instanceof HTMLDetailsElement)) return false\n disclosure.open = false\n const summary = disclosure.querySelector('summary')\n if (summary instanceof HTMLElement) summary.focus()\n event.preventDefault()\n event.stopPropagation()\n return true\n }\n const dialogElement = (): HTMLDialogElement | null => {\n const candidate = [...(mountedRoot?.querySelectorAll('[data-ui=\"search-filter-sheet\"]') ?? [])][0]\n return typeof HTMLDialogElement !== 'undefined' && candidate instanceof HTMLDialogElement\n ? candidate\n : null\n }\n const closeSheet = (): void => {\n closeMenu()\n dialogElement()?.close()\n sheetOpen.set(false)\n sheetOpener?.focus()\n }\n const openPanel = (kind: 'filter' | 'groupBy' | 'favorite'): void => {\n if (panel() !== kind) savingFavorite.set(false)\n panel.set(kind)\n menuOpen.set(true)\n suggestionsOpen.set(false)\n }\n const togglePanel = (event: Event): void => {\n menuOpener = event.currentTarget as HTMLElement\n const switching = menuOpen() && panel() !== 'filter'\n if (switching) savingFavorite.set(false)\n panel.set('filter')\n suggestionsOpen.set(false)\n if (!window.matchMedia('(max-width: 640px)').matches) {\n if (switching) event.preventDefault()\n return\n }\n event.preventDefault()\n sheetOpener = event.currentTarget as HTMLElement\n closeMenu()\n sheetOpen.set(true)\n queueMicrotask(() => {\n const dialog = dialogElement()\n if (dialog && !dialog.open) {\n dialog.showModal()\n focusControl('[data-ui=\"search-filter-sheet\"] [data-ui=\"modal-close\"]')\n }\n })\n }\n const saveFavoriteName = signal('')\n const saveFavoriteDefault = signal(false)\n const pending = signal(false)\n const error = signal('')\n // What the rendered list actually shows. Chips change before the request goes\n // out, so a request that fails must put them back here rather than leave a\n // filter on screen that never applied.\n let settled: Draft = { facets: facets(), rules: customFilterRules() }\n const retryDraft = signal<RetryDraft | null>(null)\n let mountedRoot: IslandElement | null = null\n\n // `value={query()}` only writes the attribute, which a field the reader has\n // typed into no longer displays \u2014 the live property is what they see.\n const setQuery = (value: string): void => {\n query.set(value)\n if (!mountedRoot) return\n for (const input of mountedRoot.querySelectorAll('[data-ui=\"search-filter-input\"]'))\n if (input instanceof HTMLInputElement && input.value !== value) input.value = value\n }\n\n const isActive = (kind: SearchFacet['type'], id: string): boolean =>\n facets().some((facet) => facet.type === kind && facet.id === id)\n const countOf = (kind: SearchFacet['type']): number =>\n facets().filter((facet) => facet.type === kind).length\n const activeFavoriteId = (): string => facets().find((facet) => facet.type === 'favorite')?.id ?? ''\n const groupByFacets = (): SearchFacet[] => facets().filter((facet) => facet.type === 'groupBy')\n const operatorsFor = (fieldValue: string): readonly SearchFilterOperator[] => {\n const field = config.customFilterFields.find((entry) => entry.value === fieldValue)\n return field ? defaultOperators[field.type] : []\n }\n\n const applyPayload = () => ({\n ...(manager?.applyInput ?? {}),\n query: query(),\n facets: facets(),\n filters: facets()\n .filter((facet) => facet.type === 'filter' && !customFilterRules()[facet.id])\n .map((facet) => facet.id),\n groupBy: facets()\n .filter((facet) => facet.type === 'groupBy')\n .map((facet) => facet.id),\n favoriteId: activeFavoriteId() || null,\n customFilters: Object.entries(customFilterRules()).map(([id, rule]) => ({ id, ...rule })),\n })\n\n let applyVersion = 0\n let navigationRequest: AbortController | null = null\n let startingNavigation = false\n /**\n * Sends the current facets. `consumed` is text the triggering action moved\n * out of the search field into a chip; a failed request hands it back.\n */\n const apply = async (consumed = ''): Promise<void> => {\n closeMenu()\n if (!manager?.applyFunction) return\n const version = ++applyVersion\n navigationRequest?.abort()\n const request = new AbortController()\n navigationRequest = request\n const attempted: Draft = { facets: facets(), rules: customFilterRules() }\n pending.set(true)\n error.set('')\n retryDraft.set(null)\n try {\n const value = (await callApi(manager.applyFunction, applyPayload())) as\n | { html?: unknown; href?: unknown }\n | undefined\n if (version !== applyVersion) return\n const body = document.getElementById(manager.bodyId)\n if (body && typeof value?.html === 'string') body.innerHTML = value.html\n if (typeof value?.href === 'string' && typeof value.html !== 'string') {\n // The shell owns routing, history and island reconciliation. A standalone\n // consumer without that shell keeps the native navigation fallback.\n let navigation: Promise<void> | undefined\n const detail: SearchFilterNavigateDetail = {\n id: props.id,\n href: value.href,\n signal: request.signal,\n respondWith: (result) => {\n navigation = result\n },\n }\n startingNavigation = true\n try {\n document.dispatchEvent?.(new CustomEvent('ket:search-filter-navigate', { detail }))\n } finally {\n startingNavigation = false\n }\n if (navigation) await navigation\n else window.location.assign(value.href)\n } else if (typeof value?.href === 'string') {\n history.pushState(null, '', value.href)\n }\n if (version === applyVersion) settled = attempted\n } catch (caught) {\n if (version !== applyVersion) return\n facets.set(settled.facets)\n customFilterRules.set(settled.rules)\n if (consumed && !query()) setQuery(consumed)\n if (caught instanceof Error && 'retryable' in caught && caught.retryable === true)\n retryDraft.set({ ...attempted, consumed })\n if (caught instanceof Error && caught.name === 'AbortError') return\n // The server's message is an English diagnostic, not copy for the reader.\n error.set(labels.applyError)\n } finally {\n if (version === applyVersion) pending.set(false)\n }\n }\n\n /** Sends the failed request again, chips and all, as the reader last asked for it. */\n const retry = (): void => {\n const draft = retryDraft()\n if (!draft) return\n facets.set(draft.facets)\n customFilterRules.set(draft.rules)\n if (draft.consumed && query() === draft.consumed) setQuery('')\n void apply(draft.consumed)\n }\n\n const clearFavorite = (): void => {\n if (activeFavoriteId()) facets.set(facets().filter((facet) => facet.type !== 'favorite'))\n }\n\n const removeFacet = (facet: SearchFacet): void => {\n if (facet.type !== 'favorite') clearFavorite()\n facets.set(facets().filter((held) => held.id !== facet.id))\n if (customFilterRules()[facet.id]) {\n const next = { ...customFilterRules() }\n delete next[facet.id]\n customFilterRules.set(next)\n }\n void apply()\n }\n\n const toggleFilter = (option: SearchFilterOption): void => {\n clearFavorite()\n if (isActive('filter', option.id)) {\n removeFacet({ id: option.id, type: 'filter', label: option.label })\n return\n }\n facets.set([...facets(), { id: option.id, type: 'filter', label: option.label }])\n void apply()\n }\n\n const toggleGroupBy = (option: SearchGroupByOption): void => {\n if (!isActive('groupBy', option.id) && groupByFacets().length >= (config.maxGroupBy ?? Infinity)) return\n clearFavorite()\n if (isActive('groupBy', option.id)) {\n removeFacet({ id: option.id, type: 'groupBy', label: option.label })\n return\n }\n facets.set([...facets(), { id: option.id, type: 'groupBy', label: option.label }])\n void apply()\n }\n\n const clearFilters = (): void => {\n facets.set(facets().filter((facet) => facet.type === 'field' || facet.type === 'groupBy'))\n customFilterRules.set({})\n void apply()\n }\n const selectedField = (): CustomFilterField | undefined =>\n config.customFilterFields.find((entry) => entry.value === customFilterField())\n const canAddRule = (): boolean => {\n const field = selectedField()\n const operator = customFilterOperator() as SearchFilterOperator\n if (!field || !operatorsFor(field.value).includes(operator)) return false\n if (isValuelessOperator(operator)) return true\n if (!customFilterValue().trim()) return false\n if (operator === 'between' && !customFilterEnd().trim()) return false\n const values =\n operator === 'between'\n ? [customFilterValue(), customFilterEnd()]\n : operator === 'anyOf'\n ? customFilterValue().split(',')\n : [customFilterValue()]\n if (field.type === 'number' && values.some((value) => !Number.isFinite(Number(value)))) return false\n if (field.choices && values.some((value) => !field.choices!.some((choice) => choice.value === value)))\n return false\n return true\n }\n const addCustomFilter = (): void => {\n const field = config.customFilterFields.find((entry) => entry.value === customFilterField())\n const operator = customFilterOperator() as SearchFilterOperator\n if (!field || !canAddRule()) return\n const value = isValuelessOperator(operator)\n ? ''\n : operator === 'between'\n ? `${customFilterValue().trim()},${customFilterEnd().trim()}`\n : customFilterValue().trim()\n const label = searchFilterRuleLabel({\n fieldLabel: field.label,\n operator,\n value,\n choices: field.choices,\n operatorLabels: labels.operatorLabels,\n })\n clearFavorite()\n const id = `custom-filter:${crypto.randomUUID()}`\n customFilterRules.set({ ...customFilterRules(), [id]: { field: field.value, operator, value } })\n facets.set([...facets(), { id, type: 'filter', label }])\n customFilterField.set('')\n customFilterOperator.set('')\n customFilterValue.set('')\n customFilterEnd.set('')\n void apply()\n }\n\n const handleSearchInput = (event: Event): void => {\n if (!(event.currentTarget instanceof HTMLInputElement)) return\n query.set(event.currentTarget.value)\n suggestionsOpen.set(event.currentTarget.value.trim() !== '')\n }\n\n const selectGenericSuggestion = (): void => {\n const value = query().trim()\n if (!value) return\n clearFavorite()\n facets.set([\n ...facets().filter((facet) => facet.type !== 'field'),\n { id: `query:${crypto.randomUUID()}`, type: 'field', label: value },\n ])\n setQuery('')\n suggestionsOpen.set(false)\n void apply(value)\n }\n\n const selectFieldSuggestion = (field: CustomFilterField): void => {\n const value = query().trim()\n const rule = value ? suggestedRule(field, value) : null\n if (!rule) return\n clearFavorite()\n const id = `custom-filter:${crypto.randomUUID()}`\n customFilterRules.set({ ...customFilterRules(), [id]: rule })\n facets.set([\n ...facets(),\n {\n id,\n type: 'filter',\n label: searchFilterRuleLabel({\n fieldLabel: field.label,\n operator: rule.operator,\n value: rule.value,\n choices: field.choices,\n operatorLabels: labels.operatorLabels,\n }),\n },\n ])\n setQuery('')\n suggestionsOpen.set(false)\n void apply(value)\n }\n\n const handleSearchKeydown = (event: Event): void => {\n if (!(event instanceof KeyboardEvent)) return\n if (event.key === 'Enter') {\n event.preventDefault()\n selectGenericSuggestion()\n } else if (event.key === 'Escape') {\n suggestionsOpen.set(false)\n }\n }\n\n const applyFavorite = (favorite: SearchFavorite): void => {\n facets.set([\n ...facets().filter((held) => held.type !== 'favorite'),\n { id: favorite.id, type: 'favorite', label: favorite.label },\n ])\n void apply()\n }\n\n const moveGroupBy = (facet: SearchFacet, direction: -1 | 1): void => {\n const grouped = groupByFacets()\n const from = grouped.findIndex((entry) => entry.id === facet.id)\n const to = from + direction\n if (from < 0 || to < 0 || to >= grouped.length) return\n const next = [...grouped]\n ;[next[from], next[to]] = [next[to]!, next[from]!]\n clearFavorite()\n facets.set([...facets().filter((entry) => entry.type !== 'groupBy'), ...next])\n void apply()\n }\n\n const clearGroupBy = (): void => {\n if (!groupByFacets().length) return\n clearFavorite()\n facets.set(facets().filter((facet) => facet.type !== 'groupBy'))\n void apply()\n }\n\n const saveFavorite = async (event: Event): Promise<void> => {\n event.preventDefault()\n const name = saveFavoriteName().trim()\n if (!name || !manager?.saveFavoriteFunction) return\n pending.set(true)\n error.set('')\n retryDraft.set(null)\n try {\n const value = (await callApi(manager.saveFavoriteFunction, {\n name,\n isDefault: saveFavoriteDefault(),\n state: applyPayload(),\n })) as { id?: unknown } | undefined\n const isDefault = saveFavoriteDefault()\n const favorite: SearchFavorite = {\n id: string(value?.id) || crypto.randomUUID(),\n label: name,\n isDefault,\n active: true,\n }\n favorites.set([\n ...favorites().map((entry) => (isDefault ? { ...entry, isDefault: false } : entry)),\n favorite,\n ])\n applyFavorite(favorite)\n saveFavoriteName.set('')\n saveFavoriteDefault.set(false)\n closeFavoriteForm()\n } catch {\n error.set(labels.favoriteError)\n } finally {\n pending.set(false)\n }\n }\n\n const removeFavorite = async (favorite: SearchFavorite): Promise<void> => {\n if (!manager?.deleteFavoriteFunction) return\n pending.set(true)\n error.set('')\n retryDraft.set(null)\n try {\n await callApi(manager.deleteFavoriteFunction, { ...(manager.applyInput ?? {}), id: favorite.id })\n favorites.set(favorites().filter((entry) => entry.id !== favorite.id))\n if (activeFavoriteId() === favorite.id)\n removeFacet({ id: favorite.id, type: 'favorite', label: favorite.label })\n } catch {\n error.set(labels.favoriteError)\n } finally {\n pending.set(false)\n }\n }\n\n const setDefaultFavorite = async (favorite: SearchFavorite): Promise<void> => {\n if (!manager?.setDefaultFavoriteFunction) return\n pending.set(true)\n error.set('')\n retryDraft.set(null)\n try {\n await callApi(manager.setDefaultFavoriteFunction, { ...(manager.applyInput ?? {}), id: favorite.id })\n favorites.set(favorites().map((entry) => ({ ...entry, isDefault: entry.id === favorite.id })))\n } catch {\n error.set(labels.favoriteError)\n } finally {\n pending.set(false)\n }\n }\n\n const facetChip = (facet: SearchFacet): TemplateResult => (\n <li data-ui=\"search-filter-facet\" data-type={facet.type}>\n <span>\n {facet.type === 'field'\n ? `${labels.searchGenericLabel}: `\n : facet.type === 'groupBy'\n ? `${labels.groupBy}: `\n : ''}\n {facet.label}\n </span>\n <button\n data-ui=\"search-filter-facet-remove\"\n type=\"button\"\n aria-label={`${labels.clear}: ${facet.label}`}\n title={labels.clear}\n onClick={() => removeFacet(facet)}\n >\n \u00D7\n </button>\n </li>\n )\n\n const filterMenuItem = (\n option: SearchFilterOption,\n previous: SearchFilterOption | null,\n ): TemplateResult => {\n const divider = previous && previous.group !== option.group ? <hr data-ui=\"menu-separator\" /> : null\n if (option.options?.length)\n return (\n <>\n {divider}\n <details data-ui=\"disclosure\">\n <summary data-ui=\"disclosure-summary\">{option.label}</summary>\n <div data-ui=\"disclosure-body\">\n {each(\n option.options,\n (entry) => entry.id,\n (entry, index) =>\n filterMenuItem(\n entry,\n index === 0 ? null : (option.options as SearchFilterOption[])[index - 1],\n ),\n )}\n </div>\n </details>\n </>\n )\n const active = isActive('filter', option.id)\n return (\n <>\n {divider}\n <button\n data-ui=\"menu-item\"\n type=\"button\"\n aria-pressed={String(active)}\n onClick={() => toggleFilter(option)}\n >\n <span data-ui=\"menu-item-check\" aria-hidden=\"true\">\n {active ? '\u2713' : ''}\n </span>\n <span data-ui=\"menu-item-copy\">\n <span>{option.label}</span>\n </span>\n </button>\n </>\n )\n }\n\n const groupByMenuItem = (\n option: SearchGroupByOption,\n previous: SearchGroupByOption | null,\n ): TemplateResult => {\n const divider = previous && previous.group !== option.group ? <hr data-ui=\"menu-separator\" /> : null\n if (option.options?.length)\n return (\n <>\n {divider}\n <details data-ui=\"disclosure\">\n <summary data-ui=\"disclosure-summary\">{option.label}</summary>\n <div data-ui=\"disclosure-body\">\n {each(\n option.options,\n (entry) => entry.id,\n (entry, index) =>\n groupByMenuItem(\n entry,\n index === 0 ? null : (option.options as SearchGroupByOption[])[index - 1],\n ),\n )}\n </div>\n </details>\n </>\n )\n const active = isActive('groupBy', option.id)\n if (active) return <></>\n return (\n <>\n {divider}\n <button\n data-ui=\"menu-item\"\n type=\"button\"\n disabled={groupByFacets().length >= (config.maxGroupBy ?? Infinity)}\n onClick={() => toggleGroupBy(option)}\n >\n <span data-ui=\"menu-item-check\" aria-hidden=\"true\">\n {active ? '\u2713' : ''}\n </span>\n <span data-ui=\"menu-item-copy\">\n <span>{option.label}</span>\n </span>\n </button>\n </>\n )\n }\n\n const favoriteRow = (favorite: SearchFavorite): TemplateResult => (\n <li data-ui=\"favorite-item\" data-active={String(favorite.id === activeFavoriteId())}>\n <button data-ui=\"menu-item\" type=\"button\" onClick={() => applyFavorite(favorite)}>\n <span data-ui=\"menu-item-copy\">\n <span>{favorite.label}</span>\n </span>\n </button>\n <div data-ui=\"favorite-item-actions\">\n {manager?.setDefaultFavoriteFunction && (\n <button\n data-ui=\"favorite-item-default\"\n type=\"button\"\n data-default={String(favorite.isDefault)}\n aria-label={labels.favoriteSetDefault}\n title={labels.favoriteSetDefault}\n onClick={() => setDefaultFavorite(favorite)}\n >\n {favorite.isDefault ? '\u2605' : '\u2606'}\n </button>\n )}\n {manager?.deleteFavoriteFunction && (\n <button\n data-ui=\"favorite-item-remove\"\n type=\"button\"\n aria-label={`${labels.favoriteRemove}: ${favorite.label}`}\n title={labels.favoriteRemove}\n onClick={() => removeFavorite(favorite)}\n >\n \u00D7\n </button>\n )}\n </div>\n </li>\n )\n\n const columnTitle = (label: string, count: number): TemplateResult => (\n <h3 data-ui=\"search-filter-column-title\">\n {label}\n {count ? ` (${count})` : ''}\n </h3>\n )\n\n const groupByPipeline = (): TemplateResult => {\n const grouped = groupByFacets()\n if (!grouped.length) return <></>\n return (\n <section data-ui=\"search-filter-grouping\" aria-label={labels.groupByApplied}>\n <div data-ui=\"search-filter-grouping-head\">\n <span>{labels.groupByApplied}</span>\n <button data-ui=\"search-filter-grouping-clear\" type=\"button\" onClick={clearGroupBy}>\n {labels.groupByClear}\n </button>\n </div>\n <ol data-ui=\"search-filter-grouping-list\">\n {each(\n grouped,\n (facet) => facet.id,\n (facet, index) => (\n <li data-ui=\"search-filter-grouping-item\">\n <span data-ui=\"search-filter-grouping-order\">{String(index + 1)}</span>\n <span data-ui=\"search-filter-grouping-label\">{facet.label}</span>\n <span data-ui=\"search-filter-grouping-actions\">\n {index > 0 && (\n <button\n data-ui=\"search-filter-grouping-action\"\n type=\"button\"\n aria-label={`${labels.groupByMoveEarlier}: ${facet.label}`}\n title={`${labels.groupByMoveEarlier}: ${facet.label}`}\n onClick={() => moveGroupBy(facet, -1)}\n >\n \u2191\n </button>\n )}\n {index + 1 < grouped.length && (\n <button\n data-ui=\"search-filter-grouping-action\"\n type=\"button\"\n aria-label={`${labels.groupByMoveLater}: ${facet.label}`}\n title={`${labels.groupByMoveLater}: ${facet.label}`}\n onClick={() => moveGroupBy(facet, 1)}\n >\n \u2193\n </button>\n )}\n <button\n data-ui=\"search-filter-grouping-action\"\n data-action=\"remove\"\n type=\"button\"\n aria-label={`${labels.clear}: ${facet.label}`}\n title={`${labels.clear}: ${facet.label}`}\n onClick={() => removeFacet(facet)}\n >\n \u00D7\n </button>\n </span>\n </li>\n ),\n )}\n </ol>\n </section>\n )\n }\n\n const valueEditor = (): TemplateResult | null => {\n const field = selectedField()\n const operator = customFilterOperator() as SearchFilterOperator\n if (!field || !operator || isValuelessOperator(operator)) return null\n if (field.choices)\n return (\n <select\n aria-label={labels.customFilterValue}\n multiple={operator === 'anyOf'}\n onChange={(event) => {\n if (event.currentTarget instanceof HTMLSelectElement)\n customFilterValue.set(\n Array.from(event.currentTarget.selectedOptions)\n .map((option) => option.value)\n .filter(Boolean)\n .join(','),\n )\n }}\n >\n <option value=\"\" disabled selected={!customFilterValue()}>\n {labels.customFilterValue}\n </option>\n {each(\n field.choices,\n (choice) => choice.value,\n (choice) => (\n <option value={choice.value} selected={customFilterValue().split(',').includes(choice.value)}>\n {choice.label}\n </option>\n ),\n )}\n </select>\n )\n const type =\n field.type === 'number'\n ? 'number'\n : field.type === 'date'\n ? 'date'\n : field.type === 'datetime'\n ? 'datetime-local'\n : 'text'\n return (\n <>\n <input\n type={type}\n step={field.type === 'number' ? 'any' : undefined}\n autocomplete=\"off\"\n aria-label={operator === 'between' ? labels.valueFrom : labels.customFilterValue}\n placeholder={labels.customFilterValue}\n value={customFilterValue()}\n onInput={(event) => {\n if (event.currentTarget instanceof HTMLInputElement)\n customFilterValue.set(event.currentTarget.value)\n }}\n />\n {operator === 'between' ? (\n <input\n type={type}\n step={field.type === 'number' ? 'any' : undefined}\n aria-label={labels.valueTo}\n value={customFilterEnd()}\n onInput={(event) => {\n if (event.currentTarget instanceof HTMLInputElement)\n customFilterEnd.set(event.currentTarget.value)\n }}\n />\n ) : null}\n </>\n )\n }\n const icon = (kind: 'filter' | 'groupBy' | 'favorite' | 'search'): TemplateResult => (\n <svg\n data-ui=\"search-filter-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path\n d={\n kind === 'filter'\n ? 'M4 5h16M7 12h10M10 19h4'\n : kind === 'groupBy'\n ? 'M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z'\n : kind === 'favorite'\n ? 'M6 3h12v18l-6-4-6 4z'\n : 'M21 21l-5-5M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0'\n }\n />\n </svg>\n )\n const sectionTrigger = (kind: 'groupBy' | 'favorite', label: string): TemplateResult => (\n <button\n data-ui=\"search-filter-section-toggle\"\n data-section={kind}\n type=\"button\"\n aria-expanded={String(menuOpen() && panel() === kind)}\n aria-controls={`${props.id}-panel`}\n onClick={(event) => {\n event.stopPropagation()\n menuOpener = event.currentTarget as HTMLElement\n if (menuOpen() && panel() === kind) closeMenu()\n else openPanel(kind)\n }}\n >\n {icon(kind)}\n <span>{label}</span>\n {countOf(kind) ? <span data-ui=\"menu-trigger-count\">{String(countOf(kind))}</span> : null}\n </button>\n )\n const panelContent = (): TemplateResult => (\n <div\n data-ui=\"search-filter-columns\"\n data-columns={1 + Number(hasGrouping) + Number(hasFavorites())}\n data-panel={sheetOpen() ? 'all' : panel()}\n >\n <div data-ui=\"search-filter-column\" data-facet-type=\"filter\">\n {columnTitle(labels.filters, countOf('filter'))}\n {each(\n config.filters,\n (option) => option.id,\n (option, index) => filterMenuItem(option, index === 0 ? null : config.filters[index - 1]),\n )}\n {capabilities.customFilters !== false && config.customFilterFields.length > 0 && (\n <>\n <hr data-ui=\"menu-separator\" />\n <div data-ui=\"custom-filter\">\n <div data-ui=\"custom-filter-row\">\n <select\n aria-label={labels.customFilterField}\n onChange={(event) => {\n if (event.currentTarget instanceof HTMLSelectElement) {\n customFilterField.set(event.currentTarget.value)\n customFilterOperator.set('')\n customFilterValue.set('')\n customFilterEnd.set('')\n }\n }}\n >\n <option value=\"\" disabled selected={!customFilterField()}>\n {labels.customFilterField}\n </option>\n {each(\n config.customFilterFields,\n (field) => field.value,\n (field) => (\n <option value={field.value} selected={field.value === customFilterField()}>\n {field.label}\n </option>\n ),\n )}\n </select>\n <select\n aria-label={labels.customFilterOperator}\n disabled={!customFilterField()}\n onChange={(event) => {\n if (event.currentTarget instanceof HTMLSelectElement) {\n customFilterOperator.set(event.currentTarget.value)\n customFilterValue.set('')\n customFilterEnd.set('')\n }\n }}\n >\n <option value=\"\" disabled selected={!customFilterOperator()}>\n {labels.customFilterOperator}\n </option>\n {each(\n operatorsFor(customFilterField()),\n (operator) => operator,\n (operator) => (\n <option value={operator} selected={operator === customFilterOperator()}>\n {operatorLabel(operator)}\n </option>\n ),\n )}\n </select>\n {valueEditor()}\n </div>\n <button\n data-ui=\"custom-filter-add\"\n type=\"button\"\n disabled={!canAddRule()}\n onClick={addCustomFilter}\n >\n {labels.customFilterAdd}\n </button>\n </div>\n </>\n )}\n </div>\n {hasGrouping && (\n <div data-ui=\"search-filter-column\" data-facet-type=\"groupBy\">\n {columnTitle(labels.groupBy, countOf('groupBy'))}\n {groupByPipeline()}\n {config.groupBy.length ? (\n <p data-ui=\"search-filter-grouping-add-label\">{labels.groupByAdd}</p>\n ) : null}\n {each(\n config.groupBy,\n (option) => option.id,\n (option, index) => groupByMenuItem(option, index === 0 ? null : config.groupBy[index - 1]),\n )}\n {groupByOptions.length ? <hr data-ui=\"menu-separator\" /> : null}\n {groupByOptions.length ? (\n <select\n data-ui=\"custom-group-by\"\n disabled={groupByFacets().length >= (config.maxGroupBy ?? Infinity)}\n aria-label={labels.customGroupByPlaceholder}\n onChange={(event) => {\n if (!(event.currentTarget instanceof HTMLSelectElement)) return\n const select = event.currentTarget\n const option = groupByOptions.find((entry) => entry.id === select.value)\n select.value = ''\n if (option && !isActive('groupBy', option.id)) toggleGroupBy(option)\n }}\n >\n <option value=\"\" disabled selected>\n {labels.customGroupByPlaceholder}\n </option>\n {each(\n groupByOptions,\n (option) => option.id,\n (option) => (\n <option value={option.id}>{option.label}</option>\n ),\n )}\n </select>\n ) : null}\n </div>\n )}\n {hasFavorites() && (\n <div data-ui=\"search-filter-column\" data-facet-type=\"favorite\">\n {columnTitle(labels.favorites, activeFavoriteId() ? 1 : 0)}\n {config.favoriteHref ? (\n <a data-ui=\"menu-item\" href={config.favoriteHref}>\n {labels.saveSearch}\n </a>\n ) : manager?.saveFavoriteFunction ? (\n <button\n data-ui=\"favorite-save-toggle\"\n type=\"button\"\n aria-expanded={String(savingFavorite())}\n aria-controls={`${props.id}-favorite-form`}\n onClick={() => {\n if (savingFavorite()) closeFavoriteForm()\n else {\n savingFavorite.set(true)\n queueMicrotask(() => focusControl('[data-ui=\"favorite-save\"] input[type=\"text\"]'))\n }\n }}\n >\n {labels.saveSearch}\n </button>\n ) : null}\n {manager?.saveFavoriteFunction && savingFavorite() && !config.favoriteHref ? (\n <form\n id={`${props.id}-favorite-form`}\n data-ui=\"favorite-save\"\n aria-label={labels.saveSearch}\n onSubmit={saveFavorite}\n >\n <div data-ui=\"favorite-save-row\">\n <input\n type=\"text\"\n autocomplete=\"off\"\n placeholder={labels.favoriteName}\n aria-label={labels.favoriteName}\n value={saveFavoriteName()}\n onInput={(event) => {\n if (event.currentTarget instanceof HTMLInputElement)\n saveFavoriteName.set(event.currentTarget.value)\n }}\n />\n <label>\n <input\n type=\"checkbox\"\n checked={saveFavoriteDefault()}\n onChange={(event) => {\n if (event.currentTarget instanceof HTMLInputElement)\n saveFavoriteDefault.set(event.currentTarget.checked)\n }}\n />\n {labels.favoriteDefault}\n </label>\n </div>\n <button\n data-ui=\"action\"\n data-variant=\"primary\"\n data-size=\"compact\"\n type=\"submit\"\n disabled={!saveFavoriteName().trim()}\n >\n {labels.favoriteSaveAction}\n </button>\n <button\n data-ui=\"action\"\n data-variant=\"secondary\"\n type=\"button\"\n onClick={(event) => {\n // Removing the form also detaches this target before the shared\n // outside-click dismissor runs. Keep cancellation inside the panel.\n event.stopPropagation()\n closeFavoriteForm()\n }}\n >\n {labels.favoriteCancel}\n </button>\n </form>\n ) : null}\n {favorites().length ? (\n <ul data-ui=\"favorite-list\">{each(favorites(), (favorite) => favorite.id, favoriteRow)}</ul>\n ) : (\n <p data-ui=\"search-filter-empty\">{labels.noFavorites}</p>\n )}\n </div>\n )}\n </div>\n )\n\n const notice = (): TemplateResult | null =>\n error() ? (\n <aside data-ui=\"notice\" data-pattern=\"notice\" data-tone=\"danger\" role=\"alert\">\n <div data-ui=\"notice-copy\">\n <p data-ui=\"notice-message\">{error()}</p>\n </div>\n {retryDraft() ? (\n <div data-ui=\"notice-actions\">\n <button data-ui=\"action\" data-variant=\"secondary\" type=\"button\" onClick={retry}>\n {labels.retry}\n </button>\n </div>\n ) : null}\n </aside>\n ) : null\n return {\n view: () => (\n <div\n data-ui=\"search-filter\"\n id={props.id}\n role=\"search\"\n aria-label={labels.searchLabel}\n data-name={config.name}\n data-size={config.size ?? 'default'}\n data-busy={pending() ? 'true' : null}\n onKeydown={(event: KeyboardEvent) => {\n if (event.key !== 'Escape' || sheetOpen() || !menuOpen()) return\n if (dismissDisclosure(event)) return\n event.preventDefault()\n event.stopPropagation()\n if (savingFavorite()) {\n closeFavoriteForm()\n return\n }\n closeMenu()\n queueMicrotask(() => menuOpener?.focus())\n }}\n >\n <div data-ui=\"search-filter-bar\">\n <div data-ui=\"search-filter-field\">\n {icon('search')}\n <input\n data-ui=\"search-filter-input\"\n type=\"search\"\n value={query()}\n autocomplete=\"off\"\n placeholder={labels.searchPlaceholder}\n aria-label={labels.searchLabel}\n onInput={handleSearchInput}\n onKeydown={handleSearchKeydown}\n onFocus={() => {\n if (query().trim()) suggestionsOpen.set(true)\n }}\n />\n {suggestionsOpen() && query().trim() ? (\n <div data-ui=\"search-filter-suggestions\" role=\"group\" aria-label={labels.searchGenericLabel}>\n <button data-ui=\"search-filter-suggestion\" type=\"button\" onClick={selectGenericSuggestion}>\n {labels.searchGenericLabel}: <b>\"{query().trim()}\"</b>\n </button>\n {each(\n fieldSuggestions(query().trim()),\n (field) => field.value,\n (field) => (\n <button\n data-ui=\"search-filter-suggestion\"\n type=\"button\"\n onClick={() => selectFieldSuggestion(field)}\n >\n {labels.searchFieldPrefix} <b>{field.label}</b> {labels.searchFieldPreposition}:{' '}\n <b>\"{query().trim()}\"</b>\n </button>\n ),\n )}\n </div>\n ) : null}\n </div>\n <details\n data-ui=\"menu\"\n data-variant=\"search-filter\"\n data-align=\"end\"\n data-active={countOf('filter') ? 'true' : null}\n open={menuOpen() === true ? true : undefined}\n onToggle={(event: Event) => {\n if (!(event.currentTarget instanceof HTMLDetailsElement)) return\n if (event.currentTarget.open) menuOpen.set(true)\n else closeMenu()\n }}\n >\n <summary\n data-ui=\"search-filter-toggle\"\n role=\"button\"\n aria-expanded={String(sheetOpen() || (menuOpen() && panel() === 'filter'))}\n aria-label={labels.filters}\n title={labels.filters}\n onClick={togglePanel}\n aria-controls={`${props.id}-panel`}\n >\n {icon('filter')}\n <span>{labels.filters}</span>\n {countOf('filter') ? (\n <span data-ui=\"menu-trigger-count\">{String(countOf('filter'))}</span>\n ) : null}\n </summary>\n <div\n id={`${props.id}-panel`}\n data-ui=\"menu-panel\"\n role=\"group\"\n aria-label={\n panel() === 'filter'\n ? labels.filters\n : panel() === 'groupBy'\n ? labels.groupBy\n : labels.favorites\n }\n >\n {!sheetOpen() ? panelContent() : null}\n </div>\n </details>\n {hasGrouping ? sectionTrigger('groupBy', labels.groupBy) : null}\n {hasFavorites() ? sectionTrigger('favorite', labels.favorites) : null}\n </div>\n {facets().length ? (\n <ul data-ui=\"search-filter-facets\">\n {each(facets(), (facet) => facet.id, facetChip)}\n {facets().some((facet) => facet.type === 'filter' || facet.type === 'favorite') ? (\n <li>\n <button data-ui=\"search-filter-clear\" type=\"button\" onClick={clearFilters}>\n {labels.clearFilters}\n </button>\n </li>\n ) : null}\n </ul>\n ) : null}\n {/* biome-ignore lint/a11y/useKeyWithClickEvents: Delegates native button clicks; onKeydown and onCancel handle Escape in the KetJS runtime. */}\n <dialog\n data-ui=\"search-filter-sheet\"\n aria-label={labels.toggleLabel}\n onKeydown={(event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n if (dismissDisclosure(event)) return\n event.preventDefault()\n event.stopPropagation()\n if (savingFavorite()) {\n closeFavoriteForm()\n return\n }\n closeSheet()\n }\n }}\n onCancel={(event: Event) => {\n event.preventDefault()\n closeSheet()\n }}\n onClick={(event) => {\n if (\n event.target instanceof Element &&\n event.target.closest('[data-ui=\"modal-close\"], [data-ui=\"modal-backdrop\"]')\n )\n closeSheet()\n }}\n >\n {sheetOpen() ? (\n <ModalSheet\n id={`${props.id}-sheet`}\n mode=\"client\"\n dialogSemantics=\"parent\"\n title={labels.toggleLabel}\n closeLabel={labels.close}\n body={\n <>\n {notice()}\n {panelContent()}\n </>\n }\n />\n ) : null}\n </dialog>\n {/* Below the bar, so the control the reader just used does not move under the pointer. */}\n {!sheetOpen() ? notice() : null}\n </div>\n ),\n mount: ({ root, lifetime }) => {\n mountedRoot = root\n document.addEventListener(\n 'ket:navigation-start',\n () => {\n // A link or Back/Forward supersedes an RPC which has not returned yet.\n // Our own navigation emits this event synchronously from respondWith.\n if (startingNavigation) return\n applyVersion++\n navigationRequest?.abort()\n facets.set(settled.facets)\n customFilterRules.set(settled.rules)\n pending.set(false)\n },\n { signal: lifetime },\n )\n // Autocomplete is an island-owned popup rather than a native <details>\n // disclosure, so the shared details-menu dismissor cannot see it. Keep\n // the boundary local to the component: an outside click abandons the\n // suggestions without changing the query the reader has entered.\n document.addEventListener(\n 'click',\n (event) => {\n if (!suggestionsOpen() && !menuOpen()) return\n const target = event.target\n if (target instanceof Node && (root as unknown as Node).contains(target)) return\n suggestionsOpen.set(false)\n closeMenu()\n },\n { signal: lifetime },\n )\n },\n dispose: () => {\n applyVersion++\n pending.set(false)\n dialogElement()?.close()\n mountedRoot = null\n },\n }\n}\n\nexport const searchFilter = (props: IslandProps): IslandController =>\n createSearchFilterView(props as SearchFilterIslandProps)\n", "import { each, effect, signal } from '@ketvietlab/ketjs-view'\nimport type { IslandController, IslandProps, JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Avatar, Badge, Code } from '../../primitives/status/index.tsx'\nimport type { Tone } from '../../primitives/status/index.tsx'\nimport { EmptyState, Notice } from '../../primitives/feedback/index.tsx'\nimport { FormattedDate, FormattedMoney, FormattedNumber } from '../../record/formatted-values/index.tsx'\n\nexport const HOOKS = [\n 'ket-table',\n 'kt-scroll',\n 'kt-selection-target',\n 'kt-grid',\n 'kt-select-col',\n 'kt-select-all',\n 'kt-col',\n 'kt-sort-button',\n 'kt-sort-icon',\n 'kt-row',\n 'kt-cell',\n 'kt-person',\n 'kt-select-cell',\n 'kt-row-select',\n 'kt-row-link',\n 'kt-group-row',\n 'kt-group-cell',\n 'kt-group-toggle',\n 'kt-group-indent',\n 'kt-group-count',\n 'kt-group-pager',\n 'kt-pager',\n 'kt-pager-label',\n 'kt-pager-button',\n 'kt-select-persisted',\n 'kt-caption',\n 'kt-tools',\n] as const\n\nexport type KetTableRow = Record<string, unknown>\n\n/**\n * A cell's shape as data, not code \u2014 a column's `format` has to cross the\n * island's server/browser props boundary as JSON (see `IslandController`),\n * so unlike a plain SSR table there is no `cell: (row) => JSX` closure here.\n * `custom` is the escape hatch for anything the other 7 kinds can't express.\n */\nexport type KetTableCellFormat =\n | { kind: 'text'; field: string }\n | { kind: 'number'; field: string; minimumFractionDigits?: number; maximumFractionDigits?: number }\n | { kind: 'currency'; field: string; currency: string }\n | { kind: 'date'; field: string }\n | { kind: 'status'; field: string; tones: Record<string, { label: string; tone: Tone }> }\n | { kind: 'identifier'; field: string }\n | { kind: 'person'; field: string }\n | { kind: 'link'; field: string; hrefField: string }\n | { kind: 'custom'; field: string; renderer: string; options?: Record<string, unknown> }\n\nexport type KetTableColumn = {\n key: string\n label: string\n format: KetTableCellFormat\n align?: 'start' | 'end'\n priority?: 'primary' | 'secondary' | 'tertiary'\n width?: 'narrow' | 'medium' | 'wide'\n sortable?: boolean\n /** Native URL-driven sorting; takes precedence over the RPC sort handler. */\n sortHref?: string\n sortLabel?: string\n sortDirection?: 'asc' | 'desc' | null\n /** Semantic presentation hint for server-rendered custom cells. */\n kind?: 'text' | 'number' | 'currency' | 'date' | 'status' | 'identifier' | 'person' | 'media'\n}\n\nexport type KetTableSort = { field: string; direction: 'asc' | 'desc' }\n\n/**\n * `listFunction` is the only RPC a sort or page change ever calls \u2014 over the\n * same `callApi`/`/_ket/fn/` contract `relation-select`/`search-filter`\n * already use. There is deliberately no `countFunction` here: `listInput`\n * never changes from inside KetTable itself, so the row count it would\n * recompute is always the one `KetTableConfig.total` already holds from the\n * page's own initial render \u2014 the caller counts once, server-side, the same\n * way it already builds that first page. `groupFunction` is only needed when\n * `KetTableConfig.groupBy` is non-empty.\n */\nexport type KetTableManager = {\n listFunction: string\n groupFunction?: string\n listInput?: Record<string, unknown>\n pageSize?: number\n}\n\n/**\n * Targets an *external* form by id \u2014 e.g. a screen's existing bulk-action\n * form. Each selected row becomes a hidden `{fieldName}.{id}` input targeting\n * that form, the same \"selected.<id>\" convention ketsuite's own bulk-action\n * forms already parse \u2014 so a form built for that convention needs no change.\n */\nexport type KetTableSelection = {\n formId: string\n fieldName?: string\n}\n\n/**\n * One group level's node. Reuses the same shape design-system's SSR\n * `TableGroup` and ketsuite's `loadListGroups` already return \u2014 `children`\n * (subgroups) or `rows` (a leaf group's rows) are absent until that node is\n * expanded and fetched, or until the caller pre-populates them statically\n * (a manager-less demo can supply an already-open tree with no RPC needed).\n */\nexport type KetTableGroup = {\n id: string\n label: string\n count: number\n children?: KetTableGroup[]\n rows?: KetTableRow[]\n /** Zero-based row offset for this leaf group's current page. */\n offset?: number\n /** URL-driven trees explicitly carry their expansion state and next URL. */\n open?: boolean\n href?: string\n pager?: { label: string; prev?: string; next?: string }\n}\n\nexport type KetTableLabels = {\n region?: string\n selectAll: string\n selectRow: string\n sortedAscending: string\n sortedDescending: string\n previousPage: string\n nextPage: string\n loading: string\n loadError: string\n retry: string\n empty: string\n emptyHint: string\n}\n\nexport type KetTableConfig = {\n columns: KetTableColumn[]\n /** First page, server-rendered \u2014 ignored once `groupBy` is non-empty. */\n rows: KetTableRow[]\n total: number\n idField: string\n locale?: string\n /** A `{id}`-interpolated href, replacing a function-typed `rowHref` that couldn't cross the props boundary. */\n rowHrefTemplate?: string\n /** The active group-by keys, in order \u2014 the array `search-filter`'s active Group By facets already produce. Empty \u21D2 a flat table. */\n groupBy?: string[]\n /** The server-rendered top level, when `groupBy` is non-empty. */\n groups?: KetTableGroup[]\n selection?: KetTableSelection\n /** Omit for URL-driven tables whose rows are supplied by the page route. */\n manager?: KetTableManager\n page?: number\n sort?: KetTableSort | null\n /** False when the containing toolbar owns pagination. */\n pager?: false | { prev?: string; next?: string }\n labels: KetTableLabels\n}\n\nexport type KetTableExtensions = Record<\n string,\n (value: unknown, row: KetTableRow, options: Record<string, unknown> | undefined) => TemplateResult\n>\n\n/** Server-owned collection cells keep their semantic markup and exact values.\n * URL navigation and native external-form controls require no island props.\n * The same KetTable renderer and CSS serve both this surface and the island.\n */\nexport type KetTableServerProps<Row> = {\n columns: Array<Omit<KetTableColumn, 'format' | 'sortable'> & { cell: (row: Row) => JSXChild }>\n rows: readonly Row[]\n id: (row: Row) => string\n rowHref?: (row: Row) => string\n rowLink?: boolean\n caption?: string | null\n tools?: JSXChild\n responsive?: 'scroll' | 'stack'\n gutter?: 'compact'\n selection?: KetTableSelection\n groups?: readonly (Omit<KetTableGroup, 'rows' | 'children' | 'href'> & {\n href: string\n rows?: readonly Row[]\n children?: KetTableServerProps<Row>['groups']\n })[]\n sort?: KetTableSort | null\n labels: KetTableLabels\n}\n\ntype ServerRendering = {\n rowHref?: (row: KetTableRow) => string\n rowLink?: boolean\n caption?: string | null\n tools?: JSXChild\n responsive?: 'scroll' | 'stack'\n gutter?: 'compact'\n}\n\nexport const KetTable = <Row,>(props: KetTableServerProps<Row>): TemplateResult => {\n const wrap = (row: Row): KetTableRow => ({ id: props.id(row), source: row })\n const groups = (nodes: NonNullable<KetTableServerProps<Row>['groups']>): KetTableGroup[] =>\n nodes.map((node) => ({\n ...node,\n rows: node.rows?.map(wrap),\n children: node.children ? groups(node.children) : undefined,\n }))\n const extensions: KetTableExtensions = Object.fromEntries(\n props.columns.map((column) => [\n column.key,\n (_value: unknown, row: KetTableRow) => <>{column.cell(row.source as Row)}</>,\n ]),\n )\n return createKetTableView(\n {\n id: 'server-collection',\n config: {\n columns: props.columns.map(({ cell: _cell, ...column }) => ({\n ...column,\n sortable: false,\n format: { kind: 'custom', field: 'source', renderer: column.key },\n })),\n rows: props.rows.map(wrap),\n total: props.rows.length,\n idField: 'id',\n groupBy: props.groups?.length ? ['server'] : [],\n groups: props.groups ? groups(props.groups) : undefined,\n selection: props.selection,\n sort: props.sort,\n pager: false,\n labels: props.labels,\n },\n },\n extensions,\n {\n rowHref: props.rowHref ? (row) => props.rowHref!(row.source as Row) : undefined,\n rowLink: props.rowLink,\n caption: props.caption,\n tools: props.tools,\n responsive: props.responsive,\n gutter: props.gutter,\n },\n ).view() as TemplateResult\n}\n\ntype KetTableIslandProps = IslandProps & { id: string; config: KetTableConfig }\ntype ApiPayload = { ok?: boolean; value?: unknown; message?: unknown; errors?: Array<{ message?: unknown }> }\n\nconst string = (value: unknown): string => (value == null ? '' : String(value))\n\n/**\n * Below 10,000, the exact count. At or above it, rounded down to the nearest\n * 10,000 and shown as an open-ended \"\u2265\" figure (`\"110K+\"`) instead of the\n * exact number \u2014 a filtered COUNT over a very large table is expensive\n * enough that callers are expected to cache it (see `partner.countPartners`\n * for the pattern), and a cached count is itself already a snapshot, so\n * presenting it as exact would overstate a precision nobody is maintaining.\n */\nconst formatApproxCount = (total: number): string =>\n total < 10_000 ? String(total) : `${Math.floor(total / 10_000) * 10}K+`\n\nconst callApi = async (name: string, input: unknown): Promise<unknown> => {\n const response = await fetch(`/_ket/fn/${encodeURIComponent(name)}`, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(input),\n })\n const payload = (await response.json()) as ApiPayload\n if (!response.ok || payload.ok === false) {\n const domainError = payload.errors?.[0]\n throw new Error(string(domainError?.message ?? payload.message ?? `HTTP ${response.status}`))\n }\n return payload.value\n}\n\nconst pathKey = (path: readonly string[]): string => JSON.stringify(path)\n\n/** Replaces the node identified by `path` (matching each level's `id`) via `updater`. */\nconst updateNodeAt = (\n nodes: readonly KetTableGroup[],\n path: readonly string[],\n updater: (node: KetTableGroup) => KetTableGroup,\n): KetTableGroup[] => {\n const [head, ...rest] = path\n return nodes.map((node) => {\n if (node.id !== head) return node\n return rest.length\n ? { ...node, children: updateNodeAt(node.children ?? [], rest, updater) }\n : updater(node)\n })\n}\n\nexport function createKetTableView(\n props: KetTableIslandProps,\n extensions?: KetTableExtensions,\n server?: ServerRendering,\n): IslandController {\n const { config } = props\n const labels = config.labels\n const manager = config.manager ?? { listFunction: '' }\n const pageSize = manager.pageSize ?? 50\n const columns = config.columns\n\n const rows = signal<KetTableRow[]>(config.rows ?? [])\n const total = signal(config.total ?? 0)\n const page = signal(config.page ?? 1)\n const sort = signal<KetTableSort | null>(config.sort ?? null)\n const groupBy = signal<string[]>(config.groupBy ?? [])\n const groups = signal<KetTableGroup[]>(config.groups ?? [])\n const initialOpen = (nodes: KetTableGroup[], path: string[] = []): string[] =>\n nodes.flatMap((node) => {\n const next = [...path, node.id]\n const open = node.open ?? (node.rows !== undefined || node.children !== undefined)\n return open ? [pathKey(next), ...initialOpen(node.children ?? [], next)] : []\n })\n const openGroups = signal<Set<string>>(new Set(initialOpen(config.groups ?? [])))\n const selectedIds = signal<Set<string>>(new Set())\n const loading = signal(false)\n const error = signal('')\n\n const isGrouped = (): boolean => groupBy().length > 0\n // Flat `sortField`/`sortDirection` scalars, matching the manager RPCs'\n // own input schema convention (e.g. `partner.listPartners`) rather than a\n // nested object those schemas would need extra shape to accept.\n const sortParams = (): { sortField?: string; sortDirection?: 'asc' | 'desc' } => {\n const current = sort()\n return current ? { sortField: current.field, sortDirection: current.direction } : {}\n }\n const idOf = (row: KetTableRow): string => string(row[config.idField])\n const isSelected = (row: KetTableRow): boolean => selectedIds().has(idOf(row))\n\n const toggleSelect = (row: KetTableRow): void => {\n const id = idOf(row)\n const next = new Set(selectedIds())\n if (next.has(id)) next.delete(id)\n else next.add(id)\n selectedIds.set(next)\n }\n\n const visibleRows = (): KetTableRow[] => (isGrouped() ? flattenVisibleRows(groups(), []) : rows())\n\n const flattenVisibleRows = (nodes: readonly KetTableGroup[], path: readonly string[]): KetTableRow[] =>\n nodes.flatMap((node) => {\n if (!openGroups().has(pathKey([...path, node.id]))) return []\n return [...(node.rows ?? []), ...flattenVisibleRows(node.children ?? [], [...path, node.id])]\n })\n\n const allVisibleSelected = (): boolean => {\n const visible = visibleRows()\n return visible.length > 0 && visible.every(isSelected)\n }\n\n const toggleSelectAllVisible = (): void => {\n const visible = visibleRows().map(idOf)\n const allSelected = visible.every((id) => selectedIds().has(id))\n const next = new Set(selectedIds())\n for (const id of visible) {\n if (allSelected) next.delete(id)\n else next.add(id)\n }\n selectedIds.set(next)\n }\n\n const fetchFlat = async (): Promise<void> => {\n if (!manager.listFunction) return\n loading.set(true)\n error.set('')\n try {\n // `limit`/`offset`, not `page`/`pageSize` \u2014 the same pagination shape\n // ketsuite's other manager RPCs (e.g. `relation-select`'s) already use.\n const input = {\n ...(manager.listInput ?? {}),\n ...sortParams(),\n limit: pageSize,\n offset: (page() - 1) * pageSize,\n }\n // No `countFunction` call here: `manager.listInput` never changes from\n // inside KetTable itself (sort/page are the only client-driven state),\n // so the row count a sort or page change would recompute is always the\n // same one `total` already holds from the initial config \u2014 recounting\n // on every click would just repeat the same expensive query for free.\n const listValue = await callApi(manager.listFunction, input)\n rows.set(Array.isArray(listValue) ? (listValue as KetTableRow[]) : [])\n } catch {\n error.set(labels.loadError)\n } finally {\n loading.set(false)\n }\n }\n\n const toggleSort = (column: KetTableColumn): void => {\n if (!column.sortable || isGrouped()) return\n const current = sort()\n sort.set(\n !current || current.field !== column.key\n ? { field: column.key, direction: 'asc' }\n : current.direction === 'asc'\n ? { field: column.key, direction: 'desc' }\n : null,\n )\n page.set(1)\n void fetchFlat()\n }\n\n const goToPage = (target: number): void => {\n if (target < 1 || isGrouped()) return\n page.set(target)\n void fetchFlat()\n }\n\n const nodeAt = (path: readonly string[]): KetTableGroup | null => {\n let level = groups()\n let node: KetTableGroup | null = null\n for (const id of path) {\n node = level.find((candidate) => candidate.id === id) ?? null\n if (!node) return null\n level = node.children ?? []\n }\n return node\n }\n\n const fetchGroupLevel = async (path: readonly string[]): Promise<void> => {\n const depth = path.length\n const keys = groupBy()\n loading.set(true)\n error.set('')\n try {\n if (depth === keys.length) {\n if (!manager.listFunction) return\n const node = nodeAt(path)\n const offset = node?.offset ?? 0\n const value = (await callApi(manager.listFunction, {\n ...(manager.listInput ?? {}),\n groupBy: keys,\n groupPath: path,\n ...sortParams(),\n limit: pageSize,\n offset,\n })) as unknown\n groups.set(\n updateNodeAt(groups(), path, (node) => ({\n ...node,\n rows: Array.isArray(value) ? (value as KetTableRow[]) : [],\n offset,\n })),\n )\n } else {\n if (!manager.groupFunction) return\n const value = (await callApi(manager.groupFunction, {\n ...(manager.listInput ?? {}),\n groupBy: keys,\n path,\n })) as Array<{ id: string; label: string; count: number }>\n groups.set(\n updateNodeAt(groups(), path, (node) => ({\n ...node,\n children: (value ?? []).map((child) => ({\n id: child.id,\n label: child.label,\n count: child.count,\n })),\n })),\n )\n }\n } catch {\n error.set(labels.loadError)\n } finally {\n loading.set(false)\n }\n }\n\n const toggleGroup = (path: readonly string[]): void => {\n const key = pathKey(path)\n const next = new Set(openGroups())\n if (next.has(key)) {\n next.delete(key)\n openGroups.set(next)\n return\n }\n next.add(key)\n openGroups.set(next)\n const node = nodeAt(path)\n if (node && node.children === undefined && node.rows === undefined) void fetchGroupLevel(path)\n }\n\n const goToGroupPage = (path: readonly string[], offset: number): void => {\n const node = nodeAt(path)\n if (!node || offset < 0 || offset >= node.count) return\n groups.set(updateNodeAt(groups(), path, (held) => ({ ...held, rows: undefined, offset })))\n void fetchGroupLevel(path)\n }\n\n const rowHrefFor = (row: KetTableRow): string =>\n config.rowHrefTemplate ? config.rowHrefTemplate.replace('{id}', encodeURIComponent(idOf(row))) : ''\n\n const renderCell = (row: KetTableRow, format: KetTableCellFormat): TemplateResult => {\n const value = row[format.field]\n switch (format.kind) {\n case 'text':\n return <>{string(value)}</>\n case 'number':\n return typeof value === 'number' ? (\n <FormattedNumber\n locale={config.locale}\n value={value}\n minimumFractionDigits={format.minimumFractionDigits}\n maximumFractionDigits={format.maximumFractionDigits}\n />\n ) : (\n <>\u2014</>\n )\n case 'currency':\n return typeof value === 'number' ||\n (typeof value === 'string' && /^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)$/.test(value.trim())) ? (\n <FormattedMoney value={value} currency={format.currency} locale={config.locale} />\n ) : (\n <>\u2014</>\n )\n case 'date':\n return <FormattedDate value={string(value)} locale={config.locale} />\n case 'status': {\n const entry = format.tones[string(value)]\n return entry ? <Badge label={entry.label} tone={entry.tone} /> : <>{string(value)}</>\n }\n case 'identifier':\n return value ? <Code value={string(value)} /> : <>\u2014</>\n case 'person':\n return (\n <span data-ui=\"kt-person\">\n <Avatar name={string(value)} />\n <span>{string(value)}</span>\n </span>\n )\n case 'link':\n return (\n <a data-ui=\"kt-row-link\" href={string(row[format.hrefField])}>\n {string(value)}\n </a>\n )\n case 'custom': {\n const renderer = extensions?.[format.renderer]\n return renderer ? renderer(value, row, format.options) : <>{string(value)}</>\n }\n }\n }\n\n const tableRow = (row: KetTableRow): TemplateResult => (\n <tr\n data-ui=\"kt-row\"\n data-row={idOf(row)}\n data-selected={isSelected(row) ? 'true' : null}\n data-row-href={server?.rowHref?.(row)}\n tabindex={server?.rowHref && server.rowLink === false ? 0 : null}\n >\n {config.selection ? (\n <td data-ui=\"kt-select-cell\">\n <label data-ui=\"kt-selection-target\">\n <input\n data-ui=\"kt-row-select\"\n type=\"checkbox\"\n autocomplete=\"off\"\n name={server ? `${config.selection.fieldName ?? 'selected'}.${idOf(row)}` : undefined}\n value={server ? '1' : undefined}\n form={server ? config.selection.formId : undefined}\n checked={isSelected(row)}\n aria-label={`${labels.selectRow}: ${idOf(row)}`}\n onChange={() => toggleSelect(row)}\n />\n </label>\n </td>\n ) : null}\n {each(\n columns,\n (column) => column.key,\n (column, index) => (\n <td\n data-ui=\"kt-cell\"\n data-col={column.key}\n data-align={column.align ?? 'start'}\n data-kind={column.kind ?? column.format.kind}\n data-priority={column.priority ?? 'secondary'}\n data-label={server?.responsive === 'stack' ? column.label : null}\n >\n {index === 0 && (config.rowHrefTemplate || (server?.rowHref && server.rowLink !== false)) ? (\n <a data-ui=\"kt-row-link\" data-primary=\"true\" href={server?.rowHref?.(row) ?? rowHrefFor(row)}>\n {renderCell(row, column.format)}\n </a>\n ) : (\n renderCell(row, column.format)\n )}\n </td>\n ),\n )}\n </tr>\n )\n\n const groupSpan = (): number => columns.length + (config.selection ? 1 : 0)\n\n const groupRowView = (node: KetTableGroup, path: readonly string[], depth: number): TemplateResult => {\n const open = openGroups().has(pathKey(path))\n const offset = node.offset ?? 0\n const hasPager =\n node.pager !== undefined || (node.rows !== undefined && node.count > pageSize && !node.href)\n const from = node.count === 0 ? 0 : offset + 1\n const to = Math.min(offset + (node.rows?.length ?? 0), node.count)\n return (\n <>\n <tr data-ui=\"kt-group-row\" data-depth={String(depth)}>\n <td data-ui=\"kt-group-cell\" colSpan={String(groupSpan())}>\n {node.href ? (\n <a data-ui=\"kt-group-toggle\" href={node.href} aria-expanded={String(open)}>\n <span data-ui=\"kt-group-indent\" style={`--kt-group-depth: ${depth}`} aria-hidden=\"true\" />\n <span aria-hidden=\"true\">{open ? '\u25BE' : '\u25B8'}</span>\n <span>{node.label}</span>\n <span data-ui=\"kt-group-count\">{String(node.count)}</span>\n </a>\n ) : (\n <button\n data-ui=\"kt-group-toggle\"\n type=\"button\"\n aria-expanded={String(open)}\n onClick={() => toggleGroup(path)}\n >\n <span data-ui=\"kt-group-indent\" style={`--kt-group-depth: ${depth}`} aria-hidden=\"true\" />\n <span aria-hidden=\"true\">{open ? '\u25BE' : '\u25B8'}</span>\n <span>{node.label}</span>\n <span data-ui=\"kt-group-count\">{String(node.count)}</span>\n </button>\n )}\n </td>\n </tr>\n {open && node.children\n ? each(\n node.children,\n (child) => child.id,\n (child) => groupRowView(child, [...path, child.id], depth + 1),\n )\n : null}\n {open && node.rows ? each(node.rows, idOf, tableRow) : null}\n {open && hasPager ? (\n <tr data-ui=\"kt-group-pager\">\n <td colSpan={String(groupSpan())}>\n {node.pager ? (\n navigationPager(node.pager)\n ) : (\n <div data-ui=\"kt-pager\">\n <span data-ui=\"kt-pager-label\">{`${from}\u2013${to} / ${formatApproxCount(node.count)}`}</span>\n <button\n data-ui=\"kt-pager-button\"\n data-direction=\"prev\"\n type=\"button\"\n disabled={offset === 0}\n aria-label={labels.previousPage}\n onClick={() => goToGroupPage(path, Math.max(0, offset - pageSize))}\n >\n \u2039\n </button>\n <button\n data-ui=\"kt-pager-button\"\n data-direction=\"next\"\n type=\"button\"\n disabled={offset + pageSize >= node.count}\n aria-label={labels.nextPage}\n onClick={() => goToGroupPage(path, offset + pageSize)}\n >\n \u203A\n </button>\n </div>\n )}\n </td>\n </tr>\n ) : null}\n </>\n )\n }\n\n const columnHeader = (column: KetTableColumn): TemplateResult => {\n const active = sort()?.field === column.key\n const direction = column.sortDirection ?? (active ? (sort()?.direction ?? null) : null)\n return (\n <th\n data-ui=\"kt-col\"\n data-col={column.key}\n data-align={column.align ?? 'start'}\n data-kind={column.kind ?? column.format.kind}\n data-priority={column.priority ?? 'secondary'}\n data-width={column.width ?? null}\n aria-sort={direction === 'asc' ? 'ascending' : direction === 'desc' ? 'descending' : null}\n scope=\"col\"\n >\n {column.sortHref ? (\n <a data-ui=\"kt-sort-button\" href={column.sortHref} aria-label={column.sortLabel}>\n <span>{column.label}</span>\n {direction ? (\n <span data-ui=\"kt-sort-icon\" aria-hidden=\"true\">\n {direction === 'asc' ? '\u2191' : '\u2193'}\n </span>\n ) : null}\n </a>\n ) : column.sortable && !isGrouped() ? (\n <button data-ui=\"kt-sort-button\" type=\"button\" onClick={() => toggleSort(column)}>\n <span>{column.label}</span>\n {direction ? (\n <span\n data-ui=\"kt-sort-icon\"\n aria-hidden=\"true\"\n title={direction === 'asc' ? labels.sortedAscending : labels.sortedDescending}\n >\n {direction === 'asc' ? '\u2191' : '\u2193'}\n </span>\n ) : null}\n </button>\n ) : (\n column.label\n )}\n </th>\n )\n }\n\n const navigationPager = (links: { label: string; prev?: string; next?: string }): TemplateResult => (\n <div data-ui=\"kt-pager\">\n <span data-ui=\"kt-pager-label\">{links.label}</span>\n {links.prev ? (\n <a data-ui=\"kt-pager-button\" data-direction=\"prev\" href={links.prev} aria-label={labels.previousPage}>\n \u2039\n </a>\n ) : null}\n {links.next ? (\n <a data-ui=\"kt-pager-button\" data-direction=\"next\" href={links.next} aria-label={labels.nextPage}>\n \u203A\n </a>\n ) : null}\n </div>\n )\n\n const pagerView = (): TemplateResult => {\n const totalPages = Math.max(1, Math.ceil(total() / pageSize))\n const from = total() === 0 ? 0 : (page() - 1) * pageSize + 1\n const to = Math.min(page() * pageSize, total())\n if (config.pager)\n return navigationPager({ ...config.pager, label: `${from}\u2013${to} / ${formatApproxCount(total())}` })\n return (\n <div data-ui=\"kt-pager\">\n <span data-ui=\"kt-pager-label\">{`${from}\u2013${to} / ${formatApproxCount(total())}`}</span>\n <button\n data-ui=\"kt-pager-button\"\n data-direction=\"prev\"\n type=\"button\"\n disabled={page() <= 1}\n aria-label={labels.previousPage}\n onClick={() => goToPage(page() - 1)}\n >\n \u2039\n </button>\n <button\n data-ui=\"kt-pager-button\"\n data-direction=\"next\"\n type=\"button\"\n disabled={page() >= totalPages}\n aria-label={labels.nextPage}\n onClick={() => goToPage(page() + 1)}\n >\n \u203A\n </button>\n </div>\n )\n }\n\n const persistedInputs = (): TemplateResult | null =>\n config.selection && !server ? (\n <div data-ui=\"kt-select-persisted\" aria-hidden=\"true\">\n {each(\n [...selectedIds()],\n (id) => id,\n (id) => (\n <input\n type=\"hidden\"\n name={`${config.selection?.fieldName ?? 'selected'}.${id}`}\n value=\"1\"\n form={config.selection?.formId}\n />\n ),\n )}\n </div>\n ) : null\n\n return {\n view: () => (\n <div\n data-ui=\"ket-table\"\n data-server={server ? 'true' : null}\n data-responsive={server?.responsive}\n data-gutter={server?.gutter}\n data-busy={loading() ? 'true' : null}\n >\n {server?.tools !== undefined && <div data-ui=\"kt-tools\">{server.tools}</div>}\n {error() ? (\n <Notice\n tone=\"danger\"\n title={labels.loadError}\n message={error()}\n actions={\n <button type=\"button\" onClick={() => void (isGrouped() ? undefined : fetchFlat())}>\n {labels.retry}\n </button>\n }\n />\n ) : null}\n {persistedInputs()}\n {(isGrouped() ? groups().length === 0 : rows().length === 0) ? (\n <EmptyState title={labels.empty} message={labels.emptyHint} />\n ) : (\n <div data-ui=\"kt-scroll\" tabindex=\"0\" role=\"region\" aria-label={labels.region ?? 'Results'}>\n <table data-ui=\"kt-grid\">\n {server?.caption && <caption data-ui=\"kt-caption\">{server.caption}</caption>}\n <thead>\n <tr>\n {config.selection ? (\n <th data-ui=\"kt-select-col\" scope=\"col\">\n <label data-ui=\"kt-selection-target\">\n <input\n data-ui=\"kt-select-all\"\n type=\"checkbox\"\n autocomplete=\"off\"\n checked={allVisibleSelected()}\n aria-label={labels.selectAll}\n onChange={toggleSelectAllVisible}\n />\n </label>\n </th>\n ) : null}\n {each(columns, (column) => column.key, columnHeader)}\n </tr>\n </thead>\n <tbody>\n {isGrouped()\n ? each(\n groups(),\n (node) => node.id,\n (node) => groupRowView(node, [node.id], 0),\n )\n : each(rows(), idOf, tableRow)}\n </tbody>\n </table>\n </div>\n )}\n {!isGrouped() && config.pager !== false ? pagerView() : null}\n </div>\n ),\n mount: ({ root, lifetime }) => {\n const stop = effect(() => {\n const visible = visibleRows()\n const selected = visible.filter(isSelected).length\n const checkbox = (root as unknown as HTMLElement).querySelector<HTMLInputElement>(\n '[data-ui=\"kt-select-all\"]',\n )\n if (checkbox) checkbox.indeterminate = selected > 0 && selected < visible.length\n })\n lifetime.addEventListener('abort', stop, { once: true })\n },\n dispose: () => {\n loading.set(false)\n },\n }\n}\n\nexport const ketTable = (props: IslandProps): IslandController =>\n createKetTableView(props as KetTableIslandProps)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['formatted-date', 'formatted-number', 'formatted-money'] as const\n\nexport const FormattedDate = (props: {\n value: string\n locale?: string\n emptyLabel?: string\n}): TemplateResult => {\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/u.exec(props.value)\n const date = match ? new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))) : null\n const valid =\n !!match &&\n !!date &&\n date.getUTCFullYear() === Number(match[1]) &&\n date.getUTCMonth() === Number(match[2]) - 1 &&\n date.getUTCDate() === Number(match[3])\n const formatted = valid\n ? new Intl.DateTimeFormat(props.locale ?? 'vi-VN', { dateStyle: 'medium', timeZone: 'UTC' }).format(date)\n : (props.emptyLabel ?? '\u2014')\n return (\n <time data-ui=\"formatted-date\" datetime={valid ? props.value : null}>\n {formatted}\n </time>\n )\n}\n\nexport const FormattedNumber = (props: {\n value: number\n locale?: string\n minimumFractionDigits?: number\n maximumFractionDigits?: number\n}): TemplateResult => (\n <data data-ui=\"formatted-number\" value={String(props.value)}>\n {new Intl.NumberFormat(props.locale ?? 'vi-VN', {\n minimumFractionDigits: props.minimumFractionDigits,\n maximumFractionDigits: props.maximumFractionDigits,\n }).format(props.value)}\n </data>\n)\n\nexport const FormattedMoney = (props: {\n /** Decimal text preserves database precision beyond Number.MAX_SAFE_INTEGER. */\n value: number | string\n currency: string\n locale?: string\n}): TemplateResult => (\n <data data-ui=\"formatted-money\" value={String(props.value)} data-currency={props.currency}>\n {(\n new Intl.NumberFormat(props.locale ?? 'vi-VN', { style: 'currency', currency: props.currency })\n .format as unknown as (value: number | string) => string\n )(props.value)}\n </data>\n)\n", "// @ts-check\nimport { attachDatePickers } from '../forms/date-time/runtime.js'\n\nconst focusableSelector = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n 'summary',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',')\n\n/** @param {ParentNode} root */\nconst focusables = (root) =>\n [...root.querySelectorAll(focusableSelector)].filter(\n (element) =>\n element instanceof HTMLElement &&\n !element.hidden &&\n element.getAttribute('aria-hidden') !== 'true' &&\n (typeof element.checkVisibility !== 'function' || element.checkVisibility()),\n )\n\n/**\n * Attach the small browser contract shared by server-rendered overlays.\n * The returned function restores inert state and removes every listener.\n *\n * @param {ParentNode} [root]\n * @returns {() => void}\n */\n/**\n * Popups built on <details> other than the action menu: a period filter and view\n * settings. They close on a click outside them and on Escape, like the menu.\n */\nconst DISMISSIBLE_POPUPS = ['[data-ui=\"timeframe-menu\"]', '[data-ui=\"view-settings\"]']\nconst DISMISSIBLE_POPUPS_OPEN = DISMISSIBLE_POPUPS.map((selector) => `${selector}[open]`).join(', ')\n\n/** @param {ParentNode} root */\nconst attachGlobalSearch = (root) => {\n /** @type {(() => void)[]} */\n const cleanups = []\n const launchers = [...root.querySelectorAll('[data-ui=\"global-search-trigger\"]')]\n /** @type {(() => boolean)[]} */\n const openers = []\n for (const launcher of launchers) {\n const dialog = [...root.querySelectorAll('[data-ui=\"global-search-dialog\"]')].find(\n (candidate) => candidate.id === launcher.getAttribute('aria-controls'),\n )\n if (!(launcher instanceof HTMLElement) || !(dialog instanceof HTMLDialogElement)) continue\n const input = dialog.querySelector('[data-ui=\"global-search-input\"]')\n const form = dialog.querySelector('[data-ui=\"global-search\"]')\n const template = dialog.querySelector('template')\n if (\n !(input instanceof HTMLInputElement) ||\n !(form instanceof HTMLElement) ||\n !(template instanceof HTMLTemplateElement)\n )\n continue\n const layer = template.content.querySelector('[data-ui=\"modal-layer\"]')\n const body = template.content.querySelector('[data-ui=\"modal-body\"]')\n if (!layer || !body) continue\n const parkContent = () => {\n dialog.append(form)\n template.content.append(layer)\n }\n const shortcut = launcher.querySelector('[data-ui=\"global-search-shortcut\"]')\n if (shortcut instanceof HTMLElement) {\n shortcut.textContent = /Mac|iPhone|iPad/.test(navigator.platform) ? '\u2318 K' : 'Ctrl K'\n shortcut.hidden = false\n }\n const open = () => {\n if (launcher.closest('[inert]')) return false\n if (!dialog.open) {\n if (root.querySelector('dialog[open], [data-ui=\"modal-layer\"][data-route-modal=\"true\"]')) return false\n dialog.append(layer)\n body.append(form)\n dialog.showModal()\n }\n launcher.setAttribute('aria-expanded', 'true')\n input.focus()\n input.select()\n return true\n }\n openers.push(open)\n const restore = () => {\n parkContent()\n launcher.setAttribute('aria-expanded', 'false')\n if (launcher.isConnected) launcher.focus()\n }\n /** @param {MouseEvent} event */\n const launch = (event) => {\n if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return\n if (open()) event.preventDefault()\n }\n /** @param {Event} event */\n const dismiss = (event) => {\n if (\n event.target instanceof Element &&\n event.target.closest('[data-ui=\"modal-close\"], [data-ui=\"modal-backdrop\"]')\n ) {\n dialog.close()\n }\n }\n /** @param {Event} event */\n const cancel = (event) => {\n event.preventDefault()\n event.stopPropagation()\n dialog.close()\n }\n const submit = () => dialog.close()\n launcher.addEventListener('click', launch)\n dialog.addEventListener('click', dismiss)\n dialog.addEventListener('cancel', cancel)\n dialog.addEventListener('close', restore)\n dialog.addEventListener('submit', submit)\n cleanups.push(() => {\n launcher.removeEventListener('click', launch)\n dialog.removeEventListener('click', dismiss)\n dialog.removeEventListener('cancel', cancel)\n dialog.removeEventListener('close', restore)\n dialog.removeEventListener('submit', submit)\n if (dialog.open) dialog.close()\n parkContent()\n })\n }\n /** @param {KeyboardEvent} event */\n const shortcut = (event) => {\n if (\n (event.ctrlKey || event.metaKey) &&\n event.key.toLowerCase() === 'k' &&\n !event.altKey &&\n !event.defaultPrevented\n ) {\n if (openers.some((open) => open())) event.preventDefault()\n }\n }\n document.addEventListener('keydown', shortcut)\n return () => {\n document.removeEventListener('keydown', shortcut)\n for (const cleanup of cleanups) cleanup()\n }\n}\n\nexport const attachDesignSystemInteractions = (root = document) => {\n const cleanupDatePickers = attachDatePickers(root)\n const cleanupGlobalSearch = attachGlobalSearch(root)\n const activeBeforeOpen = document.activeElement instanceof HTMLElement ? document.activeElement : null\n const modal = root.querySelector('[data-ui=\"modal-layer\"][data-route-modal=\"true\"] [role=\"dialog\"]')\n const appShell = root.querySelector('[data-ui=\"app-shell\"], [data-ui=\"shell\"]')\n const priorInert = appShell instanceof HTMLElement ? appShell.inert : false\n if (modal instanceof HTMLElement) {\n if (appShell instanceof HTMLElement && !appShell.contains(modal)) appShell.inert = true\n const first = focusables(modal)[0]\n ;(first instanceof HTMLElement ? first : modal).focus()\n }\n\n const menus = /** @type {HTMLDetailsElement[]} */ (\n [...root.querySelectorAll('[data-ui=\"menu\"]')].filter((menu) => menu instanceof HTMLDetailsElement)\n )\n /** @param {HTMLDetailsElement} menu */\n const syncMenu = (menu) => {\n const trigger = menu.querySelector('[data-ui=\"menu-trigger\"]')\n if (trigger instanceof HTMLElement) trigger.setAttribute('aria-expanded', String(menu.open))\n }\n const menuCleanups = menus.map((menu) => {\n const onToggle = () => syncMenu(menu)\n menu.addEventListener('toggle', onToggle)\n syncMenu(menu)\n return () => menu.removeEventListener('toggle', onToggle)\n })\n\n const navigationMedia = window.matchMedia('(max-width: 48rem)')\n const navigationRoot = document.documentElement\n const priorNavigationOpen = navigationRoot.dataset.kvNavigationOpen\n const navigations = /** @type {HTMLDetailsElement[]} */ (\n [...root.querySelectorAll('[data-ui=\"app-navigation\"]')].filter(\n (navigation) => navigation instanceof HTMLDetailsElement,\n )\n )\n const navigationStates = new Map()\n\n /** @param {HTMLDetailsElement} navigation */\n const restoreNavigationTargets = (navigation) => {\n const state = navigationStates.get(navigation)\n if (!state?.targets) return\n for (const { element, inert } of state.targets) element.inert = inert\n state.targets = null\n }\n\n const syncNavigationLock = () => {\n const hasOpenNavigation = navigationMedia.matches && navigations.some((navigation) => navigation.open)\n if (hasOpenNavigation) navigationRoot.dataset.kvNavigationOpen = 'true'\n else if (priorNavigationOpen === undefined) delete navigationRoot.dataset.kvNavigationOpen\n else navigationRoot.dataset.kvNavigationOpen = priorNavigationOpen\n }\n\n /** @param {HTMLDetailsElement} navigation */\n const syncNavigation = (navigation) => {\n const trigger = navigation.querySelector('[data-ui=\"navigation-trigger\"]')\n const drawer = navigation.querySelector('[data-ui=\"navigation-drawer\"]')\n if (!(trigger instanceof HTMLElement) || !(drawer instanceof HTMLElement)) return\n const mobile = navigationMedia.matches\n trigger.dataset.open = String(mobile && navigation.open)\n for (const toggle of root.querySelectorAll('[data-ui=\"navigation-toggle\"]')) {\n if (toggle.getAttribute('aria-controls') === drawer.id)\n toggle.setAttribute('aria-expanded', String(mobile && navigation.open))\n }\n if (!mobile) {\n drawer.removeAttribute('role')\n drawer.removeAttribute('aria-modal')\n drawer.removeAttribute('aria-label')\n restoreNavigationTargets(navigation)\n syncNavigationLock()\n return\n }\n drawer.setAttribute('role', 'dialog')\n drawer.setAttribute('aria-modal', 'true')\n drawer.setAttribute('aria-label', drawer.dataset.navigationLabel ?? 'Navigation')\n if (!navigation.open) {\n restoreNavigationTargets(navigation)\n syncNavigationLock()\n return\n }\n const state = navigationStates.get(navigation) ?? {}\n if (!state.targets) {\n const shell = navigation.closest('[data-ui=\"app-shell\"], [data-ui=\"shell\"]')\n const targets = shell\n ? [\n ...shell.querySelectorAll(\n ':scope > [data-ui=\"app-main\"], :scope > [data-ui=\"main\"], :scope > [data-ui=\"app-right-rail\"], :scope > [data-ui=\"app-shell-topbar\"]',\n ),\n ]\n .filter((element) => element instanceof HTMLElement)\n .map((element) => ({ element, inert: element.inert }))\n : []\n state.returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : trigger\n for (const { element } of targets) element.inert = true\n state.targets = targets\n navigationStates.set(navigation, state)\n queueMicrotask(() => {\n if (!navigation.open || !navigationMedia.matches) return\n const activeItem = drawer.querySelector('[data-ui=\"navigation-item\"][aria-current=\"page\"]')\n const first = focusables(drawer)[0]\n const focusTarget = activeItem instanceof HTMLElement ? activeItem : first\n ;(focusTarget instanceof HTMLElement ? focusTarget : drawer).focus()\n })\n }\n syncNavigationLock()\n }\n\n /** @param {HTMLDetailsElement} navigation @param {boolean} [returnFocus] */\n const closeNavigation = (navigation, returnFocus = true) => {\n const state = navigationStates.get(navigation)\n navigation.open = false\n syncNavigation(navigation)\n if (returnFocus && state?.returnFocus instanceof HTMLElement && state.returnFocus.isConnected)\n state.returnFocus.focus()\n navigationStates.delete(navigation)\n }\n\n const navigationCleanups = navigations.map((navigation) => {\n const onToggle = () => syncNavigation(navigation)\n /** @param {MouseEvent} event */\n const onClick = (event) => {\n const target = event.target\n if (!(target instanceof Element)) return\n const branchTrigger = target.closest('[data-ui=\"navigation-branch-trigger\"]')\n const branch = branchTrigger?.closest('[data-ui=\"navigation-branch\"]')\n if (branch instanceof HTMLDetailsElement && branch.open) {\n event.preventDefault()\n return\n }\n if (!navigationMedia.matches || !navigation.open) return\n if (\n target.closest('[data-ui=\"navigation-close\"]') ||\n target.closest('[data-ui=\"navigation-backdrop\"]') ||\n target.closest('[data-ui=\"navigation-item\"]')\n )\n closeNavigation(navigation)\n }\n navigation.addEventListener('toggle', onToggle)\n navigation.addEventListener('click', onClick)\n syncNavigation(navigation)\n return () => {\n navigation.removeEventListener('toggle', onToggle)\n navigation.removeEventListener('click', onClick)\n restoreNavigationTargets(navigation)\n }\n })\n const onNavigationMediaChange = () => {\n for (const navigation of navigations) {\n if (navigation.open) closeNavigation(navigation, false)\n else syncNavigation(navigation)\n }\n }\n navigationMedia.addEventListener('change', onNavigationMediaChange)\n\n for (const popover of root.querySelectorAll('[data-ui=\"popover\"][data-open=\"true\"]')) {\n const trigger = popover.querySelector('[data-ui=\"popover-trigger\"]')\n const panel = popover.querySelector('[data-ui=\"popover-panel\"]')\n if (!(trigger instanceof HTMLElement) || !(panel instanceof HTMLElement)) continue\n const box = trigger.getBoundingClientRect()\n const gap = 8\n const placement = popover.getAttribute('data-placement') ?? 'bottom-start'\n const prefersTop = placement.startsWith('top')\n const prefersEnd = placement.endsWith('end')\n const topPosition = box.top - panel.offsetHeight - gap\n const bottomPosition = box.bottom + gap\n const topFits = topPosition >= 8\n const bottomFits = bottomPosition + panel.offsetHeight <= window.innerHeight - 8\n const useTop = prefersTop ? topFits || !bottomFits : !bottomFits && topFits\n const requestedLeft = prefersEnd ? box.right - panel.offsetWidth : box.left\n const left = Math.max(8, Math.min(requestedLeft, window.innerWidth - panel.offsetWidth - 8))\n const top = Math.max(\n 8,\n Math.min(useTop ? topPosition : bottomPosition, window.innerHeight - panel.offsetHeight - 8),\n )\n panel.dataset.runtimePositioned = 'true'\n panel.dataset.runtimePlacement = `${useTop ? 'top' : 'bottom'}-${prefersEnd ? 'end' : 'start'}`\n panel.style.left = `${Math.round(left)}px`\n panel.style.top = `${Math.round(top)}px`\n panel.style.right = 'auto'\n panel.style.bottom = 'auto'\n }\n\n /** @param {KeyboardEvent} event */\n const onKeydown = (event) => {\n const openNavigation = navigations.find((navigation) => navigationMedia.matches && navigation.open)\n if (openNavigation instanceof HTMLDetailsElement) {\n const drawer = openNavigation.querySelector('[data-ui=\"navigation-drawer\"]')\n if (event.key === 'Escape') {\n closeNavigation(openNavigation)\n event.preventDefault()\n return\n }\n if (event.key === 'Tab' && drawer instanceof HTMLElement) {\n const items = focusables(drawer)\n const first = items[0]\n const last = items.at(-1)\n if (!items.length) {\n event.preventDefault()\n drawer.focus()\n return\n }\n if (event.shiftKey && (document.activeElement === first || document.activeElement === drawer)) {\n event.preventDefault()\n if (last instanceof HTMLElement) last.focus()\n return\n }\n if (!event.shiftKey && document.activeElement === last) {\n event.preventDefault()\n if (first instanceof HTMLElement) first.focus()\n return\n }\n }\n }\n const treeItem = document.activeElement?.closest('[data-ui=\"tree\"] [role=\"treeitem\"]')\n if (treeItem instanceof HTMLElement) {\n const tree = treeItem.closest('[data-ui=\"tree\"]')\n const items = /** @type {HTMLElement[]} */ (\n tree\n ? [...tree.querySelectorAll('[role=\"treeitem\"]')].filter((item) => item instanceof HTMLElement)\n : []\n )\n const activeIndex = items.indexOf(treeItem)\n let nextIndex = -1\n if (event.key === 'ArrowDown') nextIndex = Math.min(items.length - 1, activeIndex + 1)\n else if (event.key === 'ArrowUp') nextIndex = Math.max(0, activeIndex - 1)\n else if (event.key === 'Home') nextIndex = 0\n else if (event.key === 'End') nextIndex = items.length - 1\n else if (event.key === 'ArrowRight') {\n const level = Number(treeItem.getAttribute('aria-level'))\n if (\n treeItem.getAttribute('aria-expanded') === 'true' &&\n Number(items[activeIndex + 1]?.getAttribute('aria-level')) > level\n )\n nextIndex = activeIndex + 1\n } else if (event.key === 'ArrowLeft') {\n const level = Number(treeItem.getAttribute('aria-level'))\n if (level > 1)\n nextIndex = items.findLastIndex(\n (item, index) => index < activeIndex && Number(item.getAttribute('aria-level')) < level,\n )\n }\n if (nextIndex >= 0 && items[nextIndex]) {\n for (const item of items) item.tabIndex = -1\n items[nextIndex].tabIndex = 0\n items[nextIndex].focus()\n event.preventDefault()\n return\n }\n }\n const treeGridRow = document.activeElement?.closest('[data-ui=\"tree-grid-row\"]')\n if (treeGridRow instanceof HTMLElement) {\n const grid = treeGridRow.closest('[data-ui=\"tree-grid\"]')\n const rows = /** @type {HTMLElement[]} */ (\n grid\n ? [...grid.querySelectorAll('[data-ui=\"tree-grid-row\"]')].filter(\n (row) => row instanceof HTMLElement,\n )\n : []\n )\n const activeIndex = rows.indexOf(treeGridRow)\n const nextIndex =\n event.key === 'ArrowDown'\n ? Math.min(rows.length - 1, activeIndex + 1)\n : event.key === 'ArrowUp'\n ? Math.max(0, activeIndex - 1)\n : event.key === 'Home'\n ? 0\n : event.key === 'End'\n ? rows.length - 1\n : -1\n if (nextIndex >= 0 && rows[nextIndex]) {\n for (const row of rows) row.tabIndex = -1\n rows[nextIndex].tabIndex = 0\n rows[nextIndex].focus()\n event.preventDefault()\n return\n }\n }\n const activeMenu = document.activeElement?.closest('[data-ui=\"menu\"]')\n if (activeMenu instanceof HTMLDetailsElement && !activeMenu.open) {\n const trigger = activeMenu.querySelector('[data-ui=\"menu-trigger\"]')\n if (document.activeElement === trigger && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {\n activeMenu.open = true\n const items = /** @type {HTMLElement[]} */ (\n [\n ...activeMenu.querySelectorAll(\n '[role=\"menuitem\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"]',\n ),\n ].filter((item) => item instanceof HTMLElement && item.getAttribute('aria-disabled') !== 'true')\n )\n const item = event.key === 'ArrowUp' ? items.at(-1) : items[0]\n if (item instanceof HTMLElement) item.focus()\n event.preventDefault()\n return\n }\n }\n const openMenu = document.activeElement?.closest('[data-ui=\"menu\"][open]')\n if (openMenu instanceof HTMLDetailsElement) {\n const trigger = openMenu.querySelector('[data-ui=\"menu-trigger\"]')\n const items = /** @type {HTMLElement[]} */ (\n [\n ...openMenu.querySelectorAll(\n '[role=\"menuitem\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"]',\n ),\n ].filter((item) => item instanceof HTMLElement && item.getAttribute('aria-disabled') !== 'true')\n )\n const active = document.activeElement\n const activeIndex = active instanceof HTMLElement ? items.indexOf(active) : -1\n let nextIndex = -1\n if (event.key === 'ArrowDown') nextIndex = activeIndex < 0 ? 0 : (activeIndex + 1) % items.length\n else if (event.key === 'ArrowUp')\n nextIndex = activeIndex < 0 ? items.length - 1 : (activeIndex - 1 + items.length) % items.length\n else if (event.key === 'Home') nextIndex = 0\n else if (event.key === 'End') nextIndex = items.length - 1\n if (nextIndex >= 0 && items[nextIndex] instanceof HTMLElement) {\n items[nextIndex].focus()\n event.preventDefault()\n return\n }\n if ((event.key === 'Enter' || event.key === ' ') && document.activeElement === trigger) {\n if (items[0] instanceof HTMLElement) items[0].focus()\n event.preventDefault()\n return\n }\n }\n if (event.key === 'Escape' && openMenu instanceof HTMLDetailsElement) {\n openMenu.open = false\n const trigger = openMenu.querySelector('[data-ui=\"menu-trigger\"]')\n if (trigger instanceof HTMLElement) trigger.focus()\n event.preventDefault()\n return\n }\n const openPopup = [...root.querySelectorAll(DISMISSIBLE_POPUPS_OPEN)].at(-1)\n if (event.key === 'Escape' && openPopup instanceof HTMLDetailsElement) {\n openPopup.open = false\n const summary = openPopup.querySelector('summary')\n if (summary instanceof HTMLElement) summary.focus()\n event.preventDefault()\n return\n }\n const openPopover = document.activeElement?.closest('[data-ui=\"popover\"][data-open=\"true\"]')\n if (event.key === 'Escape' && openPopover instanceof HTMLElement) {\n const close = openPopover.querySelector('[data-ui=\"popover-close\"]')\n if (close instanceof HTMLElement) close.click()\n event.preventDefault()\n return\n }\n if (!(modal instanceof HTMLElement)) return\n if (event.key === 'Escape') {\n const close = modal.querySelector('[data-ui=\"modal-close\"]')\n if (close instanceof HTMLElement) close.click()\n event.preventDefault()\n return\n }\n if (event.key !== 'Tab') return\n const items = focusables(modal)\n if (!items.length) {\n event.preventDefault()\n modal.focus()\n return\n }\n const first = items[0]\n const last = items.at(-1)\n if (event.shiftKey && (document.activeElement === first || document.activeElement === modal)) {\n event.preventDefault()\n if (last instanceof HTMLElement) last.focus()\n } else if (!event.shiftKey && document.activeElement === last) {\n event.preventDefault()\n if (first instanceof HTMLElement) first.focus()\n }\n }\n /**\n * Table selection. `select-all` toggles the enabled row checkboxes of its own table, a row\n * change keeps the header checked or indeterminate, and every bulk bar bound to the same form\n * (`data-form`) shows the live count and enables its actions only while something is selected.\n *\n * @param {Element} table\n */\n const syncSelection = (table) => {\n const rows = /** @type {HTMLInputElement[]} */ (\n [...table.querySelectorAll('[data-ui=\"row-select\"]')].filter(\n (input) => input instanceof HTMLInputElement && !input.disabled,\n )\n )\n const checked = rows.filter((input) => input.checked)\n const all = table.querySelector('[data-ui=\"select-all\"]')\n if (all instanceof HTMLInputElement) {\n all.checked = rows.length > 0 && checked.length === rows.length\n all.indeterminate = checked.length > 0 && checked.length < rows.length\n }\n for (const input of rows)\n input.closest('[data-ui=\"row\"]')?.toggleAttribute('data-selected', input.checked)\n const form = rows[0]?.getAttribute('form')\n if (!form) return\n for (const bar of document.querySelectorAll(\n `[data-ui=\"bulk-actions\"][data-form=\"${CSS.escape(form)}\"]`,\n )) {\n if (checked.length) bar.setAttribute('data-has-selection', 'true')\n else bar.removeAttribute('data-has-selection')\n for (const count of bar.querySelectorAll('[data-ui=\"bulk-count\"]'))\n count.textContent = String(checked.length)\n for (const button of bar.querySelectorAll('[data-ui=\"bulk-action-list\"] button')) {\n if (button instanceof HTMLButtonElement && button.dataset.kvAlwaysDisabled !== 'true')\n button.disabled = checked.length === 0\n }\n }\n }\n /** @param {Event} event */\n const onSelectionChange = (event) => {\n const target = event.target\n if (!(target instanceof HTMLInputElement)) return\n const hook = target.getAttribute('data-ui')\n if (hook !== 'select-all' && hook !== 'row-select') return\n const table = target.closest('[data-ui=\"table\"]')\n if (!table) return\n if (hook === 'select-all') {\n for (const input of table.querySelectorAll('[data-ui=\"row-select\"]')) {\n if (input instanceof HTMLInputElement && !input.disabled) input.checked = target.checked\n }\n }\n syncSelection(table)\n }\n for (const table of root.querySelectorAll('[data-ui=\"table\"]')) {\n if (table.querySelector('[data-ui=\"row-select\"]')) syncSelection(table)\n }\n document.addEventListener('change', onSelectionChange)\n\n /** @param {MouseEvent} event */\n const onDocumentClick = (event) => {\n const target = event.target\n if (!(target instanceof Node)) return\n const toggle = target instanceof Element ? target.closest('[data-ui=\"navigation-toggle\"]') : null\n if (toggle instanceof HTMLElement && navigationMedia.matches) {\n const navigation = navigations.find(\n (entry) =>\n entry.querySelector('[data-ui=\"navigation-drawer\"]')?.id === toggle.getAttribute('aria-controls'),\n )\n if (navigation) {\n if (navigation.open) closeNavigation(navigation)\n else {\n navigation.open = true\n syncNavigation(navigation)\n }\n }\n return\n }\n for (const menu of root.querySelectorAll('[data-ui=\"menu\"][open]')) {\n if (!(menu instanceof HTMLDetailsElement)) continue\n const item = target instanceof Element ? target.closest('[data-ui=\"menu-item\"]') : null\n if (!menu.contains(target) || (item && item.getAttribute('aria-disabled') !== 'true')) menu.open = false\n }\n // Every other popup built on <details> closes when the reader clicks outside it.\n for (const popup of root.querySelectorAll(DISMISSIBLE_POPUPS_OPEN)) {\n if (popup instanceof HTMLDetailsElement && !popup.contains(target)) popup.open = false\n }\n }\n document.addEventListener('keydown', onKeydown)\n document.addEventListener('click', onDocumentClick)\n\n return () => {\n cleanupGlobalSearch()\n cleanupDatePickers()\n document.removeEventListener('keydown', onKeydown)\n document.removeEventListener('click', onDocumentClick)\n document.removeEventListener('change', onSelectionChange)\n for (const cleanup of menuCleanups) cleanup()\n navigationMedia.removeEventListener('change', onNavigationMediaChange)\n for (const cleanup of navigationCleanups) cleanup()\n if (priorNavigationOpen === undefined) delete navigationRoot.dataset.kvNavigationOpen\n else navigationRoot.dataset.kvNavigationOpen = priorNavigationOpen\n if (appShell instanceof HTMLElement) appShell.inert = priorInert\n if (activeBeforeOpen?.isConnected) activeBeforeOpen.focus()\n }\n}\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport type FieldIssue = { path: string; message: string }\n\nexport const issueFor = (issues: readonly FieldIssue[] | undefined, name: string): string | null =>\n issues?.find((issue) => issue.path === name)?.message ?? null\n\nexport type FieldFrameProps = {\n id: string\n label: string\n control: JSXChild\n help?: string | null\n error?: string | null\n required?: boolean\n span?: 'half' | 'full'\n kind: string\n}\n\nexport const FieldFrame = (props: FieldFrameProps): TemplateResult => {\n const helpId = props.help ? `${props.id}-help` : null\n const errorId = props.error ? `${props.id}-error` : null\n return (\n <div\n data-ui=\"field\"\n data-kind={props.kind}\n data-span={props.span ?? 'half'}\n data-invalid={String(!!props.error)}\n >\n <label data-ui=\"field-label\" for={props.id}>\n {props.label}\n {props.required && (\n <span data-ui=\"field-required\" aria-hidden=\"true\">\n {' *'}\n </span>\n )}\n </label>\n {props.control}\n {!!props.help && (\n <small data-ui=\"field-help\" id={helpId ?? undefined}>\n {props.help}\n </small>\n )}\n {!!props.error && (\n <small data-ui=\"field-error\" id={errorId ?? undefined}>\n {props.error}\n </small>\n )}\n </div>\n )\n}\n\nexport const describedBy = (id: string, help?: string | null, error?: string | null): string | null =>\n [help ? `${id}-help` : null, error ? `${id}-error` : null].filter(Boolean).join(' ') || null\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Field } from '../../primitives/field/index.tsx'\nimport type { FieldOption, FieldProps } from '../../primitives/field/index.tsx'\nimport { describedBy, FieldFrame, issueFor } from '../shared.tsx'\nimport type { FieldIssue } from '../shared.tsx'\n\nexport const HOOKS = ['switch-control', 'switch-track'] as const\n\ntype CommonProps = Omit<FieldProps, 'type' | 'fields' | 'control' | 'error'> & {\n issues?: readonly FieldIssue[]\n error?: string | null\n}\n\nconst normalize = (props: CommonProps): FieldProps => ({\n ...props,\n error: props.error ?? issueFor(props.issues, props.name),\n})\n\nexport const TextField = (props: CommonProps): TemplateResult => <Field {...normalize(props)} type=\"text\" />\nexport const TextArea = (props: CommonProps): TemplateResult => (\n <Field {...normalize(props)} type=\"textarea\" />\n)\nexport const NumberField = (props: CommonProps): TemplateResult => (\n <Field {...normalize(props)} type=\"number\" />\n)\nexport const MoneyField = (props: CommonProps): TemplateResult => (\n <Field {...normalize({ ...props, step: props.step ?? '0.01' })} type=\"decimal\" />\n)\nexport const SearchField = (props: CommonProps): TemplateResult => <Field {...normalize(props)} type=\"text\" />\nexport const Checkbox = (props: CommonProps): TemplateResult => (\n <Field {...normalize(props)} type=\"checkbox\" />\n)\nexport const CheckboxGroup = (props: CommonProps): TemplateResult => (\n <Field {...normalize(props)} type=\"checkbox-group\" />\n)\nexport const RadioGroup = (props: CommonProps): TemplateResult => <Field {...normalize(props)} type=\"radio\" />\nexport const Select = (props: CommonProps): TemplateResult => <Field {...normalize(props)} type=\"select\" />\n\nexport type SwitchProps = Omit<CommonProps, 'options' | 'placeholder' | 'step' | 'min' | 'max'> & {\n checked?: boolean\n value?: string\n}\n\nexport const Switch = (props: SwitchProps): TemplateResult => {\n const error = props.error ?? issueFor(props.issues, props.name)\n const checked = props.checked ?? props.value === '1'\n return (\n <FieldFrame\n id={props.id}\n label={props.label}\n help={props.help}\n error={error}\n required={props.required}\n span={props.span}\n kind=\"switch\"\n control={\n <label data-ui=\"switch-control\">\n <input\n id={props.id}\n type=\"checkbox\"\n role=\"switch\"\n name={props.name}\n value={props.value ?? '1'}\n checked={checked}\n aria-checked={String(checked)}\n disabled={props.disabled === true}\n required={props.required === true}\n aria-invalid={error ? 'true' : null}\n aria-describedby={describedBy(props.id, props.help, error)}\n />\n <span data-ui=\"switch-track\" aria-hidden=\"true\" />\n </label>\n }\n />\n )\n}\n\nexport type { CommonProps as ScalarFieldProps, FieldIssue, FieldOption }\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { describedBy, FieldFrame, issueFor } from '../shared.tsx'\nimport type { FieldIssue } from '../shared.tsx'\n\nexport const HOOKS = [\n 'combobox',\n 'combobox-input-row',\n 'combobox-toggle',\n 'combobox-listbox',\n 'combobox-option',\n 'combobox-empty',\n 'tag-picker',\n] as const\n\nexport type ComboboxOption = { value: string; label: string; description?: string; disabled?: boolean }\nexport type ComboboxProps = {\n id: string\n name: string\n /** Name used for the human-readable search text; defaults to `${name}Query`. */\n queryName?: string\n label: string\n query: string\n value?: string | null\n options: readonly ComboboxOption[]\n open: boolean\n openHref: string\n closeHref: string\n help?: string | null\n error?: string | null\n issues?: readonly FieldIssue[]\n required?: boolean\n disabled?: boolean\n loading?: boolean\n loadingLabel?: string\n noResultsLabel?: string\n span?: 'half' | 'full'\n}\n\nconst ComboControl = (\n props: ComboboxProps & { resolvedError: string | null; submitValue: boolean },\n): TemplateResult => (\n <div data-ui=\"combobox\" data-open={String(props.open)}>\n {props.submitValue && (\n <input type=\"hidden\" name={props.name} value={props.value ?? ''} disabled={props.disabled === true} />\n )}\n <div data-ui=\"combobox-input-row\">\n <input\n data-ui=\"field-control\"\n id={props.id}\n name={props.queryName ?? `${props.name}Query`}\n value={props.query}\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={String(props.open)}\n aria-controls={`${props.id}-listbox`}\n aria-invalid={props.resolvedError ? 'true' : null}\n aria-describedby={describedBy(props.id, props.help, props.resolvedError)}\n required={props.required === true}\n disabled={props.disabled === true}\n autocomplete=\"off\"\n />\n <a\n data-ui=\"combobox-toggle\"\n href={props.open ? props.closeHref : props.openHref}\n aria-label={props.open ? 'Close options' : 'Open options'}\n >\n \u25BE\n </a>\n </div>\n {props.open && (\n <div data-ui=\"combobox-listbox\" id={`${props.id}-listbox`} role=\"listbox\">\n {props.loading ? (\n <p data-ui=\"combobox-empty\">{props.loadingLabel ?? 'Loading\u2026'}</p>\n ) : props.options.length === 0 ? (\n <p data-ui=\"combobox-empty\">{props.noResultsLabel ?? 'No results'}</p>\n ) : (\n each(\n props.options,\n (option) => option.value,\n (option) => (\n <a\n data-ui=\"combobox-option\"\n href={\n option.disabled\n ? null\n : `${props.closeHref}${props.closeHref.includes('?') ? '&' : '?'}${encodeURIComponent(props.name)}=${encodeURIComponent(option.value)}`\n }\n role=\"option\"\n aria-selected={String(option.value === props.value)}\n aria-disabled={option.disabled ? 'true' : null}\n >\n <strong>{option.label}</strong>\n {option.description && <small>{option.description}</small>}\n </a>\n ),\n )\n )}\n </div>\n )}\n </div>\n)\n\nconst ComboboxField = (props: ComboboxProps & { submitValue: boolean }): TemplateResult => {\n const error = props.error ?? issueFor(props.issues, props.name)\n return (\n <FieldFrame\n {...props}\n kind=\"combobox\"\n error={error}\n control={<ComboControl {...props} resolvedError={error} />}\n />\n )\n}\n\nexport const Combobox = (props: ComboboxProps): TemplateResult => <ComboboxField {...props} submitValue />\n\nexport type MultiComboboxProps = Omit<ComboboxProps, 'value'> & {\n values: readonly string[]\n removeHref: (value: string) => string\n}\n\nexport const MultiCombobox = (props: MultiComboboxProps): TemplateResult => (\n <div data-ui=\"tag-picker\">\n <ComboboxField {...props} value={null} submitValue={false} />\n {each(\n props.values,\n (value) => value,\n (value) => (\n <>\n <input type=\"hidden\" name={props.name} value={value} disabled={props.disabled === true} />\n <a\n href={props.removeHref(value)}\n aria-label={`Remove ${props.options.find((option) => option.value === value)?.label ?? value}`}\n >\n {props.options.find((option) => option.value === value)?.label ?? value} \u00D7\n </a>\n </>\n ),\n )}\n </div>\n)\n\nexport const TagPicker = (props: MultiComboboxProps): TemplateResult => <MultiCombobox {...props} />\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { ActionGroup, Button, IconButton } from '../../primitives/actions/index.tsx'\nimport { Field } from '../../primitives/field/index.tsx'\nimport type { FieldProps } from '../../primitives/field/index.tsx'\nimport { describedBy, FieldFrame, issueFor } from '../shared.tsx'\nimport { NativeFieldControl } from '../../primitives/field/native-control.tsx'\nimport type { FieldIssue } from '../shared.tsx'\nimport { formatRange } from './date-math.js'\n\nexport const HOOKS = [\n 'date-picker',\n 'date-range',\n 'date-inputs',\n 'date-control-row',\n 'date-tools',\n 'date-range-error',\n 'date-calendar',\n 'date-calendar-head',\n 'date-calendar-title',\n 'date-calendar-body',\n 'date-field-control',\n 'date-range-preset',\n 'date-boundary',\n 'date-calendar-months',\n 'date-month',\n 'date-month-title',\n 'date-grid',\n 'date-weekday',\n 'date-cell',\n 'date-day',\n 'date-calendar-footer',\n 'date-calendar-status',\n] as const\n\nexport type DateRangePreset =\n | 'today'\n | 'yesterday'\n | 'last7'\n | 'thisMonth'\n | 'lastMonth'\n | 'last30'\n | 'last90'\nexport type DatePickerLabels = {\n open: string\n title: string\n previous: string\n next: string\n apply: string\n cancel: string\n chooseStart: string\n chooseEnd: string\n chooseDate: string\n invalidRange: string\n presets: string\n customRange: string\n invalidSelection: string\n presetLabels: Record<DateRangePreset, string>\n}\ntype CalendarOptions = {\n /** Native inputs retain the browser's date format. Calendar labels default to Vietnamese. */\n locale?: 'vi' | 'en'\n /** Business-timezone civil date; omit for the browser's local today at opening. */\n today?: string\n weekStartsOn?: 0 | 1\n calendarLabels?: Partial<Omit<DatePickerLabels, 'presetLabels'>> & {\n presetLabels?: Partial<DatePickerLabels['presetLabels']>\n }\n}\nexport type TemporalProps = Omit<FieldProps, 'type' | 'fields' | 'control' | 'error'> & {\n issues?: readonly FieldIssue[]\n error?: string | null\n}\nexport type DatePickerProps = TemporalProps & CalendarOptions\nexport type DateRangePickerProps = CalendarOptions & {\n id: string\n label: string\n start: Omit<TemporalProps, 'label' | 'span'>\n end: Omit<TemporalProps, 'label' | 'span'>\n startLabel: string\n endLabel: string\n span?: 'half' | 'full'\n /** All seven presets by default; false hides quick ranges. */\n presets?: readonly DateRangePreset[] | false\n}\nconst temporal = (props: TemporalProps): FieldProps => ({\n ...props,\n error: props.error ?? issueFor(props.issues, props.name),\n})\nconst presets: readonly DateRangePreset[] = [\n 'today',\n 'yesterday',\n 'last7',\n 'thisMonth',\n 'lastMonth',\n 'last30',\n 'last90',\n]\nconst labelsFor = (props: CalendarOptions, range: boolean): DatePickerLabels => {\n const defaults: DatePickerLabels =\n props.locale === 'en'\n ? {\n open: range ? 'Choose date range' : 'Choose date',\n title: range ? 'Date range' : 'Choose date',\n previous: 'Previous month',\n next: 'Next month',\n apply: 'Apply',\n cancel: 'Cancel',\n chooseStart: 'Choose a start date',\n chooseEnd: 'Choose an end date',\n chooseDate: 'Choose a date',\n invalidRange: 'The end date must be on or after the start date.',\n presets: 'Quick ranges',\n customRange: 'Custom',\n invalidSelection: 'Choose a valid date range.',\n presetLabels: {\n today: 'Today',\n yesterday: 'Yesterday',\n last7: 'Last 7 days',\n thisMonth: 'This month',\n lastMonth: 'Last month',\n last30: 'Last 30 days',\n last90: 'Last 90 days',\n },\n }\n : {\n open: range ? 'Ch\u1ECDn kho\u1EA3ng ng\u00E0y' : 'Ch\u1ECDn ng\u00E0y',\n title: range ? 'Kho\u1EA3ng ng\u00E0y' : 'Ch\u1ECDn ng\u00E0y',\n previous: 'Th\u00E1ng tr\u01B0\u1EDBc',\n next: 'Th\u00E1ng sau',\n apply: '\u00C1p d\u1EE5ng',\n cancel: 'H\u1EE7y',\n chooseStart: 'Ch\u1ECDn ng\u00E0y b\u1EAFt \u0111\u1EA7u',\n chooseEnd: 'Ch\u1ECDn ng\u00E0y k\u1EBFt th\u00FAc',\n chooseDate: 'Ch\u1ECDn m\u1ED9t ng\u00E0y',\n invalidRange: 'Ng\u00E0y k\u1EBFt th\u00FAc ph\u1EA3i b\u1EB1ng ho\u1EB7c sau ng\u00E0y b\u1EAFt \u0111\u1EA7u.',\n presets: 'Kho\u1EA3ng th\u1EDDi gian',\n customRange: 'T\u00F9y ch\u1EC9nh',\n invalidSelection: 'Vui l\u00F2ng ch\u1ECDn kho\u1EA3ng ng\u00E0y h\u1EE3p l\u1EC7.',\n presetLabels: {\n today: 'H\u00F4m nay',\n yesterday: 'H\u00F4m qua',\n last7: '7 ng\u00E0y qua',\n thisMonth: 'Th\u00E1ng n\u00E0y',\n lastMonth: 'Th\u00E1ng tr\u01B0\u1EDBc',\n last30: '30 ng\u00E0y qua',\n last90: '90 ng\u00E0y qua',\n },\n }\n return {\n ...defaults,\n ...props.calendarLabels,\n presetLabels: { ...defaults.presetLabels, ...props.calendarLabels?.presetLabels },\n }\n}\nconst CalendarIcon = (): TemplateResult => (\n <svg\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <rect x=\"3\" y=\"5\" width=\"18\" height=\"16\" rx=\"2\" />\n <path d=\"M16 3v4M8 3v4M3 11h18\" />\n </svg>\n)\nconst CalendarPanel = (props: {\n id: string\n labels: DatePickerLabels\n presets?: readonly DateRangePreset[]\n}): TemplateResult => (\n <div\n data-ui=\"date-calendar\"\n id={`${props.id}-calendar`}\n popover=\"auto\"\n role=\"dialog\"\n aria-labelledby={`${props.id}-calendar-title`}\n >\n <header data-ui=\"date-calendar-head\">\n <h3 data-ui=\"date-calendar-title\" id={`${props.id}-calendar-title`}>\n {props.labels.title}\n </h3>\n {(props.presets ?? []).length > 0 && (\n <div data-ui=\"date-range-preset\">\n <Field\n id={`${props.id}-preset`}\n name=\"\"\n label={props.labels.presets}\n labelHidden\n type=\"select\"\n disabled\n value=\"custom\"\n options={[\n { value: 'custom', label: props.labels.customRange },\n ...(props.presets ?? []).map((value) => ({ value, label: props.labels.presetLabels[value] })),\n ]}\n />\n </div>\n )}\n <ActionGroup\n label={props.labels.title}\n actions={[\n <IconButton\n label={props.labels.previous}\n icon=\"\u2039\"\n name=\"kv-date-command\"\n value=\"previous\"\n size=\"compact\"\n />,\n <IconButton\n label={props.labels.next}\n icon=\"\u203A\"\n name=\"kv-date-command\"\n value=\"next\"\n size=\"compact\"\n />,\n ]}\n />\n </header>\n <div data-ui=\"date-calendar-body\">\n <div data-ui=\"date-calendar-months\" />\n </div>\n <footer data-ui=\"date-calendar-footer\">\n <p data-ui=\"date-calendar-status\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" />\n <ActionGroup\n actions={[\n <Button label={props.labels.cancel} name=\"kv-date-command\" value=\"cancel\" />,\n <Button label={props.labels.apply} name=\"kv-date-command\" value=\"apply\" variant=\"primary\" />,\n ]}\n />\n </footer>\n </div>\n)\n/** Civil dates submit unchanged. Native fields also provide the no-JavaScript fallback. */\nexport const DatePicker = (props: DatePickerProps): TemplateResult => {\n const labels = labelsFor(props, false)\n return (\n <div\n data-ui=\"date-picker\"\n data-date-mode=\"single\"\n data-span={props.span ?? 'half'}\n data-date-locale={props.locale ?? 'vi'}\n data-date-today={props.today}\n data-date-week-start={props.weekStartsOn ?? 1}\n data-date-labels={JSON.stringify(labels)}\n >\n <div data-ui=\"date-inputs\">\n <FieldFrame\n {...temporal(props)}\n kind=\"date\"\n control={\n <div data-ui=\"date-field-control\">\n <div data-ui=\"date-control-row\">\n {NativeFieldControl(\n { ...temporal(props), type: 'date', appearance: 'embedded' },\n describedBy(props.id, props.help, temporal(props).error),\n )}\n <div data-ui=\"date-tools\">\n <IconButton\n label={`${labels.open}: ${props.label}`}\n icon={<CalendarIcon />}\n name=\"kv-date-open\"\n controls={`${props.id}-calendar`}\n expanded={false}\n disabled={props.disabled || props.readOnly}\n />\n </div>\n </div>\n </div>\n }\n />\n </div>\n <CalendarPanel id={props.id} labels={labels} />\n </div>\n )\n}\nconst DateBoundary = (props: TemporalProps): TemplateResult => (\n <input\n data-ui=\"date-boundary\"\n type=\"hidden\"\n id={props.id}\n name={props.name}\n value={String(props.value ?? '')}\n min={props.min}\n max={props.max}\n step={props.step}\n required={props.required === true}\n disabled={props.disabled === true}\n readonly={props.readOnly === true}\n autocomplete=\"off\"\n />\n)\n/** One visible range field; the two named civil-date boundaries remain the form contract. */\nexport const DateRangePicker = (props: DateRangePickerProps): TemplateResult => {\n const labels = labelsFor(props, true)\n const ranges = props.presets === false ? [] : [...new Set(props.presets ?? presets)]\n const start = temporal({ ...props.start, label: props.startLabel })\n const end = temporal({ ...props.end, label: props.endLabel })\n const error =\n [\n start.error ? `${props.startLabel}: ${start.error}` : '',\n end.error ? `${props.endLabel}: ${end.error}` : '',\n ]\n .filter(Boolean)\n .join(' \u00B7 ') || null\n const help = [start.help, end.help].filter(Boolean).join(' \u00B7 ') || null\n const id = `${props.id}-input`\n const disabled = props.start.disabled || props.end.disabled\n const locked = disabled || props.start.readOnly || props.end.readOnly\n return (\n <div\n data-ui=\"date-range\"\n id={props.id}\n data-date-mode=\"range\"\n data-span={props.span ?? 'full'}\n data-date-locale={props.locale ?? 'vi'}\n data-date-today={props.today}\n data-date-week-start={props.weekStartsOn ?? 1}\n data-date-labels={JSON.stringify(labels)}\n >\n <FieldFrame\n id={id}\n label={props.label}\n kind=\"date-range\"\n required={props.start.required || props.end.required}\n span={props.span ?? 'full'}\n help={help}\n error={error}\n control={\n <>\n <div data-ui=\"date-field-control\">\n <div data-ui=\"date-control-row\">\n {NativeFieldControl(\n {\n id,\n name: '',\n label: props.label,\n type: 'text',\n appearance: 'embedded',\n readOnly: true,\n disabled,\n value: formatRange(String(props.start.value ?? ''), String(props.end.value ?? '')),\n placeholder: labels.open,\n error,\n },\n describedBy(id, help, error),\n )}\n <div data-ui=\"date-tools\">\n <IconButton\n label={labels.open}\n icon={<CalendarIcon />}\n name=\"kv-date-open\"\n controls={`${props.id}-calendar`}\n expanded={false}\n disabled={locked}\n />\n </div>\n </div>\n </div>\n <p data-ui=\"date-range-error\" id={`${props.id}-range-error`} role=\"alert\" hidden>\n {labels.invalidSelection}\n </p>\n </>\n }\n />\n <DateBoundary {...props.start} label={props.startLabel} />\n <DateBoundary {...props.end} label={props.endLabel} />\n <CalendarPanel id={props.id} labels={labels} presets={ranges} />\n </div>\n )\n}\n/** Local date-time text is submitted unchanged; the app adapter owns timezone interpretation. */\nexport const DateTimePicker = (props: TemporalProps): TemplateResult => (\n <Field {...temporal(props)} type=\"datetime-local\" />\n)\nexport const TimePicker = (props: TemporalProps): TemplateResult => <Field {...temporal(props)} type=\"time\" />\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { describedBy, FieldFrame, issueFor } from '../shared.tsx'\nimport type { FieldIssue } from '../shared.tsx'\n\nexport const HOOKS = ['file-upload', 'drop-zone', 'upload-status'] as const\n\nexport type FileUploadProps = {\n id: string\n name: string\n label: string\n accept?: string\n multiple?: boolean\n required?: boolean\n disabled?: boolean\n help?: string | null\n error?: string | null\n issues?: readonly FieldIssue[]\n status?: JSXChild\n span?: 'half' | 'full'\n}\n\nconst uploadControl = (props: FileUploadProps, error: string | null, drop: boolean): TemplateResult => (\n <div data-ui={drop ? 'drop-zone' : 'file-upload'}>\n <input\n data-ui=\"field-control\"\n id={props.id}\n type=\"file\"\n name={props.name}\n accept={props.accept}\n multiple={props.multiple === true}\n required={props.required === true}\n disabled={props.disabled === true}\n aria-invalid={error ? 'true' : null}\n aria-describedby={describedBy(props.id, props.help, error)}\n />\n {props.status !== undefined && <div data-ui=\"upload-status\">{props.status}</div>}\n </div>\n)\n\nconst renderUpload = (props: FileUploadProps, drop: boolean): TemplateResult => {\n const error = props.error ?? issueFor(props.issues, props.name)\n return (\n <FieldFrame\n {...props}\n kind={drop ? 'drop-zone' : 'file-upload'}\n error={error}\n control={uploadControl(props, error, drop)}\n />\n )\n}\n\nexport const FileUpload = (props: FileUploadProps): TemplateResult => renderUpload(props, false)\nexport const DropZone = (props: FileUploadProps): TemplateResult => renderUpload(props, true)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Combobox } from '../combobox/index.tsx'\nimport type { ComboboxOption, ComboboxProps } from '../combobox/index.tsx'\n\nexport const HOOKS = ['relation-picker'] as const\n\nexport type RelationPickerProps<T> = Omit<ComboboxProps, 'options'> & {\n results: readonly T[]\n getValue: (result: T) => string\n getLabel: (result: T) => string\n getDescription?: (result: T) => string | undefined\n}\n\n/** The application supplies permission-filtered results; this renderer performs no lookup. */\nexport const RelationPicker = <T,>(props: RelationPickerProps<T>): TemplateResult => {\n const options: ComboboxOption[] = props.results.map((result) => ({\n value: props.getValue(result),\n label: props.getLabel(result),\n description: props.getDescription?.(result),\n }))\n return (\n <div data-ui=\"relation-picker\">\n <Combobox {...props} options={options} />\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Button } from '../../primitives/actions/index.tsx'\nimport { Menu } from '../../interactions/menu/index.tsx'\n\nexport const HOOKS = [\n 'search-bar',\n 'filter-bar',\n 'applied-filters',\n 'applied-filter',\n 'saved-views',\n 'saved-view',\n 'view-settings',\n 'view-settings-options',\n] as const\n\nexport const withQueryState = (\n href: string,\n changes: Readonly<Record<string, string | number | boolean | null | undefined>>,\n): string => {\n const url = new URL(href, 'http://ket.local')\n for (const [key, value] of Object.entries(changes)) {\n if (value === null || value === undefined || value === '') url.searchParams.delete(key)\n else url.searchParams.set(key, String(value))\n }\n return `${url.pathname}${url.search}${url.hash}`\n}\n\nexport type SearchBarProps = {\n action: string\n value?: string | null\n name?: string\n id?: string\n label?: string\n placeholder?: string\n submitLabel?: string\n hidden?: Readonly<Record<string, string>>\n}\n\nexport const SearchBar = (props: SearchBarProps): TemplateResult => (\n <form data-ui=\"search-bar\" action={props.action} method=\"get\" role=\"search\">\n {each(\n Object.entries(props.hidden ?? {}),\n ([name]) => name,\n ([name, value]) => (\n <input type=\"hidden\" name={name} value={value} />\n ),\n )}\n <label for={props.id ?? 'resource-search'}>{props.label ?? 'Search'}</label>\n <input\n id={props.id ?? 'resource-search'}\n type=\"search\"\n name={props.name ?? 'q'}\n value={props.value ?? ''}\n placeholder={props.placeholder}\n autocomplete=\"off\"\n />\n <Button type=\"submit\" label={props.submitLabel ?? 'Search'} size=\"compact\" />\n </form>\n)\n\nexport const FilterBar = (props: { label?: string; filters: readonly JSXChild[] }): TemplateResult => (\n <section data-ui=\"filter-bar\" aria-label={props.label ?? 'Filters'}>\n {props.filters}\n </section>\n)\n\nexport type AppliedFilter = { id: string; label: string; value: string; removeHref: string }\nexport const AppliedFilters = (props: {\n label?: string\n filters: readonly AppliedFilter[]\n clearHref?: string\n clearLabel?: string\n}): TemplateResult => (\n <nav data-ui=\"applied-filters\" aria-label={props.label ?? 'Applied filters'}>\n {each(\n props.filters,\n (filter) => filter.id,\n (filter) => (\n <a\n data-ui=\"applied-filter\"\n href={filter.removeHref}\n aria-label={`Remove ${filter.label}: ${filter.value}`}\n >\n <strong>{filter.label}</strong> {filter.value} \u00D7\n </a>\n ),\n )}\n {props.clearHref && <a href={props.clearHref}>{props.clearLabel ?? 'Clear all'}</a>}\n </nav>\n)\n\nexport type SortChoice = { id: string; label: string; href: string; active?: boolean }\nexport const SortMenu = (props: {\n id: string\n label?: string\n choices: readonly SortChoice[]\n}): TemplateResult => (\n <Menu\n id={props.id}\n label={props.label ?? 'Sort'}\n align=\"end\"\n items={props.choices.map((choice) => ({\n id: choice.id,\n label: `${choice.active ? '\u2713 ' : ''}${choice.label}`,\n href: choice.href,\n }))}\n />\n)\n\nexport type SavedView = { id: string; label: string; href: string; active?: boolean; description?: string }\nexport const SavedViews = (props: { label?: string; views: readonly SavedView[] }): TemplateResult => (\n <nav data-ui=\"saved-views\" aria-label={props.label ?? 'Saved views'}>\n {each(\n props.views,\n (view) => view.id,\n (view) => (\n <a data-ui=\"saved-view\" href={view.href} aria-current={view.active ? 'page' : null}>\n <strong>{view.label}</strong>\n {view.description && <small>{view.description}</small>}\n </a>\n ),\n )}\n </nav>\n)\n\nexport type ViewSetting = { id: string; label: string; visible: boolean; disabled?: boolean }\nexport const ViewSettings = (props: {\n id: string\n label?: string\n action: string\n settings: readonly ViewSetting[]\n version?: string\n submitLabel?: string\n}): TemplateResult => (\n <details data-ui=\"view-settings\">\n <summary>{props.label ?? 'View settings'}</summary>\n <form action={props.action} method=\"post\">\n {props.version && <input type=\"hidden\" name=\"version\" value={props.version} />}\n <div data-ui=\"view-settings-options\">\n {each(\n props.settings,\n (setting) => setting.id,\n (setting) => (\n <label>\n <input\n type=\"checkbox\"\n name=\"columns\"\n value={setting.id}\n checked={setting.visible}\n disabled={setting.disabled === true}\n />\n {setting.label}\n </label>\n ),\n )}\n </div>\n <Button type=\"submit\" label={props.submitLabel ?? 'Save view'} size=\"compact\" />\n </form>\n </details>\n)\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Button, LinkButton } from '../../primitives/actions/index.tsx'\n\nexport const HOOKS = ['inline-edit', 'inline-edit-value', 'inline-edit-form', 'inline-edit-actions'] as const\n\nexport type InlineEditProps = {\n id: string\n label: string\n value: JSXChild\n editing: boolean\n editHref: string\n cancelHref: string\n action: string\n name: string\n inputValue: string\n version?: string\n error?: string | null\n saveLabel?: string\n cancelLabel?: string\n}\n\nexport const InlineEdit = (props: InlineEditProps): TemplateResult => (\n <section data-ui=\"inline-edit\" aria-labelledby={`${props.id}-label`}>\n <strong id={`${props.id}-label`}>{props.label}</strong>\n {!props.editing ? (\n <span data-ui=\"inline-edit-value\">\n {props.value}\n <LinkButton href={props.editHref} label={`Edit ${props.label}`} size=\"compact\" />\n </span>\n ) : (\n <form data-ui=\"inline-edit-form\" action={props.action} method=\"post\">\n {props.version && <input type=\"hidden\" name=\"version\" value={props.version} />}\n <input\n id={`${props.id}-input`}\n name={props.name}\n value={props.inputValue}\n aria-label={props.label}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={props.error ? `${props.id}-error` : null}\n />\n {props.error && <small id={`${props.id}-error`}>{props.error}</small>}\n <span data-ui=\"inline-edit-actions\">\n <Button type=\"submit\" label={props.saveLabel ?? 'Save'} size=\"compact\" />\n <LinkButton href={props.cancelHref} label={props.cancelLabel ?? 'Cancel'} size=\"compact\" />\n </span>\n </form>\n )}\n </section>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { EmptyState, LoadingState } from '../../primitives/feedback/index.tsx'\nimport { PagerBar } from '../../patterns/list-chrome/index.tsx'\nimport type { PagerBarProps } from '../../patterns/list-chrome/index.tsx'\n\nexport const HOOKS = [\n 'resource-list',\n 'resource-list-item',\n 'resource-list-select',\n 'resource-list-link',\n] as const\n\nexport type ResourceListProps<Row> = {\n label: string\n rows: readonly Row[]\n id: (row: Row) => string\n href: (row: Row) => string\n primary: (row: Row) => JSXChild\n secondary?: (row: Row) => JSXChild\n meta?: (row: Row) => JSXChild\n selectedIds?: readonly string[]\n selectionName?: string\n selectionForm?: string\n loading?: boolean\n loadingLabel?: string\n emptyTitle?: string\n emptyMessage?: string\n pager?: PagerBarProps\n}\n\nexport const ResourceList = <Row,>(props: ResourceListProps<Row>): TemplateResult => {\n if (props.loading) return <LoadingState label={props.loadingLabel ?? 'Loading resources'} />\n if (props.rows.length === 0)\n return (\n <EmptyState\n title={props.emptyTitle ?? 'No resources'}\n message={props.emptyMessage ?? 'No results match this view.'}\n />\n )\n return (\n <div>\n <ul data-ui=\"resource-list\" aria-label={props.label}>\n {each(props.rows, props.id, (row) => {\n const id = props.id(row)\n return (\n <li data-ui=\"resource-list-item\" data-selected={props.selectedIds?.includes(id) ? 'true' : null}>\n {props.selectedIds !== undefined && (\n <input\n data-ui=\"resource-list-select\"\n type=\"checkbox\"\n name={props.selectionName ?? 'ids'}\n value={id}\n form={props.selectionForm}\n checked={props.selectedIds.includes(id)}\n aria-label={`Select ${id}`}\n />\n )}\n <a data-ui=\"resource-list-link\" href={props.href(row)}>\n <strong>{props.primary(row)}</strong>\n {props.secondary && <span>{props.secondary(row)}</span>}\n </a>\n {props.meta && <span>{props.meta(row)}</span>}\n </li>\n )\n })}\n </ul>\n {props.pager && <PagerBar {...props.pager} />}\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Button, LinkButton } from '../../primitives/actions.tsx'\nimport type { ActionVariant } from '../../primitives/actions.tsx'\n\nexport type ListFacet = {\n id: string\n label: string\n href: string\n active?: boolean\n count?: number | string\n}\n\nexport type ListSearch = {\n id?: string\n action: string\n name?: string\n value?: string | null\n placeholder?: string | null\n label?: string\n submitLabel?: string\n hidden?: Readonly<Record<string, string>>\n}\n\nexport type ListSortChoice = {\n value: string\n label: string\n selected?: boolean\n}\n\nexport type ListSort = {\n id?: string\n action: string\n name?: string\n label?: string\n submitLabel?: string\n choices: readonly ListSortChoice[]\n hidden?: Readonly<Record<string, string>>\n}\n\nexport type BulkAction = {\n id: string\n label: string\n name?: string\n value?: string\n variant?: ActionVariant\n disabled?: boolean\n}\n\nexport type BulkActionsProps = {\n form?: string | null\n selectedCount?: number\n summary?: JSXChild\n clearHref?: string | null\n clearLabel?: string\n actions: readonly BulkAction[]\n}\n\nexport type PagerPage = {\n label: string\n href: string\n active?: boolean\n}\n\nexport type PagerBarProps = {\n summary?: JSXChild\n previousHref?: string | null\n nextHref?: string | null\n previousLabel?: string\n nextLabel?: string\n pages?: readonly PagerPage[]\n label?: string\n}\n\nexport type ListChromeProps = {\n filtersLabel?: string\n /**\n * Filters that pick from a list rather than toggle \u2014 a compact `Menu` each.\n * They lead the facet row, at the facets' height.\n */\n filterMenus?: JSXChild\n viewsLabel?: string\n search?: ListSearch\n facets?: readonly ListFacet[]\n views?: readonly ListFacet[]\n sort?: ListSort\n status?: JSXChild\n actions?: JSXChild\n bulk?: BulkActionsProps\n pager?: PagerBarProps\n}\n\nexport const HOOKS = [\n 'list-chrome',\n 'list-chrome-row',\n 'list-search',\n 'list-search-label',\n 'list-search-input',\n 'list-search-submit',\n 'list-filter-menus',\n 'list-facets',\n 'list-facet',\n 'list-facet-count',\n 'list-view-switch',\n 'list-view',\n 'list-sort',\n 'list-sort-label',\n 'list-sort-select',\n 'list-status',\n 'list-actions',\n 'bulk-actions',\n 'bulk-summary',\n 'bulk-count',\n 'bulk-action-list',\n 'pager-bar',\n 'pager-summary',\n 'pager-pages',\n 'pager-page',\n 'pager-actions',\n 'pager-link',\n] as const\n\nconst hiddenFields = (fields: Readonly<Record<string, string>> | undefined): JSXChild =>\n fields\n ? each(\n Object.entries(fields),\n ([name]) => name,\n ([name, value]) => <input type=\"hidden\" name={name} value={value} />,\n )\n : ''\n\nconst SearchControl = (props: ListSearch): TemplateResult => (\n <form data-ui=\"list-search\" action={props.action} method=\"get\" role=\"search\">\n {hiddenFields(props.hidden)}\n <label data-ui=\"list-search-label\" for={props.id ?? `${props.name ?? 'q'}-search`}>\n {props.label ?? 'Search'}\n </label>\n <input\n data-ui=\"list-search-input\"\n id={props.id ?? `${props.name ?? 'q'}-search`}\n type=\"search\"\n name={props.name ?? 'q'}\n value={props.value ?? ''}\n placeholder={props.placeholder ?? ''}\n autocomplete=\"off\"\n />\n <span data-ui=\"list-search-submit\">\n <Button type=\"submit\" label={props.submitLabel ?? 'Search'} variant=\"tertiary\" size=\"compact\" />\n </span>\n </form>\n)\n\nconst FacetNav = (props: {\n label: string\n ui: 'list-facets' | 'list-view-switch'\n itemUi: string\n items: readonly ListFacet[]\n}): TemplateResult => (\n <nav data-ui={props.ui} aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <a\n data-ui={props.itemUi}\n data-active={item.active === true ? 'true' : null}\n href={item.href}\n aria-current={item.active === true ? 'page' : null}\n >\n <span>{item.label}</span>\n {item.count !== undefined && <span data-ui=\"list-facet-count\">{String(item.count)}</span>}\n </a>\n ),\n )}\n </nav>\n)\n\nconst SortControl = (props: ListSort): TemplateResult => (\n <form data-ui=\"list-sort\" action={props.action} method=\"get\">\n {hiddenFields(props.hidden)}\n <label data-ui=\"list-sort-label\" for={props.id ?? `${props.name ?? 'sort'}-select`}>\n {props.label ?? 'Sort'}\n </label>\n <select\n data-ui=\"list-sort-select\"\n id={props.id ?? `${props.name ?? 'sort'}-select`}\n name={props.name ?? 'sort'}\n >\n {each(\n props.choices,\n (choice) => choice.value,\n (choice) => (\n <option value={choice.value} selected={choice.selected === true}>\n {choice.label}\n </option>\n ),\n )}\n </select>\n <Button type=\"submit\" label={props.submitLabel ?? 'Apply'} variant=\"tertiary\" size=\"compact\" />\n </form>\n)\n\nexport const BulkActions = (props: BulkActionsProps): TemplateResult => (\n <div\n data-ui=\"bulk-actions\"\n data-form={props.form ?? null}\n data-has-selection={props.selectedCount && props.selectedCount > 0 ? 'true' : null}\n >\n <div data-ui=\"bulk-summary\">\n {/* `bulk-count` is the live figure the runtime updates as rows are checked. */}\n {props.summary ?? (\n <>\n <span data-ui=\"bulk-count\">{String(props.selectedCount ?? 0)}</span> selected\n </>\n )}\n {props.clearHref && (\n <LinkButton\n href={props.clearHref}\n label={props.clearLabel ?? 'Clear'}\n variant=\"tertiary\"\n size=\"compact\"\n />\n )}\n </div>\n <div data-ui=\"bulk-action-list\">\n {each(\n props.actions,\n (action) => action.id,\n (action) => (\n <Button\n type=\"submit\"\n form={props.form ?? null}\n name={action.name}\n value={action.value}\n label={action.label}\n variant={action.variant ?? 'secondary'}\n size=\"compact\"\n disabled={action.disabled === true || !(props.selectedCount && props.selectedCount > 0)}\n />\n ),\n )}\n </div>\n </div>\n)\n\nexport const PagerBar = (props: PagerBarProps): TemplateResult => (\n <nav data-ui=\"pager-bar\" aria-label={props.label ?? 'Pagination'}>\n {props.summary !== undefined && <span data-ui=\"pager-summary\">{props.summary}</span>}\n {props.pages && props.pages.length > 0 && (\n <span data-ui=\"pager-pages\">\n {each(\n props.pages,\n (page) => page.href,\n (page) => (\n <a\n data-ui=\"pager-page\"\n data-active={page.active === true ? 'true' : null}\n href={page.href}\n aria-current={page.active === true ? 'page' : null}\n >\n {page.label}\n </a>\n ),\n )}\n </span>\n )}\n <span data-ui=\"pager-actions\">\n {props.previousHref ? (\n <a\n data-ui=\"pager-link\"\n href={props.previousHref}\n rel=\"prev\"\n aria-label={props.previousLabel ?? 'Previous page'}\n >\n \u2039\n </a>\n ) : (\n <span data-ui=\"pager-link\" data-disabled=\"true\" aria-hidden=\"true\">\n \u2039\n </span>\n )}\n {props.nextHref ? (\n <a data-ui=\"pager-link\" href={props.nextHref} rel=\"next\" aria-label={props.nextLabel ?? 'Next page'}>\n \u203A\n </a>\n ) : (\n <span data-ui=\"pager-link\" data-disabled=\"true\" aria-hidden=\"true\">\n \u203A\n </span>\n )}\n </span>\n </nav>\n)\n\nexport const ListChrome = (props: ListChromeProps): TemplateResult => {\n const filters =\n props.filterMenus !== undefined ||\n (props.facets?.length ?? 0) > 0 ||\n (props.views?.length ?? 0) > 0 ||\n props.sort ? (\n <div data-ui=\"list-chrome-row\" data-row=\"filters\">\n {props.filterMenus !== undefined && <div data-ui=\"list-filter-menus\">{props.filterMenus}</div>}\n {props.facets && props.facets.length > 0 && (\n <FacetNav\n label={props.filtersLabel ?? 'Filters'}\n ui=\"list-facets\"\n itemUi=\"list-facet\"\n items={props.facets}\n />\n )}\n {props.views && props.views.length > 0 && (\n <FacetNav\n label={props.viewsLabel ?? 'Views'}\n ui=\"list-view-switch\"\n itemUi=\"list-view\"\n items={props.views}\n />\n )}\n {props.sort && <SortControl {...props.sort} />}\n </div>\n ) : null\n const meta =\n props.status !== undefined || props.actions !== undefined || props.pager ? (\n <div data-ui=\"list-chrome-row\" data-row=\"meta\">\n {props.status !== undefined && <div data-ui=\"list-status\">{props.status}</div>}\n {props.actions !== undefined && <div data-ui=\"list-actions\">{props.actions}</div>}\n {props.pager && <PagerBar {...props.pager} />}\n </div>\n ) : null\n const tail =\n filters || meta ? (\n <div data-ui=\"list-chrome-row\" data-row=\"tail\">\n {filters}\n {meta}\n </div>\n ) : null\n return (\n <div data-ui=\"list-chrome\" data-pattern=\"list-chrome\">\n <div data-ui=\"list-chrome-row\" data-row=\"query\">\n {props.search && <SearchControl {...props.search} />}\n {tail}\n </div>\n {props.bulk && <BulkActions {...props.bulk} />}\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['data-grid-scroll', 'data-grid', 'data-grid-cap', 'data-grid-cell'] as const\n\nexport type DataGridColumn<Row> = {\n key: string\n label: string\n cell: (row: Row) => JSXChild\n visible?: boolean\n pinned?: 'start' | 'end'\n width?: string\n}\n\nexport type DataGridProps<Row> = {\n label: string\n rows: readonly Row[]\n id: (row: Row) => string\n columns: readonly DataGridColumn<Row>[]\n columnOrder?: readonly string[]\n maxRows?: number\n capLabel?: (shown: number, total: number) => string\n density?: 'compact' | 'default' | 'comfortable'\n}\n\nexport const DataGrid = <Row,>(props: DataGridProps<Row>): TemplateResult => {\n const maxRows = Math.max(1, props.maxRows ?? 500)\n const rows = props.rows.slice(0, maxRows)\n const order = props.columnOrder ?? props.columns.map((column) => column.key)\n const byKey = new Map(props.columns.map((column) => [column.key, column]))\n const columns = order\n .map((key) => byKey.get(key))\n .filter((column): column is DataGridColumn<Row> => !!column && column.visible !== false)\n return (\n <div data-ui=\"data-grid-scroll\" data-density={props.density ?? 'default'}>\n {props.rows.length > rows.length && (\n <p data-ui=\"data-grid-cap\" role=\"status\">\n {props.capLabel?.(rows.length, props.rows.length) ??\n `Showing ${rows.length} of ${props.rows.length} rows`}\n </p>\n )}\n <table\n data-ui=\"data-grid\"\n aria-label={props.label}\n style={`--kv-grid-columns: ${columns.map((column) => column.width ?? 'minmax(9rem, 1fr)').join(' ')}`}\n >\n <thead>\n <tr>\n {each(\n columns,\n (column) => column.key,\n (column) => (\n <th scope=\"col\" data-pinned={column.pinned}>\n {column.label}\n </th>\n ),\n )}\n </tr>\n </thead>\n <tbody>\n {each(rows, props.id, (row) => (\n <tr>\n {each(\n columns,\n (column) => column.key,\n (column) => (\n <td data-ui=\"data-grid-cell\" data-pinned={column.pinned}>\n {column.cell(row)}\n </td>\n ),\n )}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['tree', 'tree-item', 'tree-link', 'tree-grid', 'tree-grid-row'] as const\n\nexport type TreeNode = {\n id: string\n label: JSXChild\n href?: string\n active?: boolean\n expanded?: boolean\n children?: readonly TreeNode[]\n}\n\nconst activeNode = (items: readonly TreeNode[]): TreeNode | undefined => {\n for (const item of items) {\n if (item.active) return item\n if (item.children?.length && item.expanded !== false) {\n const nested = activeNode(item.children)\n if (nested) return nested\n }\n }\n return undefined\n}\n\nconst nodes = (items: readonly TreeNode[], level: number, tabbableId: string | undefined): TemplateResult => (\n <ul role={level === 1 ? 'tree' : 'group'}>\n {each(\n items,\n (item) => item.id,\n (item) => (\n <li data-ui=\"tree-item\" role=\"none\">\n {item.href ? (\n <a\n data-ui=\"tree-link\"\n href={item.href}\n role=\"treeitem\"\n tabindex={item.id === tabbableId ? '0' : '-1'}\n aria-level={String(level)}\n aria-expanded={item.children?.length ? String(item.expanded !== false) : null}\n aria-current={item.active ? 'page' : null}\n >\n {item.label}\n </a>\n ) : (\n // biome-ignore lint/a11y/useFocusableInteractive: the serialized lowercase tabindex is the explicit focus contract for this treeitem.\n <span\n data-ui=\"tree-link\"\n role=\"treeitem\"\n tabindex={item.id === tabbableId ? '0' : '-1'}\n aria-level={String(level)}\n aria-expanded={item.children?.length ? String(item.expanded !== false) : null}\n aria-current={item.active ? 'page' : null}\n >\n {item.label}\n </span>\n )}\n {item.children?.length && item.expanded !== false\n ? nodes(item.children, level + 1, tabbableId)\n : null}\n </li>\n ),\n )}\n </ul>\n)\n\nexport const Tree = (props: { label: string; nodes: readonly TreeNode[] }): TemplateResult => (\n <nav data-ui=\"tree\" aria-label={props.label}>\n {nodes(props.nodes, 1, activeNode(props.nodes)?.id ?? props.nodes[0]?.id)}\n </nav>\n)\n\nexport type TreeGridColumn<Row> = { key: string; label: string; cell: (row: Row) => JSXChild }\nexport type TreeGridRow<Row> = { row: Row; level: number; expanded?: boolean; hasChildren?: boolean }\nexport const TreeGrid = <Row,>(props: {\n label: string\n rows: readonly TreeGridRow<Row>[]\n id: (row: Row) => string\n primary: (row: Row) => JSXChild\n columns: readonly TreeGridColumn<Row>[]\n}): TemplateResult => (\n // biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: ARIA treegrid intentionally augments the native table model with expandable hierarchical rows.\n <table data-ui=\"tree-grid\" role=\"treegrid\" aria-label={props.label}>\n <thead>\n <tr>\n <th scope=\"col\">Name</th>\n {each(\n props.columns,\n (column) => column.key,\n (column) => (\n <th scope=\"col\">{column.label}</th>\n ),\n )}\n </tr>\n </thead>\n <tbody>\n {each(\n props.rows,\n (item) => props.id(item.row),\n (item) => (\n <tr\n data-ui=\"tree-grid-row\"\n tabindex={\n props.id(item.row) === (props.rows[0] ? props.id(props.rows[0].row) : null) ? '0' : '-1'\n }\n aria-level={String(item.level)}\n aria-expanded={item.hasChildren ? String(item.expanded !== false) : null}\n >\n <th scope=\"row\" style={`--kv-tree-level: ${item.level}`}>\n {props.primary(item.row)}\n </th>\n {each(\n props.columns,\n (column) => column.key,\n (column) => (\n <td>{column.cell(item.row)}</td>\n ),\n )}\n </tr>\n ),\n )}\n </tbody>\n </table>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'data-matrix',\n 'data-matrix-head',\n 'data-matrix-heading',\n 'data-matrix-eyebrow',\n 'data-matrix-title',\n 'data-matrix-description',\n 'data-matrix-summary',\n 'data-matrix-scroll',\n 'data-matrix-table',\n 'data-matrix-column',\n 'data-matrix-row-head',\n 'data-matrix-row-leading',\n 'data-matrix-row-title',\n 'data-matrix-row-description',\n 'data-matrix-row-status',\n 'data-matrix-row-actions',\n 'data-matrix-cell',\n] as const\n\nexport type DataMatrixColumn = {\n id: string\n label: string\n description?: string | null\n}\n\nexport type DataMatrixRow = {\n id: string\n label: string\n description?: string | null\n leading?: JSXChild\n status?: JSXChild\n actions?: JSXChild\n cells: Readonly<Record<string, JSXChild>>\n}\n\nexport type DataMatrixProps = {\n label: string\n /** Localised heading for the row identity column, for example \u201CCheckpoint\u201D. */\n rowLabel: string\n columns: readonly DataMatrixColumn[]\n rows: readonly DataMatrixRow[]\n eyebrow?: string | null\n title?: string | null\n description?: string | null\n summary?: JSXChild\n /** Controls only the scrollable table's readable floor, never the viewport. */\n width?: 'default' | 'wide'\n}\n\n/**\n * Compare the same dimensions across ordered records or checkpoints.\n * Applications own cell content and workflow actions; the matrix owns table\n * semantics, sticky row identity, horizontal containment and responsive chrome.\n */\nexport const DataMatrix = (props: DataMatrixProps): TemplateResult => (\n <section data-ui=\"data-matrix\" aria-label={props.label} data-width={props.width ?? 'default'}>\n {(props.title || props.description || props.summary !== undefined) && (\n <header data-ui=\"data-matrix-head\">\n <div data-ui=\"data-matrix-heading\">\n {!!props.eyebrow && <span data-ui=\"data-matrix-eyebrow\">{props.eyebrow}</span>}\n {!!props.title && <h2 data-ui=\"data-matrix-title\">{props.title}</h2>}\n {!!props.description && <p data-ui=\"data-matrix-description\">{props.description}</p>}\n </div>\n {props.summary !== undefined && <div data-ui=\"data-matrix-summary\">{props.summary}</div>}\n </header>\n )}\n {/* biome-ignore lint/a11y/noNoninteractiveTabindex: a horizontally scrolling region must be reachable by keyboard (WCAG 2.1.1). */}\n <div data-ui=\"data-matrix-scroll\" role=\"region\" tabIndex=\"0\" aria-label={props.label}>\n <table data-ui=\"data-matrix-table\">\n <thead>\n <tr>\n <th scope=\"col\" data-ui=\"data-matrix-row-head\">\n {props.rowLabel}\n </th>\n {each(\n props.columns,\n (column) => column.id,\n (column) => (\n <th scope=\"col\" data-ui=\"data-matrix-column\">\n <strong>{column.label}</strong>\n {!!column.description && <small>{column.description}</small>}\n </th>\n ),\n )}\n </tr>\n </thead>\n <tbody>\n {each(\n props.rows,\n (row) => row.id,\n (row) => (\n <tr>\n <th scope=\"row\" data-ui=\"data-matrix-row-head\">\n <div data-ui=\"data-matrix-row-title\">\n {row.leading !== undefined && (\n <span data-ui=\"data-matrix-row-leading\">{row.leading}</span>\n )}\n <strong>{row.label}</strong>\n {row.status !== undefined && <span data-ui=\"data-matrix-row-status\">{row.status}</span>}\n </div>\n {!!row.description && (\n <small data-ui=\"data-matrix-row-description\">{row.description}</small>\n )}\n {row.actions !== undefined && <div data-ui=\"data-matrix-row-actions\">{row.actions}</div>}\n </th>\n {each(\n props.columns,\n (column) => column.id,\n (column) => (\n <td data-ui=\"data-matrix-cell\">{row.cells[column.id]}</td>\n ),\n )}\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n </section>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'bar-chart',\n 'bar-chart-rows',\n 'bar-chart-row',\n 'bar-chart-label',\n 'bar-chart-caption',\n 'bar-chart-track',\n 'bar-chart-fill',\n 'bar-chart-segment',\n 'bar-chart-value',\n 'bar-chart-scale',\n 'bar-chart-legend',\n 'bar-chart-legend-item',\n 'bar-chart-legend-swatch',\n 'bar-chart-empty',\n] as const\n\n/** How many categorical colours the stylesheet defines. Past this a legend stops being readable. */\nexport const CHART_SERIES = 6\n\n/** One part of a stacked bar. `series` is the 1-based palette slot its legend key also names. */\nexport type BarChartSegment = { series: number; value: number }\n\n/** A named magnitude, drawn against the largest in its set or against `max`. */\nexport type BarChartBar = {\n id: string\n label: string\n value: number\n caption?: string | null\n href?: string | null\n /** Parts of `value`, in legend order. Present on every bar when the chart is stacked. */\n segments?: readonly BarChartSegment[]\n}\n\nexport type BarChartKey = { id: string; label: string; series: number }\n\nexport type BarChartProps = {\n /** The accessible name of the list; the visible heading belongs to the surface around it. */\n label: string\n bars: readonly BarChartBar[]\n value: (bar: BarChartBar) => string\n /** A fixed top for the track, such as 100 for rates. Defaults to a round number above the largest bar. */\n max?: number\n /** What each stacked segment colour means. Required for a stacked chart to be readable. */\n keys?: readonly BarChartKey[]\n /** The scale spelled out under the bars, when the caller wants one printed. */\n scale?: readonly string[]\n empty?: string\n}\n\nconst finite = (value: number): number => (Number.isFinite(value) ? value : 0)\n\nconst slotOf = (series: number): string => String(((Math.max(1, Math.trunc(series)) - 1) % CHART_SERIES) + 1)\n\nconst percent = (value: number, of: number): string =>\n of <= 0 ? '0' : Math.min(100, (Math.abs(finite(value)) / of) * 100).toFixed(2)\n\n/**\n * A round number at or above `max`, for the top of a bar track.\n *\n * A track topped by the largest bar puts that bar against the frame and leaves\n * the reader deciding whether it touches; 1, 2, 2.5 or 5 times a power of ten is\n * the smallest ceiling that still reads as a number.\n */\nexport const axisCeiling = (max: number): number => {\n const value = finite(max)\n if (value <= 0) return 0\n const magnitude = 10 ** Math.floor(Math.log10(value))\n for (const step of [1, 2, 2.5, 5, 10]) if (value <= step * magnitude) return step * magnitude\n return 10 * magnitude\n}\n\n/**\n * Magnitudes against one scale.\n *\n * HTML rather than a canvas or SVG, on purpose. Bars run horizontally because\n * their labels are business names that need room, a row may link to the records\n * behind it, and every number is real text \u2014 so the chart reads the same to a\n * screen reader, a printed page and an agent comparing screens, with no script.\n *\n * Plain bars share one colour: they are one series, and a hue per row would\n * read as a legend that does not exist.\n *\n * A bar with `segments` is stacked: the fill still spans the bar's total, and\n * the segments divide it. `keys` names the segments once, above the rows.\n */\nexport const BarChart = (props: BarChartProps): TemplateResult => {\n const bars = props.bars.filter((bar) => finite(bar.value) !== 0)\n if (!bars.length) return <p data-ui=\"bar-chart-empty\">{props.empty ?? ''}</p>\n const largest = Math.max(...bars.map((bar) => Math.abs(finite(bar.value))), 0)\n const ceiling = props.max !== undefined && props.max > 0 ? props.max : axisCeiling(largest)\n const stacked = bars.some((bar) => bar.segments?.length)\n return (\n <div data-ui=\"bar-chart\" data-stacked={stacked ? 'true' : null}>\n {!!props.keys?.length && (\n <ul data-ui=\"bar-chart-legend\">\n {each(\n props.keys,\n (key) => key.id,\n (key) => (\n <li data-ui=\"bar-chart-legend-item\" data-series={slotOf(key.series)}>\n <span data-ui=\"bar-chart-legend-swatch\" aria-hidden=\"true\" />\n {key.label}\n </li>\n ),\n )}\n </ul>\n )}\n <ul data-ui=\"bar-chart-rows\" aria-label={props.label}>\n {each(\n bars,\n (bar) => bar.id,\n (bar) => (\n <li data-ui=\"bar-chart-row\">\n <span data-ui=\"bar-chart-label\">\n {bar.href ? <a href={bar.href}>{bar.label}</a> : bar.label}\n {!!bar.caption && <small data-ui=\"bar-chart-caption\">{bar.caption}</small>}\n </span>\n <span data-ui=\"bar-chart-track\" aria-hidden=\"true\">\n {bar.segments?.length ? (\n <span data-ui=\"bar-chart-fill\" style={`inline-size: ${percent(bar.value, ceiling)}%`}>\n {each(\n bar.segments,\n (segment) => String(segment.series),\n (segment) => (\n <span\n data-ui=\"bar-chart-segment\"\n data-series={slotOf(segment.series)}\n style={`inline-size: ${percent(segment.value, Math.abs(finite(bar.value)))}%`}\n />\n ),\n )}\n </span>\n ) : (\n <span\n data-ui=\"bar-chart-fill\"\n data-series={props.keys?.length ? slotOf(props.keys[0]!.series) : '1'}\n style={`inline-size: ${percent(bar.value, ceiling)}%`}\n />\n )}\n </span>\n <span data-ui=\"bar-chart-value\">{props.value(bar)}</span>\n </li>\n ),\n )}\n </ul>\n {!!props.scale?.length && (\n <div data-ui=\"bar-chart-scale\" aria-hidden=\"true\">\n {each(\n props.scale,\n (tick, at) => `${tick}:${at}`,\n (tick) => (\n <span>{tick}</span>\n ),\n )}\n </div>\n )}\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'timeframe',\n 'timeframe-menu',\n 'timeframe-trigger',\n 'timeframe-label',\n 'timeframe-value',\n 'timeframe-content',\n 'timeframe-option',\n 'timeframe-option-label',\n 'timeframe-option-detail',\n 'timeframe-range',\n 'timeframe-asof',\n 'timeframe-note',\n] as const\n\nexport type TimeframeOption = {\n id: string\n label: string\n href: string\n active?: boolean\n /** Shown under the option, e.g. the dates the preset resolves to. */\n detail?: string | null\n}\n\nexport type TimeframeFilterProps = {\n id: string\n /** What is being filtered, e.g. \"Reporting period\". Also the group's label. */\n label: string\n options: readonly TimeframeOption[]\n /** The range the active option resolves to, e.g. \"2026-08-06 \u2192 2026-09-05\". */\n range?: string | null\n /** When the figures were computed, and the words for it. */\n asOf?: string | null\n asOfLabel?: string | null\n /** Trailing note, e.g. the timezone the range is expressed in. */\n note?: string | null\n}\n\n/**\n * The period a screen is reporting on.\n *\n * A filter, not navigation: tabs mean sibling views, and a row of periods read\n * as tabs looks like five screens rather than one screen under five constraints.\n * Every option is a link, so the choice survives a copied URL and needs no\n * script. The resolved range and the build time sit beside the choice, because\n * a period label alone does not say what a number covers.\n */\nexport const TimeframeFilter = (props: TimeframeFilterProps): TemplateResult => {\n const active = props.options.find((option) => option.active) ?? props.options[0]\n return (\n <div data-ui=\"timeframe\" role=\"group\" aria-label={props.label}>\n <details data-ui=\"timeframe-menu\">\n <summary data-ui=\"timeframe-trigger\" aria-label={props.label} title={props.label}>\n <span data-ui=\"timeframe-label\">{props.label}</span>\n <span data-ui=\"timeframe-value\">{active?.label ?? ''}</span>\n </summary>\n <div data-ui=\"timeframe-content\" id={`${props.id}-options`}>\n {each(\n props.options,\n (option) => option.id,\n (option) => (\n <a\n data-ui=\"timeframe-option\"\n data-active={String(option.active === true)}\n aria-current={option.active === true ? 'true' : null}\n href={option.href}\n >\n <span data-ui=\"timeframe-option-label\">{option.label}</span>\n {!!option.detail && <small data-ui=\"timeframe-option-detail\">{option.detail}</small>}\n </a>\n ),\n )}\n </div>\n </details>\n {!!props.range && <span data-ui=\"timeframe-range\">{props.range}</span>}\n {!!props.asOf && (\n <span data-ui=\"timeframe-asof\">\n {props.asOfLabel ? `${props.asOfLabel} ${props.asOf}` : props.asOf}\n </span>\n )}\n {!!props.note && <span data-ui=\"timeframe-note\">{props.note}</span>}\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Avatar, Badge } from '../../primitives/status/index.tsx'\nimport type { Tone } from '../../primitives/status/index.tsx'\n\nexport const HOOKS = [\n 'description-list',\n 'key-value',\n 'key-value-label',\n 'key-value-value',\n 'person',\n 'person-body',\n 'avatar-group',\n 'status',\n] as const\n\nexport type KeyValueProps = { label: JSXChild; value: JSXChild; emptyLabel?: string }\nexport const KeyValue = (props: KeyValueProps): TemplateResult => (\n <div data-ui=\"key-value\">\n <dt data-ui=\"key-value-label\">{props.label}</dt>\n <dd data-ui=\"key-value-value\">{props.value ?? props.emptyLabel ?? '\u2014'}</dd>\n </div>\n)\n\n/**\n * `layout=\"strip\"` lays the pairs out in one row divided by hairlines, the\n * metadata strip of LAYOUT.md L7 (due date, SLA, owner under a record title).\n * It has no box around it; `columns` applies to the grid layout only.\n */\nexport const DescriptionList = (props: {\n label?: string\n items: readonly (KeyValueProps & { id: string })[]\n columns?: 1 | 2 | 3\n layout?: 'grid' | 'strip'\n}): TemplateResult => (\n <dl\n data-ui=\"description-list\"\n aria-label={props.label}\n data-columns={String(props.columns ?? 2)}\n data-layout={props.layout === 'strip' ? 'strip' : null}\n >\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <KeyValue {...item} />\n ),\n )}\n </dl>\n)\n\nexport type PersonProps = {\n name: string\n detail?: JSXChild\n href?: string\n size?: 'small' | 'default' | 'large'\n}\nexport const Person = (props: PersonProps): TemplateResult => (\n <span data-ui=\"person\" data-pattern=\"person\">\n <Avatar name={props.name} size={props.size} />\n <span data-ui=\"person-body\">\n {props.href ? <a href={props.href}>{props.name}</a> : <strong>{props.name}</strong>}\n {props.detail !== undefined && <small>{props.detail}</small>}\n </span>\n </span>\n)\n\nexport const AvatarGroup = (props: {\n label: string\n people: readonly { id: string; name: string }[]\n max?: number\n}): TemplateResult => {\n const visible = props.people.slice(0, props.max ?? 4)\n const remaining = props.people.length - visible.length\n return (\n <span data-ui=\"avatar-group\" role=\"group\" aria-label={props.label}>\n {each(\n visible,\n (person) => person.id,\n (person) => (\n <Avatar name={person.name} />\n ),\n )}\n {remaining > 0 && (\n <span role=\"img\" aria-label={`${remaining} more people`} title={`${remaining} more people`}>\n +{String(remaining)}\n </span>\n )}\n </span>\n )\n}\n\nexport const Status = (props: { label: string; tone?: Tone; detail?: string }): TemplateResult => (\n <span data-ui=\"status\" title={props.detail}>\n <Badge label={props.label} tone={props.tone} />\n </span>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { ActionGroup } from '../../primitives/actions/index.tsx'\nimport { Section } from '../../layouts/layout/index.tsx'\nimport { DescriptionList, Person, Status } from '../display/index.tsx'\nimport type { KeyValueProps, PersonProps } from '../display/index.tsx'\nimport type { Tone } from '../../primitives/status/index.tsx'\n\nexport const HOOKS = ['record-summary', 'record-summary-main', 'record-actions', 'record-rail'] as const\n\nexport type RecordSummaryProps = {\n title: JSXChild\n subtitle?: JSXChild\n person?: PersonProps\n status?: { label: string; tone?: Tone; detail?: string }\n facts?: readonly (KeyValueProps & { id: string })[]\n}\n\nexport const RecordSummary = (props: RecordSummaryProps): TemplateResult => (\n <header data-ui=\"record-summary\">\n <div data-ui=\"record-summary-main\">\n <h2>{props.title}</h2>\n {props.subtitle !== undefined && <p>{props.subtitle}</p>}\n {props.person && <Person {...props.person} />}\n </div>\n {props.status && <Status {...props.status} />}\n {props.facts && <DescriptionList items={props.facts} columns={3} />}\n </header>\n)\n\nexport const RecordActions = (props: { label?: string; actions: readonly JSXChild[] }): TemplateResult => (\n <nav data-ui=\"record-actions\" aria-label={props.label ?? 'Record actions'}>\n <ActionGroup actions={props.actions} />\n </nav>\n)\n\nexport type RecordRailSection = { id: string; title: string; body: JSXChild; actions?: JSXChild }\nexport const RecordRail = (props: {\n label?: string\n sections: readonly RecordRailSection[]\n}): TemplateResult => (\n <aside data-ui=\"record-rail\" aria-label={props.label ?? 'Record context'}>\n {each(\n props.sections,\n (section) => section.id,\n (section) => (\n <Section title={section.title} body={section.body} actions={section.actions} />\n ),\n )}\n </aside>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { EmptyState, LoadingState, Notice } from '../../primitives/feedback/index.tsx'\nimport { Person } from '../display/index.tsx'\n\nexport const HOOKS = [\n 'activity-timeline',\n 'activity-item',\n 'activity-marker',\n 'audit-log',\n 'audit-item',\n] as const\n\nexport type ActivityItem = {\n id: string\n actor: string\n actorDetail?: string\n action: JSXChild\n datetime: string\n timeLabel: string\n detail?: JSXChild\n redacted?: boolean\n}\n\ntype FeedProps = {\n label: string\n items: readonly ActivityItem[]\n loading?: boolean\n loadingLabel?: string\n emptyTitle?: string\n emptyMessage?: string\n error?: string | null\n redactedLabel?: string\n}\n\nconst state = (props: FeedProps): TemplateResult | null => {\n if (props.loading) return <LoadingState label={props.loadingLabel ?? 'Loading activity'} />\n if (props.error) return <Notice title=\"Activity unavailable\" message={props.error} tone=\"danger\" />\n if (!props.items.length)\n return (\n <EmptyState\n title={props.emptyTitle ?? 'No activity'}\n message={props.emptyMessage ?? 'Nothing has happened yet.'}\n />\n )\n return null\n}\n\nexport const ActivityTimeline = (props: FeedProps): TemplateResult => {\n const placeholder = state(props)\n if (placeholder) return placeholder\n return (\n <ol data-ui=\"activity-timeline\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <li data-ui=\"activity-item\">\n <span data-ui=\"activity-marker\" aria-hidden=\"true\" />\n <div>\n <Person name={item.actor} detail={item.actorDetail} size=\"small\" />\n <p>{item.redacted ? (props.redactedLabel ?? 'Details redacted') : item.action}</p>\n {item.detail !== undefined && !item.redacted && <div>{item.detail}</div>}\n <time datetime={item.datetime}>{item.timeLabel}</time>\n </div>\n </li>\n ),\n )}\n </ol>\n )\n}\n\nexport const AuditLog = (props: FeedProps): TemplateResult => {\n const placeholder = state(props)\n if (placeholder) return placeholder\n return (\n <ol data-ui=\"audit-log\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <li data-ui=\"audit-item\">\n <time datetime={item.datetime}>{item.timeLabel}</time>\n <strong>{item.actor}</strong>\n <span>{item.redacted ? (props.redactedLabel ?? 'Change redacted') : item.action}</span>\n </li>\n ),\n )}\n </ol>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { EmptyState, LoadingState, Notice } from '../../primitives/feedback/index.tsx'\n\nexport const HOOKS = ['attachments', 'attachment', 'media-gallery', 'media-item'] as const\n\nexport type AttachmentItem = {\n id: string\n name: string\n href?: string\n meta?: JSXChild\n redacted?: boolean\n}\n\nexport const Attachments = (props: {\n label: string\n items: readonly AttachmentItem[]\n loading?: boolean\n error?: string | null\n redactedLabel?: string\n}): TemplateResult => {\n if (props.loading) return <LoadingState label=\"Loading attachments\" />\n if (props.error) return <Notice title=\"Attachments unavailable\" message={props.error} tone=\"danger\" />\n if (!props.items.length) return <EmptyState title=\"No attachments\" message=\"No files have been attached.\" />\n return (\n <ul data-ui=\"attachments\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <li data-ui=\"attachment\">\n {item.redacted || !item.href ? (\n <span>{props.redactedLabel ?? item.name}</span>\n ) : (\n <a href={item.href}>{item.name}</a>\n )}\n {item.meta !== undefined && <small>{item.meta}</small>}\n </li>\n ),\n )}\n </ul>\n )\n}\n\nexport type MediaItem = {\n id: string\n src?: string\n alt: string\n caption?: JSXChild\n href?: string\n redacted?: boolean\n}\nexport const MediaGallery = (props: {\n label: string\n items: readonly MediaItem[]\n emptyTitle?: string\n redactedLabel?: string\n}): TemplateResult => {\n if (!props.items.length)\n return <EmptyState title={props.emptyTitle ?? 'No media'} message=\"No media is available.\" />\n return (\n <div data-ui=\"media-gallery\" role=\"group\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <figure data-ui=\"media-item\">\n {item.redacted || !item.src ? (\n <div role=\"img\" aria-label={props.redactedLabel ?? 'Media redacted'}>\n Restricted\n </div>\n ) : item.href ? (\n <a href={item.href}>\n <img src={item.src} alt={item.alt} loading=\"lazy\" />\n </a>\n ) : (\n <img src={item.src} alt={item.alt} loading=\"lazy\" />\n )}\n {item.caption !== undefined && <figcaption>{item.caption}</figcaption>}\n </figure>\n ),\n )}\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'app-navigation',\n 'navigation-trigger',\n 'navigation-toggle',\n 'navigation-trigger-icon',\n 'navigation-trigger-identity',\n 'navigation-trigger-label',\n 'navigation-layer',\n 'navigation-backdrop',\n 'navigation-drawer',\n 'navigation-header',\n 'navigation-identity',\n 'navigation-context',\n 'navigation-close',\n 'navigation-groups',\n 'navigation-group',\n 'navigation-group-label',\n 'navigation-items',\n 'navigation-supplementary',\n 'navigation-item',\n 'navigation-branch',\n 'navigation-branch-trigger',\n 'navigation-children',\n 'navigation-item-leading',\n 'navigation-item-copy',\n 'navigation-item-label',\n 'navigation-item-description',\n 'navigation-item-count',\n 'navigation-footer',\n] as const\n\ntype NavigationItemBase = {\n id: string\n label: string\n leading?: JSXChild\n description?: string\n count?: number\n}\n\nexport type NavigationItemData = NavigationItemBase &\n (\n | { href: string; active?: boolean; children?: never; expanded?: never }\n | { href?: never; active?: never; children: readonly NavigationItemData[]; expanded?: boolean }\n )\n\nexport type NavigationGroupData = {\n id: string\n label?: string\n items: readonly NavigationItemData[]\n}\n\nexport type AppNavigationProps = {\n id: string\n label: string\n identity?: JSXChild\n /** A topbar may provide NavigationToggle while this component owns the drawer. */\n externalTrigger?: boolean\n groups: readonly NavigationGroupData[]\n context?: JSXChild\n supplementary?: JSXChild\n footer?: JSXChild\n navigationSlot?: string\n menuLabel?: string\n closeLabel?: string\n open?: boolean\n}\n\nexport const NavigationTrigger = (props: {\n controls: string\n label: string\n identity?: JSXChild\n open?: boolean\n}): TemplateResult => (\n <summary\n data-ui=\"navigation-trigger\"\n aria-controls={props.controls}\n data-open={props.open === true ? 'true' : 'false'}\n >\n <span data-ui=\"navigation-trigger-icon\" aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n <span data-ui=\"navigation-trigger-identity\">{props.identity}</span>\n <span data-ui=\"navigation-trigger-label\">{props.label}</span>\n </summary>\n)\n\nexport const NavigationHeader = (props: {\n identity?: JSXChild\n context?: JSXChild\n closeLabel: string\n}): TemplateResult => (\n <header data-ui=\"navigation-header\">\n <div>\n {props.identity !== undefined && <div data-ui=\"navigation-identity\">{props.identity}</div>}\n {props.context !== undefined && <div data-ui=\"navigation-context\">{props.context}</div>}\n </div>\n <button data-ui=\"navigation-close\" type=\"button\" aria-label={props.closeLabel}>\n <span aria-hidden=\"true\" />\n </button>\n </header>\n)\n\nconst NavigationItemContent = (props: { item: NavigationItemData }): TemplateResult => (\n <>\n {props.item.leading !== undefined && (\n <span data-ui=\"navigation-item-leading\" aria-hidden=\"true\">\n {props.item.leading}\n </span>\n )}\n <span data-ui=\"navigation-item-copy\">\n <span data-ui=\"navigation-item-label\">{props.item.label}</span>\n {props.item.description !== undefined && (\n <span data-ui=\"navigation-item-description\">{props.item.description}</span>\n )}\n </span>\n {props.item.count !== undefined && (\n <span data-ui=\"navigation-item-count\">{String(props.item.count)}</span>\n )}\n </>\n)\n\nconst hasActiveItem = (item: NavigationItemData): boolean =>\n item.active === true || item.children?.some(hasActiveItem) === true\n\nconst renderNavigationItem = (\n props: NavigationItemData,\n branchGroup: string,\n level: number,\n): TemplateResult => {\n if (props.children !== undefined) {\n const open = props.expanded === true || hasActiveItem(props)\n return (\n <details data-ui=\"navigation-branch\" data-level={String(level)} name={branchGroup} open={open}>\n <summary data-ui=\"navigation-branch-trigger\">\n <NavigationItemContent item={props} />\n </summary>\n <div data-ui=\"navigation-children\" data-level={String(level + 1)}>\n {each(\n props.children,\n (item) => item.id,\n (item) => renderNavigationItem(item, `${props.id}-branches`, level + 1),\n )}\n </div>\n </details>\n )\n }\n return (\n <a\n data-ui=\"navigation-item\"\n data-active={props.active === true ? 'true' : null}\n data-level={String(level)}\n href={props.href}\n aria-current={props.active === true ? 'page' : null}\n >\n <NavigationItemContent item={props} />\n </a>\n )\n}\n\nexport const NavigationItem = (props: NavigationItemData): TemplateResult => (\n <>{renderNavigationItem(props, `${props.id}-root-branches`, 1)}</>\n)\n\nexport const NavigationGroup = (props: NavigationGroupData & { branchGroup?: string }): TemplateResult => (\n <section data-ui=\"navigation-group\" aria-labelledby={props.label ? `${props.id}-label` : null}>\n {props.label !== undefined && (\n <h2 data-ui=\"navigation-group-label\" id={`${props.id}-label`}>\n {props.label}\n </h2>\n )}\n <div data-ui=\"navigation-items\">\n {each(\n props.items,\n (item) => item.id,\n (item) => renderNavigationItem(item, props.branchGroup ?? `${props.id}-branches`, 1),\n )}\n </div>\n </section>\n)\n\nexport const NavigationDrawer = (props: {\n id: string\n label: string\n identity?: JSXChild\n groups: readonly NavigationGroupData[]\n context?: JSXChild\n supplementary?: JSXChild\n footer?: JSXChild\n navigationSlot?: string\n closeLabel: string\n}): TemplateResult => (\n <div data-ui=\"navigation-layer\">\n <button\n data-ui=\"navigation-backdrop\"\n type=\"button\"\n aria-label={props.closeLabel}\n aria-hidden=\"true\"\n tabIndex={-1}\n />\n <div data-ui=\"navigation-drawer\" id={props.id} data-navigation-label={props.label} tabIndex={-1}>\n <NavigationHeader identity={props.identity} context={props.context} closeLabel={props.closeLabel} />\n <nav data-ui=\"navigation-groups\" aria-label={props.label} data-ket-slot={props.navigationSlot}>\n {each(\n props.groups,\n (group) => group.id,\n (group) => (\n <NavigationGroup {...group} id={`${props.id}-${group.id}`} branchGroup={`${props.id}-branches`} />\n ),\n )}\n {props.supplementary !== undefined && (\n <div data-ui=\"navigation-supplementary\">{props.supplementary}</div>\n )}\n </nav>\n {props.footer !== undefined && <footer data-ui=\"navigation-footer\">{props.footer}</footer>}\n </div>\n </div>\n)\n\nexport const AppNavigation = (props: AppNavigationProps): TemplateResult => {\n const drawerId = `${props.id}-drawer`\n const closeLabel = props.closeLabel ?? 'Close navigation'\n return (\n <details\n data-ui=\"app-navigation\"\n data-navigation-id={props.id}\n data-external-trigger={props.externalTrigger ? 'true' : null}\n open={props.open === true}\n >\n <NavigationTrigger\n controls={drawerId}\n label={props.menuLabel ?? 'Menu'}\n identity={props.identity}\n open={props.open}\n />\n <NavigationDrawer\n id={drawerId}\n label={props.label}\n identity={props.identity}\n groups={props.groups}\n context={props.context}\n supplementary={props.supplementary}\n footer={props.footer}\n navigationSlot={props.navigationSlot}\n closeLabel={closeLabel}\n />\n </details>\n )\n}\n\nexport const NavigationToggle = (props: { controls: string; label: string }): TemplateResult => (\n <button\n data-ui=\"navigation-toggle\"\n type=\"button\"\n aria-controls={props.controls}\n aria-label={props.label}\n aria-expanded=\"false\"\n >\n <svg\n viewBox=\"0 0 24 24\"\n width=\"20\"\n height=\"20\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n aria-hidden=\"true\"\n >\n <path d=\"M4 6h16M4 12h16M4 18h16\" />\n </svg>\n </button>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { EmptyState } from '../../primitives/feedback.tsx'\nimport { Surface } from '../../layouts/index.tsx'\n\nexport type Cell = JSXChild\nexport type SortDirection = 'ascending' | 'descending'\nexport type TablePager = {\n label: string\n previousHref?: string | null\n nextHref?: string | null\n}\n\nexport type Column<Row> = {\n key: string\n label: string\n cell: (row: Row) => Cell\n align?: 'start' | 'end'\n kind?: 'text' | 'number' | 'currency' | 'date' | 'status' | 'identifier' | 'person' | 'media'\n priority?: 'primary' | 'secondary' | 'tertiary'\n width?: 'narrow' | 'medium' | 'wide'\n hidden?: boolean\n optional?: boolean\n sort?: {\n href: string\n direction?: SortDirection | null\n label?: string\n } | null\n}\n\nexport type TableSelection = {\n /** Associates row checkboxes with an external bulk-action form. */\n form?: string\n name?: string\n selectAllName?: string\n selectedIds?: readonly string[]\n disabled?: boolean\n}\n\nexport type TableGroup<Row> = {\n id: string\n label: JSXChild\n count?: number\n href?: string | null\n rows?: readonly Row[]\n children?: readonly TableGroup<Row>[]\n pager?: TablePager | null\n}\n\nexport type DataTableLabels<Row> = {\n selectAll?: string\n selectRow?: (row: Row, id: string) => string\n sortedAscending?: string\n sortedDescending?: string\n previousPage?: string\n nextPage?: string\n}\n\nexport type DataTableProps<Row> = {\n /** A shared surface contains the heading and an unframed table viewport. */\n title?: string\n actions?: JSXChild\n columns: readonly Column<Row>[]\n rows: readonly Row[]\n id: (row: Row) => string\n caption?: string | null\n rowHref?: (row: Row) => string\n selected?: (row: Row) => boolean\n emptyTitle?: string\n emptyMessage?: string\n emptyActions?: JSXChild\n /** Stack labelled cells on narrow screens, or retain horizontal scrolling. */\n responsive?: 'scroll' | 'stack'\n /** Use a denser gutter for high-volume operational lists. */\n gutter?: 'default' | 'compact'\n selection?: TableSelection | null\n groups?: readonly TableGroup<Row>[]\n shownColumns?: readonly string[]\n labels?: DataTableLabels<Row>\n}\n\nexport const HOOKS = [\n 'table-scroll',\n 'table',\n 'table-caption',\n 'select-col',\n 'select-all',\n 'col',\n 'sort-link',\n 'sort-icon',\n 'row',\n 'group-row',\n 'group-cell',\n 'group-link',\n 'group-indent',\n 'group-count',\n 'group-pager',\n 'group-pager-actions',\n 'cell',\n 'select-cell',\n 'row-select',\n 'row-link',\n] as const\n\nconst selectedColumnKeys = <Row,>(props: DataTableProps<Row>): readonly string[] =>\n props.shownColumns ?? props.columns.filter((column) => column.hidden !== true).map((column) => column.key)\n\nconst visibleColumns = <Row,>(props: DataTableProps<Row>): readonly Column<Row>[] => {\n const selected = new Set(selectedColumnKeys(props))\n return props.columns.filter((column) => selected.has(column.key))\n}\n\nconst colSpan = <Row,>(props: DataTableProps<Row>, columns: readonly Column<Row>[]): number =>\n columns.length + (props.selection ? 1 : 0)\n\nconst sortLabel = <Row,>(column: Column<Row>, labels: DataTableLabels<Row> | undefined): string | null => {\n if (!column.sort?.direction) return null\n return column.sort.direction === 'ascending'\n ? (labels?.sortedAscending ?? 'Sorted ascending')\n : (labels?.sortedDescending ?? 'Sorted descending')\n}\n\nconst columnHeader = <Row,>(\n column: Column<Row>,\n labels: DataTableLabels<Row> | undefined,\n): TemplateResult => (\n <th\n data-ui=\"col\"\n data-col={column.key}\n data-align={column.align ?? 'start'}\n data-kind={column.kind ?? 'text'}\n data-width={column.width ?? null}\n data-optional={column.optional === true ? 'true' : null}\n data-sort={column.sort?.direction ?? null}\n aria-sort={column.sort?.direction ?? null}\n scope=\"col\"\n >\n {column.sort ? (\n <a data-ui=\"sort-link\" href={column.sort.href} aria-label={column.sort.label ?? column.label}>\n <span>{column.label}</span>\n {!!column.sort.direction && (\n <span data-ui=\"sort-icon\" aria-hidden=\"true\" title={sortLabel(column, labels) ?? undefined}>\n {column.sort.direction === 'ascending' ? '\u2191' : '\u2193'}\n </span>\n )}\n </a>\n ) : (\n column.label\n )}\n </th>\n)\n\nconst selectionHeader = <Row,>(props: DataTableProps<Row>): TemplateResult | null =>\n props.selection ? (\n <th data-ui=\"select-col\" scope=\"col\">\n <input\n data-ui=\"select-all\"\n type=\"checkbox\"\n name={props.selection.selectAllName ?? 'select-all'}\n aria-label={props.labels?.selectAll ?? 'Select all rows'}\n disabled={props.selection.disabled === true}\n />\n </th>\n ) : null\n\nconst isSelected = <Row,>(props: DataTableProps<Row>, row: Row): boolean => {\n if (props.selected?.(row) === true) return true\n return props.selection?.selectedIds?.includes(props.id(row)) === true\n}\n\nconst rowCells = <Row,>(\n props: DataTableProps<Row>,\n columns: readonly Column<Row>[],\n row: Row,\n): TemplateResult => (\n <>\n {props.selection && (\n <td data-ui=\"select-cell\">\n <input\n data-ui=\"row-select\"\n type=\"checkbox\"\n name={props.selection.name ?? 'ids'}\n form={props.selection.form}\n value={props.id(row)}\n checked={isSelected(props, row)}\n aria-label={props.labels?.selectRow?.(row, props.id(row)) ?? `Select row ${props.id(row)}`}\n disabled={props.selection.disabled === true}\n />\n </td>\n )}\n {each(\n columns,\n (column) => column.key,\n (column) => (\n <td\n data-ui=\"cell\"\n data-col={column.key}\n data-linked={props.rowHref && column === columns[0] ? 'true' : null}\n data-align={column.align ?? 'start'}\n data-kind={column.kind ?? 'text'}\n data-priority={column.priority ?? 'secondary'}\n data-label={props.responsive === 'stack' ? column.label : null}\n >\n {props.rowHref && column === columns[0] ? (\n <a data-ui=\"row-link\" href={props.rowHref(row)}>\n {column.cell(row)}\n </a>\n ) : (\n column.cell(row)\n )}\n </td>\n ),\n )}\n </>\n)\n\nconst tableRow = <Row,>(\n props: DataTableProps<Row>,\n columns: readonly Column<Row>[],\n row: Row,\n): TemplateResult => (\n <tr data-ui=\"row\" data-row={props.id(row)} data-selected={isSelected(props, row) ? 'true' : null}>\n {rowCells(props, columns, row)}\n </tr>\n)\n\nconst groupPager = <Row,>(\n props: DataTableProps<Row>,\n columns: readonly Column<Row>[],\n pager: TablePager | null | undefined,\n): TemplateResult | null =>\n pager ? (\n <tr data-ui=\"group-pager\">\n <td colSpan={String(colSpan(props, columns))}>\n <span>{pager.label}</span>\n <span data-ui=\"group-pager-actions\">\n {pager.previousHref && (\n <a href={pager.previousHref} aria-label={props.labels?.previousPage ?? 'Previous page'}>\n \u2039\n </a>\n )}\n {pager.nextHref && (\n <a href={pager.nextHref} aria-label={props.labels?.nextPage ?? 'Next page'}>\n \u203A\n </a>\n )}\n </span>\n </td>\n </tr>\n ) : null\n\nconst groupRows = <Row,>(\n props: DataTableProps<Row>,\n columns: readonly Column<Row>[],\n group: TableGroup<Row>,\n depth: number,\n): TemplateResult => (\n <>\n <tr data-ui=\"group-row\" data-group={group.id} data-depth={String(depth)}>\n <th data-ui=\"group-cell\" colSpan={String(colSpan(props, columns))} scope=\"rowgroup\">\n <span data-ui=\"group-indent\" style={`--kv-group-depth: ${depth}`} aria-hidden=\"true\" />\n {group.href ? (\n <a data-ui=\"group-link\" href={group.href}>\n {group.label}\n </a>\n ) : (\n <span data-ui=\"group-link\">{group.label}</span>\n )}\n {group.count !== undefined && <span data-ui=\"group-count\">{String(group.count)}</span>}\n </th>\n </tr>\n {each(group.rows ?? [], props.id, (row) => tableRow(props, columns, row))}\n {each(\n group.children ?? [],\n (child) => child.id,\n (child) => groupRows(props, columns, child, depth + 1),\n )}\n {groupPager(props, columns, group.pager)}\n </>\n)\n\nexport const DataTable = <Row,>(props: DataTableProps<Row>): TemplateResult => {\n const columns = visibleColumns(props)\n const groups = props.groups ?? []\n const hasRows = props.rows.length > 0 || groups.length > 0\n\n const content = hasRows ? (\n <div\n data-ui=\"table-scroll\"\n data-pattern=\"data-table\"\n data-framed={props.title ? 'false' : null}\n data-responsive={props.responsive ?? 'scroll'}\n data-gutter={props.gutter === 'compact' ? 'compact' : null}\n >\n <table data-ui=\"table\" aria-label={props.caption ? null : (props.title ?? null)}>\n {!!props.caption && <caption data-ui=\"table-caption\">{props.caption}</caption>}\n <thead>\n <tr>\n {selectionHeader(props)}\n {each(\n columns,\n (column) => column.key,\n (column) => columnHeader(column, props.labels),\n )}\n </tr>\n </thead>\n <tbody>\n {groups.length > 0\n ? each(\n groups,\n (group) => group.id,\n (group) => groupRows(props, columns, group, 0),\n )\n : each(props.rows, props.id, (row) => tableRow(props, columns, row))}\n </tbody>\n </table>\n </div>\n ) : (\n <EmptyState\n title={props.emptyTitle ?? 'No records'}\n message={props.emptyMessage ?? 'There is nothing to show yet.'}\n actions={props.emptyActions}\n />\n )\n return props.title ? (\n <Surface title={props.title} actions={props.actions} padding=\"none\" body={content} />\n ) : (\n content\n )\n}\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { pageIdentity } from '../page-shell.tsx'\n\nexport const HOOKS = [\n 'list-page',\n 'list-page-context',\n 'list-page-header',\n 'list-page-heading',\n 'list-page-title-row',\n 'list-page-eyebrow',\n 'list-page-title',\n 'list-page-actions',\n 'list-page-tools',\n 'list-page-toolbar',\n 'list-page-controls',\n 'list-page-status',\n 'list-page-body',\n 'list-page-footer',\n] as const\n\nexport type ListPageProps = {\n title: string\n body: JSXChild\n /**\n * A narrow application context strip above the page identity. The application\n * owns its breadcrumbs and organisation switcher; the pattern owns the slot.\n */\n context?: JSXChild\n /**\n * `operational` is the full workspace composition used by application collection\n * screens. The unadorned variant remains available for embedded specimens.\n */\n variant?: 'operational'\n eyebrow?: string | null\n /** Primary actions beside the title of an operational page, above its query controls. */\n headerActions?: JSXChild\n /** Tool actions follow query controls by default, or join primary actions in the header. */\n actions?: JSXChild\n /** Place operational tools next to primary header actions without a separate body row. */\n actionsPlacement?: 'header'\n /** Hide only the operational tool slot without unmounting associated forms or hiding primary actions. */\n actionsHidden?: boolean\n controls?: JSXChild\n status?: JSXChild\n footer?: JSXChild\n}\n\n/**\n * The baseline for an operational collection screen.\n *\n * Applications own the translated copy, URL-driven controls and records. This\n * pattern owns their order and rhythm: identity and optional primary actions,\n * query controls, optional separate tools, then the collection itself.\n */\nexport const ListPage = (props: ListPageProps): TemplateResult => {\n const operational = props.variant === 'operational'\n const headerTools = operational && props.actionsPlacement === 'header'\n const headerActions =\n headerTools && props.actions !== undefined ? (\n <>\n {props.headerActions}\n <div data-ui=\"list-page-tools\" hidden={props.actionsHidden === true}>\n {props.actions}\n </div>\n </>\n ) : (\n props.headerActions\n )\n // Result summaries close the collection in an operational workspace. Keeping\n // the legacy `status` prop as the input lets existing screens migrate without\n // duplicating their translated count as a separate footer value.\n const toolbarStatus = operational ? undefined : props.status\n const footer = props.footer ?? (operational ? props.status : undefined)\n\n return (\n <section\n data-ui=\"list-page\"\n data-variant={props.variant ?? null}\n data-pattern=\"list\"\n data-ket-preserve-context=\"\"\n >\n {pageIdentity('list-page', {\n context: props.context,\n eyebrow: props.eyebrow,\n title: props.title,\n actions: operational ? headerActions : props.actions,\n actionsHidden: headerTools && props.headerActions == null && props.actionsHidden === true,\n })}\n {(props.controls !== undefined || toolbarStatus !== undefined) && (\n <div data-ui=\"list-page-toolbar\">\n {toolbarStatus !== undefined && <div data-ui=\"list-page-status\">{toolbarStatus}</div>}\n {props.controls !== undefined && <div data-ui=\"list-page-controls\">{props.controls}</div>}\n </div>\n )}\n {operational && !headerTools && props.actions !== undefined && (\n <div data-ui=\"list-page-actions\" hidden={props.actionsHidden === true}>\n {props.actions}\n </div>\n )}\n <div data-ui=\"list-page-body\">{props.body}</div>\n {footer !== undefined && <footer data-ui=\"list-page-footer\">{footer}</footer>}\n </section>\n )\n}\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { pageIdentityContent, type PageIdentityKind } from '../page-shell.tsx'\n\nexport const HOOKS = [\n 'record-page',\n 'record-page-context',\n 'record-page-header',\n 'record-page-title-row',\n 'record-page-heading',\n 'record-page-title',\n 'record-page-subline',\n 'record-page-status',\n 'record-page-actions',\n 'record-page-meta',\n 'record-page-controller',\n 'record-page-navigation',\n 'record-page-layout',\n 'record-page-body',\n 'record-page-aside',\n] as const\n\nexport type RecordPageSlots = {\n header: string\n body: string\n fragmentTitle?: string\n}\n\nexport type RecordPageProps = {\n title: string\n body: JSXChild\n context?: JSXChild\n variant?: 'operational'\n width?: 'default' | 'wide'\n scope?: string | null\n status?: JSXChild\n actions?: JSXChild\n meta?: JSXChild\n controller?: JSXChild\n navigation?: JSXChild\n aside?: JSXChild\n asideLabel?: string | null\n slots?: RecordPageSlots\n}\n\ntype RecordPageKind = Extract<PageIdentityKind, 'record-page' | 'form-page'>\n\nexport const recordPage = (props: RecordPageProps, compatibilityKind?: RecordPageKind): TemplateResult => {\n const kind = compatibilityKind ?? 'record-page'\n if (props.slots?.fragmentTitle !== undefined)\n return (\n <ket-fragments data-title={props.slots.fragmentTitle}>\n <template data-ket-slot={props.slots.header}>{pageIdentityContent(kind, props)}</template>\n <template data-ket-slot={props.slots.body}>{props.body}</template>\n </ket-fragments>\n )\n return (\n <section\n data-ui={kind}\n data-scope={props.scope ?? null}\n data-has-aside={String(props.aside !== undefined)}\n data-variant={props.variant ?? null}\n data-width={props.width ?? 'default'}\n data-pattern=\"record\"\n >\n {props.context !== undefined && <div data-ui={`${kind}-context`}>{props.context}</div>}\n <header data-ui={`${kind}-header`} data-ket-slot={props.slots?.header}>\n {pageIdentityContent(kind, props)}\n </header>\n {props.controller !== undefined && <div data-ui={`${kind}-controller`}>{props.controller}</div>}\n {props.navigation !== undefined && <div data-ui={`${kind}-navigation`}>{props.navigation}</div>}\n <div data-ui={`${kind}-layout`}>\n <div data-ui={`${kind}-body`} data-ket-slot={props.slots?.body}>\n {props.body}\n </div>\n {props.aside !== undefined && (\n <aside data-ui={`${kind}-aside`} aria-label={props.asideLabel ?? null}>\n {props.aside}\n </aside>\n )}\n </div>\n </section>\n )\n}\n\n/** One durable subject: create, edit, inspect or advance its workflow. */\nexport const RecordPage = (props: RecordPageProps): TemplateResult => recordPage(props)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { recordPage, type RecordPageProps, type RecordPageSlots } from '../record-page.tsx'\n\nexport const HOOKS = [\n 'form-page',\n 'form-page-context',\n 'form-page-header',\n 'form-page-title-row',\n 'form-page-heading',\n 'form-page-title',\n 'form-page-subline',\n 'form-page-status',\n 'form-page-actions',\n 'form-page-meta',\n 'form-page-controller',\n 'form-page-navigation',\n 'form-page-layout',\n 'form-page-body',\n 'form-page-aside',\n] as const\n\nexport type FormPageSlots = RecordPageSlots\nexport type FormPageProps = RecordPageProps\n\n/** @deprecated Use RecordPage. Retained while application call sites migrate. */\nexport const FormPage = (props: FormPageProps): TemplateResult => recordPage(props, 'form-page')\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { pageIdentity, type PageIdentityKind } from '../page-shell.tsx'\n\nexport const HOOKS = [\n 'workspace-page',\n 'workspace-page-context',\n 'workspace-page-header',\n 'workspace-page-heading',\n 'workspace-page-eyebrow',\n 'workspace-page-title-row',\n 'workspace-page-title',\n 'workspace-page-subline',\n 'workspace-page-status',\n 'workspace-page-actions',\n 'workspace-page-meta',\n 'workspace-page-toolbar',\n 'workspace-page-controls',\n 'workspace-page-body',\n] as const\n\nexport type WorkspacePageProps = {\n title: string\n body: JSXChild\n context?: JSXChild\n variant?: 'operational'\n /** Vertical work surfaces use flow; boards, timelines and maps use canvas. */\n layout?: 'flow' | 'canvas'\n eyebrow?: string | null\n status?: JSXChild\n meta?: JSXChild\n actions?: JSXChild\n controls?: JSXChild\n}\n\ntype CompatibilityKind = Extract<PageIdentityKind, 'dashboard-page' | 'board-page'>\n\nexport const workspacePage = (\n props: WorkspacePageProps,\n compatibilityKind?: CompatibilityKind,\n): TemplateResult => {\n const kind = compatibilityKind ?? (props.layout === 'canvas' ? 'board-page' : 'dashboard-page')\n return (\n <section\n data-ui={kind}\n data-variant={props.variant ?? null}\n data-layout={props.layout ?? 'flow'}\n data-pattern=\"workspace\"\n >\n {pageIdentity(kind, props)}\n {props.controls !== undefined && (\n <div data-ui={`${kind}-toolbar`}>\n <div data-ui={`${kind}-controls`}>{props.controls}</div>\n </div>\n )}\n <div data-ui={`${kind}-body`}>{props.body}</div>\n </section>\n )\n}\n\n/** One operational canvas, with flow and horizontal-canvas layout modes. */\nexport const WorkspacePage = (props: WorkspacePageProps): TemplateResult => workspacePage(props)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { workspacePage, type WorkspacePageProps } from '../workspace-page.tsx'\n\nexport const HOOKS = [\n 'dashboard-page',\n 'dashboard-page-context',\n 'dashboard-page-header',\n 'dashboard-page-heading',\n 'dashboard-page-eyebrow',\n 'dashboard-page-title-row',\n 'dashboard-page-title',\n 'dashboard-page-subline',\n 'dashboard-page-status',\n 'dashboard-page-actions',\n 'dashboard-page-meta',\n 'dashboard-page-body',\n] as const\n\nexport type DashboardPageProps = Omit<WorkspacePageProps, 'layout' | 'controls'>\n\n/**\n * The baseline for KPI, workflow and analytical overview screens.\n *\n * Applications own the translated copy, business actions and dashboard blocks.\n * This pattern keeps location, identity and the overview canvas in a stable,\n * compact hierarchy without pretending that the dashboard is a business record.\n *\n * @deprecated Compose the overview recipe with WorkspacePage layout=\"flow\".\n */\nexport const DashboardPage = (props: DashboardPageProps): TemplateResult =>\n workspacePage({ ...props, layout: 'flow' }, 'dashboard-page')\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { workspacePage, type WorkspacePageProps } from '../workspace-page.tsx'\n\nexport const HOOKS = [\n 'board-page',\n 'board-page-context',\n 'board-page-header',\n 'board-page-heading',\n 'board-page-eyebrow',\n 'board-page-title-row',\n 'board-page-title',\n 'board-page-subline',\n 'board-page-status',\n 'board-page-actions',\n 'board-page-meta',\n 'board-page-toolbar',\n 'board-page-controls',\n 'board-page-body',\n] as const\n\nexport type BoardPageProps = Omit<WorkspacePageProps, 'layout'>\n\n/**\n * The baseline for kanban, schedule and timeline workspaces.\n *\n * A board needs more horizontal room than a collection or form, but it still\n * belongs to the same application hierarchy. The pattern keeps location,\n * identity and global controls in stable compact bands while leaving scrolling,\n * columns and interaction to the specialised board supplied by the application.\n *\n * @deprecated Compose canvas recipes with WorkspacePage layout=\"canvas\".\n */\nexport const BoardPage = (props: BoardPageProps): TemplateResult =>\n workspacePage({ ...props, layout: 'canvas' }, 'board-page')\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport type { Tone } from '../../primitives/status.tsx'\n\nexport const HOOKS = [\n 'pipeline',\n 'pipeline-step',\n 'pipeline-arrow',\n 'pipeline-step-label',\n 'pipeline-step-value',\n 'pipeline-step-node',\n] as const\n\nexport type PipelineStep = {\n id: string\n label: string\n value: string | number\n href?: string | null\n tone?: Tone\n}\n\n/**\n * A process read left to right: the stages of a thing, and how many are sitting\n * in each one.\n *\n * Not a `Progress`, which answers \"how far along is this one record\". This\n * answers \"where is the work piling up\", so every stage carries its own count\n * and none of them is a percentage of the others \u2014 a stage can hold more than\n * the one before it, and a bar that normalised them would say the opposite.\n *\n * An ordered list, because the order is the meaning. The connecting rail and the\n * arrows between stages are drawn by the stylesheet from `pipeline-step-node`\n * and `pipeline-arrow`; both are decoration for an order the markup already\n * states, which is why the arrow carries no text to announce.\n */\nexport const Pipeline = (props: { label: string; steps: readonly PipelineStep[] }): TemplateResult => (\n <ol data-ui=\"pipeline\" aria-label={props.label}>\n {each(\n props.steps,\n (step) => step.id,\n (step) => (\n <li data-ui=\"pipeline-step\" data-tone={step.tone ?? 'neutral'}>\n <span data-ui=\"pipeline-arrow\" aria-hidden=\"true\" />\n {step.href ? (\n <a data-ui=\"pipeline-step-label\" href={step.href}>\n {step.label}\n </a>\n ) : (\n <span data-ui=\"pipeline-step-label\">{step.label}</span>\n )}\n <span data-ui=\"pipeline-step-value\">{String(step.value)}</span>\n <span data-ui=\"pipeline-step-node\" aria-hidden=\"true\" />\n </li>\n ),\n )}\n </ol>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { ActionGroup, Button, LinkButton } from '../../primitives/actions.tsx'\nimport type { ActionVariant } from '../../primitives/actions.tsx'\nimport { Field } from '../../primitives/field.tsx'\nimport type { FieldProps } from '../../primitives/field.tsx'\n\nexport const HOOKS = ['record-form', 'form-errors', 'form-grid', 'form-actions'] as const\n\nexport type RecordFormProps = {\n id?: string | null\n action: string\n method?: 'get' | 'post'\n fields: readonly FieldProps[]\n submitLabel: string\n submitVariant?: ActionVariant\n cancelHref?: string | null\n cancelLabel?: string | null\n errors?: readonly string[]\n hidden?: Record<string, string>\n}\n\nexport const RecordForm = (props: RecordFormProps): TemplateResult => (\n <form\n data-ui=\"record-form\"\n id={props.id ?? undefined}\n action={props.action}\n method={props.method ?? 'post'}\n >\n {Object.entries(props.hidden ?? {}).map(([name, value]) => (\n <input type=\"hidden\" name={name} value={value} />\n ))}\n {!!props.errors?.length && (\n <ul data-ui=\"form-errors\" role=\"alert\">\n {each(\n props.errors,\n (error, index) => `${index}:${error}`,\n (error) => (\n <li>{error}</li>\n ),\n )}\n </ul>\n )}\n <div data-ui=\"form-grid\">\n {each(\n props.fields,\n (field) => field.id,\n (field) => (\n <Field {...field} />\n ),\n )}\n </div>\n <div data-ui=\"form-actions\">\n <ActionGroup\n actions={[\n <Button label={props.submitLabel} variant={props.submitVariant ?? 'primary'} type=\"submit\" />,\n ...(props.cancelHref\n ? [\n <LinkButton\n label={props.cancelLabel ?? 'Cancel'}\n href={props.cancelHref}\n variant=\"tertiary\"\n />,\n ]\n : []),\n ]}\n />\n </div>\n </form>\n)\n", "import { each, type JSXChild, type TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Button } from '../../primitives/actions/index.tsx'\nimport { reorderClick, reorderDragStart, reorderDragOver, reorderDrop } from './runtime.ts'\n\nexport const HOOKS = [\n 'reorder-list',\n 'reorder-list-value',\n 'reorder-list-rows',\n 'reorder-list-row',\n 'reorder-list-content',\n 'reorder-list-controls',\n] as const\nexport type ReorderListProps = {\n id: string\n name: string\n label: string\n items: readonly { id: string; content: JSXChild }[]\n disabled?: boolean\n labels: { add: string; remove: string; up: string; down: string; drag: string; empty: string }\n}\n/** Editable content stays caller-owned; this control emits its ordered ids through a native change event. */\nexport const ReorderList = (props: ReorderListProps): TemplateResult => (\n // biome-ignore lint/a11y/useKeyWithClickEvents: Delegates clicks from native buttons, which already handle Enter and Space.\n <div\n id={props.id}\n role=\"group\"\n aria-label={props.label}\n data-ui=\"reorder-list\"\n onClick={reorderClick}\n onDragStart={reorderDragStart}\n onDragOver={reorderDragOver}\n onDrop={reorderDrop}\n >\n <input\n data-ui=\"reorder-list-value\"\n type=\"hidden\"\n name={props.name}\n value={JSON.stringify(props.items.map((item) => item.id))}\n disabled={props.disabled}\n autocomplete=\"off\"\n />\n <ol data-ui=\"reorder-list-rows\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item, index) => (\n <li data-ui=\"reorder-list-row\" data-reorder-id={item.id}>\n <div data-ui=\"reorder-list-content\">{item.content}</div>\n <div data-ui=\"reorder-list-controls\">\n <span data-reorder-handle draggable={!props.disabled}>\n <Button\n label={props.labels.drag}\n size=\"compact\"\n variant=\"tertiary\"\n disabled={props.disabled}\n />\n </span>\n <span data-reorder-action=\"up\">\n <Button\n label={props.labels.up}\n size=\"compact\"\n variant=\"tertiary\"\n disabled={props.disabled || index === 0}\n />\n </span>\n <span data-reorder-action=\"down\">\n <Button\n label={props.labels.down}\n size=\"compact\"\n variant=\"tertiary\"\n disabled={props.disabled || index === props.items.length - 1}\n />\n </span>\n <span data-reorder-action=\"remove\">\n <Button\n label={props.labels.remove}\n size=\"compact\"\n variant=\"tertiary\"\n disabled={props.disabled}\n />\n </span>\n </div>\n </li>\n ),\n )}\n </ol>\n {!props.items.length && <p>{props.labels.empty}</p>}\n <span data-reorder-action=\"add\">\n <Button label={props.labels.add} size=\"compact\" disabled={props.disabled} />\n </span>\n </div>\n)\n", "import { HOOKS as actionHooks } from '../primitives/actions/index.tsx'\nimport { HOOKS as feedbackHooks } from '../primitives/feedback/index.tsx'\nimport { HOOKS as fieldHooks } from '../primitives/field/index.tsx'\nimport { HOOKS as statusHooks } from '../primitives/status/index.tsx'\nimport { HOOKS as navigationHooks } from '../primitives/navigation/index.tsx'\nimport { HOOKS as progressHooks } from '../primitives/progress/index.tsx'\nimport { HOOKS as reorderListHooks } from '../interactions/reorder-list/index.tsx'\nimport { HOOKS as menuHooks } from '../interactions/menu/index.tsx'\nimport { HOOKS as popoverHooks } from '../interactions/popover/index.tsx'\nimport { HOOKS as tooltipHooks } from '../interactions/tooltip/index.tsx'\nimport { HOOKS as dialogHooks } from '../interactions/dialog/index.tsx'\nimport { HOOKS as toastHooks } from '../interactions/toast/index.tsx'\nimport { HOOKS as spinnerHooks } from '../interactions/spinner/index.tsx'\nimport { HOOKS as skeletonHooks } from '../interactions/skeleton/index.tsx'\nimport { HOOKS as lightboxHooks } from '../interactions/lightbox/index.tsx'\nimport { HOOKS as relationSelectHooks } from '../interactions/relation-select/index.tsx'\nimport { HOOKS as searchFilterHooks } from '../interactions/search-filter/index.tsx'\nimport { HOOKS as ketTableHooks } from '../interactions/ket-table/index.tsx'\nimport { HOOKS as scalarFieldHooks } from '../forms/scalar-fields/index.tsx'\nimport { HOOKS as comboboxHooks } from '../forms/combobox/index.tsx'\nimport { HOOKS as dateTimeHooks } from '../forms/date-time/index.tsx'\nimport { HOOKS as uploadHooks } from '../forms/upload/index.tsx'\nimport { HOOKS as relationPickerHooks } from '../forms/relation-picker/index.tsx'\nimport { HOOKS as listControlHooks } from '../data-operations/list-controls/index.tsx'\nimport { HOOKS as inlineEditHooks } from '../data-operations/inline-edit/index.tsx'\nimport { HOOKS as resourceListHooks } from '../data-display/resource-list/index.tsx'\nimport { HOOKS as dataGridHooks } from '../data-display/data-grid/index.tsx'\nimport { HOOKS as treeHooks } from '../data-display/tree/index.tsx'\nimport { HOOKS as matrixHooks } from '../data-display/matrix/index.tsx'\nimport { HOOKS as barChartHooks } from '../data-display/bar-chart/index.tsx'\nimport { HOOKS as timeframeHooks } from '../data-operations/timeframe-filter/index.tsx'\nimport { HOOKS as recordDisplayHooks } from '../record/display/index.tsx'\nimport { HOOKS as formattedValueHooks } from '../record/formatted-values/index.tsx'\nimport { HOOKS as recordCompositionHooks } from '../record/composition/index.tsx'\nimport { HOOKS as activityHooks } from '../record/activity/index.tsx'\nimport { HOOKS as mediaHooks } from '../record/media/index.tsx'\nimport { HOOKS as layoutHooks } from '../layouts/layout/index.tsx'\nimport { HOOKS as shellHooks } from '../layouts/shell/index.tsx'\nimport { HOOKS as appNavigationHooks } from '../layouts/app-navigation/index.tsx'\nimport { HOOKS as tableHooks } from '../patterns/data-table/index.tsx'\nimport { HOOKS as listChromeHooks } from '../patterns/list-chrome/index.tsx'\nimport { HOOKS as listPageHooks } from '../patterns/list-page/index.tsx'\nimport { HOOKS as formPageHooks } from '../patterns/form-page/index.tsx'\nimport { HOOKS as recordPageHooks } from '../patterns/record-page/index.tsx'\nimport { HOOKS as dashboardPageHooks } from '../patterns/dashboard-page/index.tsx'\nimport { HOOKS as boardPageHooks } from '../patterns/board-page/index.tsx'\nimport { HOOKS as modalHooks } from '../patterns/modal-sheet/index.tsx'\nimport { HOOKS as pipelineHooks } from '../patterns/pipeline/index.tsx'\nimport { HOOKS as formHooks } from '../patterns/record-form/index.tsx'\n\nconst GROUPS = {\n actions: actionHooks,\n feedback: feedbackHooks,\n fields: fieldHooks,\n status: statusHooks,\n navigation: navigationHooks,\n progress: progressHooks,\n menu: menuHooks,\n reorderList: reorderListHooks,\n popover: popoverHooks,\n tooltip: tooltipHooks,\n dialog: dialogHooks,\n toast: toastHooks,\n spinner: spinnerHooks,\n skeleton: skeletonHooks,\n lightbox: lightboxHooks,\n relationSelect: relationSelectHooks,\n searchFilter: searchFilterHooks,\n ketTable: ketTableHooks,\n scalarFields: scalarFieldHooks,\n combobox: comboboxHooks,\n dateTime: dateTimeHooks,\n upload: uploadHooks,\n relationPicker: relationPickerHooks,\n listControls: listControlHooks,\n inlineEdit: inlineEditHooks,\n resourceList: resourceListHooks,\n dataGrid: dataGridHooks,\n tree: treeHooks,\n matrix: matrixHooks,\n barChart: barChartHooks,\n timeframe: timeframeHooks,\n recordDisplay: recordDisplayHooks,\n formattedValues: formattedValueHooks,\n recordComposition: recordCompositionHooks,\n activity: activityHooks,\n media: mediaHooks,\n layouts: layoutHooks,\n shell: shellHooks,\n appNavigation: appNavigationHooks,\n table: tableHooks,\n listChrome: listChromeHooks,\n listPage: listPageHooks,\n formPage: formPageHooks,\n recordPage: recordPageHooks,\n dashboardPage: dashboardPageHooks,\n boardPage: boardPageHooks,\n modal: modalHooks,\n pipeline: pipelineHooks,\n form: formHooks,\n} as const\n\nexport const HOOKS: readonly string[] = [...new Set(Object.values(GROUPS).flat())].sort()\n\nexport const OWNERS: Readonly<Record<string, string[]>> = Object.freeze(\n Object.fromEntries(Object.entries(GROUPS).map(([owner, hooks]) => [owner, [...hooks]])),\n)\n\nexport { auditLayoutCss } from './layout-audit.ts'\nexport type { LayoutRule, LayoutViolation } from './layout-audit.ts'\n", "// Lucide (ISC), vendored.\n//\n// Copied rather than installed, and only the ones actually used. An icon set is\n// data \u2014 a few hundred bytes of path per glyph \u2014 and pulling a package for it\n// would put a dependency, a build step and a supply chain between this repo and\n// twelve strings. The zero-dependency audit is not a slogan we work around; it\n// is the reason a `ket` app is one `node` away from running.\n//\n// Source: https://lucide.dev \u2014 ISC licence, \u00A9 Lucide Contributors.\n// These are the same glyphs the domain contract backend theme vendors, so the two products\n// look like one product.\n\nimport { trustedMarkup } from '@ketvietlab/ketjs-view'\nimport type { Markup } from '@ketvietlab/ketjs-view'\n\n/** The inside of a 24\u00D724 <svg>, stroked with currentColor. */\nconst PATHS: Record<string, string> = {\n // \u2500\u2500 apps\n package:\n '<path d=\"M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z\" /> <path d=\"M12 22V12\" /> <polyline points=\"3.29 7 12 12 20.71 7\" /> <path d=\"m7.5 4.27 9 5.15\" />',\n warehouse:\n '<path d=\"M18 21V10a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v11\" /> <path d=\"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 1.132-1.803l7.95-3.974a2 2 0 0 1 1.837 0l7.948 3.974A2 2 0 0 1 22 8z\" /> <path d=\"M6 13h12\" /> <path d=\"M6 17h12\" />',\n 'shopping-cart':\n '<circle cx=\"8\" cy=\"21\" r=\"1\" /> <circle cx=\"19\" cy=\"21\" r=\"1\" /> <path d=\"M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12\" />',\n globe:\n '<circle cx=\"12\" cy=\"12\" r=\"10\" /> <path d=\"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20\" /> <path d=\"M2 12h20\" />',\n users:\n '<path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\" /> <path d=\"M16 3.128a4 4 0 0 1 0 7.744\" /> <path d=\"M22 21v-2a4 4 0 0 0-3-3.87\" /> <circle cx=\"9\" cy=\"7\" r=\"4\" />',\n 'layout-grid':\n '<rect width=\"7\" height=\"7\" x=\"3\" y=\"3\" rx=\"1\" /> <rect width=\"7\" height=\"7\" x=\"14\" y=\"3\" rx=\"1\" /> <rect width=\"7\" height=\"7\" x=\"14\" y=\"14\" rx=\"1\" /> <rect width=\"7\" height=\"7\" x=\"3\" y=\"14\" rx=\"1\" />',\n settings:\n '<path d=\"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915\" /> <circle cx=\"12\" cy=\"12\" r=\"3\" />',\n 'file-text':\n '<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\" /> <path d=\"M14 2v4a2 2 0 0 0 2 2h4\" /> <path d=\"M10 9H8\" /> <path d=\"M16 13H8\" /> <path d=\"M16 17H8\" />',\n 'message-circle':\n '<path d=\"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719\" />',\n 'check-circle': '<path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\" /> <path d=\"m22 4-10 10.01-3-3\" />',\n send: '<path d=\"m22 2-7 20-4-9-9-4Z\" /> <path d=\"M22 2 11 13\" />',\n download:\n '<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" /> <path d=\"m7 10 5 5 5-5\" /> <path d=\"M12 15V3\" />',\n headphones:\n '<path d=\"M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 18 0v7a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3\" />',\n calendar:\n '<path d=\"M8 2v4\" /> <path d=\"M16 2v4\" /> <rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\" /> <path d=\"M3 10h18\" />',\n 'contact-round':\n '<path d=\"M16 2v2\" /> <path d=\"M17.915 22a6 6 0 0 0-12 0\" /> <path d=\"M8 2v2\" /> <circle cx=\"12\" cy=\"12\" r=\"4\" /> <rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\" />',\n handshake:\n '<path d=\"m11 17 2 2a1 1 0 1 0 3-3\" /> <path d=\"m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 4\" /> <path d=\"m21 3 1 11h-2\" /> <path d=\"M3 3 2 14l6.5 6.5a1 1 0 1 0 3-3\" /> <path d=\"M3 4h8\" />',\n 'layout-dashboard':\n '<rect width=\"7\" height=\"9\" x=\"3\" y=\"3\" rx=\"1\" /> <rect width=\"7\" height=\"5\" x=\"14\" y=\"3\" rx=\"1\" /> <rect width=\"7\" height=\"9\" x=\"14\" y=\"12\" rx=\"1\" /> <rect width=\"7\" height=\"5\" x=\"3\" y=\"16\" rx=\"1\" />',\n store:\n '<path d=\"M15 21v-5a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v5\" /> <path d=\"M17.774 10.31a1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.451 0 1.12 1.12 0 0 0-1.548 0 2.5 2.5 0 0 1-3.452 0 1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.77-3.248l2.889-4.184A2 2 0 0 1 7 2h10a2 2 0 0 1 1.653.873l2.895 4.192a2.5 2.5 0 0 1-3.774 3.244\" /> <path d=\"M4 10.95V19a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8.05\" />',\n 'receipt-text':\n '<path d=\"M13 16H8\" /> <path d=\"M14 8H8\" /> <path d=\"M16 12H8\" /> <path d=\"M4 3a1 1 0 0 1 1-1 1.3 1.3 0 0 1 .7.2l.933.6a1.3 1.3 0 0 0 1.4 0l.934-.6a1.3 1.3 0 0 1 1.4 0l.933.6a1.3 1.3 0 0 0 1.4 0l.933-.6a1.3 1.3 0 0 1 1.4 0l.934.6a1.3 1.3 0 0 0 1.4 0l.933-.6A1.3 1.3 0 0 1 19 2a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1 1.3 1.3 0 0 1-.7-.2l-.933-.6a1.3 1.3 0 0 0-1.4 0l-.934.6a1.3 1.3 0 0 1-1.4 0l-.933-.6a1.3 1.3 0 0 0-1.4 0l-.933.6a1.3 1.3 0 0 1-1.4 0l-.934-.6a1.3 1.3 0 0 0-1.4 0l-.933.6a1.3 1.3 0 0 1-.7.2 1 1 0 0 1-1-1z\" />',\n truck:\n '<path d=\"M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2\" /> <path d=\"M15 18H9\" /> <path d=\"M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14\" /> <circle cx=\"17\" cy=\"18\" r=\"2\" /> <circle cx=\"7\" cy=\"18\" r=\"2\" />',\n 'shopping-bag':\n '<path d=\"M16 10a4 4 0 0 1-8 0\" /> <path d=\"M3.103 6.034h17.794\" /> <path d=\"M3.4 5.467a2 2 0 0 0-.4 1.2V20a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.667a2 2 0 0 0-.4-1.2l-2-2.667A2 2 0 0 0 17 2H7a2 2 0 0 0-1.6.8z\" />',\n factory:\n '<path d=\"M12 16h.01\" /> <path d=\"M16 16h.01\" /> <path d=\"M3 19a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8.5a.5.5 0 0 0-.769-.422l-4.462 2.844A.5.5 0 0 1 15 10.5v-2a.5.5 0 0 0-.769-.422L9.77 10.922A.5.5 0 0 1 9 10.5V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z\" /> <path d=\"M8 16h.01\" />',\n fingerprint:\n '<path d=\"M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4\" /> <path d=\"M14 13.12c0 2.38 0 6.38-1 8.88\" /> <path d=\"M17.29 21.02c.12-.6.43-2.3.5-3.02\" /> <path d=\"M2 12a10 10 0 0 1 18-6\" /> <path d=\"M2 16h.01\" /> <path d=\"M21.8 16c.2-2 .131-5.354 0-6\" /> <path d=\"M5 19.5C5.5 18 6 15 6 12a6 6 0 0 1 .34-2\" /> <path d=\"M8.65 22c.21-.66.45-1.32.57-2\" /> <path d=\"M9 6.8a6 6 0 0 1 9 5.2v2\" />',\n wallet:\n '<path d=\"M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1\" /> <path d=\"M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4\" />',\n banknote:\n '<rect width=\"20\" height=\"12\" x=\"2\" y=\"6\" rx=\"2\" /> <circle cx=\"12\" cy=\"12\" r=\"2\" /> <path d=\"M6 12h.01\" /> <path d=\"M18 12h.01\" />',\n hotel:\n '<path d=\"M10 22v-6.57\" /> <path d=\"M12 11h.01\" /> <path d=\"M12 7h.01\" /> <path d=\"M14 15.43V22\" /> <path d=\"M15 16a5 5 0 0 0-6 0\" /> <path d=\"M16 11h.01\" /> <path d=\"M16 7h.01\" /> <path d=\"M8 11h.01\" /> <path d=\"M8 7h.01\" /> <rect x=\"4\" y=\"2\" width=\"16\" height=\"20\" rx=\"2\" />',\n tag: '<path d=\"M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z\" /> <circle cx=\"7.5\" cy=\"7.5\" r=\".5\" fill=\"currentColor\" />',\n\n // \u2500\u2500 chrome\n search: '<path d=\"m21 21-4.34-4.34\" /> <circle cx=\"11\" cy=\"11\" r=\"8\" />',\n 'chevron-down': '<path d=\"m6 9 6 6 6-6\" />',\n 'chevron-right': '<path d=\"m9 18 6-6-6-6\" />',\n 'chevron-left': '<path d=\"m15 18-6-6 6-6\" />',\n plus: '<path d=\"M5 12h14\" /> <path d=\"M12 5v14\" />',\n 'more-horizontal':\n '<circle cx=\"5\" cy=\"12\" r=\"1\" fill=\"currentColor\" /> <circle cx=\"12\" cy=\"12\" r=\"1\" fill=\"currentColor\" /> <circle cx=\"19\" cy=\"12\" r=\"1\" fill=\"currentColor\" />',\n x: '<path d=\"M18 6 6 18\" /> <path d=\"m6 6 12 12\" />',\n list: '<path d=\"M3 5h.01\" /> <path d=\"M3 12h.01\" /> <path d=\"M3 19h.01\" /> <path d=\"M8 5h13\" /> <path d=\"M8 12h13\" /> <path d=\"M8 19h13\" />',\n 'sliders-horizontal':\n '<path d=\"M10 5H3\" /> <path d=\"M12 19H3\" /> <path d=\"M14 3v4\" /> <path d=\"M16 17v4\" /> <path d=\"M21 12h-9\" /> <path d=\"M21 19h-5\" /> <path d=\"M21 5h-7\" /> <path d=\"M8 10v4\" /> <path d=\"M8 12H3\" />',\n 'grip-vertical':\n '<circle cx=\"9\" cy=\"5\" r=\"1\" fill=\"currentColor\" /> <circle cx=\"9\" cy=\"12\" r=\"1\" fill=\"currentColor\" /> <circle cx=\"9\" cy=\"19\" r=\"1\" fill=\"currentColor\" /> <circle cx=\"15\" cy=\"5\" r=\"1\" fill=\"currentColor\" /> <circle cx=\"15\" cy=\"12\" r=\"1\" fill=\"currentColor\" /> <circle cx=\"15\" cy=\"19\" r=\"1\" fill=\"currentColor\" />',\n pencil: '<path d=\"M12 20h9\" /> <path d=\"M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z\" />',\n 'trash-2':\n '<path d=\"M3 6h18\" /> <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6\" /> <path d=\"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\" /> <path d=\"M10 11v6\" /> <path d=\"M14 11v6\" />',\n 'refresh-cw':\n '<path d=\"M21 12a9 9 0 0 0-15.38-6.36L3 8\" /> <path d=\"M3 3v5h5\" /> <path d=\"M3 12a9 9 0 0 0 15.38 6.36L21 16\" /> <path d=\"M16 16h5v5\" />',\n 'arrow-up': '<path d=\"m18 15-6-6-6 6\" />',\n 'arrow-down': '<path d=\"m6 9 6 6 6-6\" />',\n check: '<path d=\"M20 6 9 17l-5-5\" />',\n 'alert-triangle':\n '<path d=\"m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\" /> <path d=\"M12 9v4\" /> <path d=\"M12 17h.01\" />',\n info: '<circle cx=\"12\" cy=\"12\" r=\"10\" /> <path d=\"M12 16v-4\" /> <path d=\"M12 8h.01\" />',\n\n // \u2500\u2500 the foot of the sidebar\n bell: '<path d=\"M10.268 21a2 2 0 0 0 3.464 0\" /> <path d=\"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326\" />',\n mail: '<path d=\"m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7\" /> <rect x=\"2\" y=\"4\" width=\"20\" height=\"16\" rx=\"2\" />',\n moon: '<path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\" />',\n sun: '<circle cx=\"12\" cy=\"12\" r=\"4\" /> <path d=\"M12 2v2\" /> <path d=\"M12 20v2\" /> <path d=\"m4.93 4.93 1.42 1.42\" /> <path d=\"m17.66 17.66 1.41 1.41\" /> <path d=\"M2 12h2\" /> <path d=\"M20 12h2\" /> <path d=\"m6.34 17.66-1.41 1.41\" /> <path d=\"m19.07 4.93-1.41 1.41\" />',\n 'log-out':\n '<path d=\"m16 17 5-5-5-5\" /> <path d=\"M21 12H9\" /> <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" />',\n 'building-2':\n '<path d=\"M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18\" /> <path d=\"M6 12H4a2 2 0 0 0-2 2v8h20v-8a2 2 0 0 0-2-2h-2\" /> <path d=\"M10 6h4\" /> <path d=\"M10 10h4\" /> <path d=\"M10 14h4\" /> <path d=\"M10 18h4\" />',\n}\n\nconst OPEN =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" ' +\n 'stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" data-ui=\"icon\" aria-hidden=\"true\">'\n\n/** The data-ui names this file emits. See ui/hooks.ts. */\nexport const HOOKS = ['icon'] as const\n\nexport const hasIcon = (name: string): boolean => name in PATHS\n\n/**\n * One glyph, as markup the renderer writes verbatim.\n *\n * A name nobody vendored returns nothing rather than a broken glyph \u2014 a module\n * naming an icon this build does not carry should lose its icon, not its row.\n */\nexport const icon = (name: string | null | undefined): Markup =>\n trustedMarkup(name && PATHS[name] ? OPEN + PATHS[name] + '</svg>' : '')\n", "// The record-modal contract, browser half.\n//\n// A module describes a record \u2014 how to read it, its tabs, dialogs and commands \u2014\n// and this runtime owns everything that must behave the same in every module:\n// opening from a link or deep link, history, focus, Escape, the inert background,\n// loading and failure states, submitting through `/_ket/fn`, showing a refusal on\n// the field that caused it without losing what was typed, the unsaved-input guard,\n// and telling the collection behind the modal that its records changed.\n//\n// Views are render-pure: they read the context and return design-system markup.\n// They never fetch, never touch history and never query the document.\n\nimport { signal } from '@ketvietlab/ketjs-view'\nimport type { IslandController, JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Button, LoadingState, ModalSheet, Notice, TabbedView } from '@ketvietlab/design-system'\nimport {\n RECORD_NEW_ID,\n RECORD_PARAM,\n RECORD_TAB_PARAM,\n readRecordModalTarget,\n recordModalClosedHref,\n recordModalCreateHref,\n recordModalHost,\n recordModalHref,\n} from '../record-modal.tsx'\n\n// \u2500\u2500 Function calls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type RecordIssue = {\n field: string | null\n code: string\n message: string | null\n params: Record<string, unknown>\n}\n\nexport type RecordCallResult<Value = unknown> =\n | { ok: true; value: Value }\n | { ok: false; issues: RecordIssue[]; message: string | null; status: number }\n\nconst issuesOf = (source: unknown): RecordIssue[] => {\n const holder = (source ?? {}) as { errors?: unknown; issues?: unknown }\n const list = Array.isArray(holder.errors)\n ? holder.errors\n : Array.isArray(holder.issues)\n ? holder.issues\n : []\n return list.map((raw) => {\n const item = (raw ?? {}) as Record<string, unknown>\n const path = Array.isArray(item.path) ? item.path.join('.') : null\n return {\n field: typeof item.field === 'string' ? item.field : path,\n code: String(item.code ?? 'invalid'),\n message: typeof item.message === 'string' ? item.message : null,\n params: (item.params && typeof item.params === 'object' ? item.params : {}) as Record<string, unknown>,\n }\n })\n}\n\n/**\n * Call a server function as the signed-in viewer.\n *\n * A domain refusal (`{ ok: false, errors }` inside the value) and a transport or\n * permission refusal (`ok: false` on the envelope) come back in one shape, so a\n * view never has to know which layer said no.\n */\nexport const callRecordRoute = async <Value = unknown>(\n route: string,\n input: Record<string, unknown>,\n options: { signal?: AbortSignal; idempotencyKey?: string } = {},\n): Promise<RecordCallResult<Value>> => {\n const target = new URL(route, 'https://record.invalid')\n if (target.origin !== 'https://record.invalid' || !route.startsWith('/') || route.startsWith('//'))\n throw new Error('Record commands require a same-origin route')\n const response = await fetch(target.pathname + target.search, {\n method: 'POST',\n credentials: 'same-origin',\n headers: {\n 'content-type': 'application/json',\n ...(options.idempotencyKey ? { 'idempotency-key': options.idempotencyKey } : {}),\n },\n body: JSON.stringify(input),\n signal: options.signal,\n })\n const payload = (await response.json().catch(() => ({}))) as {\n ok?: boolean\n value?: unknown\n error?: { message?: string; issues?: unknown }\n message?: string\n }\n if (!response.ok || payload.ok === false)\n return {\n ok: false,\n issues: issuesOf(payload.error ?? payload),\n // A server failure is not the user's to read: its text is for the log, and\n // the modal falls back to its own words.\n message: response.status >= 500 ? null : (payload.error?.message ?? payload.message ?? null),\n status: response.status,\n }\n const value = payload.value as { ok?: boolean } | undefined\n if (value && typeof value === 'object' && value.ok === false)\n return { ok: false, issues: issuesOf(value), message: null, status: response.status }\n return { ok: true, value: payload.value as Value }\n}\n\n/** Call a declared function through the same response and refusal contract. */\nexport const callRecordFunction = <Value = unknown>(\n name: string,\n input: Record<string, unknown>,\n options: { signal?: AbortSignal; idempotencyKey?: string } = {},\n): Promise<RecordCallResult<Value>> =>\n callRecordRoute<Value>(`/_ket/fn/${encodeURIComponent(name)}`, input, options)\n\n// \u2500\u2500 Definition \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** What every record context read must return next to its data. */\nexport type RecordContextEnvelope<Data> = {\n data: Data\n /** Translated strings this record's views use, including `recordModal.*` runtime labels. */\n messages: Record<string, string>\n}\n\nexport type RecordModalContext<Data> = {\n kind: string\n id: string\n /** The modal was opened by a create action (`record=<kind>:new`); there is no record yet. */\n creating: boolean\n tab: string\n data: Data\n /** Translate a key the context shipped, interpolating `{name}` params. */\n t: (key: string, params?: Record<string, unknown>) => string\n /** The refusal for one field of the last submit, translated. */\n fieldError: (name: string) => string | null\n /** What was typed into a field before a refused submit, or the fallback. */\n draft: (name: string, fallback?: string) => string\n /** Whether a checkbox/radio value was selected before the view re-rendered. */\n draftChecked: (name: string, value?: string, fallback?: boolean) => boolean\n /**\n * What a preview command answered, for the view to render. Null until that\n * command has run in this layer, and again as soon as anything moves.\n */\n outcome: <T>(command: string) => T | null\n /**\n * A submit has been in flight long enough to be worth saying so. A command the\n * server answers at once never sets it, so a button bound to it does not flash\n * its spinner; the runtime stops a second submit either way.\n */\n busy: boolean\n dialog: { name: string; params: Record<string, string> } | null\n href: (tab: string) => string\n /** View-local state, set by `data-record-state` controls (click, or change for inputs). */\n state: (key: string, fallback?: string) => string\n}\n\n/** Attachments a command uploaded before its function ran, by form field. */\nexport type RecordUploads = Record<string, { id: string; name: string | null }>\n\n/** Read-only context routes compose permission-checked function calls on the server. */\nexport const readRecordContextRoute = async <Data,>(\n href: string,\n signal?: AbortSignal,\n): Promise<RecordCallResult<Data | null>> => {\n const url = new URL(href, location.href)\n if (url.origin !== location.origin) throw new Error('Record context must stay same-origin')\n const response = await fetch(url.href, {\n credentials: 'same-origin',\n signal,\n headers: { accept: 'application/json' },\n })\n if (!response.ok) return { ok: false, issues: [], message: null, status: response.status }\n return { ok: true, value: (await response.json()) as Data | null }\n}\n\n/** One extra call of a multi-step command \u2014 see `RecordModalCommand.also`. */\nexport type RecordModalCommandStep<Data> = {\n fn: string\n input: (\n form: FormData,\n context: RecordModalContext<Data>,\n uploads: RecordUploads,\n ) => Record<string, unknown>\n /** Skip this step \u2014 a module not installed, a permission the viewer lacks. */\n when?: (context: RecordModalContext<Data>) => boolean\n}\n\nexport type RecordModalCommand<Data> = ({ fn: string; route?: never } | { fn?: never; route: string }) & {\n /** Same-origin destination after a successful command; evaluated by the runtime. */\n navigate?: (value: unknown, context: RecordModalContext<Data>) => string\n /** Map server paths to stable native field names using the submitted snapshot. */\n issueField?: (field: string, form: FormData, context: RecordModalContext<Data>) => string\n /** Map the submitted form to the function's input. */\n input: (\n form: FormData,\n context: RecordModalContext<Data>,\n uploads: RecordUploads,\n ) => Record<string, unknown>\n /**\n * Further calls made right after `fn`/`input` succeeds, in order, as part of\n * the same user-facing action \u2014 one busy state, one success notice, one set\n * of field refusals \u2014 stopping at the first one that fails. For an edit that\n * spans functions in different modules that cannot call one another (a\n * single `product.saveTemplate` cannot also configure stock tracking or\n * tax), this is what lets one \"Save\" button still read as one save.\n */\n also?: readonly RecordModalCommandStep<Data>[]\n /**\n * File fields stored through `/files` before the function runs, each mapped to\n * the attachment's metadata (`resModel`, `resId`, `resField`, `public`). The\n * stored attachment ids reach `input` as its third argument.\n */\n upload?: Record<string, (form: FormData, context: RecordModalContext<Data>) => Record<string, string>>\n /** A destructive command asks first; declining leaves the record untouched. */\n confirm?: (context: RecordModalContext<Data>) => string | null\n /**\n * What happens after success. Defaults to `close`. `reload` reads the record\n * again behind a loading state and closes a dialog; `refresh` reads it again in\n * place, keeping the tab, the open dialog and any islands mounted in them.\n * `open` is for a create command: the modal switches to the record it created\n * (the id from `created`, else the function value's `id`) on the tab named by\n * `openTab`, replacing the `:new` history entry.\n */\n after?: 'close' | 'reload' | 'refresh' | 'stay' | 'open' | { tab: string } | { dialog: string | null }\n /**\n * The command asks what would happen instead of making it happen: its function\n * writes nothing, so the record is not re-read, the collection is not told and\n * what was typed stays on screen. The answer reaches the view through\n * `context.outcome`, beside the very form that asked for it, so the person can\n * read the consequence and then submit the command that commits it. `after` is\n * not consulted \u2014 a preview always stays in its layer.\n */\n preview?: boolean\n /** The id of the record a create command made, read from the function's value. */\n created?: (value: unknown) => string | null\n /** Tab to open on the created record when `after` is `open`. */\n openTab?: string\n}\n\nexport type RecordModalTab<Data> = {\n id: string\n label: (context: RecordModalContext<Data>) => string\n visible?: (context: RecordModalContext<Data>) => boolean\n view: (context: RecordModalContext<Data>) => JSXChild\n}\n\nexport type RecordModalDialog<Data> = {\n title: (context: RecordModalContext<Data>) => string\n size?: 'default' | 'large'\n view: (context: RecordModalContext<Data>) => JSXChild\n /** Fixed actions for this dialog layer, outside its scrolling body. */\n actions?: (context: RecordModalContext<Data>) => JSXChild\n}\n\nexport type RecordModalDefinition<Data> = {\n kind: string\n size?: 'default' | 'large'\n /**\n * Compatibility override for workflows with an explicitly sized tabbed surface.\n * By default the runtime holds the tallest rendered tab as a min-height,\n * capped by the available viewport. Single-tab and nested dialogs size to content.\n */\n fixedHeight?: string\n /**\n * A permission-checked read returning `{ data, messages }`. For a create action\n * the default input is `{}` (no id): the read returns the empty record's defaults,\n * the choices its form needs and the viewer's permissions.\n */\n context:\n | { fn: string; input?: (id: string, creating: boolean) => Record<string, unknown> }\n | { route: (id: string, creating: boolean) => string; query?: readonly string[] }\n title: (context: RecordModalContext<Data>) => string\n description?: (context: RecordModalContext<Data>) => string | null\n /**\n * What state this record is in, beside the modal's title: a badge, not a\n * strip. It stays put while the body scrolls, and a reader looking at the\n * title learns the state without reading down into the form.\n */\n status?: (context: RecordModalContext<Data>) => JSXChild\n /** A strip above the body \u2014 a customer, a summary \u2014 for what a badge cannot hold. */\n header?: (context: RecordModalContext<Data>) => JSXChild\n /**\n * A footer strip below the body, outside the scrolling area and the same on\n * every tab \u2014 the natural home for a record's primary actions (save, close,\n * more) so they stay reachable without hunting through the tab that happens\n * to hold the save button. Return `undefined` (not `''`) to render no footer\n * at all, e.g. while creating, when the create form owns its own submit.\n */\n actions?: (context: RecordModalContext<Data>) => JSXChild | undefined\n tabs?: readonly RecordModalTab<Data>[]\n /**\n * Tabs other modules add to this record, read from its context: they follow the\n * declared tabs, in composition order, and render through the same TabbedView.\n * A module that owns the record cannot know them when it is built.\n */\n extensionTabs?: (context: RecordModalContext<Data>) => readonly RecordModalTab<Data>[]\n /** The body of a record without tabs. */\n body?: (context: RecordModalContext<Data>) => JSXChild\n dialogs?: Record<string, RecordModalDialog<Data>>\n commands?: Record<string, RecordModalCommand<Data>>\n /**\n * Runtime labels (`recordModal.*`) in the page's language, available before the\n * record's context has loaded. Without them the loading state has only the\n * built-in English defaults, because `messages` arrive with the context.\n */\n labels?: Record<string, string> | (() => Record<string, string>)\n /**\n * Keep read contexts in memory so reopening a record shows at once and is read\n * again quietly (stale-while-revalidate). On by default; set `false` for a kind\n * whose context must never be shown before a fresh read.\n */\n cache?: boolean\n}\n\n/** How many record contexts one island keeps for instant reopening. */\nexport const RECORD_MODAL_CACHE_SIZE = 30\n\n/**\n * English defaults for every label the runtime itself shows. A module passes its\n * own language through `labels`; these only guarantee a reader never sees a key.\n */\nexport const RECORD_MODAL_LABELS: Readonly<Record<string, string>> = Object.freeze({\n 'recordModal.close': 'Close',\n 'recordModal.loading': 'Loading\u2026',\n 'recordModal.loadFailed': 'The record could not be read.',\n 'recordModal.notFound': 'The record is gone or you cannot see it.',\n 'recordModal.retry': 'Retry',\n 'recordModal.errorTitle': 'Not saved',\n 'recordModal.saveFailed': 'That did not work. Try again.',\n 'recordModal.savedTitle': 'Saved',\n 'recordModal.saved': 'The change is in.',\n 'recordModal.unsaved': 'Discard what you typed?',\n 'recordModal.uploadFailed': 'The file could not be uploaded. Try again.',\n})\n\n/**\n * The text for a key: the loaded context's messages, then the messages of the\n * last record this modal opened, then the module's labels, then the defaults.\n * Unknown refusal codes use a friendly fallback; other unknown keys fall through to themselves.\n */\nexport const resolveRecordModalLabel = (\n key: string,\n sources: {\n messages?: Record<string, string> | null\n previous?: Record<string, string> | null\n labels?: Record<string, string> | null\n },\n): string =>\n sources.messages?.[key] ??\n sources.previous?.[key] ??\n sources.labels?.[key] ??\n RECORD_MODAL_LABELS[key] ??\n (/^E_[A-Z_]+$/u.test(key) ? resolveRecordModalLabel('recordModal.saveFailed', sources) : key)\n\n/**\n * The text for a refusal code. A server may refuse with any code; one no source\n * translates reads as the save failure rather than as the code itself.\n */\nexport const resolveRecordModalIssue = (\n code: string,\n sources: Parameters<typeof resolveRecordModalLabel>[1],\n): string => {\n const text = resolveRecordModalLabel(code, sources)\n return text === code ? resolveRecordModalLabel('recordModal.saveFailed', sources) : text\n}\n\n/** Field a form (or its submitter) uses to name the command it runs. */\nexport const RECORD_COMMAND_FIELD = '__command'\n/** Field a link or button uses to open a dialog of the same record. */\nexport const RECORD_DIALOG_ATTRIBUTE = 'data-record-dialog'\n\n// \u2500\u2500 Runtime helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst interpolate = (text: string, params: Record<string, unknown> = {}): string =>\n text.replace(/\\{([a-zA-Z0-9_]+)\\}/gu, (whole, name: string) =>\n name in params ? String(params[name] ?? '') : whole,\n )\n\nconst focusable = [\n 'a[href]',\n 'button:not(:disabled)',\n 'input:not(:disabled):not([type=\"hidden\"])',\n 'select:not(:disabled)',\n 'textarea:not(:disabled)',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(', ')\n\nconst focusablesIn = (element: HTMLElement): HTMLElement[] =>\n [...element.querySelectorAll<HTMLElement>(focusable)].filter(\n (item) =>\n !item.hidden &&\n item.getAttribute('aria-hidden') !== 'true' &&\n !item.matches('[data-ui=\"modal-backdrop\"]'),\n )\n\n/** Whether anything in a layer was typed into since it rendered. Same rule as route modals. */\nexport const recordLayerHasDraft = (layer: HTMLElement): boolean => {\n if (layer.querySelector('[data-record-dirty=\"true\"]')) return true\n for (const field of layer.querySelectorAll<HTMLInputElement>('input:not([type=\"hidden\"])')) {\n if (field.disabled) continue\n if (field.type === 'checkbox' || field.type === 'radio') {\n if (field.checked !== field.defaultChecked) return true\n } else if (field.value !== field.defaultValue) return true\n }\n for (const area of layer.querySelectorAll<HTMLTextAreaElement>('textarea'))\n if (!area.disabled && area.value !== area.defaultValue) return true\n for (const select of layer.querySelectorAll<HTMLSelectElement>('select')) {\n if (select.disabled) continue\n for (const option of select.options) if (option.selected !== option.defaultSelected) return true\n }\n return false\n}\n\n/** Make everything outside `keep` inert, walking up to the body; returns the undo. */\nconst inertOutside = (keep: HTMLElement): (() => void) => {\n const changed: Array<{ element: HTMLElement; inert: boolean }> = []\n let node: HTMLElement | null = keep\n while (node && node !== document.body) {\n const parent: HTMLElement | null = node.parentElement\n if (!parent) break\n for (const sibling of parent.children)\n if (sibling instanceof HTMLElement && sibling !== node && !sibling.matches('script, style, template')) {\n changed.push({ element: sibling, inert: sibling.inert })\n sibling.inert = true\n }\n node = parent\n }\n return () => {\n for (const { element, inert } of changed) element.inert = inert\n }\n}\n\nconst uuid = (): string =>\n typeof crypto !== 'undefined' && 'randomUUID' in crypto\n ? crypto.randomUUID()\n : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`\n\ntype Status = 'idle' | 'loading' | 'ready' | 'error'\n\ntype DraftState = {\n values: Record<string, string>\n checks: Record<string, boolean>\n initialValues: Record<string, string>\n initialChecks: Record<string, boolean>\n}\n\n/** Compare against the first render, including fields on tabs no longer mounted. */\nexport const recordDraftHasChanges = (draft: DraftState): boolean =>\n Object.entries(draft.values).some(([key, value]) => value !== draft.initialValues[key]) ||\n Object.entries(draft.checks).some(([key, value]) => value !== draft.initialChecks[key])\n\ntype DraftScope = 'record' | 'dialog'\n\nconst emptyDraftState = (): DraftState => ({ values: {}, checks: {}, initialValues: {}, initialChecks: {} })\nconst draftCheckKey = (name: string, value: string): string => `${name}\\u0000${value}`\n\n/** Name prefix of a checkbox that ticks every checkbox of its form sharing the rest of its name. */\nexport const CHECK_ALL = '__all:'\n\nconst after_ = <Data,>(command: RecordModalCommand<Data>) => command.after ?? 'close'\n\n/**\n * How long a command may run before its button says so. Below this the answer\n * arrives while the reader is still lifting their finger, and a spinner shown\n * and withdrawn inside that window is noise; above it, silence would read as a\n * click that did nothing.\n */\nexport const BUSY_AFTER_MS = 400\n\n/**\n * A flag that turns on late and off at once. Work that finishes inside the\n * window never raises it, so a progress indicator bound to it appears only when\n * there is progress to report, and never flashes on its way back out.\n */\nexport const delayedFlag = (\n show: (value: boolean) => void,\n after: number = BUSY_AFTER_MS,\n): { set: (running: boolean) => void; stop: () => void } => {\n let timer: ReturnType<typeof setTimeout> | undefined\n const stop = (): void => {\n clearTimeout(timer)\n timer = undefined\n }\n return {\n set: (running) => {\n stop()\n if (!running) {\n show(false)\n return\n }\n timer = setTimeout(() => show(true), after)\n },\n stop,\n }\n}\n\n/** Controls inside a row do their own thing; the row's destination is for the rest of it. */\nconst rowControl = 'a, button, input, select, textarea, label, summary, details, [data-ui=\"select-cell\"]'\n\n/**\n * Where a click is asking to go: the link it landed on, or the row it landed\n * in. A table row carries its destination on the row itself (`rowLink: false`),\n * so the whole row is one target rather than a link around the first cell. The\n * shell would navigate such a row through the navigation layer, which fetches\n * the collection again and leaves the modal unopened, so this reads it first.\n */\nexport const openerHref = (element: Element | null): string | null => {\n const anchor = element?.closest<HTMLAnchorElement>('a[href]')\n if (anchor) return anchor.target && anchor.target !== '_self' ? null : anchor.href\n if (element?.closest(rowControl)) return null\n return element?.closest<HTMLElement>('[data-row-href]')?.getAttribute('data-row-href') ?? null\n}\n\n/**\n * Create the island controller for one record kind.\n *\n * The returned factory is what a module exports as its island client:\n * `export const followupModal = createRecordModal(definition)`.\n */\nexport const createRecordModal =\n <Data,>(definition: RecordModalDefinition<Data>) =>\n (): IslandController => {\n const open = signal<{ id: string; tab: string } | null>(null)\n const status = signal<Status>('idle')\n const envelope = signal<RecordContextEnvelope<Data> | null>(null)\n const failure = signal<string | null>(null)\n const issues = signal<RecordIssue[]>([])\n const recordDrafts = signal<DraftState>(emptyDraftState())\n const dialogDrafts = signal<DraftState>(emptyDraftState())\n // `running` is the guard \u2014 one command at a time \u2014 and `busy` is what the\n // views show. They are not the same thing: a save the server answers in\n // twenty milliseconds would otherwise flash the button through its spinner\n // and back, which reads as a glitch rather than as progress. The spinner\n // waits; the guard does not.\n const running = signal(false)\n const busy = signal(false)\n const showBusy = delayedFlag((value) => busy.set(value))\n // A command that succeeded while the modal stayed open. Saving is usually\n // answered before the button could say anything, and a record that looks\n // the same afterwards leaves the reader unsure anything happened, so the\n // form says so where it would have said the opposite.\n const saved = signal(false)\n const setRunning = (value: boolean): void => {\n running.set(value)\n showBusy.set(value)\n if (value) saved.set(false)\n }\n const dialog = signal<{ name: string; params: Record<string, string> } | null>(null)\n const version = signal(0)\n const viewState = signal<Record<string, string>>({})\n // What the last preview command answered. One at a time: a second preview\n // replaces the first, and anything that moves the layer clears it, so a\n // consequence is never read beside a selection it was not computed from.\n const outcome = signal<{ command: string; value: unknown } | null>(null)\n\n // Contexts this island has read, newest last: reopening a record (or the create\n // form) renders immediately and revalidates in place. Bounded and page-scoped;\n // a successful command or `ket:records-changed` for this kind drops the entries.\n const cache = new Map<string, RecordContextEnvelope<Data>>()\n const remember = (id: string, value: RecordContextEnvelope<Data>): void => {\n cache.delete(id)\n cache.set(id, value)\n while (cache.size > RECORD_MODAL_CACHE_SIZE) cache.delete(cache.keys().next().value as string)\n }\n\n let root: HTMLElement | null = null\n let request: AbortController | null = null\n let pushed = false\n let returnFocus: HTMLElement | null = null\n let dialogReturnFocus: HTMLElement | null = null\n let releaseInert: (() => void) | null = null\n\n // Messages of the last record this modal loaded: opening the next record clears\n // the envelope, and its loading state still needs words rather than keys.\n let previousMessages: Record<string, string> | null = null\n const labels = (): Record<string, string> =>\n typeof definition.labels === 'function' ? definition.labels() : (definition.labels ?? {})\n const sources = () => ({ messages: envelope()?.messages, previous: previousMessages, labels: labels() })\n const t = (key: string, params?: Record<string, unknown>): string =>\n interpolate(resolveRecordModalLabel(key, sources()), params)\n const issueText = (issue: RecordIssue): string =>\n issue.message ?? interpolate(resolveRecordModalIssue(issue.code, sources()), issue.params)\n const visibleTabs = (context: RecordModalContext<Data>) =>\n [...(definition.tabs ?? []), ...(definition.extensionTabs?.(context) ?? [])].filter(\n (tab) => tab.visible?.(context) ?? true,\n )\n\n const contextFor = (\n current: { id: string; tab: string },\n data: Data,\n scope: DraftScope = 'record',\n ): RecordModalContext<Data> => {\n const draftState = (): DraftState => (scope === 'dialog' ? dialogDrafts() : recordDrafts())\n const base: RecordModalContext<Data> = {\n kind: definition.kind,\n id: current.id,\n creating: current.id === RECORD_NEW_ID,\n tab: current.tab,\n data,\n t,\n fieldError: (name) => {\n const hit = issues().find((issue) => issue.field === name)\n return hit ? issueText(hit) : null\n },\n draft: (name, fallback = '') => {\n const draft = draftState()\n draft.initialValues[name] ??= fallback\n return draft.values[name] ?? fallback\n },\n draftChecked: (name, value = '1', fallback = false) => {\n const draft = draftState()\n const key = draftCheckKey(name, value)\n draft.initialChecks[key] ??= fallback\n return draft.checks[key] ?? fallback\n },\n outcome: <T,>(command: string) => {\n const held = outcome()\n return held?.command === command ? (held.value as T) : null\n },\n busy: busy(),\n dialog: dialog(),\n href: (tab) => recordModalHref(location.href, { kind: definition.kind, id: current.id, tab }),\n state: (key, fallback = '') => viewState()[key] ?? fallback,\n }\n const tabs = visibleTabs(base)\n if (tabs.length && !tabs.some((tab) => tab.id === current.tab)) base.tab = tabs[0]!.id\n return base\n }\n\n const load = async (id: string, quiet = false): Promise<void> => {\n request?.abort()\n const controller = new AbortController()\n request = controller\n // A record opened before shows at once from the in-memory cache and is read\n // again quietly behind it; the reader never waits on a frame they have seen.\n const cached = definition.cache === false ? undefined : cache.get(id)\n if (cached) {\n previousMessages = cached.messages ?? previousMessages\n envelope.set(cached)\n status.set('ready')\n quiet = true\n }\n if (!quiet) status.set('loading')\n failure.set(null)\n try {\n const creating = id === RECORD_NEW_ID\n const result =\n 'route' in definition.context\n ? await readRecordContextRoute<RecordContextEnvelope<Data>>(\n definition.context.route(id, creating),\n controller.signal,\n )\n : await callRecordFunction<RecordContextEnvelope<Data> | null>(\n definition.context.fn,\n definition.context.input ? definition.context.input(id, creating) : creating ? {} : { id },\n { signal: controller.signal },\n )\n if (controller.signal.aborted) return\n if (!result.ok || !result.value) {\n failure.set(result.ok ? 'recordModal.notFound' : (result.message ?? 'recordModal.loadFailed'))\n status.set('error')\n return\n }\n previousMessages = result.value.messages ?? previousMessages\n if (definition.cache !== false) remember(id, result.value)\n envelope.set(result.value)\n status.set('ready')\n } catch (caught) {\n if (controller.signal.aborted || (caught as Error)?.name === 'AbortError') return\n failure.set('recordModal.loadFailed')\n status.set('error')\n }\n }\n\n const layers = (): HTMLElement[] =>\n root ? [...root.querySelectorAll<HTMLElement>('[data-ui=\"modal-layer\"][data-client-modal=\"true\"]')] : []\n const recordLayer = (): HTMLElement | null => layers()[0] ?? null\n const topLayer = (): HTMLElement | null => layers().at(-1) ?? null\n\n // Hold only the height actually seen for this record, never the viewport height.\n let tallestRecordHeight = 0\n const fitRecordHeight = (): void => {\n if (definition.fixedHeight || status() !== 'ready') return\n const sheet = recordLayer()?.querySelector<HTMLElement>('[data-ui=\"modal-sheet\"][data-height=\"fixed\"]')\n if (!sheet) return\n // Match ModalSheet's compact contract: mobile dialogs occupy the screen.\n // Do not retain that viewport height as a desktop content measurement.\n if (window.matchMedia('(max-width: 47.9375rem)').matches) {\n sheet.style.setProperty('height', '100dvh', 'important')\n sheet.style.removeProperty('min-height')\n return\n }\n const tabs = sheet.querySelector<HTMLElement>('[data-ui=\"tabbed-view\"]')\n if (!tabs) return\n const panel = tabs.querySelector<HTMLElement>('[data-ui=\"tab-panel\"]')\n const scrollTop = panel?.scrollTop ?? 0\n sheet.style.setProperty('height', 'auto', 'important')\n sheet.style.setProperty('min-height', '0')\n tabs.style.setProperty('height', 'auto')\n tallestRecordHeight = Math.max(tallestRecordHeight, Math.ceil(sheet.getBoundingClientRect().height))\n tabs.style.removeProperty('height')\n const cap = getComputedStyle(sheet).maxHeight\n sheet.style.setProperty(\n 'min-height',\n cap === 'none' ? `${tallestRecordHeight}px` : `min(${tallestRecordHeight}px, ${cap})`,\n )\n if (panel) panel.scrollTop = scrollTop\n }\n\n const keepDrafts = (current: HTMLElement | null, scope: DraftScope): void => {\n if (!current) return\n const previous = scope === 'dialog' ? dialogDrafts() : recordDrafts()\n const kept: DraftState = {\n values: { ...previous.values },\n checks: { ...previous.checks },\n initialValues: { ...previous.initialValues },\n initialChecks: { ...previous.initialChecks },\n }\n for (const control of current.querySelectorAll<\n HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement\n >('input[name], select[name], textarea[name]')) {\n if (control.disabled || control.name === RECORD_COMMAND_FIELD) continue\n if (control instanceof HTMLInputElement) {\n if (control.type === 'file' || control.type === 'hidden') continue\n if (control.type === 'checkbox' || control.type === 'radio') {\n const key = draftCheckKey(control.name, control.value)\n kept.initialChecks[key] ??= control.defaultChecked\n kept.checks[key] = control.checked\n // Keep checkbox/radio values available to draft(), but compare these\n // groups by checked state: several controls may share one name.\n if (control.type === 'checkbox') kept.values[control.name] = control.checked ? control.value : ''\n else if (control.checked) kept.values[control.name] = control.value\n kept.initialValues[control.name] = kept.values[control.name] ?? ''\n continue\n }\n }\n kept.initialValues[control.name] ??=\n control instanceof HTMLSelectElement\n ? ([...control.options].find((option) => option.defaultSelected)?.value ??\n control.options[0]?.value ??\n '')\n : control.defaultValue\n kept.values[control.name] = control.value\n }\n if (scope === 'dialog') dialogDrafts.set(kept)\n else recordDrafts.set(kept)\n }\n\n const keepAllDrafts = (): void => {\n const currentLayers = layers()\n keepDrafts(currentLayers[0] ?? null, 'record')\n if (currentLayers.length > 1) keepDrafts(currentLayers.at(-1) ?? null, 'dialog')\n }\n\n const mayDiscard = (current: HTMLElement | null): boolean => {\n if (!current) return true\n const scope = dialog() && current === topLayer() ? 'dialog' : 'record'\n keepDrafts(current, scope)\n const draft = scope === 'dialog' ? dialogDrafts() : recordDrafts()\n const hasFile = [...current.querySelectorAll<HTMLInputElement>('input[type=\"file\"]')].some(\n (field) => !field.disabled && Boolean(field.files?.length),\n )\n if (!recordDraftHasChanges(draft) && !hasFile) return true\n return globalThis.confirm(t('recordModal.unsaved'))\n }\n\n const afterRender = (preferred?: () => HTMLElement | null): void => {\n requestAnimationFrame(() => {\n const currentLayers = layers()\n const record = currentLayers[0]\n const top = currentLayers.at(-1)\n if (!top) return\n if (record) record.inert = currentLayers.length > 1\n const target = preferred?.()\n if (target?.isConnected && top.contains(target)) {\n target.focus()\n return\n }\n if (top.contains(document.activeElement)) return\n const sheet = top.querySelector<HTMLElement>('[data-ui=\"modal-sheet\"]')\n const first = focusablesIn(top).find((item) => !item.matches('[data-ui=\"modal-close\"]'))\n ;(first ?? sheet)?.focus()\n })\n }\n\n const show = (\n id: string,\n tab: string | null,\n how: 'push' | 'replace' | 'none',\n preferredFocus?: () => HTMLElement | null,\n ): void => {\n const current = open()\n const sameRecord = current?.id === id\n const nextTab = tab ?? (sameRecord ? current.tab : '')\n if (!sameRecord) {\n tallestRecordHeight = 0\n returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null\n issues.set([])\n saved.set(false)\n recordDrafts.set(emptyDraftState())\n dialogDrafts.set(emptyDraftState())\n viewState.set({})\n outcome.set(null)\n dialog.set(null)\n dialogReturnFocus = null\n envelope.set(null)\n void load(id)\n }\n open.set({ id, tab: nextTab })\n const href = recordModalHref(location.href, { kind: definition.kind, id, tab: nextTab || null })\n if (how === 'push') {\n // The shell's own navigation snapshots scroll onto the entry it leaves\n // before pushing (`saveScroll` in packages/ketjs/src/server/http.ts) so\n // going back restores it; this push must do the same, or closing the\n // modal later restores no scroll and the page it sits over jumps to top.\n history.replaceState(\n { ...(history.state ?? {}), __ketScroll: [window.scrollX, window.scrollY] },\n '',\n location.href,\n )\n history.pushState({ ...(history.state ?? {}), __ketRecordModal: definition.kind }, '', href)\n pushed = true\n } else if (how === 'replace') history.replaceState(history.state ?? {}, '', href)\n if (root && !releaseInert) releaseInert = inertOutside(root)\n afterRender(preferredFocus)\n }\n\n const hide = (how: 'history' | 'replace' | 'none'): void => {\n request?.abort()\n open.set(null)\n tallestRecordHeight = 0\n dialog.set(null)\n issues.set([])\n saved.set(false)\n recordDrafts.set(emptyDraftState())\n dialogDrafts.set(emptyDraftState())\n viewState.set({})\n outcome.set(null)\n status.set('idle')\n envelope.set(null)\n releaseInert?.()\n releaseInert = null\n if (how === 'history' && pushed) {\n pushed = false\n history.back()\n } else if (how !== 'none') {\n pushed = false\n history.replaceState(history.state ?? {}, '', recordModalClosedHref(location.href))\n }\n const target = returnFocus\n returnFocus = null\n dialogReturnFocus = null\n if (target?.isConnected) requestAnimationFrame(() => target.focus())\n }\n\n const close = (): void => {\n if (dialog()) {\n if (!mayDiscard(topLayer())) return\n const target = dialogReturnFocus\n dialogReturnFocus = null\n dialog.set(null)\n dialogDrafts.set(emptyDraftState())\n issues.set([])\n outcome.set(null)\n afterRender(() => (target?.isConnected ? target : null))\n return\n }\n if (!mayDiscard(recordLayer())) return\n hide('history')\n }\n\n const run = async (name: string, form: HTMLFormElement, submitter: HTMLElement | null): Promise<void> => {\n const command = definition.commands?.[name]\n const current = open()\n const data = envelope()?.data\n if (!command || !current || data === undefined || running()) return\n const formData = new FormData(form, submitter instanceof HTMLButtonElement ? submitter : null)\n const currentLayer = form.closest<HTMLElement>('[data-ui=\"modal-layer\"][data-client-modal=\"true\"]')\n const scope: DraftScope = dialog() && currentLayer === topLayer() ? 'dialog' : 'record'\n keepDrafts(currentLayer, scope)\n const context = contextFor(current, data, scope)\n if (command.confirm) {\n const message = command.confirm(context)\n if (message && !globalThis.confirm(message)) return\n }\n setRunning(true)\n issues.set([])\n try {\n const uploads: RecordUploads = {}\n for (const [field, target] of Object.entries(command.upload ?? {})) {\n const file = formData.get(field)\n if (!(file instanceof File) || file.size === 0) continue\n const body = new FormData()\n for (const [key, value] of Object.entries(target(formData, context))) body.append(key, value)\n body.append('file', file, file.name)\n const response = await fetch('/files', { method: 'POST', credentials: 'same-origin', body })\n const stored = (await response.json().catch(() => null)) as {\n id?: unknown\n name?: unknown\n message?: unknown\n } | null\n if (!response.ok || !stored?.id) {\n issues.set([\n {\n field: null,\n code: 'recordModal.uploadFailed',\n message: typeof stored?.message === 'string' ? stored.message : null,\n params: {},\n },\n ])\n return\n }\n uploads[field] = {\n id: String(stored.id),\n name: typeof stored.name === 'string' ? stored.name : null,\n }\n }\n const invoke = (fn: string, input: Record<string, unknown>) =>\n callRecordFunction(fn, input, { idempotencyKey: uuid() })\n let result = command.route\n ? await callRecordRoute(command.route, command.input(formData, context, uploads), {\n idempotencyKey: uuid(),\n })\n : await invoke(command.fn!, command.input(formData, context, uploads))\n // Further calls run only once the first succeeds, and stop at the first\n // one that does not \u2014 a later step's success never papers over an\n // earlier failure.\n for (const step of result.ok ? (command.also ?? []) : []) {\n if (step.when && !step.when(context)) continue\n result = await invoke(step.fn, step.input(formData, context, uploads))\n if (!result.ok) break\n }\n if (!result.ok) {\n // The layer was snapshotted before the busy state rendered, including unchecked controls.\n // A refused submit leaves an answer that was computed from something else.\n outcome.set(null)\n issues.set(\n result.issues.length\n ? result.issues.map((issue) => ({\n ...issue,\n field:\n issue.field && command.issueField\n ? command.issueField(issue.field, formData, context)\n : issue.field,\n }))\n : [\n {\n field: null,\n code: result.message ?? 'recordModal.saveFailed',\n message: result.message,\n params: {},\n },\n ],\n )\n return\n }\n if (command.preview) {\n // Nothing changed, so nothing is dropped, re-read or announced \u2014 and the\n // drafts snapshotted before this submit stay, so the selection is still on\n // screen beside the answer it produced.\n outcome.set({ command: name, value: result.value })\n version.set(version() + 1)\n return\n }\n if (scope === 'dialog') dialogDrafts.set(emptyDraftState())\n else recordDrafts.set(emptyDraftState())\n // A command that stays in its layer leaves its answer for the view, which is\n // how something the server can only say once \u2014 a one-time credential \u2014 reaches\n // the reader. Every other `after` replaces the layer, so the answer goes.\n outcome.set(after_(command) === 'stay' ? { command: name, value: result.value } : null)\n form.reset()\n const value = result.value as { id?: unknown } | null | undefined\n const createdId =\n after_(command) === 'open'\n ? (command.created?.(result.value) ??\n (value && typeof value === 'object' && value.id != null ? String(value.id) : null))\n : null\n // What the command changed is no longer what the cache holds.\n cache.delete(current.id)\n if (createdId) cache.delete(createdId)\n const announce = (): void => {\n document.dispatchEvent(\n new CustomEvent('ket:records-changed', {\n detail: { kind: definition.kind, ids: [createdId ?? current.id] },\n }),\n )\n }\n if (command.navigate) {\n const destination = new URL(command.navigate(result.value, context), location.href)\n if (destination.origin !== location.origin)\n throw new Error('Record navigation must stay same-origin')\n hide('none')\n location.assign(destination.href)\n return\n }\n const after = after_(command)\n // A modal that closes says so by closing; one that stays owes an answer.\n if (after !== 'close') saved.set(true)\n if (after === 'open') {\n dialog.set(null)\n dialogDrafts.set(emptyDraftState())\n // Replace `:new` in the address bar before the collection refreshes: the\n // shell re-fetches `location.href`, which must already name the new record.\n if (createdId) show(createdId, command.openTab ?? null, 'replace')\n else hide('history')\n announce()\n return\n }\n announce()\n if (after === 'close') hide('history')\n else if (after === 'reload') {\n const target = dialogReturnFocus\n dialogReturnFocus = null\n dialog.set(null)\n dialogDrafts.set(emptyDraftState())\n await load(current.id)\n afterRender(() => (target?.isConnected ? target : null))\n } else if (after === 'refresh') await load(current.id, true)\n else if (after === 'stay') version.set(version() + 1)\n else if ('tab' in after) {\n dialogReturnFocus = null\n dialog.set(null)\n dialogDrafts.set(emptyDraftState())\n show(current.id, after.tab, 'replace')\n await load(current.id)\n } else {\n const target = after.dialog ? null : dialogReturnFocus\n dialogReturnFocus = after.dialog && submitter instanceof HTMLElement ? submitter : null\n dialog.set(after.dialog ? { name: after.dialog, params: {} } : null)\n dialogDrafts.set(emptyDraftState())\n await load(current.id, true)\n afterRender(() => (target?.isConnected ? target : null))\n }\n } catch {\n issues.set([{ field: null, code: 'recordModal.saveFailed', message: null, params: {} }])\n } finally {\n setRunning(false)\n }\n }\n\n const formIssues = (context: RecordModalContext<Data>): JSXChild => {\n const general = issues().filter((issue) => !issue.field)\n const fieldless = issues().filter(\n (issue) => issue.field && !root?.querySelector(`[name=\"${CSS.escape(issue.field)}\"]`),\n )\n const all = [...general, ...fieldless]\n if (!all.length)\n return saved()\n ? Notice({\n title: context.t('recordModal.savedTitle'),\n message: context.t('recordModal.saved'),\n tone: 'positive',\n })\n : ''\n return Notice({\n title: context.t('recordModal.errorTitle'),\n message: all.map(issueText).join(' \u00B7 '),\n tone: 'danger',\n })\n }\n\n const recordBody = (): TemplateResult | string => {\n const current = open()\n if (!current) return ''\n if (status() === 'error')\n return (\n <>\n {Notice({\n title: t('recordModal.errorTitle'),\n message: t(failure() ?? 'recordModal.loadFailed'),\n tone: 'danger',\n })}\n {Button({ label: t('recordModal.retry'), variant: 'secondary', name: 'intent', value: 'retry' })}\n </>\n )\n const data = envelope()?.data\n if (status() !== 'ready' || data === undefined) return LoadingState({ label: t('recordModal.loading') })\n const context = contextFor(current, data)\n const tabs = visibleTabs(context)\n const active = tabs.find((tab) => tab.id === context.tab)\n const header = definition.header?.(context) ?? ''\n const issueNotice = formIssues(context)\n const body = active ? active.view(context) : (definition.body?.(context) ?? '')\n if (tabs.length)\n return TabbedView({\n id: `record-view-${definition.kind.replaceAll('.', '-')}`,\n label: definition.title(context),\n items: tabs.map((tab) => ({\n id: tab.id,\n label: tab.label(context),\n href: context.href(tab.id),\n active: tab.id === context.tab,\n })),\n context:\n header !== '' || issueNotice !== '' ? (\n <>\n {header}\n {issueNotice}\n </>\n ) : undefined,\n body,\n })\n return (\n <>\n {header}\n {issueNotice}\n {body}\n </>\n )\n }\n\n const dialogLayer = (): TemplateResult | string => {\n const current = open()\n const opened = dialog()\n const data = envelope()?.data\n if (!current || !opened || data === undefined) return ''\n const spec = definition.dialogs?.[opened.name]\n if (!spec) return ''\n const context = contextFor(current, data, 'dialog')\n return ModalSheet({\n id: `record-dialog-${definition.kind.replaceAll('.', '-')}-${opened.name}`,\n mode: 'client',\n presentation: 'dialog',\n size: spec.size ?? 'default',\n title: spec.title(context),\n actions: spec.actions?.(context),\n closeLabel: t('recordModal.close'),\n body: (\n <>\n {formIssues(context)}\n {spec.view(context)}\n </>\n ),\n })\n }\n\n return {\n view: () => {\n version()\n const current = open()\n if (!current) return recordModalHost(definition.kind)\n const data = envelope()?.data\n const context = data === undefined ? null : contextFor(current, data)\n return (\n <>\n {recordModalHost(definition.kind)}\n {ModalSheet({\n id: `record-modal-${definition.kind.replaceAll('.', '-')}`,\n mode: 'client',\n presentation: 'dialog',\n size: definition.size ?? 'default',\n // The runtime measures rendered tabs and holds their largest height.\n // Start at natural height so loading/short records never fill the viewport.\n height:\n (definition.tabs?.length ?? 0) + (definition.extensionTabs ? 1 : 0) > 1 ? 'fixed' : 'content',\n fixedHeight: definition.fixedHeight ?? 'auto',\n title: context ? definition.title(context) : t('recordModal.loading'),\n description: context ? (definition.description?.(context) ?? null) : null,\n status: context ? definition.status?.(context) : undefined,\n actions: context ? definition.actions?.(context) : undefined,\n closeLabel: t('recordModal.close'),\n body: recordBody(),\n })}\n {dialogLayer()}\n </>\n )\n },\n mount: ({ root: host, lifetime }) => {\n root = host as unknown as HTMLElement\n\n // Links anywhere on the page that name this kind open it; a link naming the\n // record already open switches its tab. Runs before the navigation layer.\n document.addEventListener(\n 'click',\n (event) => {\n if (\n event.defaultPrevented ||\n event.button !== 0 ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey\n )\n return\n const element = event.target instanceof Element ? event.target : null\n const inModal = element?.closest('[data-ui=\"modal-layer\"][data-client-modal=\"true\"]')\n if (inModal && root?.contains(inModal)) {\n // `data-ui=\"modal-close\"` is the chrome's own \u00D7 control; `data-record-close`\n // marks a module's own labeled close button placed elsewhere in the body\n // (the two can't share one attribute \u2014 a labeled button needs `data-ui=\"action\"`\n // for its styling, not the icon-only close control's).\n if (\n element?.closest('[data-ui=\"modal-close\"], [data-ui=\"modal-backdrop\"], [data-record-close]')\n ) {\n event.preventDefault()\n close()\n return\n }\n const stateControl = element?.closest<HTMLElement>(\n 'button[data-record-state], a[data-record-state]',\n )\n if (stateControl) {\n event.preventDefault()\n keepAllDrafts()\n viewState.set({\n ...viewState(),\n [stateControl.getAttribute('data-record-state') ?? '']:\n stateControl.getAttribute('data-record-value') ?? '',\n })\n return\n }\n const opener = element?.closest<HTMLElement>(`[${RECORD_DIALOG_ATTRIBUTE}]`)\n if (opener) {\n event.preventDefault()\n keepDrafts(recordLayer(), 'record')\n const focused = document.activeElement\n dialogReturnFocus =\n focused instanceof HTMLElement && opener.contains(focused)\n ? focused\n : opener.querySelector<HTMLElement>(focusable)\n dialogDrafts.set(emptyDraftState())\n const params: Record<string, string> = {}\n for (const [key, value] of Object.entries(opener.dataset))\n if (key.startsWith('recordParam') && value !== undefined)\n params[key.slice('recordParam'.length).replace(/^./u, (c) => c.toLowerCase())] = value\n issues.set([])\n outcome.set(null)\n dialog.set({ name: opener.getAttribute(RECORD_DIALOG_ATTRIBUTE) ?? '', params })\n afterRender()\n return\n }\n if (element?.closest('button[name=\"intent\"][value=\"retry\"]')) {\n event.preventDefault()\n const current = open()\n if (current) void load(current.id)\n return\n }\n }\n const href = openerHref(element)\n if (!href) return\n const url = new URL(href, location.href)\n if (url.origin !== location.origin || url.pathname !== location.pathname) return\n const target = readRecordModalTarget(url)\n if (!target || target.kind !== definition.kind) return\n event.preventDefault()\n const current = open()\n const contextQuery = 'route' in definition.context ? (definition.context.query ?? []) : []\n const previousUrl = new URL(location.href)\n const changedContext = contextQuery.some(\n (key) => previousUrl.searchParams.get(key) !== url.searchParams.get(key),\n )\n if (changedContext) {\n // These keys belong to the context reader (for example item pagination).\n // Preserve collection filters and record drafts while replacing this slice.\n keepDrafts(recordLayer(), 'record')\n for (const key of contextQuery) {\n const value = url.searchParams.get(key)\n if (value === null) previousUrl.searchParams.delete(key)\n else previousUrl.searchParams.set(key, value)\n }\n history.replaceState(history.state ?? {}, '', previousUrl.href)\n if (current?.id === target.id) void load(current.id)\n }\n if (current?.id === target.id) {\n if ((target.tab ?? '') === current.tab) return\n // Switching tab is not discarding: what was typed rides along as drafts,\n // which the views read back, so no prompt stands between two tabs.\n keepDrafts(recordLayer(), 'record')\n issues.set([])\n // The answer belonged to the tab that asked for it.\n outcome.set(null)\n show(\n target.id,\n target.tab ?? null,\n 'replace',\n () =>\n recordLayer()?.querySelector<HTMLElement>('[data-ui=\"tab\"][data-active=\"true\"]') ?? null,\n )\n return\n }\n show(target.id, target.tab ?? null, 'push')\n },\n // Capture: the navigation layer listens on the document too, and was\n // installed first, so a bubbling listener would see the page fetched.\n { signal: lifetime, capture: true },\n )\n\n // A row is reached by keyboard, not by pointer alone: Enter and Space on\n // the focused row open what clicking it opens.\n document.addEventListener(\n 'keydown',\n (event) => {\n if (event.defaultPrevented || (event.key !== 'Enter' && event.key !== ' ')) return\n const element = event.target instanceof Element ? event.target : null\n if (!element?.matches('[data-row-href][tabindex=\"0\"]')) return\n const href = element.getAttribute('data-row-href')\n const url = href ? new URL(href, location.href) : null\n if (!url || url.origin !== location.origin || url.pathname !== location.pathname) return\n const target = readRecordModalTarget(url)\n if (!target || target.kind !== definition.kind) return\n event.preventDefault()\n show(target.id, target.tab ?? null, 'push')\n },\n { signal: lifetime, capture: true },\n )\n\n document.addEventListener(\n 'submit',\n (event) => {\n const form = event.target instanceof HTMLFormElement ? event.target : null\n if (!form || !root?.contains(form)) return\n event.preventDefault()\n const submitter = (event as SubmitEvent).submitter ?? null\n const name =\n (submitter instanceof HTMLButtonElement && submitter.name === RECORD_COMMAND_FIELD\n ? submitter.value\n : null) ?? String(new FormData(form).get(RECORD_COMMAND_FIELD) ?? '')\n void run(name, form, submitter)\n },\n { signal: lifetime },\n )\n\n document.addEventListener(\n 'keydown',\n (event) => {\n if (!open()) return\n const layers = root?.querySelectorAll<HTMLElement>(\n '[data-ui=\"modal-layer\"][data-client-modal=\"true\"]',\n )\n const top = layers?.[layers.length - 1]\n if (!top) return\n if (event.key === 'Escape') {\n event.preventDefault()\n close()\n return\n }\n if (event.key !== 'Tab') return\n const items = focusablesIn(top)\n if (!items.length) {\n event.preventDefault()\n top.querySelector<HTMLElement>('[data-ui=\"modal-sheet\"]')?.focus()\n return\n }\n const first = items[0]!\n const last = items.at(-1)!\n if (\n event.shiftKey &&\n (document.activeElement === first || !top.contains(document.activeElement))\n ) {\n event.preventDefault()\n last.focus()\n } else if (\n !event.shiftKey &&\n (document.activeElement === last || !top.contains(document.activeElement))\n ) {\n event.preventDefault()\n first.focus()\n }\n },\n { signal: lifetime },\n )\n\n // An input or select carrying `data-record-state` feeds view state as it\n // changes; a file chosen in a `data-record-submit` input submits its form.\n // A design-system `RelationSelect`'s own hidden native select carries no\n // such marker (the package knows nothing of this runtime), but it fires a\n // real `change` too \u2014 keyed by its `name`, which every field in this kit\n // already sets to the same key `state()` reads it back under.\n document.addEventListener(\n 'change',\n (event) => {\n const control = event.target instanceof Element ? event.target : null\n if (!control || !root?.contains(control)) return\n // A checkbox named `__all:<prefix>` ticks or clears every checkbox of its\n // form whose name starts with that prefix, so one control stands for a\n // row or a whole section. Any tick in such a form is kept as a draft, so\n // the view re-renders and each \"all\" box reads whether it is now full.\n if (control instanceof HTMLInputElement && control.type === 'checkbox' && control.form) {\n const form = control.form\n if (form.querySelector(`input[type=\"checkbox\"][name^=\"${CHECK_ALL}\"]`)) {\n if (control.name.startsWith(CHECK_ALL)) {\n const prefix = control.name.slice(CHECK_ALL.length)\n for (const target of form.querySelectorAll<HTMLInputElement>('input[type=\"checkbox\"]'))\n if (\n !target.disabled &&\n !target.name.startsWith(CHECK_ALL) &&\n target.name.startsWith(prefix)\n )\n target.checked = control.checked\n }\n keepAllDrafts()\n return\n }\n }\n const stateKey =\n control.getAttribute('data-record-state') ??\n (['relation-native', 'reorder-list-value'].includes(control.getAttribute('data-ui') ?? '')\n ? control.getAttribute('name')\n : null)\n if ((control instanceof HTMLSelectElement || control instanceof HTMLInputElement) && stateKey) {\n keepAllDrafts()\n viewState.set({ ...viewState(), [stateKey]: control.value })\n return\n }\n if (\n control instanceof HTMLInputElement &&\n control.type === 'file' &&\n control.hasAttribute('data-record-submit') &&\n control.files?.length\n )\n control.form?.requestSubmit()\n },\n { signal: lifetime },\n )\n\n // A form marked `data-record-dropzone` takes a dropped file into its file\n // input and submits, so dragging a photo in is the same command as choosing it.\n const dropzoneOf = (event: Event): HTMLFormElement | null => {\n const element = event.target instanceof Element ? event.target : null\n const zone = element?.closest<HTMLFormElement>('form[data-record-dropzone]') ?? null\n return zone && root?.contains(zone) ? zone : null\n }\n document.addEventListener(\n 'dragover',\n (event) => {\n const zone = dropzoneOf(event)\n if (!zone) return\n event.preventDefault()\n zone.dataset.drag = '1'\n },\n { signal: lifetime },\n )\n document.addEventListener(\n 'dragleave',\n (event) => {\n const zone = dropzoneOf(event)\n if (!zone) return\n const related = (event as DragEvent).relatedTarget\n if (related instanceof Node && zone.contains(related)) return\n delete zone.dataset.drag\n },\n { signal: lifetime },\n )\n document.addEventListener(\n 'drop',\n (event) => {\n const zone = dropzoneOf(event)\n if (!zone) return\n event.preventDefault()\n delete zone.dataset.drag\n const files = (event as DragEvent).dataTransfer?.files\n const input = zone.querySelector<HTMLInputElement>('input[type=\"file\"]')\n if (!input || !files?.length) return\n const transfer = new DataTransfer()\n transfer.items.add(files[0]!)\n input.files = transfer.files\n zone.requestSubmit()\n },\n { signal: lifetime },\n )\n\n // Islands a view places with `recordIsland` start when their host appears\n // and stop when it leaves, through the page's own island manager.\n const islandSelector = 'ket-island, div[data-ket-island]'\n const holdsIsland = (node: Node): node is Element =>\n node instanceof Element &&\n (node.matches(islandSelector) || node.querySelector(islandSelector) !== null)\n const observer = new MutationObserver((records) => {\n fitRecordHeight()\n for (const record of records) {\n for (const node of record.removedNodes)\n if (holdsIsland(node))\n document.dispatchEvent(new CustomEvent('ket:islands-detach', { detail: { root: node } }))\n for (const node of record.addedNodes)\n if (holdsIsland(node) && node.isConnected)\n document.dispatchEvent(new CustomEvent('ket:islands-attach', { detail: { root: node } }))\n }\n })\n observer.observe(root, { childList: true, subtree: true, characterData: true })\n lifetime.addEventListener('abort', () => observer.disconnect())\n window.addEventListener('resize', fitRecordHeight, { signal: lifetime })\n // Images and native disclosures change layout without adding text nodes.\n root.addEventListener('load', fitRecordHeight, { capture: true, signal: lifetime })\n root.addEventListener('toggle', fitRecordHeight, { capture: true, signal: lifetime })\n // Web fonts can arrive after the first record measurement.\n void document.fonts?.ready.then(() => {\n if (!lifetime.aborted) fitRecordHeight()\n })\n fitRecordHeight()\n\n // Another island (or this one) changed records of this kind: drop them so the\n // next opening reads fresh data first.\n document.addEventListener(\n 'ket:records-changed',\n (event) => {\n const detail = (event as CustomEvent<{ kind?: string; ids?: unknown[] }>).detail\n if (detail?.kind !== definition.kind) return\n if (!Array.isArray(detail.ids) || !detail.ids.length) cache.clear()\n else for (const id of detail.ids) cache.delete(String(id))\n },\n { signal: lifetime },\n )\n\n // Back and forward over entries this modal created change only the modal.\n document.addEventListener(\n 'ket:popstate',\n (event) => {\n const target = readRecordModalTarget(location.href)\n const mine = target?.kind === definition.kind\n if (!mine && !open()) return\n event.preventDefault()\n if (mine && target) show(target.id, target.tab ?? null, 'none')\n else hide('none')\n },\n { signal: lifetime },\n )\n\n // Cross-collection links (for example global search results) use the\n // shell navigation first. This persistent island is not remounted when\n // those slots change, so open the destination record after URL commit.\n // A refresh of an already open record must not reset its draft or tab.\n document.addEventListener(\n 'ket:navigation-complete',\n () => {\n const target = readRecordModalTarget(location.href)\n if (target?.kind === definition.kind && !open()) show(target.id, target.tab ?? null, 'none')\n },\n { signal: lifetime },\n )\n\n const initial = readRecordModalTarget(location.href)\n if (initial?.kind === definition.kind) show(initial.id, initial.tab ?? null, 'none')\n\n lifetime.addEventListener('abort', () => {\n request?.abort()\n showBusy.stop()\n releaseInert?.()\n releaseInert = null\n root = null\n })\n },\n }\n }\n\n/**\n * Place an island inside a record view. The runtime starts it when the host\n * appears and disposes it when the host leaves; props must be JSON.\n */\nexport const recordIsland = (name: string, props: Record<string, unknown>): TemplateResult => (\n <ket-island data-island={name} data-props={JSON.stringify(props)} />\n)\n\nexport {\n RECORD_NEW_ID,\n RECORD_PARAM,\n RECORD_TAB_PARAM,\n recordModalHref,\n recordModalCreateHref,\n recordModalClosedHref,\n readRecordModalTarget,\n}\n\nexport {\n RecordModalForm,\n RecordActionForm,\n RecordCloseTrigger,\n RecordDialogTrigger,\n RecordCommandForm,\n recordStateSelectControl,\n} from './record-modal-form.tsx'\n", "// The record-modal contract, server half.\n//\n// A collection row opens its record in a modal island (design-system contract\n// \"Collections open records in a modal\"). The server's part is small on purpose:\n// it writes links that name a record, and it renders a closed host. Everything a\n// reader does inside the modal \u2014 opening, switching tabs, submitting, closing \u2014\n// happens in the browser, through the runtime in `client/record-modal.tsx`.\n//\n// One URL shape for every module keeps deep links, the back button and the list's\n// own query state working the same way everywhere:\n//\n// /admin/<collection>?<list state>&record=<kind>:<id>&tab=<tab>\n\nimport type { IslandDefinition, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['record-modal-host'] as const\n\n/** Query parameter naming the open record. Shared by every record modal. */\nexport const RECORD_PARAM = 'record'\n/** Query parameter naming the open tab of the record modal. */\nexport const RECORD_TAB_PARAM = 'tab'\n/**\n * The id a create action names: `record=<kind>:new` opens the same modal with an\n * empty record. A kind whose ids could literally be `new` must not use this.\n */\nexport const RECORD_NEW_ID = 'new'\n\nexport type RecordModalTarget = {\n /** Stable record kind, `<module>.<name>`; never contains a colon. */\n kind: string\n id: string\n tab?: string | null\n}\n\nconst kindPattern = /^[a-z][a-z0-9_]*(?:\\.[a-zA-Z][a-zA-Z0-9_]*)+$/u\n\nexport const isRecordKind = (kind: string): boolean => kindPattern.test(kind)\n\n/**\n * The link a row, card or create action uses to open a record.\n *\n * It keeps every other query parameter, so the collection behind the modal keeps\n * its filters and page. Without scripting it is still a real URL; the server\n * renders the collection and the island opens the record once it hydrates.\n */\nexport const recordModalHref = (url: URL | string, target: RecordModalTarget): string => {\n if (!isRecordKind(target.kind)) throw new TypeError(`invalid record kind \"${target.kind}\"`)\n const next = new URL(String(url), 'http://ket.local')\n next.searchParams.set(RECORD_PARAM, `${target.kind}:${target.id}`)\n if (target.tab) next.searchParams.set(RECORD_TAB_PARAM, target.tab)\n else next.searchParams.delete(RECORD_TAB_PARAM)\n return `${next.pathname}${next.search}`\n}\n\n/**\n * The link a collection's create action uses. It opens the record modal of that\n * kind with no record yet; the create command then switches it to the new record.\n */\nexport const recordModalCreateHref = (\n url: URL | string,\n target: { kind: string; tab?: string | null },\n): string => recordModalHref(url, { kind: target.kind, id: RECORD_NEW_ID, tab: target.tab ?? null })\n\n/** Whether a target asks for the create form rather than an existing record. */\nexport const isRecordModalCreate = (target: Pick<RecordModalTarget, 'id'> | null | undefined): boolean =>\n target?.id === RECORD_NEW_ID\n\n/** The URL with no record open, for closing and for rendering the collection. */\nexport const recordModalClosedHref = (url: URL | string): string => {\n const next = new URL(String(url), 'http://ket.local')\n next.searchParams.delete(RECORD_PARAM)\n next.searchParams.delete(RECORD_TAB_PARAM)\n return `${next.pathname}${next.search}`\n}\n\n/** Which record a URL asks for, or null. The id may itself contain colons. */\nexport const readRecordModalTarget = (url: URL | string): RecordModalTarget | null => {\n const parsed = new URL(String(url), 'http://ket.local')\n const raw = parsed.searchParams.get(RECORD_PARAM) ?? ''\n const split = raw.indexOf(':')\n if (split <= 0 || split === raw.length - 1) return null\n const kind = raw.slice(0, split)\n if (!isRecordKind(kind)) return null\n return { kind, id: raw.slice(split + 1), tab: parsed.searchParams.get(RECORD_TAB_PARAM) }\n}\n\n/** The closed host. Identical on the server and in the first client render. */\nexport const recordModalHost = (kind: string): TemplateResult => (\n <span data-ui=\"record-modal-host\" data-record-kind={kind} hidden />\n)\n\n/**\n * The island declaration a module places through `backend:runtime`.\n *\n * Props are empty: the host must not carry record data into a page that does not\n * show a record, and the record it opens is read through a permission-checked\n * function once a reader asks for it.\n */\nexport const defineRecordModalIsland = (options: {\n kind: string\n /** Browser module, relative to the declaring module's assets directory. */\n client: string\n export: string\n}): IslandDefinition => {\n if (!isRecordKind(options.kind)) throw new TypeError(`invalid record kind \"${options.kind}\"`)\n return {\n props: {},\n client: options.client,\n export: options.export,\n view: () => ({ view: () => recordModalHost(options.kind) }),\n }\n}\n", "// Record-modal form markup, owned by the kit.\n//\n// A record modal's views are module code; markup belongs to `ketsuite/ui`. These\n// are the pieces a view needs beyond the design system's fields and buttons: the\n// form the runtime submits (optionally naming its command), a select whose choice\n// feeds view state before anything is submitted, and a trigger that opens a dialog\n// of the same record.\n\nimport { ActionGroup, Field } from '@ketvietlab/design-system'\nimport type { FieldOption, FieldProps } from '@ketvietlab/design-system'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { RECORD_COMMAND_FIELD } from './record-modal.tsx'\n\n/**\n * The form a record modal's runtime submits. A `command` becomes the hidden\n * `__command` field; without one, the submitting button names it.\n */\nexport const RecordModalForm = (props: {\n kind: string\n fields: readonly FieldProps[]\n body?: JSXChild\n /** Structural edits and retained drafts remain dirty after a view-state render. */\n dirty?: boolean\n actions?: readonly JSXChild[]\n command?: string | null\n hidden?: Readonly<Record<string, string>>\n /** Lets a button outside this form submit it via the HTML `form` attribute. */\n id?: string\n}): TemplateResult => (\n <form\n data-ui=\"record-form\"\n method=\"post\"\n action=\"\"\n data-record-kind={props.kind}\n id={props.id}\n data-record-dirty={props.dirty === true ? 'true' : null}\n >\n {props.command ? (\n // autocomplete=\"off\" like every other input the kit writes: the ui contract\n // holds for a hidden field too, and a browser restoring one would submit a\n // command the reader never chose.\n <input type=\"hidden\" name={RECORD_COMMAND_FIELD} value={props.command} autocomplete=\"off\" />\n ) : (\n ''\n )}\n {Object.entries(props.hidden ?? {}).map(([name, value]) => (\n <input type=\"hidden\" name={name} value={value} autocomplete=\"off\" />\n ))}\n <div data-ui=\"form-grid\">{props.fields.map((item) => Field(item))}</div>\n {props.body}\n {props.actions?.length ? <div data-ui=\"form-actions\">{ActionGroup({ actions: props.actions })}</div> : ''}\n </form>\n)\n\n/**\n * The control of a select whose choice changes what the rest of the form offers.\n * Use it as a design-system field's `control`; `data-record-state` makes the\n * runtime re-render the view with the new choice.\n */\nexport const recordStateSelectControl = (props: {\n id: string\n name: string\n state: string\n value: string\n options: readonly FieldOption[]\n required?: boolean\n disabled?: boolean\n invalid?: boolean\n}): TemplateResult => (\n <select\n data-ui=\"field-control\"\n data-record-state={props.state}\n id={props.id}\n name={props.name}\n required={props.required === true}\n disabled={props.disabled === true}\n aria-invalid={props.invalid ? 'true' : null}\n >\n {props.options.map((option) => (\n <option value={option.value} selected={option.value === props.value}>\n {option.label}\n </option>\n ))}\n </select>\n)\n\n/**\n * Opens a dialog of the open record. `id` reaches the dialog as `params.id`;\n * without it the dialog opens for a new child (for example a new team member).\n */\nexport const RecordDialogTrigger = (props: {\n dialog: string\n id?: string | null\n children: JSXChild\n}): TemplateResult => (\n <span data-record-dialog={props.dialog} data-record-param-id={props.id ?? null}>\n {props.children}\n </span>\n)\n\n/**\n * A bare form for an action that names no field of its own \u2014 generate, remove.\n * `data-layout=\"actions\"` opts the form out of the record-form grid (built for a\n * field/label pair), which would otherwise stretch a lone submit button to fill it.\n */\nexport const RecordActionForm = (props: {\n kind: string\n command: string\n hidden?: Record<string, string>\n children: JSXChild\n}): TemplateResult => (\n <form data-ui=\"record-form\" data-layout=\"actions\" method=\"post\" action=\"\" data-record-kind={props.kind}>\n <input type=\"hidden\" name={RECORD_COMMAND_FIELD} value={props.command} autocomplete=\"off\" />\n {Object.entries(props.hidden ?? {}).map(([name, value]) => (\n <input type=\"hidden\" name={name} value={value} autocomplete=\"off\" />\n ))}\n {props.children}\n </form>\n)\n\n/**\n * A form with no field or button of its own, submitted only through the HTML\n * `form` attribute on buttons placed elsewhere \u2014 a menu whose every item names\n * its own command on itself, so one empty form serves the whole menu.\n */\nexport const RecordCommandForm = (props: { kind: string; id: string }): TemplateResult => (\n <form id={props.id} data-record-kind={props.kind} method=\"post\" action=\"\" hidden />\n)\n\n/**\n * Marks its child as a labeled close control the runtime's own click handler\n * recognizes from anywhere in the record's body \u2014 distinct from\n * `data-ui=\"modal-close\"`, the icon-only corner control's own styling hook.\n */\nexport const RecordCloseTrigger = (props: { children: JSXChild }): TemplateResult => (\n <span data-record-close=\"true\">{props.children}</span>\n)\n\n/**\n * A record's main image beside the first rows of its form: the viewer (a\n * thumbnail opening the image set), and under it the controls that change it.\n *\n * The whole block is one dropzone form: dropping a photo on the thumbnail, or\n * picking one, submits `uploadCommand` straight away (`data-record-submit`). Remove\n * is a button of that same form naming its own command, so it never carries the\n * file input with it.\n */\nexport const RecordImageField = (props: {\n kind: string\n id: string\n /** The viewer island (`backend.lightbox`), or null when there is no image yet. */\n viewer: JSXChild | null\n uploadCommand?: string | null\n removeCommand?: string | null\n labels: { empty: string; upload: string; replace: string; remove: string; drop: string }\n busy?: boolean\n}): TemplateResult => {\n const hasImage = props.viewer != null\n const editable = Boolean(props.uploadCommand)\n return (\n <form\n data-ui=\"record-image\"\n id={props.id}\n method=\"post\"\n action=\"\"\n enctype=\"multipart/form-data\"\n data-record-kind={props.kind}\n data-record-dropzone={editable ? 'true' : null}\n data-busy={props.busy ? 'true' : 'false'}\n >\n {editable ? (\n <input type=\"hidden\" name={RECORD_COMMAND_FIELD} value={props.uploadCommand} autocomplete=\"off\" />\n ) : (\n ''\n )}\n <div data-ui=\"record-image-frame\" title={editable ? props.labels.drop : undefined}>\n {hasImage ? props.viewer : <span data-ui=\"record-image-empty\">{props.labels.empty}</span>}\n </div>\n {editable || (hasImage && props.removeCommand) ? (\n <div data-ui=\"record-image-actions\">\n {editable ? (\n <label data-ui=\"record-image-upload\" data-variant=\"tertiary\">\n <input\n type=\"file\"\n autocomplete=\"off\"\n name=\"file\"\n accept=\"image/avif,image/gif,image/jpeg,image/png,image/webp\"\n data-record-submit=\"true\"\n disabled={props.busy === true}\n />\n <span>{hasImage ? props.labels.replace : props.labels.upload}</span>\n </label>\n ) : (\n ''\n )}\n {hasImage && props.removeCommand ? (\n <button\n type=\"submit\"\n data-ui=\"action\"\n data-variant=\"tertiary\"\n data-size=\"compact\"\n name={RECORD_COMMAND_FIELD}\n value={props.removeCommand}\n formnovalidate\n disabled={props.busy === true}\n >\n {props.labels.remove}\n </button>\n ) : (\n ''\n )}\n </div>\n ) : (\n ''\n )}\n </form>\n )\n}\n\n/** A form with an independent image column; narrow surfaces stack the image above it. */\nexport const RecordFormWithImage = (props: { form: JSXChild; image: JSXChild }): TemplateResult => (\n <div data-ui=\"record-form-with-image\">\n {props.form}\n {props.image}\n </div>\n)\n", "import { signal } from '@ketvietlab/ketjs-view'\nimport { Button, Notice, Section } from '@ketvietlab/design-system'\nimport { callRecordFunction } from './record-modal.tsx'\nimport { RecordModalForm } from './record-modal-form.tsx'\nimport type { VariantEditorAttribute } from './variant-editor-view.tsx'\n\n/** Creates a shared attribute without replacing the variant editor's unsaved setup. */\nexport const createVariantAttributeForm = (props: {\n id: string\n kind: string\n enabled: boolean\n launcher?: boolean\n returnFocus?: () => void\n disabled: () => boolean\n t: (key: string) => string\n added: (attribute: VariantEditorAttribute) => void\n}) => {\n const open = signal(false)\n const busy = signal(false)\n const name = signal('')\n const values = signal('')\n const errors = signal<Record<string, string>>({})\n const failure = signal(false)\n let attributeId = ''\n let disposed = false\n const valueIds = new Map<string, string>()\n const formId = `${props.id}-create-attribute`\n const focus = (id: string) =>\n queueMicrotask(() => {\n if (disposed) return\n const target = document.getElementById(id)\n ;(target?.querySelector('button') ?? target)?.focus()\n })\n const close = () => {\n open.set(false)\n name.set('')\n values.set('')\n errors.set({})\n failure.set(false)\n valueIds.clear()\n if (props.returnFocus) queueMicrotask(props.returnFocus)\n else focus(`${formId}-toggle`)\n }\n const submit = async (event: Event) => {\n event.preventDefault()\n event.stopPropagation()\n if (!props.enabled || props.disabled() || busy()) return\n const names = values()\n .split(/\\r?\\n/u)\n .map((value) => value.trim())\n .filter(Boolean)\n const invalid: Record<string, string> = {}\n if (!name().trim()) invalid.name = props.t('attributeNameRequired')\n if (!names.length) invalid.values = props.t('attributeValuesRequired')\n if (new Set(names.map((value) => value.normalize('NFC').toLowerCase())).size !== names.length)\n invalid.values = props.t('attributeValuesDuplicate')\n errors.set(invalid)\n failure.set(false)\n if (Object.keys(invalid).length) {\n focus(`${formId}-${invalid.name ? 'name' : 'values'}`)\n return\n }\n const input = {\n id: attributeId,\n name: name().trim(),\n displayType: 'select',\n createVariant: 'always',\n values: names.map((value) => {\n if (!valueIds.has(value)) valueIds.set(value, crypto.randomUUID())\n return { id: valueIds.get(value)!, name: value }\n }),\n }\n busy.set(true)\n try {\n const result = await callRecordFunction('product.saveAttributeDraft', input)\n if (disposed) return\n if (!result.ok) {\n const fields: Record<string, string> = {}\n for (const issue of result.issues) {\n if (issue.field === 'name')\n fields.name = props.t(\n issue.code === 'product.error.attribute.duplicateName'\n ? 'attributeNameDuplicate'\n : 'attributeNameRequired',\n )\n else if (issue.field?.startsWith('values')) fields.values = props.t('attributeValuesInvalid')\n }\n errors.set(fields)\n failure.set(!Object.keys(fields).length)\n focus(`${formId}-${fields.name ? 'name' : fields.values ? 'values' : 'submit'}`)\n return\n }\n props.added({\n attributeId: input.id,\n name: input.name,\n createVariant: input.createVariant,\n values: input.values.map((value) => ({ valueId: value.id, name: value.name })),\n })\n document.dispatchEvent(\n new CustomEvent('ket:records-changed', {\n detail: { kind: 'product.attribute', ids: [input.id] },\n }),\n )\n close()\n } catch {\n if (disposed) return\n failure.set(true)\n focus(`${formId}-submit`)\n } finally {\n busy.set(false)\n }\n }\n return {\n open,\n busy,\n start: (query = '') => {\n if (!props.enabled || props.disabled() || busy()) return\n attributeId = crypto.randomUUID()\n name.set(query)\n open.set(true)\n focus(`${formId}-name`)\n },\n attach: (root: HTMLElement, lifetime: AbortSignal) => {\n lifetime.addEventListener('abort', () => {\n disposed = true\n })\n root.addEventListener(\n 'click',\n (event) => {\n const button = event.target instanceof Element ? event.target.closest('button') : null\n if (!props.enabled || !button || button.disabled || busy() || props.disabled()) return\n if (button.name === 'createAttribute') {\n attributeId = crypto.randomUUID()\n open.set(true)\n focus(`${formId}-name`)\n } else if (button.name === 'cancelAttribute') close()\n },\n { signal: lifetime },\n )\n },\n view: () =>\n !props.enabled || (props.launcher === false && !open()) ? null : (\n <div\n onInput={(event: Event) => {\n const input = event.target\n if (!(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)) return\n if (input.name === 'attributeName') name.set(input.value)\n if (input.name === 'attributeValues') values.set(input.value)\n }}\n onSubmit={submit}\n >\n {!open() ? (\n <span id={`${formId}-toggle`} tabIndex={-1}>\n <Button name=\"createAttribute\" label={props.t('createAttribute')} disabled={props.disabled()} />\n </span>\n ) : (\n <Section\n title={props.t('createAttribute')}\n description={props.t('createAttributeHint')}\n body={\n <>\n {failure() ? (\n <Notice\n tone=\"danger\"\n title={props.t('attributeCreateFailed')}\n message={props.t('attributeRetryHint')}\n />\n ) : null}\n <RecordModalForm\n id={formId}\n kind={props.kind}\n dirty={!!name() || !!values()}\n fields={[\n {\n id: `${formId}-name`,\n name: 'attributeName',\n label: props.t('attributeName'),\n value: name(),\n required: true,\n disabled: busy(),\n error: errors().name,\n placeholder: props.t('attributeNameExample'),\n span: 'full',\n },\n {\n id: `${formId}-values`,\n name: 'attributeValues',\n label: props.t('attributeValues'),\n type: 'textarea',\n value: values(),\n required: true,\n disabled: busy(),\n error: errors().values,\n help: props.t('attributeValuesHint'),\n placeholder: props.t('attributeValuesExample'),\n span: 'full',\n },\n ]}\n actions={[\n <span id={`${formId}-submit`} tabIndex={-1}>\n <Button\n type=\"submit\"\n label={props.t('createAndAddAttribute')}\n variant=\"primary\"\n loading={busy()}\n />\n </span>,\n <Button\n name=\"cancelAttribute\"\n label={props.t('cancelAttribute')}\n variant=\"tertiary\"\n disabled={busy()}\n />,\n ]}\n />\n </>\n }\n />\n )}\n </div>\n ),\n }\n}\n"],
|
|
5
|
+
"mappings": "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ACA,YACF,ECXA,OAAS,QAAAC,OAAY,sBAqBO,cAAAC,GASlB,QAAAC,OATkB,4BAjBrB,IAAMC,GAAQ,CAAC,cAAe,aAAc,gBAAiB,YAAY,ECJhF,OAAS,QAAAC,OAAY,sBAiFjB,OA2BF,YAAAC,GA1BI,OAAAC,GADF,QAAAC,OAAA,4BA9EG,IAAMC,GAAQ,CACnB,iBACA,qBACA,oBACA,0BACA,8BACA,2BACA,mBACA,sBACA,oBACA,oBACA,sBACA,qBACA,mBACA,oBACA,mBACA,yBACA,mBACA,2BACA,kBACA,oBACA,4BACA,sBACA,0BACA,uBACA,wBACA,8BACA,wBACA,mBACF,EChCA,OAAS,QAAAC,OAAY,sBA0If,OAqCJ,YAAAC,GApCM,OAAAC,GADF,QAAAC,OAAA,4BAzDC,IAAMC,GAAQ,CACnB,eACA,QACA,gBACA,aACA,aACA,MACA,YACA,YACA,MACA,YACA,aACA,aACA,eACA,cACA,cACA,sBACA,OACA,cACA,aACA,UACF,EC3CM,mBAAAC,GAEE,OAAAC,GAFF,QAAAC,OAAA,4BAxDC,IAAMC,GAAQ,CACnB,YACA,oBACA,mBACA,oBACA,sBACA,oBACA,kBACA,oBACA,kBACA,oBACA,qBACA,mBACA,iBACA,kBACF,ECgCM,OACE,OAAAC,GADF,QAAAC,OAAA,4BA/CC,IAAMC,GAAQ,CACnB,cACA,sBACA,qBACA,wBACA,sBACA,oBACA,sBACA,qBACA,sBACA,mBACA,yBACA,yBACA,qBACA,mBACA,mBACF,EChBO,IAAMC,GAAQ,CACnB,YACA,oBACA,mBACA,sBACA,oBACA,kBACA,oBACA,mBACA,oBACA,iBACA,uBACA,uBACA,mBACA,iBACA,iBACF,ECuBI,OASM,OAAAC,GATN,QAAAC,OAAA,4BCvCG,IAAMC,GAAQ,CACnB,iBACA,yBACA,wBACA,yBACA,yBACA,2BACA,uBACA,yBACA,wBACA,yBACA,sBACA,qBACF,ECbO,IAAMC,GAAQ,CACnB,aACA,qBACA,oBACA,qBACA,qBACA,uBACA,mBACA,qBACA,oBACA,qBACA,kBACA,qBACA,sBACA,iBACF,EClBA,OAAS,QAAAC,OAAY,sBAyCb,OACE,OAAAC,GADF,QAAAC,OAAA,4BArCD,IAAMC,GAAQ,CACnB,WACA,gBACA,iBACA,sBACA,sBACA,oBACF,ECXA,OAAS,QAAAC,OAAY,sBAuBnB,OAOI,OAAAC,GAPJ,QAAAC,OAAA,4BAhBK,IAAMC,GAAQ,CAAC,cAAe,cAAe,YAAa,cAAc,ECP/E,OAAS,QAAAC,OAAgD,sBAiCrD,cAAAC,GAeQ,QAAAC,OAfR,4BA7BG,IAAMC,GAAQ,CACnB,eACA,qBACA,oBACA,mBACA,uBACA,uBACF,ECuCA,IAAMC,GAAS,CACb,QAASC,GACT,SAAUA,GACV,OAAQA,GACR,OAAQA,GACR,WAAYA,GACZ,SAAUA,GACV,KAAMA,GACN,YAAaA,GACb,QAASA,GACT,QAASA,GACT,OAAQA,GACR,MAAOA,GACP,QAASA,GACT,SAAUA,GACV,SAAUA,GACV,eAAgBA,GAChB,aAAcA,GACd,SAAUA,GACV,aAAcA,GACd,SAAUA,GACV,SAAUA,GACV,OAAQA,GACR,eAAgBA,GAChB,aAAcA,GACd,WAAYA,GACZ,aAAcA,GACd,SAAUA,GACV,KAAMA,GACN,OAAQA,GACR,SAAUA,GACV,UAAWA,GACX,cAAeA,GACf,gBAAiBA,GACjB,kBAAmBA,GACnB,SAAUA,GACV,MAAOA,GACP,QAASA,GACT,MAAOA,GACP,cAAeA,GACf,MAAOA,GACP,WAAYA,GACZ,SAAUA,GACV,SAAUA,GACV,WAAYA,GACZ,cAAeA,GACf,UAAWA,GACX,MAAOA,GACP,SAAUA,GACV,KAAMA,EACR,EAEaA,GAA2B,CAAC,GAAG,IAAI,IAAI,OAAO,OAAOD,EAAM,EAAE,KAAK,CAAC,CAAC,EAAE,KAAK,EAE3EE,GAA6C,OAAO,OAC/D,OAAO,YAAY,OAAO,QAAQF,EAAM,EAAE,IAAI,CAAC,CAACG,EAAOC,CAAK,IAAM,CAACD,EAAO,CAAC,GAAGC,CAAK,CAAC,CAAC,CAAC,CACxF,EC9FA,OAAS,iBAAAC,OAAqB,sBAI9B,IAAMC,GAAgC,CAEpC,QACE,wOACF,UACE,yOACF,gBACE,iKACF,MACE,uHACF,MACE,yKACF,cACE,0MACF,SACE,wXACF,YACE,gLACF,iBACE,2IACF,eAAgB,kFAChB,KAAM,4DACN,SACE,0GACF,WACE,2IACF,SACE,oHACF,gBACE,sKACF,UACE,iSACF,mBACE,0MACF,MACE,uXACF,eACE,sgBACF,MACE,6PACF,eACE,kNACF,QACE,2QACF,YACE,gYACF,OACE,iLACF,SACE,qIACF,MACE,sRACF,IAAK,4NAGL,OAAQ,iEACR,eAAgB,4BAChB,gBAAiB,6BACjB,eAAgB,8BAChB,KAAM,8CACN,kBACE,gKACF,EAAG,kDACH,KAAM,uIACN,qBACE,sMACF,gBACE,2TACF,OAAQ,kFACR,UACE,6KACF,aACE,2IACF,WAAY,8BACZ,aAAc,4BACd,MAAO,+BACP,iBACE,qIACF,KAAM,kFAGN,KAAM,uLACN,KAAM,0GACN,KAAM,kDACN,IAAK,qQACL,UACE,yGACF,aACE,0MACJ,EAEMC,GACJ,mMAeK,IAAMC,GAAQC,GACnBC,GAAcD,GAAQE,GAAMF,CAAI,EAAIG,GAAOD,GAAMF,CAAI,EAAI,SAAW,EAAE,EChHxE,OAAS,UAAAI,OAAc,sBC4ErB,cAAAC,OAAA,4BC3DA,OAYI,OAAAC,GAZJ,QAAAC,OAAA,4BAZK,IAAMC,GAAmBC,GAY9BF,GAAC,QACC,UAAQ,cACR,OAAO,OACP,OAAO,GACP,mBAAkBE,EAAM,KACxB,GAAIA,EAAM,GACV,oBAAmBA,EAAM,QAAU,GAAO,OAAS,KAElD,UAAAA,EAAM,QAILH,GAAC,SAAM,KAAK,SAAS,KAAMI,GAAsB,MAAOD,EAAM,QAAS,aAAa,MAAM,EAE1F,GAED,OAAO,QAAQA,EAAM,QAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAACE,EAAMC,CAAK,IACnDN,GAAC,SAAM,KAAK,SAAS,KAAMK,EAAM,MAAOC,EAAO,aAAa,MAAM,CACnE,EACDN,GAAC,OAAI,UAAQ,YAAa,SAAAG,EAAM,OAAO,IAAKI,GAASC,EAAMD,CAAI,CAAC,EAAE,EACjEJ,EAAM,KACNA,EAAM,SAAS,OAASH,GAAC,OAAI,UAAQ,eAAgB,SAAAS,GAAY,CAAE,QAASN,EAAM,OAAQ,CAAC,EAAE,EAAS,IACzG,EFo/BQ,mBAAAO,GAmeR,OAAAC,GAneQ,QAAAC,OAAA,4BAhgCV,IAAMC,GAAYC,GAAmC,CACnD,IAAMC,EAAUD,GAAU,CAAC,EAM3B,OALa,MAAM,QAAQC,EAAO,MAAM,EACpCA,EAAO,OACP,MAAM,QAAQA,EAAO,MAAM,EACzBA,EAAO,OACP,CAAC,GACK,IAAKC,GAAQ,CACvB,IAAMC,EAAQD,GAAO,CAAC,EAChBE,EAAO,MAAM,QAAQD,EAAK,IAAI,EAAIA,EAAK,KAAK,KAAK,GAAG,EAAI,KAC9D,MAAO,CACL,MAAO,OAAOA,EAAK,OAAU,SAAWA,EAAK,MAAQC,EACrD,KAAM,OAAOD,EAAK,MAAQ,SAAS,EACnC,QAAS,OAAOA,EAAK,SAAY,SAAWA,EAAK,QAAU,KAC3D,OAASA,EAAK,QAAU,OAAOA,EAAK,QAAW,SAAWA,EAAK,OAAS,CAAC,CAC3E,CACF,CAAC,CACH,EASaE,GAAkB,MAC7BC,EACAC,EACAC,EAA6D,CAAC,IACzB,CACrC,IAAMC,EAAS,IAAI,IAAIH,EAAO,wBAAwB,EACtD,GAAIG,EAAO,SAAW,0BAA4B,CAACH,EAAM,WAAW,GAAG,GAAKA,EAAM,WAAW,IAAI,EAC/F,MAAM,IAAI,MAAM,6CAA6C,EAC/D,IAAMI,EAAW,MAAM,MAAMD,EAAO,SAAWA,EAAO,OAAQ,CAC5D,OAAQ,OACR,YAAa,cACb,QAAS,CACP,eAAgB,mBAChB,GAAID,EAAQ,eAAiB,CAAE,kBAAmBA,EAAQ,cAAe,EAAI,CAAC,CAChF,EACA,KAAM,KAAK,UAAUD,CAAK,EAC1B,OAAQC,EAAQ,MAClB,CAAC,EACKG,EAAW,MAAMD,EAAS,KAAK,EAAE,MAAM,KAAO,CAAC,EAAE,EAMvD,GAAI,CAACA,EAAS,IAAMC,EAAQ,KAAO,GACjC,MAAO,CACL,GAAI,GACJ,OAAQZ,GAASY,EAAQ,OAASA,CAAO,EAGzC,QAASD,EAAS,QAAU,IAAM,KAAQC,EAAQ,OAAO,SAAWA,EAAQ,SAAW,KACvF,OAAQD,EAAS,MACnB,EACF,IAAME,EAAQD,EAAQ,MACtB,OAAIC,GAAS,OAAOA,GAAU,UAAYA,EAAM,KAAO,GAC9C,CAAE,GAAI,GAAO,OAAQb,GAASa,CAAK,EAAG,QAAS,KAAM,OAAQF,EAAS,MAAO,EAC/E,CAAE,GAAI,GAAM,MAAOC,EAAQ,KAAe,CACnD,EAGaE,GAAqB,CAChCC,EACAP,EACAC,EAA6D,CAAC,IAE9DH,GAAuB,YAAY,mBAAmBS,CAAI,CAAC,GAAIP,EAAOC,CAAO,EAgNxE,IAAMO,GAAwD,OAAO,OAAO,CACjF,oBAAqB,QACrB,sBAAuB,gBACvB,yBAA0B,gCAC1B,uBAAwB,2CACxB,oBAAqB,QACrB,yBAA0B,YAC1B,yBAA0B,gCAC1B,yBAA0B,QAC1B,oBAAqB,oBACrB,sBAAuB,0BACvB,2BAA4B,4CAC9B,CAAC,EAkCM,IAAMC,GAAuB,YAWpC,IAAMC,GAAY,CAChB,UACA,wBACA,4CACA,wBACA,0BACA,iCACF,EAAE,KAAK,IAAI,EG9XX,OAAS,UAAAC,OAAc,sBAyJT,OAOE,YAAAC,GAPF,OAAAC,GAOE,QAAAC,OAPF,4BAlJP,IAAMC,GAA8BC,GASrC,CACJ,IAAMC,EAAOC,GAAO,EAAK,EACnBC,EAAOD,GAAO,EAAK,EACnBE,EAAOF,GAAO,EAAE,EAChBG,EAASH,GAAO,EAAE,EAClBI,EAASJ,GAA+B,CAAC,CAAC,EAC1CK,EAAUL,GAAO,EAAK,EACxBM,EAAc,GACdC,EAAW,GACTC,EAAW,IAAI,IACfC,EAAS,GAAGX,EAAM,EAAE,oBACpBY,EAASC,GACb,eAAe,IAAM,CACnB,GAAIJ,EAAU,OACd,IAAMK,EAAS,SAAS,eAAeD,CAAE,GACvCC,GAAQ,cAAc,QAAQ,GAAKA,IAAS,MAAM,CACtD,CAAC,EACGC,EAAQ,IAAM,CAClBd,EAAK,IAAI,EAAK,EACdG,EAAK,IAAI,EAAE,EACXC,EAAO,IAAI,EAAE,EACbC,EAAO,IAAI,CAAC,CAAC,EACbC,EAAQ,IAAI,EAAK,EACjBG,EAAS,MAAM,EACXV,EAAM,YAAa,eAAeA,EAAM,WAAW,EAClDY,EAAM,GAAGD,CAAM,SAAS,CAC/B,EACMK,EAAS,MAAOC,GAAiB,CAGrC,GAFAA,EAAM,eAAe,EACrBA,EAAM,gBAAgB,EAClB,CAACjB,EAAM,SAAWA,EAAM,SAAS,GAAKG,EAAK,EAAG,OAClD,IAAMe,EAAQb,EAAO,EAClB,MAAM,QAAQ,EACd,IAAKc,GAAUA,EAAM,KAAK,CAAC,EAC3B,OAAO,OAAO,EACXC,EAAkC,CAAC,EAOzC,GANKhB,EAAK,EAAE,KAAK,IAAGgB,EAAQ,KAAOpB,EAAM,EAAE,uBAAuB,GAC7DkB,EAAM,SAAQE,EAAQ,OAASpB,EAAM,EAAE,yBAAyB,GACjE,IAAI,IAAIkB,EAAM,IAAKC,GAAUA,EAAM,UAAU,KAAK,EAAE,YAAY,CAAC,CAAC,EAAE,OAASD,EAAM,SACrFE,EAAQ,OAASpB,EAAM,EAAE,0BAA0B,GACrDM,EAAO,IAAIc,CAAO,EAClBb,EAAQ,IAAI,EAAK,EACb,OAAO,KAAKa,CAAO,EAAE,OAAQ,CAC/BR,EAAM,GAAGD,CAAM,IAAIS,EAAQ,KAAO,OAAS,QAAQ,EAAE,EACrD,MACF,CACA,IAAMC,EAAQ,CACZ,GAAIb,EACJ,KAAMJ,EAAK,EAAE,KAAK,EAClB,YAAa,SACb,cAAe,SACf,OAAQc,EAAM,IAAKC,IACZT,EAAS,IAAIS,CAAK,GAAGT,EAAS,IAAIS,EAAO,OAAO,WAAW,CAAC,EAC1D,CAAE,GAAIT,EAAS,IAAIS,CAAK,EAAI,KAAMA,CAAM,EAChD,CACH,EACAhB,EAAK,IAAI,EAAI,EACb,GAAI,CACF,IAAMmB,EAAS,MAAMC,GAAmB,6BAA8BF,CAAK,EAC3E,GAAIZ,EAAU,OACd,GAAI,CAACa,EAAO,GAAI,CACd,IAAME,EAAiC,CAAC,EACxC,QAAWC,KAASH,EAAO,OACrBG,EAAM,QAAU,OAClBD,EAAO,KAAOxB,EAAM,EAClByB,EAAM,OAAS,wCACX,yBACA,uBACN,EACOA,EAAM,OAAO,WAAW,QAAQ,IAAGD,EAAO,OAASxB,EAAM,EAAE,wBAAwB,GAE9FM,EAAO,IAAIkB,CAAM,EACjBjB,EAAQ,IAAI,CAAC,OAAO,KAAKiB,CAAM,EAAE,MAAM,EACvCZ,EAAM,GAAGD,CAAM,IAAIa,EAAO,KAAO,OAASA,EAAO,OAAS,SAAW,QAAQ,EAAE,EAC/E,MACF,CACAxB,EAAM,MAAM,CACV,YAAaqB,EAAM,GACnB,KAAMA,EAAM,KACZ,cAAeA,EAAM,cACrB,OAAQA,EAAM,OAAO,IAAKF,IAAW,CAAE,QAASA,EAAM,GAAI,KAAMA,EAAM,IAAK,EAAE,CAC/E,CAAC,EACD,SAAS,cACP,IAAI,YAAY,sBAAuB,CACrC,OAAQ,CAAE,KAAM,oBAAqB,IAAK,CAACE,EAAM,EAAE,CAAE,CACvD,CAAC,CACH,EACAN,EAAM,CACR,MAAQ,CACN,GAAIN,EAAU,OACdF,EAAQ,IAAI,EAAI,EAChBK,EAAM,GAAGD,CAAM,SAAS,CAC1B,QAAE,CACAR,EAAK,IAAI,EAAK,CAChB,CACF,EACA,MAAO,CACL,KAAAF,EACA,KAAAE,EACA,MAAO,CAACuB,EAAQ,KAAO,CACjB,CAAC1B,EAAM,SAAWA,EAAM,SAAS,GAAKG,EAAK,IAC/CK,EAAc,OAAO,WAAW,EAChCJ,EAAK,IAAIsB,CAAK,EACdzB,EAAK,IAAI,EAAI,EACbW,EAAM,GAAGD,CAAM,OAAO,EACxB,EACA,OAAQ,CAACgB,EAAmBC,IAA0B,CACpDA,EAAS,iBAAiB,QAAS,IAAM,CACvCnB,EAAW,EACb,CAAC,EACDkB,EAAK,iBACH,QACCV,GAAU,CACT,IAAMY,EAASZ,EAAM,kBAAkB,QAAUA,EAAM,OAAO,QAAQ,QAAQ,EAAI,KAC9E,CAACjB,EAAM,SAAW,CAAC6B,GAAUA,EAAO,UAAY1B,EAAK,GAAKH,EAAM,SAAS,IACzE6B,EAAO,OAAS,mBAClBrB,EAAc,OAAO,WAAW,EAChCP,EAAK,IAAI,EAAI,EACbW,EAAM,GAAGD,CAAM,OAAO,GACbkB,EAAO,OAAS,mBAAmBd,EAAM,EACtD,EACA,CAAE,OAAQa,CAAS,CACrB,CACF,EACA,KAAM,IACJ,CAAC5B,EAAM,SAAYA,EAAM,WAAa,IAAS,CAACC,EAAK,EAAK,KACxDJ,GAAC,OACC,QAAUoB,GAAiB,CACzB,IAAMI,EAAQJ,EAAM,QACdI,aAAiB,kBAAoBA,aAAiB,uBACxDA,EAAM,OAAS,iBAAiBjB,EAAK,IAAIiB,EAAM,KAAK,EACpDA,EAAM,OAAS,mBAAmBhB,EAAO,IAAIgB,EAAM,KAAK,EAC9D,EACA,SAAUL,EAET,SAACf,EAAK,EAKLJ,GAACiC,GAAA,CACC,MAAO9B,EAAM,EAAE,iBAAiB,EAChC,YAAaA,EAAM,EAAE,qBAAqB,EAC1C,KACEF,GAAAF,GAAA,CACG,UAAAW,EAAQ,EACPV,GAACkC,GAAA,CACC,KAAK,SACL,MAAO/B,EAAM,EAAE,uBAAuB,EACtC,QAASA,EAAM,EAAE,oBAAoB,EACvC,EACE,KACJH,GAACmC,GAAA,CACC,GAAIrB,EACJ,KAAMX,EAAM,KACZ,MAAO,CAAC,CAACI,EAAK,GAAK,CAAC,CAACC,EAAO,EAC5B,OAAQ,CACN,CACE,GAAI,GAAGM,CAAM,QACb,KAAM,gBACN,MAAOX,EAAM,EAAE,eAAe,EAC9B,MAAOI,EAAK,EACZ,SAAU,GACV,SAAUD,EAAK,EACf,MAAOG,EAAO,EAAE,KAChB,YAAaN,EAAM,EAAE,sBAAsB,EAC3C,KAAM,MACR,EACA,CACE,GAAI,GAAGW,CAAM,UACb,KAAM,kBACN,MAAOX,EAAM,EAAE,iBAAiB,EAChC,KAAM,WACN,MAAOK,EAAO,EACd,SAAU,GACV,SAAUF,EAAK,EACf,MAAOG,EAAO,EAAE,OAChB,KAAMN,EAAM,EAAE,qBAAqB,EACnC,YAAaA,EAAM,EAAE,wBAAwB,EAC7C,KAAM,MACR,CACF,EACA,QAAS,CACPH,GAAC,QAAK,GAAI,GAAGc,CAAM,UAAW,SAAU,GACtC,SAAAd,GAACoC,EAAA,CACC,KAAK,SACL,MAAOjC,EAAM,EAAE,uBAAuB,EACtC,QAAQ,UACR,QAASG,EAAK,EAChB,EACF,EACAN,GAACoC,EAAA,CACC,KAAK,kBACL,MAAOjC,EAAM,EAAE,iBAAiB,EAChC,QAAQ,WACR,SAAUG,EAAK,EACjB,CACF,EACF,GACF,EAEJ,EAjEAN,GAAC,QAAK,GAAI,GAAGc,CAAM,UAAW,SAAU,GACtC,SAAAd,GAACoC,EAAA,CAAO,KAAK,kBAAkB,MAAOjC,EAAM,EAAE,iBAAiB,EAAG,SAAUA,EAAM,SAAS,EAAG,EAChG,EAiEJ,CAEN,CACF,E5D8hBY,cAAAkC,EAgDF,QAAAC,MAhDE,4BA9oBZ,IAAMC,EAAS,gBAETC,GAAaC,GAAgB,KAAK,MAAM,KAAK,UAAUA,CAAK,CAAC,EAE7DC,GAAQ,CAACC,EAAgCC,EAAaC,EAAkC,CAAC,KAC5FF,EAAOC,CAAG,GAAKA,GAAK,QAAQ,aAAc,CAACE,EAAGC,IAAiB,OAAOF,EAAOE,CAAI,GAAK,IAAIA,CAAI,GAAG,CAAC,EAG/FC,GAAW,CAACC,EAAsBC,IACtC,UAAU,mBAAmBD,CAAY,CAAC,SAASC,CAAI,GAEnDC,GAAc,uDAEdC,GAASC,GAA4B,OAAO,SAASA,CAAM,EAAIA,EAAO,eAAe,OAAO,EAAI,SAEhGC,GAAWb,GAA0B,CACzC,IAAMY,EAAS,OAAOZ,CAAK,EAC3B,OAAO,OAAO,SAASY,CAAM,EAAIA,EAAS,CAC5C,EAEME,GAAad,GAA2B,kBAAkB,KAAKA,EAAM,KAAK,CAAC,EAE3Ee,GAAUf,GAA0B,CACxC,GAAI,CAACc,GAAUd,CAAK,EAAG,OAAOA,EAAM,KAAK,GAAK,IAC9C,IAAMY,EAASC,GAAQb,CAAK,EAC5B,MAAO,GAAGY,EAAS,EAAI,IAAM,EAAE,GAAGD,GAAMC,CAAM,CAAC,EACjD,EAGMI,GAAU,EAEZC,GAAc,EACZC,GAAUC,GACdA,EAAS,IAAKC,IAAa,CAAE,GAAGA,EAAS,SAAU,CAAE,GAAGA,EAAQ,QAAS,EAAG,IAAKA,EAAQ,GAAI,KAAM,EAAM,EAAE,EAEtG,SAASC,GAAwBC,EAA6C,CACnF,IAAMpB,EAASoB,EAAM,OACfC,EAAI,CAACpB,EAAaC,IAAqCH,GAAMC,EAAQC,EAAKC,CAAM,EAChFoB,EAAWF,EAAM,SAEnBG,EAAQ1B,GAAMuB,EAAM,KAAK,EACvBI,EAAYC,EAAOF,EAAM,SAAS,EAClCG,EAAYD,EAAOF,EAAM,SAAS,EAClCI,EAAQF,EAA4B5B,GAAM0B,EAAM,KAAK,CAAC,EACtDK,EAAOH,EAAcT,GAAOO,EAAM,QAAQ,CAAC,EAE3CM,EAAaJ,EAAiB,CAAC,CAAC,EAChCK,EAASL,EAAO,EAAK,EACrBM,EAAUN,EAAsB,IAAI,EACpCO,EAAYP,EAA+B,CAAC,CAAC,EAC7CQ,EAASR,EAAsB,IAAI,EACnCS,EAAQT,EAAO,EAAK,EAEpBU,EAAYV,EAAsB,IAAI,EACtCW,EAAWX,EAAsB,IAAI,EACrCY,EAASC,GAAelB,EAAM,eAAgBA,EAAM,EAAE,EACxDmB,EAA2B,KAC3BC,EAAW,GAETC,EAAQ,IAAY,CACxBP,EAAM,IAAI,EAAI,EACdD,EAAO,IAAI,IAAI,CACjB,EACMS,EAAYC,GAAoC,CACpDhB,EAAM,IAAIgB,CAAI,EACdF,EAAM,CACR,EACMG,EAAWD,GAAsB,CACrCf,EAAK,IAAIe,CAAI,EACbF,EAAM,CACR,EACMI,GAAY,CAAC5C,EAAa6C,IAC9BF,EAAQhB,EAAK,EAAE,IAAKmB,GAASA,EAAI,MAAQ9C,EAAM,CAAE,GAAG8C,EAAK,GAAGD,CAAM,EAAIC,CAAI,CAAC,EACvEC,EAAS/C,GAAiC2B,EAAK,EAAE,KAAMmB,GAAQA,EAAI,MAAQ9C,CAAG,EAG9EgD,EAAY,CAACC,EAAkBC,IAA4B,CAC/D,WAAW,IAAM,CACf,GAAIX,GAAY,CAACD,EAAM,QAErBA,EAAK,cAA2BW,CAAQ,IACvCC,EAAWZ,EAAK,cAA2BY,CAAQ,EAAI,QAClD,MAAM,CAChB,EAAG,CAAC,CACN,EACMC,GAAgB,QAAQhC,EAAM,EAAE,6BAChCiC,EAAgBvD,GAA0B,gBAAgBF,CAAM,aAAaE,CAAK,KAGlFwD,EAAe,IACnB3B,EAAM,EAAE,OAAQ4B,GAASA,EAAK,gBAAkB,cAAgBA,EAAK,OAAO,OAAS,CAAC,EAElFC,GAAiBT,GAA4B,CACjD,IAAMU,EAAmB,CAAC,EAC1B,QAAWF,KAAQD,EAAa,EAAG,CACjC,IAAMI,EAAUX,EAAI,SAASQ,EAAK,WAAW,EAC7C,GAAI,CAACG,GAAW,CAACH,EAAK,OAAO,KAAMzD,GAAUA,EAAM,UAAY4D,CAAO,EAAG,OAAO,KAChFD,EAAO,KAAKC,CAAO,CACrB,CACA,OAAOD,EAAO,KAAK,GAAG,CACxB,EAGME,GAAa,IAA2B,CAC5C,IAAMC,EAAO,IAAI,IACXC,EAAQ,IAAI,IAClB,OAAAjC,EAAK,EAAE,QAAQ,CAACmB,EAAKe,IAAU,CAC7B,GAAI,CAACf,EAAI,OAAQ,OACjB,IAAMgB,EAAcP,GAAcT,CAAG,EACrC,GAAIgB,GAAe,KAAM,OACzB,IAAMC,EAAQJ,EAAK,IAAIG,CAAW,EAC9BC,IAAU,OAAWJ,EAAK,IAAIG,EAAaD,CAAK,GAElDD,EAAM,IAAId,EAAI,IAAKiB,EAAQ,CAAC,EAC5BH,EAAM,IAAIjC,EAAK,EAAEoC,CAAK,EAAG,IAAKF,EAAQ,CAAC,EAE3C,CAAC,EACMD,CACT,EAEMI,GAAa,IACjB,IAAI,IACFrC,EAAK,EACF,OAAQmB,GAAQA,EAAI,QAAUS,GAAcT,CAAG,GAAK,IAAI,EACxD,IAAKA,GAAQA,EAAI,GAAG,CACzB,EAEImB,GAAWnB,GACfO,EAAa,EAAE,QAASC,GAAS,CAC/B,IAAMzD,EAAQyD,EAAK,OAAO,KAAMY,GAAUA,EAAM,UAAYpB,EAAI,SAASQ,EAAK,WAAW,CAAC,EAC1F,OAAOzD,EAAQ,CAAC,CAAE,KAAAyD,EAAM,MAAAzD,CAAM,CAAC,EAAI,CAAC,CACtC,CAAC,EAEGsE,GAAWrB,GACfmB,GAAQnB,CAAG,EAAE,OAAO,CAACsB,EAAO,CAAE,MAAAvE,CAAM,IAAMuE,EAAQ1D,GAAQb,EAAM,UAAU,EAAGa,GAAQa,EAAU,CAAC,CAAC,EAE7F8C,GAAsB,IAAqC,CAC/D,IAAMC,EAASjB,EAAa,EAC5B,GAAI,CAACiB,EAAO,OAAQ,MAAO,CAAC,EAC5B,IAAMC,EAAMD,EAAO,OACjB,CAACE,EAAKlB,IACJkB,EAAI,QAASC,GACXnB,EAAK,OAAO,IAAKzD,IAAW,CAAE,GAAG4E,EAAQ,CAACnB,EAAK,WAAW,EAAGzD,EAAM,OAAQ,EAAE,CAC/E,EACF,CAAC,CAAC,CAAC,CACL,EACM6E,EAAQ,IAAI,IAChB/C,EAAK,EACF,OAAQmB,GAAQA,EAAI,MAAM,EAC1B,IAAIS,EAAa,EACjB,OAAQvD,GAAuBA,GAAO,IAAI,CAC/C,EACA,OAAOuE,EAAI,OAAQI,GAAa,CAACD,EAAM,IAAIJ,EAAO,IAAKhB,GAASqB,EAASrB,EAAK,WAAW,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CACxG,EAGMsB,GAAoBD,GACxBtB,EAAa,EAAE,OACXA,EAAa,EACV,IACEC,GACCA,EAAK,OAAO,KAAMzD,GAAUA,EAAM,UAAY8E,EAASrB,EAAK,WAAW,CAAC,GAAG,MAC3ElC,EAAE,SAAU,CAAE,KAAMkC,EAAK,IAAK,CAAC,CACnC,EACC,KAAK,KAAK,EACblC,EAAE,gBAAgB,EAElByD,GAAU,CAACC,EAAqBrB,IACpC9B,EAAK,EAAE,OAAQmB,GAAQA,EAAI,QAAUA,EAAI,SAASgC,CAAW,IAAMrB,CAAO,EAAE,OAExEsB,GAAgB,IACpBrD,EAAM,EAAE,KAAM4B,GAASA,EAAK,OAAO,KAAMzD,GAAU,CAACc,GAAUd,EAAM,UAAU,CAAC,CAAC,EAE5EmF,GAAclC,GAAsB,CAACnC,GAAUmC,EAAI,MAAM,GAAK,CAACnC,GAAUmC,EAAI,MAAM,EACnFmC,GAAc,IAAetD,EAAK,EAAE,KAAKqD,EAAU,EAEnDE,GAAU,IACd,CAAC7D,GACDQ,EAAO,GACPsD,GAAc,KAAK,GACnB,CAAClD,EAAM,GACPyB,GAAW,EAAE,KAAO,GACpBM,GAAW,EAAE,KAAO,GACpBe,GAAc,GACdE,GAAY,EAGRG,GAAa9B,GAAsE,CACvF,IAAM+B,EAAS5D,EAAU,EAAE,KAAMyC,GAAUA,EAAM,cAAgBZ,EAAK,WAAW,GAAG,QAAU,CAAC,EAC/F,MAAO,CACL,GAAG+B,EACH,GAAG/B,EAAK,OAAO,OAAQzD,GAAU,CAACwF,EAAO,KAAMnB,GAAUA,EAAM,UAAYrE,EAAM,OAAO,CAAC,CAC3F,CACF,EAEMyF,GAAgBR,GAAgC,sBAAsBA,CAAW,OAEjFK,GAAgBI,GAA2B,CAC/C,GAAIpE,EAAM,GACV,KAAMA,EAAM,KACZ,QAASE,GAAYF,EAAM,kBAAoB,GAC/C,SAAU,GACV,YAAa,IAAMmB,GAAM,cAA2Ba,EAAa,GAAG,MAAM,EAC1E,SAAUtB,EACV,EAAAT,EACA,MAAQoE,GAAc,CACpB/D,EAAU,IAAI,CAAC,GAAGA,EAAU,EAAG+D,CAAS,CAAC,EAEzClE,EAAM,UAAY,CAAC,GAAGA,EAAM,UAAWkE,CAAS,EAChD/C,EAAS,CACP,GAAGf,EAAM,EACT,CACE,YAAa8D,EAAU,YACvB,KAAMA,EAAU,KAChB,cAAeA,EAAU,cACzB,YAAa,SACb,OAAQA,EAAU,OAAO,IAAK3F,IAAW,CAAE,GAAGA,EAAO,WAAY,GAAI,EAAE,CACzE,CACF,CAAC,EAEDmD,EAAUsC,GAAaE,EAAU,WAAW,CAAC,CAC/C,CACF,CAAC,EAGKC,EAAWX,GAA8B,CAC7C,IAAMU,EAAY/D,EAAU,EAAE,KAAMyC,GAAUA,EAAM,cAAgBY,CAAW,EAC1EU,IACL/C,EAAS,CACP,GAAGf,EAAM,EACT,CACE,YAAAoD,EACA,KAAMU,EAAU,KAChB,cAAeA,EAAU,cACzB,YAAa,SACb,OAAQ,CAAC,CACX,CACF,CAAC,EACDxC,EAAUsC,GAAaR,CAAW,EAAG3B,EAAa,EACpD,EAEMuC,EAAkBC,GACtB,CACE,GAAI,GAAGxE,EAAM,EAAE,oBACf,OAAQ,CACN,KAAM,iBACN,UAAWC,EAAE,cAAc,EAC3B,QAAS,CAAC,EACV,OAAQ,CAAE,GAAGD,EAAM,eAAgB,OAAQC,EAAE,cAAc,EAAG,OAAQA,EAAE,iBAAiB,CAAE,CAC7F,CACF,EACA,CACE,QAAS,IACPK,EAAU,EACP,OAAQ+D,GAAc,CAAC9D,EAAM,EAAE,KAAM4B,GAASA,EAAK,cAAgBkC,EAAU,WAAW,CAAC,EACzF,IAAKA,IAAe,CACnB,MAAOA,EAAU,YACjB,MAAOA,EAAU,KACjB,YAAaA,EAAU,OAAO,IAAK3F,GAAUA,EAAM,IAAI,EAAE,KAAK,QAAK,CACrE,EAAE,EACN,SAAU,IAAM,CAACwB,GAAYQ,EAAO,GAAKsD,GAAc,KAAK,EAC5D,iBAAkB,GAClB,SAAWS,GAAWH,EAAQG,EAAO,KAAK,EAC1C,GAAIvE,GAAYF,EAAM,gBAClB,CAAE,SAAW0E,GAAkBV,GAAc,MAAMU,CAAK,CAAE,EAC1D,CAAC,CACP,CACF,EAEMC,EAAchB,GAA8B,CAChDrC,EAASf,EAAM,EAAE,OAAQ4B,GAASA,EAAK,cAAgBwB,CAAW,CAAC,EACnEnC,EACEhB,EAAK,EAAE,IAAKmB,GAAQ,CAClB,IAAM6B,EAAW,CAAE,GAAG7B,EAAI,QAAS,EACnC,cAAO6B,EAASG,CAAW,EACpB,CAAE,GAAGhC,EAAK,SAAA6B,CAAS,CAC5B,CAAC,CACH,EACA/C,EAAW,IAAIA,EAAW,EAAE,OAAQmE,GAAOA,IAAOjB,CAAW,CAAC,EAC9D9B,EAAUG,EAAa,CACzB,EAGM6C,EAAe,CAAClB,EAAqBH,IAA6B,CACtE,IAAMrB,EAAO5B,EAAM,EAAE,KAAMwC,GAAUA,EAAM,cAAgBY,CAAW,EACtE,GAAI,CAACxB,EAAM,OACX,IAAM2C,EAAUb,GAAU9B,CAAI,EACxB4C,EAASvB,EAAS,OAAQlB,GAAY,CAACH,EAAK,OAAO,KAAMzD,GAAUA,EAAM,UAAY4D,CAAO,CAAC,EACnG,GAAI,CAACyC,EAAO,OAAQ,OACpB,IAAMC,EAAY7E,EAAM,MAAM,KAAM4C,GAAUA,EAAM,cAAgBY,CAAW,EACzEsB,EAAQF,EAAO,QAASzC,GAAY,CACxC,IAAMmC,EAASK,EAAQ,KAAM/B,IAAUA,GAAM,UAAYT,CAAO,EAChE,GAAI,CAACmC,EAAQ,MAAO,CAAC,EACrB,IAAMS,GAAaF,GAAW,OAAO,KAAMtG,IAAUA,GAAM,UAAY4D,CAAO,GAAG,YAAc,IAC/F,MAAO,CAAC,CAAE,QAAAA,EAAS,KAAMmC,EAAO,KAAM,WAAAS,EAAW,CAAC,CACpD,CAAC,EACKC,EAAS7C,GAAoBwC,EAAQ,UAAW/B,GAAUA,EAAM,UAAYT,CAAO,EACzFhB,EACEf,EAAM,EAAE,IAAKwC,GACXA,EAAM,cAAgBY,EAClB,CACE,GAAGZ,EACH,OAAQ,CAAC,GAAGA,EAAM,OAAQ,GAAGkC,CAAK,EAAE,KAAK,CAACG,EAAGC,KAAMF,EAAMC,EAAE,OAAO,EAAID,EAAME,GAAE,OAAO,CAAC,CACxF,EACAtC,CACN,CACF,CACF,EAEMuC,EAAc,CAAC3B,EAAqBrB,IACxChB,EACEf,EAAM,EAAE,IAAK4B,GACXA,EAAK,cAAgBwB,EACjB,CAAE,GAAGxB,EAAM,OAAQA,EAAK,OAAO,OAAQzD,GAAUA,EAAM,UAAY4D,CAAO,CAAE,EAC5EH,CACN,CACF,EAEIoD,GAAW,CAAC5B,EAAqBrB,EAAiB4C,IACtD5D,EACEf,EAAM,EAAE,IAAK4B,GACXA,EAAK,cAAgBwB,EACjB,CACE,GAAGxB,EACH,OAAQA,EAAK,OAAO,IAAKzD,GACvBA,EAAM,UAAY4D,EAAU,CAAE,GAAG5D,EAAO,WAAAwG,CAAW,EAAIxG,CACzD,CACF,EACAyD,CACN,CACF,EAGIqD,GAAS,CAAChC,EAAkCiC,KAAwB,CACxE,GAAI,GACJ,IAAK,OAAO,EAAE9F,EAAW,GACzB,SAAA6D,EACA,YAAa,KACb,QAAS,KACT,OAAQ,IACR,OAAQ,IACR,OAAQ,GACR,OAAQ,CAAC,EACT,KAAAiC,CACF,GAEMC,GAAS,IAAY,CACzB,IAAM/D,EAAM6D,GAAO,CAAC,EAAG,EAAI,EAC3BhE,EAAQ,CAAC,GAAGhB,EAAK,EAAGmB,CAAG,CAAC,EACxBE,EAAU,iBAAiBF,EAAI,GAAG,qBAAqBA,EAAI,GAAG,gBAAgB,CAChF,EAEMgE,GAAW,IAAY,CAC3B,IAAMC,EAAU1C,GAAoB,EAAE,IAAK2C,GAAQL,GAAOK,EAAK,EAAK,CAAC,EAChED,EAAQ,SACbpE,EAAQ,CAAC,GAAGhB,EAAK,EAAG,GAAGoF,CAAO,CAAC,EAC/B/D,EAAUI,EAAa,aAAa2D,EAAQ,CAAC,EAAG,GAAG,EAAE,CAAC,EACxD,EAEME,GAAajH,GAAsB,CACvC2B,EAAK,IAAIA,EAAK,EAAE,IAAKmB,GAASA,EAAI,MAAQ9C,EAAM,CAAE,GAAG8C,EAAK,KAAM,CAACA,EAAI,IAAK,EAAIA,CAAI,CAAC,CACrF,EAEMoE,GAAalH,GAAsB,CACvC,IAAM8C,EAAMC,EAAM/C,CAAG,EACrB,GAAI,CAAC8C,EAAK,OAGV,GAAIA,EAAI,GAAI,CACVF,GAAU5C,EAAK,CAAE,OAAQ,CAAC8C,EAAI,MAAO,CAAC,EACtC,MACF,CACA,IAAMe,EAAQlC,EAAK,EAAE,QAAQmB,CAAG,EAC1BJ,EAAOf,EAAK,EAAEkC,EAAQ,CAAC,GAAKlC,EAAK,EAAEkC,EAAQ,CAAC,EAClDlB,EAAQhB,EAAK,EAAE,OAAQuC,GAAUA,EAAM,MAAQlE,CAAG,CAAC,EACnDgD,EAAiBI,EAAPV,EAAoB,aAAaA,EAAK,GAAG,GAAmB,QAAjB,EAA4BU,EAAa,UAAU,CAAC,CAC3G,EAEM+D,GAAgBC,GAA0B,CAC9CzE,EACEhB,EAAK,EACF,OAAQmB,GAAQA,EAAI,IAAMsE,CAAM,EAChC,IAAKtE,IAAS,CAAE,GAAGA,EAAK,OAAAsE,CAAO,EAAE,CACtC,EACApE,EAAUI,EAAagE,EAAS,aAAe,aAAa,EAAGhE,EAAa,QAAQ,CAAC,CACvF,EAOMiE,GAAY,CAACvE,EAAUwE,IAAuC,CAClE3F,EAAK,IAAIA,EAAK,EAAE,IAAKuC,GAAWA,EAAM,MAAQpB,EAAI,IAAM,CAAE,GAAGoB,EAAO,OAAAoD,CAAO,EAAIpD,CAAM,CAAC,EACtF5C,EAAQ,CACN,GAAGA,EACH,SAAUA,EAAM,SAAS,IAAKL,GAAaA,EAAQ,KAAO6B,EAAI,GAAK,CAAE,GAAG7B,EAAS,OAAAqG,CAAO,EAAIrG,CAAQ,CACtG,CACF,EAEMsG,GAAc,MAAOzE,EAAU0E,IAA8B,CACjE,IAAMC,EAAY3E,EAAI,OAAO,OAAS,EACtC,GAAI,GAACA,EAAI,IAAM,CAAC3B,EAAM,MAAM,QAAWsG,GAAa,CAACtG,EAAM,MAAM,SAC7D,EAAAe,EAAU,GAAK,CAACsF,EAAK,KAAK,WAAW,QAAQ,GACjD,CAAAtF,EAAU,IAAIY,EAAI,GAAG,EACrBhB,EAAQ,IAAI,IAAI,EAChB,GAAI,CACF,IAAM4F,EAAO,IAAI,SACjBA,EAAK,OAAO,WAAY,iBAAiB,EACzCA,EAAK,OAAO,QAAS5E,EAAI,EAAE,EAC3B4E,EAAK,OAAO,WAAY,OAAO,EAC/BA,EAAK,OAAO,SAAU,OAAO,EAC7BA,EAAK,OAAO,OAAQF,EAAMA,EAAK,IAAI,EACnC,IAAMG,EAAW,MAAM,MAAM,SAAU,CAAE,OAAQ,OAAQ,YAAa,cAAe,KAAAD,CAAK,CAAC,EACrFE,EAAU,MAAMD,EAAS,KAAK,EAAE,MAAM,IAAM,IAAI,EACtD,GAAI,CAACA,EAAS,IAAM,OAAOC,GAAQ,IAAO,SAAU,MAAM,IAAI,MAAMxG,EAAE,aAAa,CAAC,EACpF,IAAMyG,EAAW,MAAMC,GAAmB,4BAA6B,CACrE,GAAIF,EAAO,GACX,aAAcA,EAAO,GACrB,UAAW9E,EAAI,GACf,IAAK0E,EAAK,KACV,QAAS,EACX,CAAC,EACD,GAAI,CAACK,EAAS,GAAI,MAAM,IAAI,MAAMA,EAAS,SAAWA,EAAS,OAAO,CAAC,GAAG,SAAWzG,EAAE,aAAa,CAAC,EACrG,IAAM2G,EAAWjF,EAAI,OAAO,KAAMkF,GAAUA,EAAM,OAAO,EACrDD,GAAU,MAAMD,GAAmB,4BAA6B,CAAE,GAAIC,EAAS,OAAQ,CAAC,EAC5FV,GAAUvE,EAAK,CACb,CAAE,QAAS8E,EAAO,GAAI,aAAcA,EAAO,GAAI,IAAKJ,EAAK,KAAM,QAAS,EAAK,EAC7E,GAAG1E,EAAI,OAAO,OAAQkF,GAAUA,IAAUD,CAAQ,CACpD,CAAC,EACD,SAAS,cACP,IAAI,YAAY,sBAAuB,CAAE,OAAQ,CAAE,KAAM5G,EAAM,KAAM,IAAK,CAACG,EAAM,UAAU,CAAE,CAAE,CAAC,CAClG,CACF,OAAS2G,EAAO,CACdnG,EAAQ,IAAImG,aAAiB,MAAQA,EAAM,QAAU7G,EAAE,aAAa,CAAC,CACvE,QAAE,CACAc,EAAU,IAAI,IAAI,CACpB,EACF,EAEMgG,GAAc,MAAOpF,GAA4B,CACrD,IAAMqF,EAAUrF,EAAI,OAAO,CAAC,EAE5B,GADI,CAACqF,GAAW,CAAChH,EAAM,MAAM,QAAUe,EAAU,GAC7C,CAAC,WAAW,QAAQd,EAAE,oBAAoB,CAAC,EAAG,OAClDc,EAAU,IAAIY,EAAI,GAAG,EACrBhB,EAAQ,IAAI,IAAI,EAChB,IAAMsG,EAAS,MAAMN,GAAmB,4BAA6B,CAAE,GAAIK,EAAQ,OAAQ,CAAC,EAAE,MAC5F,IAAM,IACR,EAEA,GADAjG,EAAU,IAAI,IAAI,EACd,CAACkG,GAAQ,GAAI,CACftG,EAAQ,IAAIsG,GAAU,CAACA,EAAO,GAAMA,EAAO,SAAWhH,EAAE,YAAY,EAAKA,EAAE,YAAY,CAAC,EACxF,MACF,CACA,IAAMiH,EAAOvF,EAAI,OAAO,MAAM,CAAC,EAE/BuE,GACEvE,EACAuF,EAAK,IAAI,CAACL,EAAOnE,KAAW,CAAE,GAAGmE,EAAO,QAASnE,IAAU,CAAE,EAAE,CACjE,CACF,EAEMyE,GAAa,CAACxF,EAAUyF,IAC5BnG,EAAO,KACLU,EAAI,OAAO,IAAKkF,IAAW,CACzB,IAAK5H,GAAS4H,EAAM,aAAc,OAAO,EACzC,UAAW5H,GAAS4H,EAAM,aAAc,OAAO,EAC/C,IAAKA,EAAM,KAAO5G,EAAE,OAAO,EAC3B,QAAS4G,EAAM,GACjB,EAAE,EACF,EACAO,CACF,EAEIC,GAAQ,IAAY,CACxBjH,EAAU,IAAID,EAAM,SAAS,EAC7BG,EAAU,IAAIH,EAAM,SAAS,EAC7BI,EAAM,IAAI9B,GAAM0B,EAAM,KAAK,CAAC,EAC5BK,EAAK,IAAIZ,GAAOO,EAAM,QAAQ,CAAC,EAC/BS,EAAU,IAAI,CAAC,CAAC,EAChBD,EAAQ,IAAI,IAAI,EAChBG,EAAM,IAAI,EAAK,CACjB,EAEMwG,GAAO,SAA2B,CACtC,GAAIvD,GAAQ,EAAG,OACfrD,EAAO,IAAI,EAAI,EACfC,EAAQ,IAAI,IAAI,EAChBC,EAAU,IAAI,CAAC,CAAC,EAChB,IAAM2G,EAAW/G,EAAK,EAChByG,EAAS,MAAMN,GAIlB3G,EAAM,aAAc,CACrB,WAAYG,EAAM,WAClB,MAAOI,EAAM,EAAE,IAAK4B,IAAU,CAC5B,YAAaA,EAAK,YAClB,OAAQA,EAAK,OAAO,IAAKzD,IAAW,CAAE,QAASA,EAAM,QAAS,WAAYA,EAAM,WAAW,KAAK,CAAE,EAAE,CACtG,EAAE,EACF,SAAU6I,EAAS,IAAK5F,IAAS,CAC/B,GAAIA,EAAI,IAAM,KACd,SAAUA,EAAI,SACd,YAAaA,EAAI,YACjB,QAASA,EAAI,QACb,OAAQA,EAAI,OACZ,OAAQA,EAAI,OACZ,OAAQA,EAAI,MACd,EAAE,CACJ,CAAC,EAAE,MAAM,IAAM,IAAI,EAEnB,GADAjB,EAAO,IAAI,EAAK,EACZ,CAACuG,EAAQ,CACXtG,EAAQ,IAAIV,EAAE,YAAY,CAAC,EAC3B,MACF,CACA,GAAI,CAACgH,EAAO,GAAI,CACd,IAAMO,EAAgC,CAAC,EACvC,QAAWC,KAASR,EAAO,OAAQ,CACjC,IAAMS,EAAQ,+BAA+B,KAAKD,EAAM,OAAS,EAAE,EAC7D9F,EAAM+F,EAAQH,EAAS,OAAOG,EAAM,CAAC,CAAC,CAAC,EAAI,OAC7C/F,IAAK6F,EAAM7F,EAAI,GAAG,EAAI8F,EAAM,SAAWA,EAAM,KACnD,CACA7G,EAAU,IAAI4G,CAAK,EACnB7G,EAAQ,IAAIsG,EAAO,SAAWA,EAAO,OAAO,CAAC,GAAG,SAAWhH,EAAE,YAAY,CAAC,EACtE,OAAO,KAAKuH,CAAK,EAAE,QACrBhH,EAAK,IAAIA,EAAK,EAAE,IAAKmB,GAAS6F,EAAM7F,EAAI,GAAG,EAAI,CAAE,GAAGA,EAAK,KAAM,EAAK,EAAIA,CAAI,CAAC,EAC/E,MACF,CACA,IAAMJ,EAAO0F,EAAO,MAAM,MACtB1F,IAAMpB,EAAQ1B,GAAM8C,CAAI,GAC5B8F,GAAM,EACNxG,EAAO,IAAIZ,EAAE,QAAS,CAAE,QAASgH,EAAO,MAAM,SAAW,EAAG,SAAUA,EAAO,MAAM,UAAY,CAAE,CAAC,CAAC,EACnG,SAAS,cACP,IAAI,YAAY,sBAAuB,CAAE,OAAQ,CAAE,KAAMjH,EAAM,KAAM,IAAK,CAACG,EAAM,UAAU,CAAE,CAAE,CAAC,CAClG,CACF,EAIMwH,GAAWC,GAAuB,CACtC,IAAMC,EAASD,EAAM,kBAAkB,QAAUA,EAAM,OAAO,QAAQ,QAAQ,EAAI,KAClF,GAAI,CAACC,GAAUA,EAAO,OAASrJ,GAAUqJ,EAAO,UAAY,CAACC,GAAeD,EAAO,KAAK,EAAG,OAC3F,GAAM,CAACE,EAAQlJ,EAAM,EAAE,EAAIgJ,EAAO,MAAM,MAAM,GAAG,EACjD,GAAIE,IAAW,YAAajC,GAAUjH,CAAG,UAChCkJ,IAAW,YAAa,CAC/B,IAAM5F,EAAO5B,EAAM,EAAE,KAAMwC,GAAUA,EAAM,cAAgBlE,CAAG,EAC1DsD,GACF0C,EACEhG,EACAoF,GAAU9B,CAAI,EAAE,IAAKzD,GAAUA,EAAM,OAAO,CAC9C,EACFmD,EAAUsC,GAAatF,CAAG,CAAC,CAC7B,SAAWkJ,IAAW,aAAcpD,EAAW9F,CAAG,UACzCkJ,IAAW,YAAahC,GAAUlH,CAAG,UACrCkJ,IAAW,WAAYpC,GAAS,UAChCoC,IAAW,SAAUrC,GAAO,UAC5BqC,IAAW,cAAe/B,GAAa,EAAI,UAC3C+B,IAAW,aAAc/B,GAAa,EAAK,UAC3C+B,IAAW,cAAe,CACjC,IAAMpG,EAAMC,EAAM/C,CAAG,EACjB8C,GAAUoF,GAAYpF,CAAG,CAC/B,MAAWoG,IAAW,UACpBV,GAAM,EACNxF,EAAUG,EAAa,EAE3B,EAEM8F,GAAkBpJ,GAA2BA,EAAM,WAAW,YAAY,GAAMwB,GAAY,CAACQ,EAAO,EAEpGsH,GAAYJ,GAAuB,CACvC,IAAMK,EAAUL,EAAM,OACtB,GAAI,GAAC1H,GAAYQ,EAAO,IACxB,GAAIuH,aAAmB,kBAAoBA,EAAQ,OAAS,WAAY,CACtE,IAAMP,EAAQ,qBAAqB,KAAKO,EAAQ,IAAI,EACpD,GAAI,CAACP,EAAO,OACRO,EAAQ,QAASpD,EAAa6C,EAAM,CAAC,EAAI,CAACO,EAAQ,KAAK,CAAC,EACvD3C,EAAYoC,EAAM,CAAC,EAAIO,EAAQ,KAAK,CAC3C,SAAWA,aAAmB,kBAAmB,CAC/C,GAAM,CAACC,EAAMrJ,EAAK8E,CAAW,EAAIsE,EAAQ,KAAK,MAAM,GAAG,EACjDtG,EAAMuG,IAAS,SAAWrJ,EAAM+C,EAAM/C,CAAG,EAAI,OAC/C8C,GAAOgC,GACTlC,GAAUE,EAAI,IAAK,CAAE,SAAU,CAAE,GAAGA,EAAI,SAAU,CAACgC,CAAW,EAAGsE,EAAQ,KAAM,CAAE,CAAC,CACtF,SAAWA,aAAmB,kBAAoBA,EAAQ,OAAS,OAAQ,CACzE,GAAM,CAACC,EAAMrJ,CAAG,EAAIoJ,EAAQ,KAAK,MAAM,GAAG,EACpCtG,EAAMuG,IAAS,SAAWrJ,EAAM+C,EAAM/C,CAAG,EAAI,OAC7CwH,EAAO4B,EAAQ,QAAQ,CAAC,EAC9BA,EAAQ,MAAQ,GACZtG,GAAO0E,GAAWD,GAAYzE,EAAK0E,CAAI,CAC7C,EACF,EAEM8B,GAAWP,GAAuB,CACtC,IAAMK,EAAUL,EAAM,OACtB,GAAI,CAAC1H,GAAYQ,EAAO,GAAK,EAAEuH,aAAmB,kBAAmB,OACrE,GAAM,CAACC,EAAMrJ,EAAKG,CAAI,EAAIiJ,EAAQ,KAAK,MAAM,GAAG,EAChD,GAAI,GAACpJ,GAAO,CAACG,IACb,GAAIkJ,IAAS,QAAS3C,GAAS1G,EAAKG,EAAMiJ,EAAQ,KAAK,UAC9CC,IAAS,SAAW,CAAC,cAAe,UAAW,SAAU,QAAQ,EAAE,SAASlJ,CAAI,EAAG,CAC1F,IAAMoJ,EAAepJ,IAAS,UAAYA,IAAS,SACnDyC,GAAU5C,EAAK,CAAE,CAACG,CAAI,EAAGoJ,EAAeH,EAAQ,MAAQA,EAAQ,OAAS,IAAK,CAAiB,CACjG,EACF,EAGMI,GAAiBrJ,GAAqC,CAC1D,GAAM,CAACkJ,EAAMrJ,EAAKyJ,CAAK,EAAItJ,EAAK,MAAM,GAAG,EACzC,GAAI,CAACH,GAAO,CAACyJ,EAAO,OACpB,GAAIJ,IAAS,QACX,OAAO3H,EAAM,EACV,KAAM4B,GAASA,EAAK,cAAgBtD,CAAG,GACtC,OAAO,KAAMH,GAAUA,EAAM,UAAY4J,CAAK,GAAG,WACvD,IAAM3G,EAAMC,EAAM/C,CAAG,EACrB,GAAK8C,EACL,IAAIuG,IAAS,QAAS,OAAOvG,EAAI,SAAS2G,CAAK,GAAK,GACpD,GAAIJ,IAAS,QAAS,OAAO,OAAOvG,EAAI2G,CAAiB,GAAK,EAAE,EAElE,EAKMC,GAAe,IAAY,CAC/B,GAAKpH,EACL,SAAWqH,KAASrH,EAAK,iBAAmC,yCAAyC,EAAG,CACtG,IAAMwC,EAAc6E,EAAM,KAAK,MAAM,EAAkB,EAAE,EACnDC,EACJlI,EAAM,EACH,KAAM4B,GAASA,EAAK,cAAgBwB,CAAW,GAC9C,OAAO,KAAMjF,GAAUA,EAAM,UAAY8J,EAAM,KAAK,GAAK,GAC3DA,EAAM,UAAYC,IAAMD,EAAM,QAAUC,EAC9C,CACA,QAAWR,KAAW9G,EAAK,iBACzB,sEACF,EAAG,CACD,GAAI8G,IAAY,SAAS,eAAiBA,aAAmB,iBAAkB,SAC/E,IAAMQ,EAAOJ,GAAcJ,EAAQ,IAAI,EACnCQ,IAAS,QAAaR,EAAQ,QAAUQ,IAAMR,EAAQ,MAAQQ,EACpE,EACF,EAGMC,GAAcvG,GAAsC,CACxD,IAAMwG,EAAMxG,EAAK,OAAO,OAAQzD,GAAU,CAACc,GAAUd,EAAM,UAAU,GAAKa,GAAQb,EAAM,UAAU,IAAM,CAAC,EACnGkK,EAAUzG,EAAK,OAAO,KAAMzD,GAAU,CAACc,GAAUd,EAAM,UAAU,CAAC,EACxE,OACEJ,EAAC,OAAI,UAAQ,wBAAwB,iBAAgB6D,EAAK,YACvD,SAAA0G,GAAW,CACV,QAASF,EAAI,OACT1I,EAAE,iBAAkB,CAClB,KAAM0I,EAAI,IAAKjK,GAAU,GAAGA,EAAM,IAAI,IAAIe,GAAOf,EAAM,UAAU,CAAC,EAAE,EAAE,KAAK,IAAI,CACjF,CAAC,EACDuB,EAAE,iBAAiB,EACvB,KAAM2I,GAAWnI,EAAW,EAAE,SAAS0B,EAAK,WAAW,EACvD,KACE7D,EAAC,OAAI,UAAQ,8BACV,SAAAwK,GACC3G,EAAK,OACJzD,GAAUA,EAAM,QAChBA,GACCqK,GAAW,CACT,GAAI,GAAG/I,EAAM,EAAE,UAAUmC,EAAK,WAAW,IAAIzD,EAAM,OAAO,GAC1D,KAAM,SAASyD,EAAK,WAAW,IAAIzD,EAAM,OAAO,GAChD,MAAOA,EAAM,KACb,MAAOA,EAAM,WACb,KAAM,MACN,SAAU,CAACwB,GAAYQ,EAAO,EAC9B,KAAMT,EAAE,YAAa,CAAE,MAAOyD,GAAQvB,EAAK,YAAazD,EAAM,OAAO,CAAE,CAAC,EACxE,MAAOc,GAAUd,EAAM,UAAU,EAAI,KAAOuB,EAAE,eAAe,CAC/D,CAAC,CACL,EACF,CAEJ,CAAC,EACH,CAEJ,EAEM+I,GAAY7G,GAA4C,CAC5D,IAAM2C,EAAUb,GAAU9B,CAAI,EACxB8G,EAAQ/I,EAAW4E,EAAU3C,EAAK,OAClC+G,EAAW,IAAI,IAAI/G,EAAK,OAAO,IAAKzD,GAAUA,EAAM,OAAO,CAAC,EAC5DyK,EAAYhH,EAAK,gBAAkB,aACzC,OACE5D,EAAC,MAAG,UAAQ,sBAAsB,aAAY,OAAO,CAAC4D,EAAK,OAAO,MAAM,EACtE,UAAA5D,EAAC,OAAI,UAAQ,2BACV,UAAA6K,GAAc,CACb,GAAI,GAAGpJ,EAAM,EAAE,WAAWmC,EAAK,WAAW,GAC1C,KAAM,UAAUA,EAAK,WAAW,GAChC,MAAOA,EAAK,KACZ,KAAM,OACN,SAAU,CAACjC,GAAYQ,EAAO,EAC9B,QAASuI,EAAM,IAAKvK,IAAW,CAC7B,MAAOA,EAAM,QACb,MAAOA,EAAM,KACb,QAASwK,EAAS,IAAIxK,EAAM,OAAO,CACrC,EAAE,EACF,KAAMyK,EACFlJ,EAAE,eAAe,EACjBkC,EAAK,OAAO,OACVlC,EAAE,iBAAkB,CAAE,MAAOkC,EAAK,OAAO,OAAQ,MAAO2C,EAAQ,MAAO,CAAC,EACxE7E,EAAE,YAAY,CACtB,CAAC,EACD1B,EAAC,OAAI,UAAQ,8BACV,UAAA4K,EAAYE,GAAM,CAAE,MAAOpJ,EAAE,WAAW,EAAG,KAAM,SAAU,CAAC,EAAI,KAChEC,GAAYgJ,EAAS,KAAOpE,EAAQ,OACjCwE,EAAO,CACL,KAAM9K,EACN,MAAO,aAAa2D,EAAK,WAAW,GACpC,MAAOlC,EAAE,WAAW,EACpB,QAAS,WACT,KAAM,UACN,SAAUS,EAAO,CACnB,CAAC,EACD,KACHR,EACGqJ,GAAW,CACT,KAAM/K,EACN,MAAO,cAAc2D,EAAK,WAAW,GACrC,MAAOlC,EAAE,uBAAwB,CAAE,KAAMkC,EAAK,IAAK,CAAC,EACpD,KAAMqH,GAAK,GAAG,EACd,KAAM,UACN,SAAU9I,EAAO,CACnB,CAAC,EACD,MACN,GACF,EACCyB,EAAK,OAAO,OAASuG,GAAWvG,CAAI,EAAI,MAC3C,CAEJ,EAEMsH,GAAiB,IACrBC,GAAQ,CACN,QAASzJ,EAAE,gBAAgB,EAC3B,MAAOA,EAAE,iBAAiB,EAC1B,YAAaA,EAAE,gBAAgB,EAC/B,QAASC,EACP5B,EAAC,OAAI,UAAQ,wBAAwB,GAAI,GAAG0B,EAAM,EAAE,oBACjD,SAAAuE,EAAgB,KAAK,EACxB,EACE,OACJ,KACEhG,EAAC,OAAI,UAAQ,sBACV,UAAAgC,EAAM,EAAE,OACPjC,EAAC,MAAG,UAAQ,uBACT,SAAAwK,GACCvI,EAAM,EACL4B,GAASA,EAAK,YACdA,GAAS6G,GAAS7G,CAAI,CACzB,EACF,EAEAwH,GAAW,CACT,MAAO1J,EAAE,iBAAiB,EAC1B,QAASC,EAAWD,EAAE,qBAAqB,EAAI,EACjD,CAAC,EAEF+D,GAAc,KAAK,GACtB,CAEJ,CAAC,EAEG4F,GAAW,CAACjI,EAAUxC,IAAsC,CAChE,IAAM0H,EAAQlF,EAAI,OAAO,CAAC,EAC1B,OAAOkF,EACLgD,GAAc,CACZ,KAAM,CACJ,IAAK5K,GAAS4H,EAAM,aAAc,OAAO,EACzC,UAAW5H,GAAS4H,EAAM,aAAc,OAAO,EAC/C,IAAKA,EAAM,KAAO5G,EAAE,OAAO,CAC7B,EACA,OAAQD,EAAM,eACd,KAAAb,EACA,OAASiI,GAAYD,GAAWxF,EAAKyF,CAAO,CAC9C,CAAC,EAED9I,EAAC,QAAK,UAAQ,iBAAiB,YAAWa,EACvC,SAAAA,IAAS,QAAU,GAAKc,EAAE,OAAO,EACpC,CAEJ,EAEM6J,GAAa,CAACnI,EAAUoI,IAA6B,CAEzD,IAAMC,EAAY9J,GAAYF,EAAM,MAAM,SAAW,CAAC2B,EAAI,OAAO,QAAU3B,EAAM,MAAM,QACjFiK,EAAOlJ,EAAU,IAAMY,EAAI,IACjC,OACEpD,EAAC,OACC,KAAK,QACL,aAAY0B,EAAE,OAAO,EACrB,UAAQ,uBACR,YAAW,OAAOe,EAAS,IAAMW,EAAI,GAAG,EACxC,YAAW,OAAOsI,CAAI,EACtB,WAAarC,GAAqB,CAC5B,CAACoC,GAAa,CAACrI,EAAI,KACvBiG,EAAM,eAAe,EACrB5G,EAAS,IAAIW,EAAI,GAAG,EACtB,EACA,YAAciG,GAAqB,CACjC,IAAMsC,EAAUtC,EAAM,cAClBsC,aAAmB,MAAStC,EAAM,cAA0B,SAASsC,CAAO,GAChFlJ,EAAS,IAAI,IAAI,CACnB,EACA,OAAS4G,GAAqB,CAC5B,GAAI,CAACoC,GAAa,CAACrI,EAAI,GAAI,OAC3BiG,EAAM,eAAe,EACrB5G,EAAS,IAAI,IAAI,EACjB,IAAMqF,EAAOuB,EAAM,cAAc,QAAQ,CAAC,EACtCvB,GAAWD,GAAYzE,EAAK0E,CAAI,CACtC,EAEC,UAAAuD,GAASjI,EAAK,OAAO,EACtBpD,EAAC,OAAI,UAAQ,+BACV,UAAAyL,GAAarI,EAAI,GACdwI,GAAS,CACP,GAAI,GAAGJ,CAAM,SACb,KAAM,SAASpI,EAAI,GAAG,GACtB,MAAOA,EAAI,OAAO,OAAS1B,EAAE,cAAc,EAAIA,EAAE,aAAa,EAC9D,OAAQb,GACR,SAAU6K,GAAQvJ,EAAO,EACzB,OAAQT,EAAE,WAAW,EACrB,KAAM,MACR,CAAC,EACD,KACH+J,GAAa,CAACrI,EAAI,GAAKrD,EAAC,SAAM,UAAQ,sBAAuB,SAAA2B,EAAE,gBAAgB,EAAE,EAAW,KAC5FC,GAAYF,EAAM,MAAM,QAAU2B,EAAI,OAAO,OAC1C2H,EAAO,CACL,KAAM9K,EACN,MAAO,eAAemD,EAAI,GAAG,GAC7B,MAAO1B,EAAE,aAAa,EACtB,QAAS,WACT,KAAM,UACN,SAAUgK,GAAQvJ,EAAO,CAC3B,CAAC,EACD,MACN,GACF,CAEJ,EAEM0J,GAAW,CAACzI,EAAUoI,EAAgBM,IAA8B,CACxE,IAAM3L,EAAQ,OAAOiD,EAAI0I,CAAK,GAAK,EAAE,EAC/BC,EAAS,CACb,GAAI,GAAGP,CAAM,IAAIM,CAAK,GACtB,KAAM,SAAS1I,EAAI,GAAG,IAAI0I,CAAK,GAC/B,MAAOpK,EAAEoK,CAAK,EACd,MAAA3L,EACA,SAAU,CAACwB,GAAYQ,EAAO,CAChC,EACA,OAAO2J,IAAU,UAAYA,IAAU,SACnCE,EAAM,CACJ,GAAGD,EACH,KAAM,UACN,KAAM,MACN,MAAO9K,GAAUd,CAAK,EAAI,KAAOuB,EAAE,eAAe,CACpD,CAAC,EACDuK,GAAUF,CAAM,CACtB,EAEMG,GAAY,CAAC9I,EAAUc,EAA4BgD,IAClD9D,EAAI,OACLc,EAAM,IAAId,EAAI,GAAG,EACZ0H,GAAM,CAAE,MAAOpJ,EAAE,YAAa,CAAE,IAAKwC,EAAM,IAAId,EAAI,GAAG,CAAE,CAAC,EAAG,KAAM,QAAS,CAAC,EACjFkC,GAAWlC,CAAG,EAAU0H,GAAM,CAAE,MAAOpJ,EAAE,YAAY,EAAG,KAAM,QAAS,CAAC,EACxEwF,EAAK,IAAI9D,EAAI,GAAG,EAAU0H,GAAM,CAAE,MAAOpJ,EAAE,YAAY,EAAG,KAAM,SAAU,CAAC,EAC1E0B,EAAI,GACF,KADa0H,GAAM,CAAE,MAAOpJ,EAAE,KAAK,EAAG,KAAM,MAAO,CAAC,EALnCoJ,GAAM,CAAE,MAAOpJ,EAAE,UAAU,EAAG,KAAM,SAAU,CAAC,EASnEyK,GAAU,CACd/I,EACAe,EACAD,EACAgD,IACmB,CACnB,IAAMsE,EAAS,GAAG/J,EAAM,EAAE,QAAQ2B,EAAI,GAAG,GAEnCgJ,EAAU,GAAGZ,CAAM,SACnBa,EAAY9H,GAAQnB,CAAG,EAAE,OAAO,CAAC,CAAE,MAAAjD,CAAM,IAAMa,GAAQb,EAAM,UAAU,IAAM,CAAC,EAC9E+I,EAAQ7G,EAAU,EAAEe,EAAI,GAAG,EACjC,OACEpD,EAAC,WACC,UAAQ,qBACR,KAAK,WACL,cAAa,OAAOoD,EAAI,MAAM,EAC9B,YAAW,OAAOA,EAAI,IAAI,EAC1B,eAAc,OAAOc,EAAM,IAAId,EAAI,GAAG,GAAK,EAAQ8F,GAAU5D,GAAWlC,CAAG,CAAC,EAE5E,UAAApD,EAAC,OAAI,UAAQ,0BACX,UAAAD,EAAC,QAAK,UAAQ,4BAA6B,aAAIoE,EAAQ,CAAC,GAAG,EAC1DkH,GAASjI,EAAK,OAAO,EACtBpD,EAAC,OAAI,UAAQ,2BACX,UAAAD,EAAC,UAAO,GAAIqM,EAAS,kBAAiB,OAAOlF,EAAK,IAAI9D,EAAI,GAAG,CAAC,EAC3D,SAAA8B,GAAiB9B,EAAI,QAAQ,EAChC,EACCA,EAAI,YAAcrD,EAAC,SAAO,SAAAqD,EAAI,YAAY,EAAW,MACxD,EACApD,EAAC,OAAI,UAAQ,0BACV,UAAAkM,GAAU9I,EAAKc,EAAOgD,CAAI,EAC3BnH,EAAC,QAAK,UAAQ,2BAA4B,SAAAe,GAAM2D,GAAQrB,CAAG,CAAC,EAAE,GAChE,EACApD,EAAC,OAAI,UAAQ,6BACV,UAAA+K,EAAO,CACN,KAAM9K,EACN,MAAO,aAAamD,EAAI,GAAG,GAC3B,MAAOA,EAAI,KAAO1B,EAAE,UAAU,EAAeA,EAAXC,EAAa,OAAY,SAAN,EACrD,QAAS,WACT,KAAM,UACN,SAAUyB,EAAI,KACd,SAAUA,EAAI,KAAOoI,EAAS,KAC9B,YAAaY,CACf,CAAC,EACAzK,EACGoJ,EAAO,CACL,KAAM9K,EACN,MAAO,aAAamD,EAAI,GAAG,GAC3B,MAAQA,EAAI,GAAmBA,EAAI,OAAS1B,EAAE,SAAS,EAAIA,EAAE,SAAS,EAArDA,EAAE,QAAQ,EAC3B,QAAS,WACT,KAAM,UACN,SAAUS,EAAO,EACjB,YAAaiK,CACf,CAAC,EACD,MACN,GACF,EACClD,EAAQnJ,EAAC,KAAE,UAAQ,2BAA4B,SAAAmJ,EAAM,EAAO,KAC5D9F,EAAI,KACHpD,EAAC,OAAI,UAAQ,0BAA0B,GAAIwL,EACxC,UAAAD,GAAWnI,EAAKoI,CAAM,EACvBxL,EAAC,OAAI,UAAQ,wBACV,UAAAuK,GACC5G,EAAa,EACZC,GAASA,EAAK,YACdA,GACC0I,GAAO,CACL,GAAI,GAAGd,CAAM,IAAI5H,EAAK,WAAW,GACjC,KAAM,SAASR,EAAI,GAAG,IAAIQ,EAAK,WAAW,GAC1C,MAAOA,EAAK,KACZ,MAAOR,EAAI,SAASQ,EAAK,WAAW,GAAK,GACzC,SAAU,CAACjC,GAAY,CAACyB,EAAI,QAAUjB,EAAO,EAC7C,QAAS,CACP,CAAE,MAAO,GAAI,MAAOT,EAAE,SAAU,CAAE,KAAMkC,EAAK,IAAK,CAAC,CAAE,EACrD,GAAGA,EAAK,OAAO,IAAKzD,IAAW,CAAE,MAAOA,EAAM,QAAS,MAAOA,EAAM,IAAK,EAAE,CAC7E,CACF,CAAC,CACL,EACC0L,GAASzI,EAAKoI,EAAQ,aAAa,EACnCK,GAASzI,EAAKoI,EAAQ,SAAS,EAC/BK,GAASzI,EAAKoI,EAAQ,QAAQ,EAC9BK,GAASzI,EAAKoI,EAAQ,QAAQ,GACjC,EACAxL,EAAC,KAAE,UAAQ,2BACR,WACC,GAAG0B,EAAE,WAAW,CAAC,IAAIZ,GAAME,GAAQa,EAAU,CAAC,CAAC,CAAC,GAChD,GAAGwK,EAAU,IACX,CAAC,CAAE,KAAAzI,EAAM,MAAAzD,CAAM,IAAM,GAAGyD,EAAK,IAAI,IAAIzD,EAAM,IAAI,KAAKW,GAAME,GAAQb,EAAM,UAAU,CAAC,CAAC,EACtF,CACF,EAAE,KAAK,QAAK,EACX,MAAMW,GAAM2D,GAAQrB,CAAG,CAAC,CAAC,IAC5B,GACF,EACE,MACN,CAEJ,EAEMmJ,GAAeC,GAAqD,CACxE,IAAMC,EAAQD,EAAQ,MAAM,EAAGrL,EAAO,EAAE,IAAI+D,EAAgB,EACtDyD,EAAO6D,EAAQ,OAASC,EAAM,OACpC,OAAOC,GAAO,CACZ,KAAM,OACN,MAAOhL,EAAE,UAAW,CAAE,MAAO8K,EAAQ,MAAO,CAAC,EAC7C,QAAS7D,EAAO,EAAI,GAAG8D,EAAM,KAAK,IAAI,CAAC,IAAI/K,EAAE,cAAe,CAAE,MAAOiH,CAAK,CAAC,CAAC,GAAK8D,EAAM,KAAK,IAAI,EAChG,QAAS1B,EAAO,CACd,KAAM9K,EACN,MAAO,WACP,MAAOyB,EAAE,WAAY,CAAE,MAAO8K,EAAQ,MAAO,CAAC,EAC9C,QAAS,UACT,KAAM,UACN,QAASvB,GAAK,MAAM,EACpB,SAAU9I,EAAO,CACnB,CAAC,CACH,CAAC,CACH,EAEMwK,GAAe,IAAsB,CACzC,IAAMzI,EAAQF,GAAW,EACnBkD,EAAO5C,GAAW,EAClBkI,EAAU7K,EAAWgD,GAAoB,EAAI,CAAC,EAC9CiI,EAAc3K,EAAK,EAAE,OAAQmB,GAAQA,EAAI,MAAM,EAAE,OACjDyJ,EAAUlL,IAAagC,EAAa,EAAE,OAAS,GAAK1B,EAAK,EAAE,OAAS,GAC1E,OAAOkJ,GAAQ,CACb,QAASzJ,EAAE,cAAc,EACzB,MAAOA,EAAE,eAAe,EACxB,YAAaO,EAAK,EAAE,OAASP,EAAE,gBAAiB,CAAE,OAAQkL,EAAa,MAAO3K,EAAK,EAAE,MAAO,CAAC,EAAI,KACjG,QAAS4K,EACP7M,EAAC,OAAI,UAAQ,yBACV,UAAA+K,EAAO,CACN,KAAM9K,EACN,MAAO,SACP,MAAOyB,EAAE,YAAY,EACrB,QAAS,YACT,KAAM,UACN,QAASuJ,GAAK,MAAM,EACpB,SAAU9I,EAAO,CACnB,CAAC,EACAF,EAAK,EAAE,KAAMmB,GAAQA,EAAI,IAAM,CAACA,EAAI,MAAM,EACvC2H,EAAO,CACL,KAAM9K,EACN,MAAO,cACP,MAAOyB,EAAE,aAAa,EACtB,QAAS,WACT,KAAM,UACN,SAAUS,EAAO,CACnB,CAAC,EACD,KACHyK,EACG7B,EAAO,CACL,KAAM9K,EACN,MAAO,aACP,MAAOyB,EAAE,YAAY,EACrB,QAAS,WACT,KAAM,UACN,SAAUS,EAAO,CACnB,CAAC,EACD,MACN,EACE,OACJ,KACEnC,EAAC,OAAI,UAAQ,sBACV,UAAAwM,EAAQ,OAASD,GAAYC,CAAO,EAAI,KACxCvK,EAAK,EAAE,OACNlC,EAAC,OAAI,UAAQ,sBAAsB,KAAK,OACrC,SAAAwK,GACCtI,EAAK,EACJmB,GAAQA,EAAI,IACZA,GAAQ+I,GAAQ/I,EAAKnB,EAAK,EAAE,QAAQmB,CAAG,EAAGc,EAAOgD,CAAI,CACxD,EACF,EACEsF,EAAQ,OAAS,KACnBpB,GAAW,CACT,MAAO1J,EAAE,eAAe,EACxB,QAASiC,EAAa,EAAE,OAASjC,EAAE,mBAAmB,EAAIA,EAAE,wBAAwB,CACtF,CAAC,GAEL,CAEJ,CAAC,CACH,EAEMoL,GAAa,IAAgB,CACjC,IAAM5I,EAAQF,GAAW,EAAE,KACrB+I,EAAY9K,EAAK,EAAE,OACtBmB,GAAQA,EAAI,IAAM,CAACA,EAAI,QAAUxB,EAAM,SAAS,KAAMoL,GAAMA,EAAE,KAAO5J,EAAI,EAAE,GAAG,MACjF,EAAE,OACI6J,EAAWhL,EAAK,EAAE,OAAQmB,GAAQ,CAACA,EAAI,IAAMA,EAAI,MAAM,EAAE,OACzD8J,EAAQ,CACZD,EAAWvL,EAAE,gBAAiB,CAAE,MAAOuL,CAAS,CAAC,EAAI,GACrDF,EAAYrL,EAAE,iBAAkB,CAAE,MAAOqL,CAAU,CAAC,EAAI,EAC1D,EAAE,OAAO,OAAO,EACV1C,EAAUhF,GAAc,GAAKE,GAAY,EAC/C,OACEvF,EAAC,UAAO,UAAQ,wBACd,UAAAD,EAAC,KACC,UAAQ,yBACR,YAAWmE,GAASmG,EAAU,SAAW,UACzC,YAAU,SAET,SAAAlI,EAAO,EACJT,EAAE,QAAQ,EACVwC,EACExC,EAAE,kBAAkB,EACpB2I,EACE3I,EAAE,gBAAgB,EACjBY,EAAO,IAAMC,EAAM,EAAI2K,EAAM,KAAK,QAAK,GAAKxL,EAAE,gBAAgB,EAAI,IAC7E,EACCC,EACGoJ,EAAO,CACL,KAAM9K,EACN,MAAO,QACP,MAAOyB,EAAE,OAAO,EAChB,QAAS,YACT,KAAM,UACN,SAAU,CAACa,EAAM,GAAKJ,EAAO,CAC/B,CAAC,EACD,MACN,CAEJ,EAIMgL,GAAWC,GAAsB3L,EAAM,EAAE,EACzC4L,GAAa,IACjB,SAAS,cAAiC,gBAAgBF,EAAQ,IAAI,EAExE,MAAO,CACL,KAAM,IAGJnN,EAAC,OACC,UAAQ,iBACR,GAAIyB,EAAM,GACV,YAAWU,EAAO,EAAI,OAAS,KAC/B,QAASiH,GACT,SAAUK,GACV,QAASG,GAER,UAAAjI,EAAW,KAAO+K,GAAO,CAAE,MAAOhL,EAAE,UAAU,EAAG,QAAS,EAAG,CAAC,EAC9DU,EAAQ,EAAIsK,GAAO,CAAE,MAAOhL,EAAE,YAAY,EAAG,QAASU,EAAQ,GAAK,GAAI,KAAM,QAAS,CAAC,EAAI,KAC3F8I,GAAe,EACfyB,GAAa,EACbG,GAAW,EACZ/M,EAAC,QAAK,GAAIoN,GAAU,oBAAmB5K,EAAM,EAAI,OAAS,KAAM,OAAM,GAAC,EACtEG,EAAO,MAAM,GAChB,EAEF,MAAO,CAAC,CAAE,KAAA4K,EAAM,SAAAC,CAAS,IAAM,CAC7B3K,EAAO0K,EACPC,EAAS,iBAAiB,QAAS,IAAM,CACvC1K,EAAW,GACXD,EAAO,IACT,CAAC,EACD6C,GAAc,OAAO7C,EAAM2K,CAAQ,EACnC,IAAMC,EAAa5K,EAAK,cAA2B,QAAQnB,EAAM,EAAE,qBAAqB,EACpF+L,GAAYxH,EAAgB,QAAQ,CAAE,KAAMwH,EAAwC,SAAAD,CAAS,CAAC,EAClGA,EAAS,iBAAiB,QAAS,IAAMvH,EAAgB,UAAU,CAAC,EACpEtD,EAAO,OAAO6K,CAAQ,EAEtB3K,EAAK,iBACH,SACCyG,GAAU,CACT,IAAMoE,EAAUpE,EAAM,OACtB,GAAI,EAAEoE,aAAmB,oBAAqB,OAC9C,IAAMrI,EAAcqI,EAAQ,QAAqB,mCAAmC,GAAG,QACpF,UACH,GAAI,CAACrI,EAAa,OAClB,IAAMsI,EAASxL,EAAW,EAAE,OAAQmE,GAAOA,IAAOjB,CAAW,EAC7DlD,EAAW,IAAIuL,EAAQ,KAAO,CAAC,GAAGC,EAAQtI,CAAW,EAAIsI,CAAM,CACjE,EACA,CAAE,OAAQH,EAAU,QAAS,EAAK,CACpC,EAEA,SAAS,iBACP,SACClE,GAAU,CACL,EAAEA,EAAM,kBAAkB,kBAAoBA,EAAM,OAAO,KAAO8D,KACtE9D,EAAM,eAAe,EACrBA,EAAM,yBAAyB,EAC1B7D,GAAQ,GAAQuD,GAAK,EAC5B,EACA,CAAE,OAAQwE,EAAU,QAAS,EAAK,CACpC,EACA,IAAMI,EAAWC,GAAO,IAAM,CAC5B,IAAMtE,EAAS+D,GAAW,EACrB/D,IACLA,EAAO,SAAW9D,GAAQ,EACtBrD,EAAO,EAAGmH,EAAO,aAAa,YAAa,MAAM,EAChDA,EAAO,gBAAgB,WAAW,EACzC,CAAC,EACKuE,EAAWD,GAAO,IAAM,CAC5B5L,EAAM,EACNC,EAAK,EACL,eAAe+H,EAAY,CAC7B,CAAC,EACDuD,EAAS,iBAAiB,QAAS,IAAM,CACvCI,EAAS,EACTE,EAAS,CACX,CAAC,CACH,CACF,CACF,CAGO,IAAMT,GAAyB/G,GAAuB,GAAGA,CAAE,QAErDyH,GAAiBrM,GAC5BD,GAAwBC,CAAsC",
|
|
6
|
+
"names": ["each", "effect", "signal", "each", "Fragment", "jsx", "jsxs", "HOOKS", "ActionContent", "props", "Button", "IconButton", "ActionGroup", "props", "jsx", "each", "_", "index", "action", "Fragment", "jsx", "jsxs", "HOOKS", "Badge", "props", "jsx", "each", "Fragment", "jsx", "jsxs", "HOOKS", "NOTICE_ICONS", "Notice", "props", "EmptyState", "each", "each", "jsx", "jsxs", "HOOKS", "Fragment", "jsx", "jsxs", "Fragment", "jsx", "jsxs", "HOOKS", "Fragment", "jsx", "jsxs", "HOOKS", "Disclosure", "props", "jsxs", "jsx", "Section", "each", "Fragment", "jsx", "NativeFieldControl", "props", "describedBy", "option", "jsx", "jsxs", "HOOKS", "hasError", "props", "Field", "helpId", "errorId", "describedBy", "label", "Disclosure", "each", "field", "option", "NativeFieldControl", "each", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "each", "Fragment", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "jsx", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "jsx", "HOOKS", "each", "jsx", "HOOKS", "each", "effect", "signal", "jsx", "jsxs", "HOOKS", "array", "value", "string", "callApi", "name", "input", "requestSignal", "response", "payload", "domainError", "createRelationSelectView", "props", "callbacks", "islandId", "config", "labels", "manager", "multiple", "chosen", "selected", "isChosen", "options", "availableOptions", "disabled", "open", "dialog", "query", "rows", "loading", "error", "editor", "pendingRemove", "activeRequest", "searchTimer", "disposed", "rowId", "row", "rowLabel", "rowDescription", "selectedLabel", "entry", "filteredOptions", "needle", "held", "choose", "label", "description", "id", "unchoose", "labelOf", "loadRows", "request", "excluded", "caught", "openDialog", "closeDialog", "searchManager", "event", "save", "editing", "remove", "editorField", "field", "fieldId", "editorForm", "managerRow", "dialogView", "handleKeydown", "trigger", "handleInvalid", "root", "lifetime", "first", "stop", "native", "target", "each", "signal", "jsx", "jsxs", "HOOKS", "MIN_ZOOM", "MAX_ZOOM", "ZOOM_STEP", "fill", "template", "params", "_", "name", "clamp", "value", "min", "max", "createLightbox", "labels", "id", "items", "open", "index", "zoom", "pan", "returnFocus", "pointers", "gesture", "resetView", "show", "set", "at", "trigger", "hide", "step", "delta", "total", "zoomTo", "next", "centerOf", "points", "sum", "point", "distanceOf", "a", "b", "endPointer", "event", "completed", "dx", "finished", "item", "scale", "offset", "many", "center", "dy", "lifetime", "LightboxThumb", "props", "jsx", "fill", "event", "each", "signal", "Fragment", "jsx", "jsxs", "HOOKS", "each", "effect", "signal", "jsx", "HOOKS", "Fragment", "jsx", "jsxs", "HOOKS", "focusableSelector", "DISMISSIBLE_POPUPS", "DISMISSIBLE_POPUPS_OPEN", "selector", "jsx", "jsxs", "issueFor", "issues", "name", "issue", "FieldFrame", "props", "helpId", "errorId", "describedBy", "id", "help", "error", "jsx", "jsxs", "HOOKS", "normalize", "props", "issueFor", "TextField", "Field", "MoneyField", "props", "jsx", "Field", "normalize", "CheckboxGroup", "props", "jsx", "Field", "normalize", "Select", "props", "jsx", "Field", "normalize", "each", "Fragment", "jsx", "jsxs", "HOOKS", "Fragment", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "uploadControl", "props", "error", "drop", "describedBy", "renderUpload", "issueFor", "FieldFrame", "DropZone", "props", "renderUpload", "jsx", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "each", "each", "Fragment", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "Fragment", "jsx", "jsxs", "HOOKS", "each", "Fragment", "jsx", "jsxs", "HOOKS", "Fragment", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "HOOKS", "jsx", "jsxs", "HOOKS", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "GROUPS", "HOOKS", "OWNERS", "owner", "hooks", "trustedMarkup", "PATHS", "OPEN", "icon", "name", "trustedMarkup", "PATHS", "OPEN", "signal", "jsx", "jsx", "jsxs", "RecordModalForm", "props", "RECORD_COMMAND_FIELD", "name", "value", "item", "Field", "ActionGroup", "Fragment", "jsx", "jsxs", "issuesOf", "source", "holder", "raw", "item", "path", "callRecordRoute", "route", "input", "options", "target", "response", "payload", "value", "callRecordFunction", "name", "RECORD_MODAL_LABELS", "RECORD_COMMAND_FIELD", "focusable", "signal", "Fragment", "jsx", "jsxs", "createVariantAttributeForm", "props", "open", "signal", "busy", "name", "values", "errors", "failure", "attributeId", "disposed", "valueIds", "formId", "focus", "id", "target", "close", "submit", "event", "names", "value", "invalid", "input", "result", "callRecordFunction", "fields", "issue", "query", "root", "lifetime", "button", "Section", "Notice", "RecordModalForm", "Button", "jsx", "jsxs", "ACTION", "clone", "value", "label", "labels", "key", "params", "_", "name", "imageUrl", "attachmentId", "size", "IMAGE_TYPES", "money", "amount", "decimal", "isDecimal", "signed", "PREVIEW", "rowSequence", "toRows", "variants", "variant", "createVariantEditorView", "props", "t", "editable", "saved", "listPrice", "signal", "catalogue", "lines", "rows", "openExtras", "saving", "problem", "rowIssues", "notice", "dirty", "imageBusy", "dragOver", "viewer", "createLightbox", "host", "disposed", "touch", "setLines", "next", "setRows", "updateRow", "patch", "row", "rowOf", "focusSoon", "selector", "fallback", "pickerTrigger", "actionButton", "variantLines", "line", "combinationOf", "picked", "valueId", "duplicates", "seen", "clash", "index", "combination", "first", "incomplete", "extraOf", "entry", "priceOf", "total", "missingCombinations", "groups", "all", "acc", "prefix", "taken", "valueIds", "combinationLabel", "usageOf", "attributeId", "invalidExtras", "rowInvalid", "invalidRows", "blocked", "attributeForm", "optionsOf", "listed", "firstValueOf", "createVariantAttributeForm", "attribute", "addLine", "attributePicker", "createRelationSelectView", "option", "query", "removeLine", "id", "selectValues", "options", "adding", "savedLine", "added", "priceExtra", "order", "a", "b", "removeValue", "setExtra", "newRow", "open", "addRow", "generate", "created", "ids", "toggleRow", "removeRow", "setAllActive", "active", "setImages", "images", "uploadImage", "file", "replacing", "body", "response", "stored", "attached", "callRecordFunction", "previous", "image", "error", "removeImage", "current", "result", "rest", "openImages", "trigger", "reset", "save", "snapshot", "byRow", "issue", "match", "onClick", "event", "button", "editableAction", "action", "onChange", "control", "kind", "onInput", "decimalField", "expectedValue", "name2", "syncControls", "input", "want", "extrasView", "set", "invalid", "Disclosure", "each", "MoneyField", "lineView", "shown", "selected", "noVariant", "CheckboxGroup", "Badge", "Button", "IconButton", "icon", "attributesView", "Section", "EmptyState", "rowThumb", "LightboxThumb", "imageBlock", "bodyId", "canUpload", "busy", "related", "DropZone", "rowField", "field", "common", "Field", "TextField", "rowStatus", "rowView", "titleId", "breakdown", "Select", "missingView", "missing", "names", "Notice", "variantsView", "activeCount", "toolbar", "footerView", "archiving", "v", "creating", "parts", "saveForm", "variantEditorSaveForm", "saveButton", "root", "lifetime", "pickerRoot", "details", "others", "stopSave", "effect", "stopSync", "variantEditor"]
|
|
7
|
+
}
|