@ketvietlab/ketsuite 0.1.24 → 0.1.29
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 -871
- package/dist/modules/account_backend/index.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-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/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/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 +11 -5
- package/dist/modules/account_backend/screens/opening-balances.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/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-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/address_backend/index.d.ts.map +1 -1
- package/dist/modules/address_backend/index.js +2 -0
- 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/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/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 +99 -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 +26 -0
- package/dist/modules/backend/index.d.ts.map +1 -1
- package/dist/modules/backend/index.js +23 -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/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 -0
- 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/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/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 +51 -51
- 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 -0
- package/dist/modules/crm_backend/messages.d.ts.map +1 -1
- package/dist/modules/crm_backend/messages.js +34 -0
- 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/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/flow/functions.d.ts.map +1 -1
- package/dist/modules/flow/functions.js +20 -39
- package/dist/modules/flow/functions.js.map +1 -1
- package/dist/modules/flow_backend/index.js +2 -1
- package/dist/modules/flow_backend/index.js.map +1 -1
- package/dist/modules/flow_backend/routes.d.ts.map +1 -1
- package/dist/modules/flow_backend/routes.js +181 -126
- 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/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/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/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/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.d.ts.map +1 -1
- package/dist/modules/hospitality_billing/messages.js +2 -0
- 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 -1
- 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 +4 -0
- 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/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/page-frame.d.ts +3 -1
- 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/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/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 +1 -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/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 +6 -3
- 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/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/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/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 +113 -103
- 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/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/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 -2
- package/dist/modules/pricing_backend/index.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 +22 -30
- package/dist/modules/product/product-modal-context.js.map +1 -1
- package/dist/modules/product/product-record-tabs.d.ts +14 -0
- package/dist/modules/product/product-record-tabs.d.ts.map +1 -0
- package/dist/modules/product/product-record-tabs.js +23 -0
- package/dist/modules/product/product-record-tabs.js.map +1 -0
- 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 +46 -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 +3 -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 +236 -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 +31 -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 +342 -252
- 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/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_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/index.js +116 -208
- package/dist/modules/purchase_backend/index.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/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/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-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_backend/index.js +83 -32
- package/dist/modules/sale_backend/index.js.map +1 -1
- 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.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/overview.js +2 -2
- package/dist/modules/sale_backend/screens/overview.js.map +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_backend/functions.d.ts +10 -0
- package/dist/modules/stock_backend/functions.d.ts.map +1 -0
- package/dist/modules/stock_backend/functions.js +19 -0
- package/dist/modules/stock_backend/functions.js.map +1 -0
- package/dist/modules/stock_backend/index.js +33 -10
- package/dist/modules/stock_backend/index.js.map +1 -1
- package/dist/modules/stock_backend/routes.d.ts.map +1 -1
- package/dist/modules/stock_backend/routes.js +168 -59
- package/dist/modules/stock_backend/routes.js.map +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/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/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-list.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/replenishment-list.js +11 -22
- package/dist/modules/stock_backend/screens/replenishment-list.js.map +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/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/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 +257 -0
- package/dist/modules/stock_backend/search.d.ts.map +1 -0
- package/dist/modules/stock_backend/search.js +349 -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 +510 -2
- package/dist/modules/user_backend/messages.d.ts.map +1 -1
- package/dist/modules/user_backend/messages.js +552 -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/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 +6 -3
- 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 +178 -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 +192 -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 +39 -1
- package/dist/ui/client/live-doc-shell.d.ts.map +1 -1
- package/dist/ui/client/live-doc-shell.js +41 -2
- package/dist/ui/client/live-doc-shell.js.map +1 -1
- package/dist/ui/client/live-doc-view.d.ts +29 -1
- package/dist/ui/client/live-doc-view.d.ts.map +1 -1
- package/dist/ui/client/live-doc-view.js +138 -4
- package/dist/ui/client/live-doc-view.js.map +1 -1
- package/dist/ui/client/live-doc.d.mts +8 -4
- package/dist/ui/client/live-doc.d.mts.map +1 -1
- package/dist/ui/client/live-doc.mjs +10 -10
- package/dist/ui/client/live-doc.mjs.map +3 -3
- 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 +62 -6
- package/dist/ui/client/record-modal.d.ts.map +1 -1
- package/dist/ui/client/record-modal.js +237 -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 +6 -7
- package/dist/ui/layout.d.ts.map +1 -1
- package/dist/ui/layout.js +25 -10
- 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/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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../../../ui/client/user-workflow.ts", "../../../../../../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/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/
|
|
4
|
-
"sourcesContent": ["export function installUserWorkflow(lifetime: AbortSignal): () => void {\n let abort: AbortController | undefined,\n timer: ReturnType<typeof setTimeout> | undefined,\n sequence = 0\n async function check(form: HTMLFormElement) {\n const input = form.querySelector<HTMLInputElement>('input[type=email]'),\n status = form.querySelector<HTMLElement>('[data-email-status]')\n if (!input || !status) return\n abort?.abort()\n clearTimeout(timer)\n const request = ++sequence\n const value = input.value.trim(),\n next = form.querySelector<HTMLButtonElement>('[data-email-next]'),\n retry = form.querySelector<HTMLButtonElement>('[data-email-retry]')\n const paint = (message: string, valid: boolean) => {\n status.textContent = message\n if (next) next.disabled = !valid\n input.setAttribute('aria-invalid', String(!valid))\n form.dataset.emailAvailable = valid ? 'true' : 'false'\n }\n if (retry) retry.hidden = true\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)) {\n paint(value ? 'Email ch\u01B0a \u0111\u00FAng \u0111\u1ECBnh d\u1EA1ng.' : 'Nh\u1EADp email \u0111\u1EC3 ki\u1EC3m tra.', false)\n return\n }\n paint('\u0110ang ki\u1EC3m tra email\u2026', false)\n timer = setTimeout(async () => {\n abort = new AbortController()\n try {\n const response = await fetch(form.dataset.emailCheck!, {\n method: 'POST',\n cache: 'no-store',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({ email: value }),\n signal: AbortSignal.any([abort.signal, AbortSignal.timeout(10000)]),\n })\n if (!response.ok) throw new Error('email-check-failed')\n const result = (await response.json()) as { available: boolean; ok: boolean }\n if (request !== sequence || !form.isConnected) return\n if (!result.ok) throw new Error('email-check-failed')\n paint(\n result.available ? 'Email c\u00F3 th\u1EC3 s\u1EED d\u1EE5ng.' : 'Email \u0111\u00E3 \u0111\u01B0\u1EE3c s\u1EED d\u1EE5ng. Nh\u1EADp email kh\u00E1c.',\n result.available,\n )\n } catch {\n if (request !== sequence || !form.isConnected) return\n paint('Ch\u01B0a ki\u1EC3m tra \u0111\u01B0\u1EE3c email. Vui l\u00F2ng th\u1EED l\u1EA1i.', false)\n if (retry) retry.hidden = false\n }\n }, 400)\n }\n function scope(form: HTMLFormElement, changed = false) {\n const company = form.querySelector<HTMLSelectElement>('select[name=\"companyId\"]'),\n branch = form.querySelector<HTMLSelectElement>('select[name=\"branchId\"]'),\n kind = form.querySelector<HTMLSelectElement>('select[name=\"scopeKind\"]')?.value\n if (branch && company) {\n if (changed) branch.value = ''\n for (const option of branch.options) {\n if (option.value) {\n option.hidden = option.dataset.companyId !== company.value\n option.disabled = option.hidden\n }\n }\n }\n const key =\n kind === 'tenant'\n ? 'tenant'\n : kind === 'branch'\n ? `branch:${company?.value}:${branch?.value}`\n : `company:${company?.value}`\n for (const input of form.querySelectorAll<HTMLInputElement>('[data-assigned-scopes]')) {\n const scopes = JSON.parse(input.dataset.assignedScopes ?? '[]') as string[]\n const assigned = scopes.includes(key)\n input.disabled = input.dataset.roleDisabled === 'true' || assigned\n const row = input.closest('[data-role-option]')\n const assignedLabel = row?.querySelector<HTMLElement>('[data-role-assigned]')\n const description = row?.querySelector<HTMLElement>('[data-role-description]')\n if (assignedLabel) assignedLabel.hidden = !assigned\n if (description) description.hidden = assigned\n if (input.disabled) input.checked = false\n }\n const companyField = company?.closest<HTMLElement>('[data-ui=\"form-field\"]')\n const branchField = branch?.closest<HTMLElement>('[data-ui=\"form-field\"]')\n if (companyField) companyField.hidden = kind === 'tenant'\n if (branchField) branchField.hidden = kind === 'tenant' || kind === 'company'\n }\n function scan() {\n for (const form of document.querySelectorAll<HTMLFormElement>('form[data-user-workflow]'))\n if (!form.dataset.initialized) {\n form.dataset.initialized = 'true'\n scope(form)\n void check(form)\n }\n }\n document.addEventListener(\n 'input',\n (event) => {\n const input = event.target as HTMLInputElement,\n form = input.closest<HTMLFormElement>('form[data-user-workflow]')\n if (!form) return\n if (input.type === 'email') void check(form)\n if (input.matches('[data-role-search]')) {\n let n = 0\n for (const row of form.querySelectorAll<HTMLElement>('[data-role-option]')) {\n row.hidden = !row.textContent?.toLocaleLowerCase('vi').includes(input.value.toLocaleLowerCase('vi'))\n if (!row.hidden) n++\n }\n const empty = form.querySelector<HTMLElement>('[data-role-empty]')\n if (empty) empty.hidden = n > 0\n }\n },\n { signal: lifetime },\n )\n document.addEventListener(\n 'change',\n (event) => {\n const input = event.target as HTMLElement,\n form = input.closest<HTMLFormElement>('form[data-user-workflow]')\n if (form && input.matches('select[name=\"companyId\"],select[name=\"branchId\"],select[name=\"scopeKind\"]'))\n scope(form, input.matches('select[name=\"companyId\"]'))\n },\n { signal: lifetime },\n )\n document.addEventListener(\n 'click',\n (event) => {\n const target = event.target as HTMLElement\n if (target.matches('[data-email-retry]')) void check(target.closest('form')!)\n },\n { signal: lifetime },\n )\n document.addEventListener(\n 'submit',\n (event) => {\n const form = event.target as HTMLFormElement\n if (\n form.matches('form[data-user-workflow]') &&\n form.querySelector('input[type=email]') &&\n form.dataset.emailAvailable !== 'true'\n )\n event.preventDefault()\n },\n { signal: lifetime },\n )\n const observer = new MutationObserver(scan)\n observer.observe(document.documentElement, { childList: true, subtree: true })\n scan()\n return () => {\n abort?.abort()\n clearTimeout(timer)\n observer.disconnect()\n }\n}\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}\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 >\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 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 = ['badge', 'tag', 'tag-remove', 'count-badge', 'avatar', 'code'] as const\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 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-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\nexport const Stack = (props: {\n items: readonly JSXChild[]\n gap?: 'compact' | 'default' | 'loose'\n}): TemplateResult => (\n <div data-ui=\"stack\" data-pattern=\"stack\" data-gap={props.gap ?? 'default'}>\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 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 description?: string | null\n status?: JSXChild\n actions?: JSXChild\n meta?: JSXChild\n}\n\ntype PageIdentityContentProps = PageIdentityProps & {\n groupDescription?: boolean\n}\n\n/** The single identity band shared by every full-page component and recipe. */\nexport const pageIdentityContent = (\n kind: PageIdentityKind,\n props: PageIdentityContentProps,\n): 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 data-ui={`${kind}-actions`} data-kv-page-identity=\"actions\">\n {props.actions}\n </div>\n )}\n </div>\n {(props.groupDescription || props.status !== undefined) &&\n (!!props.description || props.status !== undefined) ? (\n <div data-ui={`${kind}-subline`} data-kv-page-identity=\"subline\">\n {!!props.description && (\n <p data-ui={`${kind}-description`} data-kv-page-identity=\"description\">\n {props.description}\n </p>\n )}\n {props.status !== undefined && (\n <span data-ui={`${kind}-status`} data-kv-page-identity=\"status\">\n {props.status}\n </span>\n )}\n </div>\n ) : (\n !!props.description && (\n <p data-ui={`${kind}-description`} data-kv-page-identity=\"description\">\n {props.description}\n </p>\n )\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 !== undefined && (\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 { pageIdentity, pageIdentityContent, type PageIdentityProps } from '../page-identity/index.tsx'\n\nexport const HOOKS = [\n 'app-shell',\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-description',\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 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-mode={props.mode ?? 'viewport'}\n >\n <aside data-ui=\"app-sidebar\">{props.sidebar}</aside>\n {props.mode === 'embedded' ? (\n <div data-ui=\"app-main\">{props.main}</div>\n ) : (\n <main data-ui=\"app-main\">{props.main}</main>\n )}\n {props.rightRail !== undefined && <aside data-ui=\"app-right-rail\">{props.rightRail}</aside>}\n </div>\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 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 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 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'\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 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-label={props.closeLabel} 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 role=\"dialog\"\n aria-modal={embedded ? 'false' : 'true'}\n aria-labelledby={`${props.id}-title`}\n aria-describedby={props.description ? `${props.id}-description` : null}\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'\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", "// @ts-check\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\nexport const attachDesignSystemInteractions = (root = document) => {\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 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\"]',\n ),\n ]\n .filter((element) => element instanceof HTMLElement)\n .map((element) => ({ element, inert: element.inert }))\n : []\n for (const { element } of targets) element.inert = true\n state.targets = targets\n state.returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : trigger\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 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 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 { Field } from '../../primitives/field/index.tsx'\nimport type { FieldProps } from '../../primitives/field/index.tsx'\nimport { issueFor } from '../shared.tsx'\nimport type { FieldIssue } from '../shared.tsx'\n\nexport const HOOKS = ['date-range'] as const\n\ntype TemporalProps = Omit<FieldProps, 'type' | 'fields' | 'control' | 'error'> & {\n issues?: readonly FieldIssue[]\n error?: string | null\n}\n\nconst temporal = (props: TemporalProps): FieldProps => ({\n ...props,\n error: props.error ?? issueFor(props.issues, props.name),\n})\n\n/** Civil dates are passed through as YYYY-MM-DD text and are never converted through UTC. */\nexport const DatePicker = (props: TemporalProps): TemplateResult => <Field {...temporal(props)} type=\"date\" />\n\nexport type DateRangePickerProps = {\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}\n\nexport const DateRangePicker = (props: DateRangePickerProps): TemplateResult => (\n <fieldset data-ui=\"date-range\" data-span={props.span ?? 'full'}>\n <legend>{props.label}</legend>\n <Field {...temporal({ ...props.start, label: props.startLabel })} type=\"date\" />\n <Field {...temporal({ ...props.end, label: props.endLabel })} type=\"date\" />\n </fieldset>\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)\n\nexport const TimePicker = (props: TemporalProps): TemplateResult => <Field {...temporal(props)} type=\"time\" />\n\nexport type { TemporalProps }\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\nexport const DescriptionList = (props: {\n label?: string\n items: readonly (KeyValueProps & { id: string })[]\n columns?: 1 | 2 | 3\n}): TemplateResult => (\n <dl data-ui=\"description-list\" aria-label={props.label} data-columns={String(props.columns ?? 2)}>\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 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 value: number\n currency: string\n locale?: string\n}): TemplateResult => (\n <data data-ui=\"formatted-money\" value={String(props.value)} data-currency={props.currency}>\n {new Intl.NumberFormat(props.locale ?? 'vi-VN', { style: 'currency', currency: props.currency }).format(\n props.value,\n )}\n </data>\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-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 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 <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 data-ui=\"navigation-backdrop\" type=\"button\" aria-label={props.closeLabel} tabIndex={-1} />\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 data-ui=\"app-navigation\" data-navigation-id={props.id} open={props.open === true}>\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", "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-description',\n 'list-page-actions',\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 description?: string | null\n actions?: JSXChild\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 primary action first, query\n * controls second, then the result status and the collection itself.\n */\nexport const ListPage = (props: ListPageProps): TemplateResult => {\n const operational = props.variant === 'operational'\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 data-ui=\"list-page\" data-variant={props.variant ?? null} data-pattern=\"list\">\n {pageIdentity('list-page', {\n context: props.context,\n eyebrow: props.eyebrow,\n title: props.title,\n description: props.description,\n actions: props.actions,\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 <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-description',\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 description?: 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}>\n {pageIdentityContent(kind, { ...props, groupDescription: true })}\n </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, groupDescription: true })}\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-description',\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-description',\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 description?: 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-description',\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-description',\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 { 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 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 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 popover: popoverHooks,\n tooltip: tooltipHooks,\n dialog: dialogHooks,\n toast: toastHooks,\n spinner: spinnerHooks,\n skeleton: skeletonHooks,\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", "import { installUserWorkflow } from './user-workflow.ts'\nimport type { BrowserBehavior, BrowserNavigation } from '@ketvietlab/ketjs'\nimport { attachDesignSystemInteractions } from '@ketvietlab/design-system'\n\nconst themeStorageKey = 'ket.backend.theme'\nconst dismissibleDropdown = [\n '[data-ui=\"search-menu\"]',\n '[data-ui=\"col-config\"]',\n '[data-ui=\"bulk-actions\"]',\n '[data-ui=\"viewer\"]',\n '[data-ui=\"record-more\"]',\n].join(', ')\n\ntype KetBrowserGlobals = typeof globalThis & {\n confirm: (message?: string) => boolean\n}\n\nconst browserGlobals = globalThis as KetBrowserGlobals\nconst eventElement = (event: Event): Element | null => (event.target instanceof Element ? event.target : null)\n\ntype ThemePreference = 'light' | 'dark'\n\nconst themePreference = (value: string | null | undefined): ThemePreference | null =>\n value === 'light' || value === 'dark' ? value : null\n\nconst storedTheme = (): ThemePreference | null => {\n try {\n return themePreference(localStorage.getItem(themeStorageKey))\n } catch {\n return null\n }\n}\n\nconst saveTheme = (theme: ThemePreference): void => {\n try {\n localStorage.setItem(themeStorageKey, theme)\n } catch {\n // Theme selection still works for this document when storage is unavailable.\n }\n}\n\nconst resolvedTheme = (): ThemePreference =>\n themePreference(document.documentElement.dataset.theme) ??\n (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')\n\nconst syncThemeToggle = (): void => {\n const pressed = String(resolvedTheme() === 'dark')\n for (const control of document.querySelectorAll<HTMLElement>(\n '[data-ui=\"sidebar-tools\"] [data-ui=\"action\"][name=\"theme\"]',\n ))\n control.setAttribute('aria-pressed', pressed)\n}\n\n/** Keep the shell preference explicit only after the reader chooses one. */\nconst installThemeToggle = (signal: AbortSignal): void => {\n const preference = storedTheme()\n if (preference) document.documentElement.dataset.theme = preference\n syncThemeToggle()\n\n document.addEventListener(\n 'click',\n (event) => {\n const control = eventElement(event)?.closest<HTMLButtonElement>(\n '[data-ui=\"sidebar-tools\"] [data-ui=\"action\"][name=\"theme\"]',\n )\n if (!control || control.disabled) return\n const next = resolvedTheme() === 'dark' ? 'light' : 'dark'\n document.documentElement.dataset.theme = next\n saveTheme(next)\n syncThemeToggle()\n },\n { signal },\n )\n\n matchMedia('(prefers-color-scheme: dark)').addEventListener(\n 'change',\n () => {\n if (!themePreference(document.documentElement.dataset.theme)) syncThemeToggle()\n },\n { signal },\n )\n\n addEventListener(\n 'storage',\n (event) => {\n if (event.key !== themeStorageKey) return\n const next = themePreference(event.newValue)\n if (next) document.documentElement.dataset.theme = next\n else delete document.documentElement.dataset.theme\n syncThemeToggle()\n },\n { signal },\n )\n}\n\nconst updateSelection = (table: Element): void => {\n const rows = [...table.querySelectorAll<HTMLInputElement>('[data-ui=\"row-select\"]:not(:disabled)')]\n const checked = rows.filter((input) => input.checked).length\n const all = table.querySelector<HTMLInputElement>('[data-ui=\"select-all\"]')\n if (all) {\n all.checked = rows.length > 0 && checked === rows.length\n all.indeterminate = checked > 0 && checked < rows.length\n }\n if (!checked) {\n const shell = table.closest('[data-ui=\"app-shell\"]')\n for (const menu of shell?.querySelectorAll<HTMLDetailsElement>('[data-ui=\"bulk-actions\"][open]') ?? [])\n menu.removeAttribute('open')\n }\n}\n\nconst installTableSelection = (signal: AbortSignal, navigation: BrowserNavigation): void => {\n document.addEventListener(\n 'change',\n (event) => {\n const target = eventElement(event)\n if (!(target instanceof HTMLInputElement)) return\n if (!target.matches('[data-ui=\"select-all\"], [data-ui=\"row-select\"]')) return\n const table = target.closest('[data-ui=\"table\"]')\n if (!table) return\n if (target.matches('[data-ui=\"select-all\"]'))\n for (const input of table.querySelectorAll<HTMLInputElement>('[data-ui=\"row-select\"]:not(:disabled)'))\n input.checked = target.checked\n updateSelection(table)\n },\n { signal },\n )\n\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 target = eventElement(event)\n if (\n target?.closest(\n 'a, button, input, select, textarea, label, summary, details, [data-ui=\"select-cell\"]',\n )\n )\n return\n const row = target?.closest<HTMLElement>('[data-ui=\"row\"][data-row-href]')\n const href = row?.getAttribute('data-row-href')\n if (!href) return\n event.preventDefault()\n void navigation.navigate(href)\n },\n { signal },\n )\n document.addEventListener(\n 'keydown',\n (event) => {\n if (event.defaultPrevented || (event.key !== 'Enter' && event.key !== ' ')) return\n const target = eventElement(event)\n const row = target?.matches('[data-ui=\"row\"][data-row-href][tabindex=\"0\"]') ? target : null\n const href = row?.getAttribute('data-row-href')\n if (!href) return\n event.preventDefault()\n navigateTo(href, navigation)\n },\n { signal },\n )\n}\n\nconst openDropdowns = (): HTMLDetailsElement[] => [\n ...document.querySelectorAll<HTMLDetailsElement>(`${dismissibleDropdown}[open]`),\n]\n\nconst installDropdownDismiss = (signal: AbortSignal): void => {\n document.addEventListener(\n 'click',\n (event) => {\n const current = eventElement(event)?.closest(dismissibleDropdown)\n for (const dropdown of openDropdowns()) {\n if (dropdown !== current && !dropdown.contains(current ?? null)) dropdown.removeAttribute('open')\n }\n },\n { signal },\n )\n\n document.addEventListener(\n 'keydown',\n (event) => {\n if (event.key !== 'Escape') return\n const dropdowns = openDropdowns()\n if (!dropdowns.length) return\n const focused = document.activeElement?.closest<HTMLDetailsElement>(dismissibleDropdown)\n const current = focused?.open ? focused : dropdowns.at(-1)\n current?.removeAttribute('open')\n current?.querySelector<HTMLElement>('summary')?.focus()\n event.preventDefault()\n },\n { signal },\n )\n}\n\nconst installGlobalFilter = (signal: AbortSignal): void => {\n document.addEventListener(\n 'click',\n (event) => {\n const trigger = eventElement(event)?.closest<HTMLElement>('[data-ui=\"chrome-search-toggle\"]')\n const controls = trigger?.getAttribute('aria-controls')\n if (!trigger || !controls) return\n const dialog = document.getElementById(controls)\n if (!(dialog instanceof HTMLDialogElement) || dialog.open) return\n dialog.showModal()\n trigger.setAttribute('aria-expanded', 'true')\n requestAnimationFrame(() =>\n dialog\n .querySelector<HTMLElement>('[data-presentation=\"modal\"] [data-ui=\"chrome-search-input\"]')\n ?.focus(),\n )\n },\n { signal },\n )\n\n document.addEventListener(\n 'cancel',\n (event) => {\n if (!eventElement(event)?.matches('[data-ui=\"chrome-search-modal\"]')) return\n event.preventDefault()\n },\n { capture: true, signal },\n )\n\n document.addEventListener(\n 'close',\n (event) => {\n const dialog = eventElement(event)\n if (!(dialog instanceof HTMLDialogElement)) return\n if (!dialog.matches('[data-ui=\"chrome-search-modal\"]')) return\n const trigger = document.querySelector<HTMLElement>(\n `[data-ui=\"chrome-search-toggle\"][aria-controls=\"${CSS.escape(dialog.id)}\"]`,\n )\n trigger?.setAttribute('aria-expanded', 'false')\n trigger?.focus()\n },\n { capture: true, signal },\n )\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 activeRouteModal = (): HTMLElement | null =>\n document.querySelector<HTMLElement>('[data-ui=\"modal-layer\"][data-route-modal=\"true\"]')\n\nconst modalFocusables = (modal: HTMLElement): HTMLElement[] =>\n [...modal.querySelectorAll<HTMLElement>(focusable)].filter(\n (item) =>\n !item.hidden &&\n item.getAttribute('aria-hidden') !== 'true' &&\n !item.matches('[data-ui=\"modal-backdrop\"]'),\n )\n\nconst navigateTo = (href: string, navigation: BrowserNavigation): void => {\n void navigation.navigate(href)\n}\n\nconst eventStartedInNestedModal = (event: KeyboardEvent, routeModal: HTMLElement): boolean =>\n event\n .composedPath()\n .some(\n (target) =>\n target instanceof HTMLElement &&\n target !== routeModal &&\n target.matches('[data-ui=\"modal-layer\"][data-presentation=\"dialog\"]'),\n )\n\n/**\n * Whether anything in the modal has been typed into since the server rendered it.\n *\n * Comparing each control against its own default is what makes this work without\n * any bookkeeping: the page is server-rendered, so the default *is* the state the\n * reader was given. A modal with no form \u2014 a reader, an inspector \u2014 has nothing\n * to compare and is never dirty, so the guard costs those screens nothing.\n */\nconst modalHasDraft = (modal: HTMLElement): boolean => {\n for (const field of modal.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 modal.querySelectorAll<HTMLTextAreaElement>('textarea'))\n if (!area.disabled && area.value !== area.defaultValue) return true\n for (const select of modal.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/**\n * The guard, and what it is not.\n *\n * Leaving a modal is a link, and a link still works with scripting off \u2014 that is\n * the point of building it as one. So this catches the stray backdrop click, the\n * reflexive Escape and the tab clicked without thinking about the note already\n * typed; it is not a lock, and nothing here should be read as one. The wording\n * comes from the server because this file cannot translate.\n */\nconst mayLeaveModal = (modal: HTMLElement): boolean => {\n if (!modalHasDraft(modal)) return true\n const message = modal.getAttribute('data-unsaved-prompt')\n // No prompt declared means the screen did not ask to be guarded.\n if (!message) return true\n return browserGlobals.confirm(message)\n}\n\n/** Progressive keyboard behavior for URL-owned create/edit workspaces. */\n/**\n * Hear when the thing a screen is waiting for actually changed.\n *\n * The screens this replaces reloaded the whole document on a timer, which threw\n * away scroll, focus and anything typed, on a schedule unrelated to when the\n * work finished. Here the server says so, and the page asks for the fragment it\n * already knows how to swap \u2014 so the reader keeps their place.\n *\n * The connection belongs to the element. A fragment swap replaces it, and the\n * observer opens the new one and closes the old; a screen with nothing to wait\n * for renders no element and opens nothing.\n */\nconst installLiveRegion = (signal: AbortSignal, navigation: BrowserNavigation): (() => void) | undefined => {\n if (typeof EventSource !== 'function') return\n\n let watched: Element | null = null\n let source: EventSource | null = null\n const close = () => {\n source?.close()\n source = null\n watched = null\n }\n\n const sync = (): void => {\n const region = document.querySelector('[data-ui=\"live-region\"][data-stream]')\n if (region === watched) return\n close()\n const id = region instanceof HTMLElement ? (region.dataset.stream ?? '') : ''\n if (!id) return\n watched = region\n source = new EventSource(`/_ket/stream/${encodeURIComponent(id)}`)\n // What the chunk says is the deployment's business. That something was said\n // is the whole signal: the screen is server-rendered, so the way to find out\n // what changed is to ask for it.\n const refresh = () => navigateTo(location.href, navigation)\n source.onmessage = refresh\n source.addEventListener('done', () => {\n close()\n refresh()\n })\n // EventSource reconnects on its own, and a stream that has ended answers 404\n // rather than reopening. Leaving the error alone would retry that forever.\n source.onerror = () => {\n if (source?.readyState === EventSource.CLOSED) close()\n }\n }\n\n sync()\n const observer = new MutationObserver(sync)\n observer.observe(document.body, { childList: true, subtree: true })\n addEventListener('pagehide', close, { signal })\n return () => {\n observer.disconnect()\n close()\n }\n}\n\nconst installRouteModal = (signal: AbortSignal, navigation: BrowserNavigation): (() => void) => {\n const focused = new WeakSet<HTMLElement>()\n const focusModal = (): void => {\n const modal = activeRouteModal()\n if (!modal || focused.has(modal)) return\n focused.add(modal)\n requestAnimationFrame(() => {\n const sheet = modal.querySelector<HTMLElement>('[data-ui=\"modal-sheet\"]')\n const target = modalFocusables(modal).find((item) => !item.matches('[data-ui=\"modal-close\"]'))\n ;(target ?? sheet)?.focus()\n })\n }\n focusModal()\n const observer = new MutationObserver(focusModal)\n observer.observe(document.body, { childList: true, subtree: true })\n\n // The backdrop and the X are ordinary links, so the guard has to run before\n // the navigation layer sees the click rather than inside it.\n document.addEventListener(\n 'click',\n (event) => {\n const modal = activeRouteModal()\n if (!modal) return\n // A tab inside a modal is a third way out of it. It looks like a control\n // that stays on the screen, which is exactly why losing a half-written\n // note to one went unnoticed: the reader never meant to leave. It is a\n // link like the other two, and it leaves the same way.\n const leaving = event\n .composedPath()\n .find(\n (target) =>\n target instanceof HTMLElement &&\n (target.matches('[data-ui=\"modal-close\"], [data-ui=\"modal-backdrop\"], a[data-ui=\"tab\"]') ||\n target.matches('a[data-ui=\"action\"][href]')),\n )\n if (!leaving || !modal.contains(leaving as HTMLElement)) return\n if (!mayLeaveModal(modal)) {\n event.preventDefault()\n event.stopPropagation()\n }\n },\n { capture: true, signal },\n )\n\n document.addEventListener(\n 'click',\n (event) => {\n const container = document.querySelector<HTMLElement>('[data-save-before-navigation]')\n const form =\n container instanceof HTMLFormElement ? container : container?.querySelector<HTMLFormElement>('form')\n const anchor = eventElement(event)?.closest<HTMLAnchorElement>('a[href]')\n if (!container || !form || !anchor || activeRouteModal() || !modalHasDraft(container)) return\n event.preventDefault()\n event.stopPropagation()\n if (!browserGlobals.confirm(container.dataset.saveBeforeNavigation)) return\n let destination = form.querySelector<HTMLInputElement>('input[name=\"returnTo\"]')\n if (!destination) {\n destination = document.createElement('input')\n destination.type = 'hidden'\n destination.name = 'returnTo'\n form.append(destination)\n }\n destination.value = anchor.href\n form.requestSubmit()\n },\n { capture: true, signal },\n )\n document.addEventListener(\n 'keydown',\n (event) => {\n const modal = activeRouteModal()\n if (!modal) return\n // Relation-select removes its dialog synchronously on Escape. The event's\n // composed path is stable even after that DOM subtree disappears, so the\n // route layer must use it instead of querying the now-removed target.\n if (eventStartedInNestedModal(event, modal)) return\n if (event.key === 'Escape') {\n const close = modal.querySelector<HTMLAnchorElement>('[data-ui=\"modal-close\"]')\n if (!close) return\n event.preventDefault()\n if (!mayLeaveModal(modal)) return\n navigateTo(close.getAttribute('href') ?? close.href, navigation)\n return\n }\n if (event.key !== 'Tab') return\n const items = modalFocusables(modal)\n if (!items.length) return\n const first = items[0]\n const last = items.at(-1)!\n if (event.shiftKey && document.activeElement === first) {\n event.preventDefault()\n last.focus()\n } else if (!event.shiftKey && document.activeElement === last) {\n event.preventDefault()\n first.focus()\n }\n },\n { signal },\n )\n return () => observer.disconnect()\n}\n\n/**\n * A record modal changed something the collection behind it shows.\n *\n * The modal lives outside the content slot, so re-fetching the current URL as a\n * fragment refreshes the list without closing, reloading or rebuilding the modal.\n */\nconst installRecordRefresh = (signal: AbortSignal, navigation: BrowserNavigation): void => {\n document.addEventListener(\n 'ket:records-changed',\n () => {\n if (!document.querySelector('[data-ket-slot=\"backend.content\"]')) return\n void navigation.navigate(location.href, { replace: true })\n },\n { signal },\n )\n}\n\nexport const backendShell: BrowserBehavior = ({ navigation, lifetime }) => {\n installRecordRefresh(lifetime, navigation)\n const cleanupDesignSystem = attachDesignSystemInteractions(document)\n const cleanupUserWorkflow = installUserWorkflow(lifetime)\n installThemeToggle(lifetime)\n installTableSelection(lifetime, navigation)\n installDropdownDismiss(lifetime)\n installGlobalFilter(lifetime)\n const cleanupRouteModal = installRouteModal(lifetime, navigation)\n const cleanupLiveRegion = installLiveRegion(lifetime, navigation)\n return () => {\n cleanupDesignSystem()\n cleanupLiveRegion?.()\n cleanupRouteModal()\n cleanupUserWorkflow()\n }\n}\n"],
|
|
5
|
-
"mappings": "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,GAAQ,CACnB,SACA,cACA,cACA,eACA,iBACA,iBACA,QACA,aACA,cACA,gBACA,gBACA,UACA,eACF,ECjBA,OAAS,QAAAC,OAAY,sBCArB,OAAS,QAAAC,OAAY,sBC8BnB,mBAAAC,GAGM,OAAAC,GAIF,QAAAC,OAPJ,4BCGA,OAKE,OAAAC,GALF,QAAAC,OAAA,4BA9BK,IAAMC,GAAQ,CACnB,YACA,cACA,WACA,iBACA,OACA,eACA,cACA,eACA,eACA,iBACA,aACA,mBACA,eACA,cACA,eACA,YACA,YACA,gBACA,iBACA,iBACA,sBACF,EFkCwE,OAAsB,YAAAC,GAAtB,OAAAC,GAuE9D,QAAAC,OAvE8D,4BAvDjE,IAAMC,GAAQ,CACnB,QACA,SACA,OACA,YACA,UACA,eACA,kBACA,gBACA,sBACA,kBACA,aACA,qBACA,kBACA,UACA,eACA,kBACA,gBACA,sBACA,kBACA,eACA,eACA,aACA,YACA,eACA,eACA,eACA,aACA,eACA,cACA,YACA,YACA,eACA,SACA,cACA,eACA,eACA,cACA,cACA,iBACA,SACA,cACA,eACA,eACA,eACF,EGjDA,OAAS,QAAAC,OAAY,sBAKqB,mBAAAC,GAAA,OAAAC,OAAA,4BJ0EtC,OAGI,OAAAC,GAHJ,QAAAC,OAAA,4BA1EG,IAAMC,GAAQ,CACnB,QACA,cACA,iBACA,gBACA,gBACA,eACA,qBACA,aACA,aACF,EKfA,OAAS,QAAAC,OAAY,sBAgEL,OACE,OAAAC,GADF,QAAAC,OAAA,4BA7DT,IAAMC,GAAQ,CACnB,cACA,aACA,sBACA,8BACA,2BACA,WACA,WACA,mBACA,iBACA,iBACA,OACA,MACA,YACA,cACA,sBACA,WACF,ECEQ,OACE,OAAAC,GADF,QAAAC,OAAA,4BApBD,IAAMC,GAAQ,CACnB,WACA,iBACA,iBACA,iBACA,cACF,ECRA,OAAS,QAAAC,OAAY,sBAwFjB,OA6CQ,YAAAC,GApCY,OAAAC,GATpB,QAAAC,OAAA,4BArFG,IAAMC,GAAQ,CACnB,OACA,eACA,qBACA,aACA,cACA,oBACA,aACA,iBACA,YACA,oBAC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",
|
|
6
|
-
"names": ["installUserWorkflow", "lifetime", "abort", "timer", "sequence", "check", "form", "input", "status", "request", "value", "next", "retry", "paint", "message", "valid", "response", "result", "scope", "changed", "company", "branch", "kind", "option", "key", "assigned", "row", "assignedLabel", "description", "companyField", "branchField", "scan", "event", "n", "empty", "
|
|
3
|
+
"sources": ["../../../../ui/client/bulk-selection.ts", "../../../../ui/client/user-workflow.ts", "../../../../../../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/client/table-selection-view.tsx"],
|
|
4
|
+
"sourcesContent": ["/**\n * External row checkboxes belong to their native form, even when the form is\n * hidden. Keep each legacy bulk form and the public ListPage action slot in sync\n * with that association instead of using selection anywhere in the app shell.\n */\nexport const installBulkSelection = (root: Document, signal: AbortSignal): (() => void) => {\n const hasContent = (node: Node): boolean => {\n if (node.nodeType === 3) return Boolean(node.textContent?.trim())\n if (!(node instanceof Element)) return false\n if (node.matches('[data-ui=\"bulk-form\"]')) return !(node as HTMLFormElement).hidden\n if (node.matches('[data-ui=\"inline\"], [data-ui=\"list-page-tools\"]'))\n return [...node.childNodes].some(hasContent)\n return true\n }\n\n const sync = (): void => {\n if (signal.aborted) return\n const slots = new Set<HTMLElement>()\n for (const form of root.querySelectorAll<HTMLFormElement>('form[data-ui=\"bulk-form\"]')) {\n const selected = [...form.elements].some(\n (control) =>\n control instanceof HTMLInputElement &&\n !control.disabled &&\n ((control.matches('[data-ui=\"row-select\"], [data-ui=\"kt-row-select\"]') && control.checked) ||\n (control.type === 'hidden' &&\n control.parentElement?.matches('[data-ui=\"kt-select-persisted\"]') &&\n control.value === '1')),\n )\n form.hidden = !selected\n if (!selected)\n for (const menu of form.querySelectorAll<HTMLDetailsElement>('details[data-ui=\"bulk-actions\"][open]'))\n menu.open = false\n const tools = form.closest<HTMLElement>('[data-ui=\"list-page-tools\"]')\n if (tools) slots.add(tools)\n const slot = form.closest<HTMLElement>('[data-ui=\"list-page-actions\"]')\n if (slot) slots.add(slot)\n }\n for (const slot of slots) slot.hidden = ![...slot.childNodes].some(hasContent)\n }\n\n let queued = false\n const schedule = (): void => {\n if (queued || signal.aborted) return\n queued = true\n queueMicrotask(() => {\n queued = false\n sync()\n })\n }\n root.addEventListener('change', schedule, { signal })\n root.addEventListener('reset', schedule, { signal })\n const observer = new MutationObserver(schedule)\n observer.observe(root, {\n subtree: true,\n childList: true,\n attributes: true,\n attributeFilter: ['checked', 'disabled', 'form'],\n })\n const cleanup = (): void => observer.disconnect()\n signal.addEventListener('abort', cleanup, { once: true })\n sync()\n return cleanup\n}\n", "export function installUserWorkflow(lifetime: AbortSignal): () => void {\n let abort: AbortController | undefined,\n timer: ReturnType<typeof setTimeout> | undefined,\n sequence = 0\n async function check(form: HTMLFormElement) {\n const input = form.querySelector<HTMLInputElement>('input[type=email]'),\n status = form.querySelector<HTMLElement>('[data-email-status]')\n if (!input || !status) return\n abort?.abort()\n clearTimeout(timer)\n const request = ++sequence\n const value = input.value.trim(),\n next = form.querySelector<HTMLButtonElement>('button[type=\"submit\"][name=\"step\"]'),\n retry = form.querySelector<HTMLElement>('[data-email-retry]')\n const paint = (message: string, valid: boolean) => {\n status.textContent = message\n if (next) next.disabled = !valid\n input.setAttribute('aria-invalid', String(!valid))\n form.dataset.emailAvailable = valid ? 'true' : 'false'\n }\n if (retry) retry.hidden = true\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)) {\n paint(value ? 'Email ch\u01B0a \u0111\u00FAng \u0111\u1ECBnh d\u1EA1ng.' : 'Nh\u1EADp email \u0111\u1EC3 ki\u1EC3m tra.', false)\n return\n }\n paint('\u0110ang ki\u1EC3m tra email\u2026', false)\n timer = setTimeout(async () => {\n abort = new AbortController()\n try {\n const response = await fetch(form.dataset.emailCheck!, {\n method: 'POST',\n cache: 'no-store',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({ email: value }),\n signal: AbortSignal.any([abort.signal, AbortSignal.timeout(10000)]),\n })\n if (!response.ok) throw new Error('email-check-failed')\n const result = (await response.json()) as { available: boolean; ok: boolean }\n if (request !== sequence || !form.isConnected) return\n if (!result.ok) throw new Error('email-check-failed')\n paint(\n result.available ? 'Email c\u00F3 th\u1EC3 s\u1EED d\u1EE5ng.' : 'Email \u0111\u00E3 \u0111\u01B0\u1EE3c s\u1EED d\u1EE5ng. Nh\u1EADp email kh\u00E1c.',\n result.available,\n )\n } catch {\n if (request !== sequence || !form.isConnected) return\n paint('Ch\u01B0a ki\u1EC3m tra \u0111\u01B0\u1EE3c email. Vui l\u00F2ng th\u1EED l\u1EA1i.', false)\n if (retry) retry.hidden = false\n }\n }, 400)\n }\n function scope(form: HTMLFormElement, changed = false) {\n const company = form.querySelector<HTMLSelectElement>('select[name=\"companyId\"]'),\n branch = form.querySelector<HTMLSelectElement>('select[name=\"branchId\"]'),\n kind = form.querySelector<HTMLSelectElement>('select[name=\"scopeKind\"]')?.value\n if (branch && company) {\n if (changed) branch.value = ''\n for (const option of branch.options) {\n if (option.value) {\n option.hidden = option.dataset.companyId !== company.value\n option.disabled = option.hidden\n }\n }\n }\n const key =\n kind === 'tenant'\n ? 'tenant'\n : kind === 'branch'\n ? `branch:${company?.value}:${branch?.value}`\n : `company:${company?.value}`\n for (const input of form.querySelectorAll<HTMLInputElement>('[data-assigned-scopes]')) {\n const scopes = JSON.parse(input.dataset.assignedScopes ?? '[]') as string[]\n const assigned = scopes.includes(key)\n input.disabled = input.dataset.roleDisabled === 'true' || assigned\n const row = input.closest('[data-role-option]')\n const assignedLabel = row?.querySelector<HTMLElement>('[data-role-assigned]')\n const description = row?.querySelector<HTMLElement>('[data-role-description]')\n if (assignedLabel) assignedLabel.hidden = !assigned\n if (description) description.hidden = assigned\n if (input.disabled) input.checked = false\n }\n const companyField = company?.closest<HTMLElement>('[data-ui=\"form-field\"]')\n const branchField = branch?.closest<HTMLElement>('[data-ui=\"form-field\"]')\n if (companyField) companyField.hidden = kind === 'tenant'\n if (branchField) branchField.hidden = kind === 'tenant' || kind === 'company'\n }\n function scan() {\n for (const form of document.querySelectorAll<HTMLFormElement>('form[data-user-workflow]'))\n if (!form.dataset.initialized) {\n form.dataset.initialized = 'true'\n scope(form)\n void check(form)\n }\n }\n document.addEventListener(\n 'input',\n (event) => {\n const input = event.target as HTMLInputElement,\n form = input.closest<HTMLFormElement>('form[data-user-workflow]')\n if (!form) return\n if (input.type === 'email') void check(form)\n if (input.matches('[data-role-search]')) {\n let n = 0\n for (const row of form.querySelectorAll<HTMLElement>('[data-role-option]')) {\n row.hidden = !row.textContent?.toLocaleLowerCase('vi').includes(input.value.toLocaleLowerCase('vi'))\n if (!row.hidden) n++\n }\n const empty = form.querySelector<HTMLElement>('[data-role-empty]')\n if (empty) empty.hidden = n > 0\n }\n },\n { signal: lifetime },\n )\n document.addEventListener(\n 'change',\n (event) => {\n const input = event.target as HTMLElement,\n form = input.closest<HTMLFormElement>('form[data-user-workflow]')\n if (form && input.matches('select[name=\"companyId\"],select[name=\"branchId\"],select[name=\"scopeKind\"]'))\n scope(form, input.matches('select[name=\"companyId\"]'))\n },\n { signal: lifetime },\n )\n document.addEventListener(\n 'click',\n (event) => {\n const target = event.target as HTMLElement\n const retry = target.closest<HTMLElement>('[data-email-retry]')\n if (retry) void check(retry.closest('form')!)\n },\n { signal: lifetime },\n )\n document.addEventListener(\n 'submit',\n (event) => {\n const form = event.target as HTMLFormElement\n if (\n form.matches('form[data-user-workflow]') &&\n form.querySelector('input[type=email]') &&\n form.dataset.emailAvailable !== 'true'\n )\n event.preventDefault()\n },\n { signal: lifetime },\n )\n const observer = new MutationObserver(scan)\n observer.observe(document.documentElement, { childList: true, subtree: true })\n scan()\n return () => {\n abort?.abort()\n clearTimeout(timer)\n observer.disconnect()\n }\n}\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 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-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\nexport const Stack = (props: {\n items: readonly JSXChild[]\n gap?: 'compact' | 'default' | 'loose'\n}): TemplateResult => (\n <div data-ui=\"stack\" data-pattern=\"stack\" data-gap={props.gap ?? 'default'}>\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 description?: string | null\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\ntype PageIdentityContentProps = PageIdentityProps & {\n groupDescription?: boolean\n}\n\n/** The single identity band shared by every full-page component and recipe. */\nexport const pageIdentityContent = (\n kind: PageIdentityKind,\n props: PageIdentityContentProps,\n): 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.groupDescription || props.status !== undefined) &&\n (!!props.description || props.status !== undefined) ? (\n <div data-ui={`${kind}-subline`} data-kv-page-identity=\"subline\">\n {!!props.description && (\n <p data-ui={`${kind}-description`} data-kv-page-identity=\"description\">\n {props.description}\n </p>\n )}\n {props.status !== undefined && (\n <span data-ui={`${kind}-status`} data-kv-page-identity=\"status\">\n {props.status}\n </span>\n )}\n </div>\n ) : (\n !!props.description && (\n <p data-ui={`${kind}-description`} data-kv-page-identity=\"description\">\n {props.description}\n </p>\n )\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-description',\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 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 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 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\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 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 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 { Field } from '../../primitives/field/index.tsx'\nimport type { FieldProps } from '../../primitives/field/index.tsx'\nimport { issueFor } from '../shared.tsx'\nimport type { FieldIssue } from '../shared.tsx'\n\nexport const HOOKS = ['date-range'] as const\n\ntype TemporalProps = Omit<FieldProps, 'type' | 'fields' | 'control' | 'error'> & {\n issues?: readonly FieldIssue[]\n error?: string | null\n}\n\nconst temporal = (props: TemporalProps): FieldProps => ({\n ...props,\n error: props.error ?? issueFor(props.issues, props.name),\n})\n\n/** Civil dates are passed through as YYYY-MM-DD text and are never converted through UTC. */\nexport const DatePicker = (props: TemporalProps): TemplateResult => <Field {...temporal(props)} type=\"date\" />\n\nexport type DateRangePickerProps = {\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}\n\nexport const DateRangePicker = (props: DateRangePickerProps): TemplateResult => (\n <fieldset data-ui=\"date-range\" data-span={props.span ?? 'full'}>\n <legend>{props.label}</legend>\n <Field {...temporal({ ...props.start, label: props.startLabel })} type=\"date\" />\n <Field {...temporal({ ...props.end, label: props.endLabel })} type=\"date\" />\n </fieldset>\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)\n\nexport const TimePicker = (props: TemporalProps): TemplateResult => <Field {...temporal(props)} type=\"time\" />\n\nexport type { TemporalProps }\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\nexport const DescriptionList = (props: {\n label?: string\n items: readonly (KeyValueProps & { id: string })[]\n columns?: 1 | 2 | 3\n}): TemplateResult => (\n <dl data-ui=\"description-list\" aria-label={props.label} data-columns={String(props.columns ?? 2)}>\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-description',\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 description?: 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 description: props.description,\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-description',\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 description?: 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}>\n {pageIdentityContent(kind, { ...props, groupDescription: true })}\n </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, groupDescription: true })}\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-description',\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-description',\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 description?: 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-description',\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-description',\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", "import { installBulkSelection } from './bulk-selection.ts'\nimport { installUserWorkflow } from './user-workflow.ts'\nimport type { BrowserBehavior, BrowserNavigation } from '@ketvietlab/ketjs'\nimport { attachDesignSystemInteractions } from '@ketvietlab/design-system'\nimport type { SearchFilterNavigateDetail } from '@ketvietlab/design-system'\n\nconst themeStorageKey = 'ket.backend.theme'\nconst dismissibleDropdown = [\n '[data-ui=\"search-menu\"]',\n '[data-ui=\"col-config\"]',\n '[data-ui=\"bulk-actions\"]',\n '[data-ui=\"viewer\"]',\n '[data-ui=\"record-more\"]',\n].join(', ')\n\ntype KetBrowserGlobals = typeof globalThis & {\n confirm: (message?: string) => boolean\n}\n\nconst browserGlobals = globalThis as KetBrowserGlobals\nconst eventElement = (event: Event): Element | null => (event.target instanceof Element ? event.target : null)\n\ntype ThemePreference = 'light' | 'dark'\n\nconst themePreference = (value: string | null | undefined): ThemePreference | null =>\n value === 'light' || value === 'dark' ? value : null\n\nconst storedTheme = (): ThemePreference | null => {\n try {\n return themePreference(localStorage.getItem(themeStorageKey))\n } catch {\n return null\n }\n}\n\nconst saveTheme = (theme: ThemePreference): void => {\n try {\n localStorage.setItem(themeStorageKey, theme)\n } catch {\n // Theme selection still works for this document when storage is unavailable.\n }\n}\n\nconst resolvedTheme = (): ThemePreference =>\n themePreference(document.documentElement.dataset.theme) ??\n (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')\n\nconst syncThemeToggle = (): void => {\n const pressed = String(resolvedTheme() === 'dark')\n for (const control of document.querySelectorAll<HTMLElement>(\n '[data-ui=\"sidebar-tools\"] [data-ui=\"action\"][name=\"theme\"]',\n ))\n control.setAttribute('aria-pressed', pressed)\n}\n\n/** Keep the shell preference explicit only after the reader chooses one. */\nconst installThemeToggle = (signal: AbortSignal): void => {\n const preference = storedTheme()\n if (preference) document.documentElement.dataset.theme = preference\n syncThemeToggle()\n\n document.addEventListener(\n 'click',\n (event) => {\n const control = eventElement(event)?.closest<HTMLButtonElement>(\n '[data-ui=\"sidebar-tools\"] [data-ui=\"action\"][name=\"theme\"]',\n )\n if (!control || control.disabled) return\n const next = resolvedTheme() === 'dark' ? 'light' : 'dark'\n document.documentElement.dataset.theme = next\n saveTheme(next)\n syncThemeToggle()\n },\n { signal },\n )\n\n matchMedia('(prefers-color-scheme: dark)').addEventListener(\n 'change',\n () => {\n if (!themePreference(document.documentElement.dataset.theme)) syncThemeToggle()\n },\n { signal },\n )\n\n addEventListener(\n 'storage',\n (event) => {\n if (event.key !== themeStorageKey) return\n const next = themePreference(event.newValue)\n if (next) document.documentElement.dataset.theme = next\n else delete document.documentElement.dataset.theme\n syncThemeToggle()\n },\n { signal },\n )\n}\n\nconst updateSelection = (table: Element): void => {\n const rows = [\n ...table.querySelectorAll<HTMLInputElement>(\n ':is([data-ui=\"row-select\"], [data-ui=\"kt-row-select\"]):not(:disabled)',\n ),\n ]\n const checked = rows.filter((input) => input.checked).length\n const all = table.querySelector<HTMLInputElement>('[data-ui=\"select-all\"], [data-ui=\"kt-select-all\"]')\n if (all) {\n all.checked = rows.length > 0 && checked === rows.length\n all.indeterminate = checked > 0 && checked < rows.length\n }\n}\n\nconst installTableSelection = (signal: AbortSignal, navigation: BrowserNavigation): void => {\n document.addEventListener(\n 'change',\n (event) => {\n const target = eventElement(event)\n if (!(target instanceof HTMLInputElement)) return\n if (\n !target.matches(\n '[data-ui=\"select-all\"], [data-ui=\"row-select\"], [data-ui=\"ket-table\"][data-server=\"true\"] input',\n )\n )\n return\n const table = target.closest(\n '[data-ui=\"table\"], [data-ui=\"ket-table\"][data-server=\"true\"] [data-ui=\"kt-grid\"]',\n )\n if (!table) return\n if (target.matches('[data-ui=\"select-all\"], [data-ui=\"kt-select-all\"]'))\n for (const input of table.querySelectorAll<HTMLInputElement>(\n ':is([data-ui=\"row-select\"], [data-ui=\"kt-row-select\"]):not(:disabled)',\n ))\n input.checked = target.checked\n updateSelection(table)\n },\n { signal },\n )\n\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 target = eventElement(event)\n if (\n target?.closest(\n 'a, button, input, select, textarea, label, summary, details, [data-ui=\"select-cell\"], [data-ui=\"kt-select-cell\"]',\n )\n )\n return\n const row = target?.closest<HTMLElement>(\n '[data-ui=\"row\"][data-row-href], [data-ui=\"kt-row\"][data-row-href]',\n )\n const href = row?.getAttribute('data-row-href')\n if (!href) return\n event.preventDefault()\n void navigation.navigate(href)\n },\n { signal },\n )\n document.addEventListener(\n 'keydown',\n (event) => {\n if (event.defaultPrevented || (event.key !== 'Enter' && event.key !== ' ')) return\n const target = eventElement(event)\n const row = target?.matches(':is([data-ui=\"row\"], [data-ui=\"kt-row\"])[data-row-href][tabindex=\"0\"]')\n ? target\n : null\n const href = row?.getAttribute('data-row-href')\n if (!href) return\n event.preventDefault()\n navigateTo(href, navigation)\n },\n { signal },\n )\n}\n\nconst openDropdowns = (): HTMLDetailsElement[] => [\n ...document.querySelectorAll<HTMLDetailsElement>(\n dismissibleDropdown\n .split(', ')\n .map((selector) => `${selector}[open]`)\n .join(', '),\n ),\n]\n\nconst installDropdownDismiss = (signal: AbortSignal): void => {\n document.addEventListener(\n 'click',\n (event) => {\n const current = eventElement(event)?.closest(dismissibleDropdown)\n for (const dropdown of openDropdowns()) {\n if (dropdown !== current && !dropdown.contains(current ?? null)) dropdown.removeAttribute('open')\n }\n },\n { signal },\n )\n\n document.addEventListener(\n 'keydown',\n (event) => {\n if (event.defaultPrevented || event.key !== 'Escape') return\n const dropdowns = openDropdowns()\n if (!dropdowns.length) return\n const focused = document.activeElement?.closest<HTMLDetailsElement>(dismissibleDropdown)\n const current = focused?.open ? focused : dropdowns.at(-1)\n current?.removeAttribute('open')\n current?.querySelector<HTMLElement>('summary')?.focus()\n event.preventDefault()\n },\n { signal },\n )\n}\n\nconst installGlobalFilter = (signal: AbortSignal): void => {\n document.addEventListener(\n 'click',\n (event) => {\n const trigger = eventElement(event)?.closest<HTMLElement>('[data-ui=\"chrome-search-toggle\"]')\n const controls = trigger?.getAttribute('aria-controls')\n if (!trigger || !controls) return\n const dialog = document.getElementById(controls)\n if (!(dialog instanceof HTMLDialogElement) || dialog.open) return\n dialog.showModal()\n trigger.setAttribute('aria-expanded', 'true')\n requestAnimationFrame(() =>\n dialog\n .querySelector<HTMLElement>('[data-presentation=\"modal\"] [data-ui=\"chrome-search-input\"]')\n ?.focus(),\n )\n },\n { signal },\n )\n\n document.addEventListener(\n 'cancel',\n (event) => {\n if (!eventElement(event)?.matches('[data-ui=\"chrome-search-modal\"]')) return\n event.preventDefault()\n },\n { capture: true, signal },\n )\n\n document.addEventListener(\n 'close',\n (event) => {\n const dialog = eventElement(event)\n if (!(dialog instanceof HTMLDialogElement)) return\n if (!dialog.matches('[data-ui=\"chrome-search-modal\"]')) return\n const trigger = document.querySelector<HTMLElement>(\n `[data-ui=\"chrome-search-toggle\"][aria-controls=\"${CSS.escape(dialog.id)}\"]`,\n )\n trigger?.setAttribute('aria-expanded', 'false')\n trigger?.focus()\n },\n { capture: true, signal },\n )\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 activeRouteModal = (): HTMLElement | null =>\n document.querySelector<HTMLElement>('[data-ui=\"modal-layer\"][data-route-modal=\"true\"]')\n\nconst modalFocusables = (modal: HTMLElement): HTMLElement[] =>\n [...modal.querySelectorAll<HTMLElement>(focusable)].filter(\n (item) =>\n !item.hidden &&\n item.getAttribute('aria-hidden') !== 'true' &&\n !item.matches('[data-ui=\"modal-backdrop\"]'),\n )\n\nconst navigateTo = (href: string, navigation: BrowserNavigation): void => {\n void navigation.navigate(href)\n}\n\nconst eventStartedInNestedModal = (event: KeyboardEvent, routeModal: HTMLElement): boolean =>\n event\n .composedPath()\n .some(\n (target) =>\n target instanceof HTMLElement &&\n target !== routeModal &&\n target.matches('[data-ui=\"modal-layer\"][data-presentation=\"dialog\"]'),\n )\n\n/**\n * Whether anything in the modal has been typed into since the server rendered it.\n *\n * Comparing each control against its own default is what makes this work without\n * any bookkeeping: the page is server-rendered, so the default *is* the state the\n * reader was given. A modal with no form \u2014 a reader, an inspector \u2014 has nothing\n * to compare and is never dirty, so the guard costs those screens nothing.\n */\nconst modalHasDraft = (modal: HTMLElement): boolean => {\n for (const field of modal.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 modal.querySelectorAll<HTMLTextAreaElement>('textarea'))\n if (!area.disabled && area.value !== area.defaultValue) return true\n for (const select of modal.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/**\n * The guard, and what it is not.\n *\n * Leaving a modal is a link, and a link still works with scripting off \u2014 that is\n * the point of building it as one. So this catches the stray backdrop click, the\n * reflexive Escape and the tab clicked without thinking about the note already\n * typed; it is not a lock, and nothing here should be read as one. The wording\n * comes from the server because this file cannot translate.\n */\nconst mayLeaveModal = (modal: HTMLElement): boolean => {\n if (!modalHasDraft(modal)) return true\n const message = modal.getAttribute('data-unsaved-prompt')\n // No prompt declared means the screen did not ask to be guarded.\n if (!message) return true\n return browserGlobals.confirm(message)\n}\n\n/** Progressive keyboard behavior for URL-owned create/edit workspaces. */\n/**\n * Hear when the thing a screen is waiting for actually changed.\n *\n * The screens this replaces reloaded the whole document on a timer, which threw\n * away scroll, focus and anything typed, on a schedule unrelated to when the\n * work finished. Here the server says so, and the page asks for the fragment it\n * already knows how to swap \u2014 so the reader keeps their place.\n *\n * The connection belongs to the element. A fragment swap replaces it, and the\n * observer opens the new one and closes the old; a screen with nothing to wait\n * for renders no element and opens nothing.\n */\nconst installLiveRegion = (signal: AbortSignal, navigation: BrowserNavigation): (() => void) | undefined => {\n if (typeof EventSource !== 'function') return\n\n let watched: Element | null = null\n let source: EventSource | null = null\n const close = () => {\n source?.close()\n source = null\n watched = null\n }\n\n const sync = (): void => {\n const region = document.querySelector('[data-ui=\"live-region\"][data-stream]')\n if (region === watched) return\n close()\n const id = region instanceof HTMLElement ? (region.dataset.stream ?? '') : ''\n if (!id) return\n watched = region\n source = new EventSource(`/_ket/stream/${encodeURIComponent(id)}`)\n // What the chunk says is the deployment's business. That something was said\n // is the whole signal: the screen is server-rendered, so the way to find out\n // what changed is to ask for it.\n const refresh = () => navigateTo(location.href, navigation)\n source.onmessage = refresh\n source.addEventListener('done', () => {\n close()\n refresh()\n })\n // EventSource reconnects on its own, and a stream that has ended answers 404\n // rather than reopening. Leaving the error alone would retry that forever.\n source.onerror = () => {\n if (source?.readyState === EventSource.CLOSED) close()\n }\n }\n\n sync()\n const observer = new MutationObserver(sync)\n observer.observe(document.body, { childList: true, subtree: true })\n addEventListener('pagehide', close, { signal })\n return () => {\n observer.disconnect()\n close()\n }\n}\n\nconst installRouteModal = (signal: AbortSignal, navigation: BrowserNavigation): (() => void) => {\n const focused = new WeakSet<HTMLElement>()\n const focusModal = (): void => {\n const modal = activeRouteModal()\n if (!modal || focused.has(modal)) return\n focused.add(modal)\n requestAnimationFrame(() => {\n const sheet = modal.querySelector<HTMLElement>('[data-ui=\"modal-sheet\"]')\n const target = modalFocusables(modal).find((item) => !item.matches('[data-ui=\"modal-close\"]'))\n ;(target ?? sheet)?.focus()\n })\n }\n focusModal()\n const observer = new MutationObserver(focusModal)\n observer.observe(document.body, { childList: true, subtree: true })\n\n // The backdrop and the X are ordinary links, so the guard has to run before\n // the navigation layer sees the click rather than inside it.\n document.addEventListener(\n 'click',\n (event) => {\n const modal = activeRouteModal()\n if (!modal) return\n // A tab inside a modal is a third way out of it. It looks like a control\n // that stays on the screen, which is exactly why losing a half-written\n // note to one went unnoticed: the reader never meant to leave. It is a\n // link like the other two, and it leaves the same way.\n const leaving = event\n .composedPath()\n .find(\n (target) =>\n target instanceof HTMLElement &&\n (target.matches('[data-ui=\"modal-close\"], [data-ui=\"modal-backdrop\"], a[data-ui=\"tab\"]') ||\n target.matches('a[data-ui=\"action\"][href]')),\n )\n if (!leaving || !modal.contains(leaving as HTMLElement)) return\n if (!mayLeaveModal(modal)) {\n event.preventDefault()\n event.stopPropagation()\n }\n },\n { capture: true, signal },\n )\n\n document.addEventListener(\n 'click',\n (event) => {\n const container = document.querySelector<HTMLElement>('[data-save-before-navigation]')\n const form =\n container instanceof HTMLFormElement ? container : container?.querySelector<HTMLFormElement>('form')\n const anchor = eventElement(event)?.closest<HTMLAnchorElement>('a[href]')\n if (!container || !form || !anchor || activeRouteModal() || !modalHasDraft(container)) return\n event.preventDefault()\n event.stopPropagation()\n if (!browserGlobals.confirm(container.dataset.saveBeforeNavigation)) return\n let destination = form.querySelector<HTMLInputElement>('input[name=\"returnTo\"]')\n if (!destination) {\n destination = document.createElement('input')\n destination.type = 'hidden'\n destination.name = 'returnTo'\n form.append(destination)\n }\n destination.value = anchor.href\n form.requestSubmit()\n },\n { capture: true, signal },\n )\n document.addEventListener(\n 'keydown',\n (event) => {\n const modal = activeRouteModal()\n if (!modal) return\n // Relation-select removes its dialog synchronously on Escape. The event's\n // composed path is stable even after that DOM subtree disappears, so the\n // route layer must use it instead of querying the now-removed target.\n if (eventStartedInNestedModal(event, modal)) return\n if (event.key === 'Escape') {\n const close = modal.querySelector<HTMLAnchorElement>('[data-ui=\"modal-close\"]')\n if (!close) return\n event.preventDefault()\n if (!mayLeaveModal(modal)) return\n navigateTo(close.getAttribute('href') ?? close.href, navigation)\n return\n }\n if (event.key !== 'Tab') return\n const items = modalFocusables(modal)\n if (!items.length) return\n const first = items[0]\n const last = items.at(-1)!\n if (event.shiftKey && document.activeElement === first) {\n event.preventDefault()\n last.focus()\n } else if (!event.shiftKey && document.activeElement === last) {\n event.preventDefault()\n first.focus()\n }\n },\n { signal },\n )\n return () => observer.disconnect()\n}\n\n/**\n * A record modal changed something the collection behind it shows.\n *\n * The modal lives outside the content slot, so re-fetching the current URL as a\n * fragment refreshes the list without closing, reloading or rebuilding the modal.\n */\nconst installRecordRefresh = (signal: AbortSignal, navigation: BrowserNavigation): void => {\n document.addEventListener(\n 'ket:records-changed',\n () => {\n if (!document.querySelector('[data-ket-slot=\"backend.content\"]')) return\n void navigation.navigate(location.href, { replace: true })\n },\n { signal },\n )\n}\n\nexport const backendShell: BrowserBehavior = ({ navigation, lifetime }) => {\n document.addEventListener(\n 'ket:search-filter-navigate',\n (event) => {\n const { id, href, signal, respondWith } = (event as CustomEvent<SearchFilterNavigateDetail>).detail\n respondWith(\n navigation.navigate(href, { preserveContext: true, fallback: 'error', signal }).then(() => {\n const control = matchMedia('(max-width: 640px)').matches\n ? 'search-filter-toggle'\n : 'search-filter-input'\n document\n .getElementById(id)\n ?.querySelector<HTMLElement>(`[data-ui=\"${control}\"]`)\n ?.focus({ preventScroll: true })\n }),\n )\n },\n { signal: lifetime },\n )\n installRecordRefresh(lifetime, navigation)\n const cleanupDesignSystem = attachDesignSystemInteractions(document)\n const cleanupUserWorkflow = installUserWorkflow(lifetime)\n installThemeToggle(lifetime)\n installTableSelection(lifetime, navigation)\n const cleanupBulkSelection = installBulkSelection(document, lifetime)\n installDropdownDismiss(lifetime)\n installGlobalFilter(lifetime)\n const cleanupRouteModal = installRouteModal(lifetime, navigation)\n const cleanupLiveRegion = installLiveRegion(lifetime, navigation)\n return () => {\n cleanupDesignSystem()\n cleanupBulkSelection()\n cleanupLiveRegion?.()\n cleanupRouteModal()\n cleanupUserWorkflow()\n }\n}\n"],
|
|
5
|
+
"mappings": "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CA,OACA,cACA,oBACA,kBACF,ECbA,OAAS,QAAAC,OAAY,sBAwBjB,mBAAAC,GACE,OAAAC,GADF,QAAAC,OAAA,4BArBG,IAAMC,GAAQ,CACnB,SACA,cACA,cACA,eACA,iBACA,iBACA,QACA,aACA,cACA,gBACA,gBACA,UACA,eACF,ECjBA,OAAS,QAAAC,OAAY,sBCArB,OAAS,QAAAC,OAAY,sBCyEX,cAAAC,GAwBE,QAAAC,OAxBF,4BAvEH,IAAMC,GAAQ,CACnB,cACA,iBACA,cACA,aACA,gBACA,kBACA,cACA,oBACA,cACA,aACA,eACF,ECkBE,mBAAAC,GAGM,OAAAC,GAIF,QAAAC,OAPJ,4BCyBiC,OAqG/B,YAAAC,GArG+B,OAAAC,GAG7B,QAAAC,OAH6B,4BArD5B,IAAMC,GAAQ,CACnB,YACA,mBACA,aACA,mBACA,uBACA,YACA,mBACA,qBACA,wBACA,8BACA,yBACA,uBACA,gBACA,sBACA,uBACA,cACA,WACA,iBACA,OACA,eACA,cACA,eACA,eACA,iBACA,aACA,mBACA,eACA,cACA,eACA,YACA,YACA,gBACA,iBACA,iBACA,sBACF,EHmBwE,OAAsB,YAAAC,GAAtB,OAAAC,GAuE9D,QAAAC,OAvE8D,4BAvDjE,IAAMC,GAAQ,CACnB,QACA,SACA,OACA,YACA,UACA,eACA,kBACA,gBACA,sBACA,kBACA,aACA,qBACA,kBACA,UACA,eACA,kBACA,gBACA,sBACA,kBACA,eACA,eACA,aACA,YACA,eACA,eACA,eACA,aACA,eACA,cACA,YACA,YACA,eACA,SACA,cACA,eACA,eACA,cACA,cACA,iBACA,SACA,cACA,eACA,eACA,eACF,EIjDA,OAAS,QAAAC,OAAY,sBAKqB,mBAAAC,GAAA,OAAAC,OAAA,4BL0EtC,OAGI,OAAAC,GAHJ,QAAAC,OAAA,4BA1EG,IAAMC,GAAQ,CACnB,QACA,cACA,iBACA,gBACA,gBACA,eACA,qBACA,aACA,aACF,EMfA,OAAS,QAAAC,OAAY,sBAgEL,OACE,OAAAC,GADF,QAAAC,OAAA,4BA7DT,IAAMC,GAAQ,CACnB,cAC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",
|
|
6
|
+
"names": ["installBulkSelection", "root", "signal", "hasContent", "node", "sync", "slots", "form", "selected", "control", "menu", "tools", "slot", "queued", "schedule", "observer", "cleanup", "installUserWorkflow", "lifetime", "abort", "timer", "sequence", "check", "form", "input", "status", "request", "value", "next", "retry", "paint", "message", "valid", "response", "result", "scope", "changed", "company", "branch", "kind", "option", "key", "assigned", "row", "assignedLabel", "description", "companyField", "branchField", "scan", "event", "n", "empty", "observer", "each", "Fragment", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "each", "Fragment", "jsx", "jsxs", "HOOKS", "each", "each", "jsx", "jsxs", "HOOKS", "Fragment", "jsx", "jsxs", "Fragment", "jsx", "jsxs", "HOOKS", "Fragment", "jsx", "jsxs", "HOOKS", "each", "Fragment", "jsx", "jsx", "jsxs", "HOOKS", "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", "each", "signal", "jsx", "jsxs", "HOOKS", "each", "signal", "Fragment", "jsx", "jsxs", "HOOKS", "each", "effect", "signal", "jsx", "HOOKS", "Fragment", "jsx", "jsxs", "HOOKS", "focusableSelector", "focusables", "root", "element", "DISMISSIBLE_POPUPS", "DISMISSIBLE_POPUPS_OPEN", "selector", "attachGlobalSearch", "cleanups", "launchers", "openers", "launcher", "dialog", "candidate", "input", "form", "template", "layer", "body", "parkContent", "shortcut", "open", "restore", "launch", "event", "dismiss", "cancel", "submit", "cleanup", "attachDesignSystemInteractions", "cleanupGlobalSearch", "activeBeforeOpen", "modal", "appShell", "priorInert", "first", "menus", "menu", "syncMenu", "trigger", "menuCleanups", "onToggle", "navigationMedia", "navigationRoot", "priorNavigationOpen", "navigations", "navigation", "navigationStates", "restoreNavigationTargets", "state", "inert", "syncNavigationLock", "syncNavigation", "drawer", "mobile", "toggle", "shell", "targets", "activeItem", "focusTarget", "closeNavigation", "returnFocus", "navigationCleanups", "onClick", "target", "branch", "onNavigationMediaChange", "popover", "panel", "box", "gap", "placement", "prefersTop", "prefersEnd", "topPosition", "bottomPosition", "topFits", "bottomFits", "useTop", "requestedLeft", "left", "top", "onKeydown", "openNavigation", "items", "last", "treeItem", "tree", "item", "activeIndex", "nextIndex", "level", "index", "treeGridRow", "grid", "rows", "row", "activeMenu", "openMenu", "active", "openPopup", "summary", "openPopover", "close", "syncSelection", "table", "checked", "all", "bar", "count", "button", "onSelectionChange", "hook", "onDocumentClick", "entry", "popup", "jsx", "jsxs", "jsx", "jsxs", "HOOKS", "each", "Fragment", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "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", "themeStorageKey", "dismissibleDropdown", "browserGlobals", "eventElement", "event", "themePreference", "value", "storedTheme", "saveTheme", "theme", "resolvedTheme", "syncThemeToggle", "pressed", "control", "installThemeToggle", "signal", "preference", "next", "updateSelection", "table", "rows", "checked", "input", "all", "installTableSelection", "navigation", "target", "href", "navigateTo", "openDropdowns", "selector", "installDropdownDismiss", "current", "dropdown", "dropdowns", "focused", "installGlobalFilter", "trigger", "controls", "dialog", "focusable", "activeRouteModal", "modalFocusables", "modal", "item", "eventStartedInNestedModal", "routeModal", "modalHasDraft", "field", "area", "select", "option", "mayLeaveModal", "message", "installLiveRegion", "watched", "source", "close", "sync", "region", "id", "refresh", "observer", "installRouteModal", "focusModal", "sheet", "leaving", "container", "form", "anchor", "destination", "items", "first", "last", "installRecordRefresh", "backendShell", "lifetime", "respondWith", "cleanupDesignSystem", "attachDesignSystemInteractions", "cleanupUserWorkflow", "installUserWorkflow", "cleanupBulkSelection", "installBulkSelection", "cleanupRouteModal", "cleanupLiveRegion"]
|
|
7
7
|
}
|