create-mithaq 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/openstatus-time-picker.txt +21 -0
- package/index.js +457 -63
- package/package.json +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/package.json +5 -3
- package/template/apps/{asas-platform → asas-backoffice}/src/api.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/auth-frame.tsx +12 -12
- package/template/apps/asas-backoffice/src/components.ts +35 -0
- package/template/apps/asas-backoffice/src/first-user.ts +14 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/links.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/locale-switch.tsx +7 -5
- package/template/apps/asas-backoffice/src/no-organization.tsx +5 -0
- package/template/apps/asas-backoffice/src/redirect.ts +1 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/request-headers.ts +1 -1
- package/template/apps/asas-backoffice/src/routeTree.gen.ts +206 -0
- package/template/apps/asas-backoffice/src/router.tsx +18 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/files.ts +10 -0
- package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/screen.json +426 -64
- package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/types.ts +18 -0
- package/template/apps/{asas-platform/src/routes/(screens)/platform.$.tsx → asas-backoffice/src/routes/(screens)/backoffice.$.tsx} +4 -4
- package/template/apps/{asas-platform/src/routes/(screens)/platform.index.tsx → asas-backoffice/src/routes/(screens)/backoffice.index.tsx} +3 -3
- package/template/apps/{asas-platform/src/routes/(screens)/platform.tsx → asas-backoffice/src/routes/(screens)/backoffice.tsx} +3 -3
- package/template/apps/{asas-platform → asas-backoffice}/src/routes/__root.tsx +2 -2
- package/template/apps/asas-backoffice/src/routes/api/$.ts +34 -0
- package/template/apps/asas-backoffice/src/routes/auth.create-first-user.tsx +113 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/routes/auth.sign-in.tsx +9 -4
- package/template/apps/{asas-platform → asas-backoffice}/src/routes/unauthorized.tsx +24 -12
- package/template/apps/{asas-platform → asas-backoffice}/src/screen-form.tsx +5 -3
- package/template/apps/{asas-platform → asas-backoffice}/src/screen-list.tsx +6 -5
- package/template/apps/{asas-platform → asas-backoffice}/src/screen-names.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/screens.tsx +46 -26
- package/template/apps/asas-backoffice/src/server.ts +30 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/slide.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/start.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/styles.css +9 -7
- package/template/apps/{asas-platform → asas-backoffice}/tsconfig.json +2 -2
- package/template/apps/{asas-platform → asas-backoffice}/vite.config.ts +7 -1
- package/template/apps/asas-tanstack/package.json +4 -2
- package/template/apps/asas-tanstack/src/api.ts +1 -1
- package/template/apps/asas-tanstack/src/auth-frame.tsx +12 -12
- package/template/apps/asas-tanstack/src/choose-password.tsx +150 -0
- package/template/apps/asas-tanstack/src/components.ts +35 -0
- package/template/apps/asas-tanstack/src/copy.ts +416 -79
- package/template/apps/asas-tanstack/src/first-run.ts +16 -0
- package/template/apps/asas-tanstack/src/links.ts +1 -1
- package/template/apps/asas-tanstack/src/locale-switch.tsx +7 -5
- package/template/apps/asas-tanstack/src/no-organization.tsx +108 -0
- package/template/apps/asas-tanstack/src/redirect.ts +20 -1
- package/template/apps/asas-tanstack/src/request-headers.ts +1 -1
- package/template/apps/asas-tanstack/src/routeTree.gen.ts +42 -0
- package/template/apps/asas-tanstack/src/router.tsx +12 -1
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +9 -2
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +475 -116
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +4 -2
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +7 -2
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +83 -319
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +3 -1
- package/template/apps/asas-tanstack/src/routes/__root.tsx +1 -1
- package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +14 -7
- package/template/apps/asas-tanstack/src/routes/auth.first-run.tsx +114 -0
- package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +2 -2
- package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +5 -5
- package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +7 -100
- package/template/apps/asas-tanstack/src/routes/auth.set-password.tsx +16 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +27 -15
- package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +59 -38
- package/template/apps/asas-tanstack/src/routes/index.tsx +18 -14
- package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +24 -12
- package/template/apps/asas-tanstack/src/screen-form.tsx +5 -3
- package/template/apps/asas-tanstack/src/screen-list.tsx +6 -5
- package/template/apps/asas-tanstack/src/screen-names.ts +1 -1
- package/template/apps/asas-tanstack/src/screens.tsx +46 -26
- package/template/apps/asas-tanstack/src/server.ts +10 -0
- package/template/apps/asas-tanstack/src/slide.ts +1 -1
- package/template/apps/asas-tanstack/src/styles.css +7 -3
- package/template/apps/asas-tanstack/tsconfig.json +2 -2
- package/template/apps/asas-tanstack/vite.config.ts +7 -1
- package/template/package.json +3 -3
- package/template/templates/asas/.claude/skills/add-field/SKILL.md +27 -0
- package/template/templates/asas/.claude/skills/add-kayan/SKILL.md +26 -0
- package/template/templates/asas/.claude/skills/eject-admin-piece/SKILL.md +28 -0
- package/template/templates/asas/.claude/skills/upgrade-mithaq/SKILL.md +35 -0
- package/template/templates/asas/.env.example +22 -5
- package/template/templates/asas/AGENTS.md +101 -0
- package/template/templates/asas/CLAUDE.md +1 -0
- package/template/templates/asas/README.md +1094 -0
- package/template/templates/asas/package.json +19 -4
- package/template/templates/asas/src/asas.ts +81 -56
- package/template/templates/asas/src/{platform-http.ts → backoffice-http.ts} +48 -27
- package/template/templates/asas/src/dev-setup.ts +112 -0
- package/template/templates/asas/src/local-database.ts +22 -0
- package/template/templates/asas/src/messages/account.json +129 -103
- package/template/templates/asas/src/messages/auditLog.json +223 -105
- package/template/templates/asas/src/messages/backofficeLog.json +132 -0
- package/template/templates/asas/src/messages/courses.json +256 -214
- package/template/templates/asas/src/messages/enrollments.json +45 -27
- package/template/templates/asas/src/messages/invitation.json +72 -56
- package/template/templates/asas/src/messages/localUsers.json +34 -28
- package/template/templates/asas/src/messages/media.json +31 -21
- package/template/templates/asas/src/messages/member.json +59 -39
- package/template/templates/asas/src/messages/memberRoles.json +37 -31
- package/template/templates/asas/src/messages/mithaq.json +206 -162
- package/template/templates/asas/src/messages/organization.json +63 -41
- package/template/templates/asas/src/messages/organizationRole.json +59 -47
- package/template/templates/asas/src/messages/roles.json +59 -45
- package/template/templates/asas/src/messages/session.json +99 -79
- package/template/templates/asas/src/messages/sessions.json +110 -80
- package/template/templates/asas/src/messages/settings.json +51 -53
- package/template/templates/asas/src/messages/tags.json +31 -21
- package/template/templates/asas/src/messages/user.json +109 -87
- package/template/templates/asas/src/messages/verification.json +59 -47
- package/template/templates/asas/src/migrations/20261014221355_tags_restrict/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014221355_tags_restrict/snapshot.json +3881 -0
- package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/snapshot.json +3894 -0
- package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/snapshot.json +3907 -0
- package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/snapshot.json +3907 -0
- package/template/templates/asas/src/migrations/20261015042444_organization_names/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261015042444_organization_names/snapshot.json +3920 -0
- package/template/templates/asas/src/migrations/20261015053212_member_last_seen/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261015053212_member_last_seen/snapshot.json +3933 -0
- package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/migration.sql +15 -0
- package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/snapshot.json +4113 -0
- package/template/templates/asas/src/migrations/20261015095752_portal/migration.sql +2 -0
- package/template/templates/asas/src/migrations/20261015095752_portal/snapshot.json +4113 -0
- package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/migration.sql +9 -0
- package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/snapshot.json +4302 -0
- package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/migration.sql +27 -0
- package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/snapshot.json +4289 -0
- package/template/templates/asas/src/migrations/schema.ts +1 -0
- package/template/templates/asas/src/mithaq.ts +4 -406
- package/template/templates/asas/src/organizations.ts +3 -0
- package/template/templates/asas/src/seed.ts +147 -45
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/home.tsx +9 -9
- package/template/templates/asas/src/ui-backoffice/organization-actions.tsx +121 -0
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/record-page.tsx +4 -4
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/role-cells.tsx +13 -6
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/user-actions.tsx +46 -48
- package/template/templates/asas/src/ui-dashboard/dashboard.tsx +10 -4
- package/template/templates/asas/src/ui-dashboard/home.tsx +73 -0
- package/template/templates/asas/src/ui-dashboard/members-card.tsx +147 -0
- package/template/templates/asas/src/ui-dashboard/members.tsx +637 -438
- package/template/templates/asas/src/ui-dashboard/versions.tsx +22 -8
- package/template/templates/asas/src/ui-portal/courses-home.tsx +96 -0
- package/template/templates/asas/src/ui-portal/home.tsx +9 -13
- package/template/templates/features/audit-log/audit-log/audit-log.ts +284 -0
- package/template/templates/features/audit-log/backoffice-log/backoffice-log.ts +67 -0
- package/template/templates/features/audit-log/index.ts +4 -0
- package/template/templates/features/audit-log/package.json +13 -0
- package/template/templates/features/auth/index.ts +11 -0
- package/template/templates/{asas/src → features/auth}/local-users/local-users.ts +7 -7
- package/template/templates/{auth → features/auth}/package.json +2 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/account.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/account.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/invitation.access.ts +1 -1
- package/template/templates/{asas/src/kayan → features}/auth/user/invitation.ts +3 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/member.access.ts +4 -4
- package/template/templates/{asas/src/kayan → features}/auth/user/member.screens.ts +5 -5
- package/template/templates/{asas/src/kayan → features}/auth/user/member.ts +4 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/organization.access.ts +6 -6
- package/template/templates/{asas/src/kayan → features}/auth/user/organization.screens.ts +6 -6
- package/template/templates/{asas/src/kayan → features}/auth/user/organization.ts +2 -1
- package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.ts +3 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/session.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/session.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/user.access.ts +8 -7
- package/template/templates/{asas/src/kayan → features}/auth/user/user.screens.ts +9 -9
- package/template/templates/{asas/src/kayan → features}/auth/user/user.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/verification.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/verification.ts +2 -2
- package/template/templates/{asas/src → features/courses}/courses/courses.ts +122 -133
- package/template/templates/features/courses/courses/cover-reads.ts +33 -0
- package/template/templates/features/courses/courses/ui/archived-badge.tsx +9 -0
- package/template/templates/{asas/src → features/courses}/courses/ui/fee-cell.tsx +1 -1
- package/template/templates/{asas/src → features/courses}/courses/ui/status-cell.tsx +5 -3
- package/template/templates/features/courses/courses/ui/unpublish-button.tsx +15 -0
- package/template/templates/features/courses/domain/archive.ts +39 -0
- package/template/templates/features/courses/domain/on-offer.ts +6 -0
- package/template/templates/features/courses/enrollments/enrollments.ts +309 -0
- package/template/templates/{asas/src → features/courses}/enrollments/ui/session-cell.tsx +17 -7
- package/template/templates/{asas/src → features/courses/enrollments}/ui/session-labels.tsx +3 -3
- package/template/templates/features/courses/index.ts +8 -0
- package/template/templates/features/courses/package.json +24 -0
- package/template/templates/features/courses/portal/ui/catalogue.tsx +86 -0
- package/template/templates/features/courses/portal/ui/course.tsx +176 -0
- package/template/templates/features/courses/portal/ui/enrollments.tsx +170 -0
- package/template/templates/features/courses/portal/ui/parts.tsx +95 -0
- package/template/templates/features/courses/portal/ui/reads.ts +76 -0
- package/template/templates/features/courses/portal/ui/rules.ts +87 -0
- package/template/templates/features/courses/portal/ui/shell.tsx +194 -0
- package/template/templates/features/courses/sessions/seats.ts +42 -0
- package/template/templates/{asas/src → features/courses}/sessions/sessions.ts +45 -14
- package/template/templates/features/media/index.ts +10 -0
- package/template/templates/{asas/src → features/media}/media/media.ts +34 -40
- package/template/templates/{asas/src → features/media}/media/ui/preview.tsx +2 -2
- package/template/templates/features/media/package.json +19 -0
- package/template/templates/features/roles/index.ts +4 -0
- package/template/templates/{asas/src → features/roles}/member-roles/member-roles.ts +15 -15
- package/template/templates/features/roles/package.json +21 -0
- package/template/templates/{asas/src → features/roles}/roles/roles.ts +23 -23
- package/template/templates/{asas/src → features/roles}/roles/ui/role-form.tsx +65 -24
- package/template/templates/{asas/src → features/roles}/roles/ui/roles.tsx +49 -25
- package/template/templates/features/settings/index.ts +3 -0
- package/template/templates/features/settings/package.json +13 -0
- package/template/templates/{asas/src → features/settings}/settings/settings.ts +12 -17
- package/template/templates/features/tags/index.ts +3 -0
- package/template/templates/features/tags/package.json +14 -0
- package/template/templates/{asas/src → features/tags}/tags/tags.ts +15 -15
- package/template/templates/mithaq/package.json +33 -0
- package/template/templates/mithaq/src/mail/index.ts +32 -0
- package/template/templates/mithaq/src/mail/mailers.ts +36 -0
- package/template/templates/{asas/src/audit-log/sink.ts → mithaq/src/mithaq/audit-sink.ts} +31 -9
- package/template/templates/{auth/src/platform-roles.ts → mithaq/src/mithaq/auth/backoffice-roles.ts} +44 -23
- package/template/templates/mithaq/src/mithaq/auth/index.ts +1859 -0
- package/template/templates/mithaq/src/mithaq/auth/organizations.ts +80 -0
- package/template/templates/{asas/src → mithaq/src/mithaq}/pages.ts +16 -7
- package/template/templates/mithaq/src/mithaq/rich-text-nodes.ts +19 -0
- package/template/templates/mithaq/src/mithaq.ts +593 -0
- package/template/templates/mithaq/src/roles/default-member-roles.ts +29 -0
- package/template/templates/{asas → mithaq}/src/roles/grants.ts +1 -1
- package/template/templates/mithaq/src/roles/member-access.ts +11 -0
- package/template/templates/{asas/src → mithaq/src/roles}/statements.ts +6 -4
- package/template/templates/mithaq/src/roles/system-roles.ts +16 -0
- package/template/templates/{asas/src → mithaq/src/roles}/tenant-access.ts +96 -57
- package/template/templates/{asas/src → mithaq/src/roles}/tenant-roles.ts +84 -25
- package/template/templates/{asas → mithaq}/src/steps/expected-updated-at.ts +2 -2
- package/template/templates/{asas → mithaq}/src/storage/index.ts +3 -1
- package/template/templates/mithaq/src/ui/activity-page.tsx +33 -0
- package/template/templates/mithaq/src/ui/activity.tsx +527 -0
- package/template/templates/{asas → mithaq}/src/ui/app-shell.tsx +27 -19
- package/template/templates/mithaq/src/ui/rich-text-nodes.tsx +33 -0
- package/template/templates/{asas → mithaq}/src/ui/screen-nav.tsx +1 -1
- package/template/templates/{asas → mithaq}/src/ui/top-shell.tsx +8 -8
- package/template/apps/asas-platform/src/components/mithaq/cells.tsx +0 -112
- package/template/apps/asas-platform/src/components/mithaq/controls.tsx +0 -650
- package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +0 -53
- package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +0 -108
- package/template/apps/asas-platform/src/components/mithaq/form.tsx +0 -1467
- package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +0 -108
- package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +0 -368
- package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +0 -153
- package/template/apps/asas-platform/src/components/mithaq/list-query.ts +0 -432
- package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +0 -74
- package/template/apps/asas-platform/src/components/mithaq/list.tsx +0 -865
- package/template/apps/asas-platform/src/components/mithaq/nav.tsx +0 -871
- package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +0 -1024
- package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +0 -127
- package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +0 -400
- package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +0 -6
- package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +0 -1083
- package/template/apps/asas-platform/src/components/mithaq/upload.tsx +0 -686
- package/template/apps/asas-platform/src/components/ui/drawer.tsx +0 -216
- package/template/apps/asas-platform/src/redirect.ts +0 -1
- package/template/apps/asas-platform/src/routeTree.gen.ts +0 -181
- package/template/apps/asas-platform/src/router.tsx +0 -7
- package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +0 -4
- package/template/apps/asas-platform/src/routes/api/$.ts +0 -30
- package/template/apps/asas-platform/src/server.ts +0 -20
- package/template/apps/asas-tanstack/src/components/mithaq/cells.tsx +0 -112
- package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +0 -650
- package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +0 -53
- package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +0 -108
- package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +0 -1467
- package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +0 -108
- package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +0 -368
- package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +0 -153
- package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +0 -432
- package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +0 -74
- package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +0 -865
- package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +0 -871
- package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +0 -1024
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +0 -127
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +0 -400
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +0 -6
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +0 -1083
- package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +0 -686
- package/template/apps/asas-tanstack/src/components/ui/drawer.tsx +0 -215
- package/template/templates/asas/src/audit-log/audit-log.ts +0 -187
- package/template/templates/asas/src/audit-log/ui/activity.tsx +0 -243
- package/template/templates/asas/src/courses/ui/archive-button.tsx +0 -25
- package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +0 -11
- package/template/templates/asas/src/courses/ui/on-offer.ts +0 -6
- package/template/templates/asas/src/enrollments/enrollments.ts +0 -137
- package/template/templates/asas/src/rich-text-nodes.ts +0 -11
- package/template/templates/asas/src/roles.ts +0 -16
- package/template/templates/asas/src/ui/rich-text-nodes.tsx +0 -15
- package/template/templates/asas/src/ui-platform/organization-actions.tsx +0 -114
- package/template/templates/auth/src/index.ts +0 -690
- /package/template/apps/{asas-platform → asas-backoffice}/components.json +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/gitignore +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/public/favicon.svg +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/accordion.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/alert-dialog.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/avatar.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/badge.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/breadcrumb.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/calendar.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/card.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/chart.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/checkbox.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/collapsible.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/combobox.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dialog.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/direction.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/directional-icon.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dropdown-menu.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/field.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-otp.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/label.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/popover.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/radio-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/resizable.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/scroll-area.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/select.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/separator.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sheet.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sidebar.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/skeleton.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/slider.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/spinner.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/switch.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/table.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tabs.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/textarea.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tooltip.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/copy.ts +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/hooks/use-mobile.ts +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/lib/utils.ts +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/theme.ts +0 -0
- /package/template/templates/{asas/src/kayan → features}/auth/user/index.ts +0 -0
- /package/template/templates/{asas/src → features/courses}/courses/ui/no-courses.tsx +0 -0
- /package/template/templates/{asas → mithaq}/components.json +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/defaults.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/local.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/r2.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/s3.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/uploadthing.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/ui/kpi-card.tsx +0 -0
- /package/template/templates/{asas → mithaq}/src/ui/record-page.tsx +0 -0
- /package/template/templates/{asas → mithaq}/src/ui/table-page.tsx +0 -0
|
@@ -1,1024 +0,0 @@
|
|
|
1
|
-
import { Fragment, use, useMemo, useRef, useState, type ComponentType, type Key, type ReactElement, type ReactNode, type RefObject } from 'react'
|
|
2
|
-
import { ChevronDown, XIcon } from 'lucide-react'
|
|
3
|
-
import { kindOf, matchesWhere, regionsIn } from 'mithaq/admin'
|
|
4
|
-
import { ScreenContext, useLocale, useScreen } from 'mithaq/admin/react'
|
|
5
|
-
import type {
|
|
6
|
-
ButtonClient,
|
|
7
|
-
ButtonProps,
|
|
8
|
-
CellProps as ScreenCellProps,
|
|
9
|
-
ClientField,
|
|
10
|
-
ClientKayan,
|
|
11
|
-
ComponentProps,
|
|
12
|
-
ControlProps as ScreenControlProps,
|
|
13
|
-
EmptyProps,
|
|
14
|
-
MenuProps,
|
|
15
|
-
FrameClient,
|
|
16
|
-
LayoutProps,
|
|
17
|
-
LeafClient,
|
|
18
|
-
NodeClient,
|
|
19
|
-
NodeKind,
|
|
20
|
-
NodeOf,
|
|
21
|
-
PageClient,
|
|
22
|
-
PageProps,
|
|
23
|
-
ProviderProps,
|
|
24
|
-
Row,
|
|
25
|
-
RegionsClient,
|
|
26
|
-
ShellProps,
|
|
27
|
-
Translate,
|
|
28
|
-
} from 'mithaq/admin'
|
|
29
|
-
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
|
|
30
|
-
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel, AccordionTrigger } from '@/components/ui/accordion'
|
|
31
|
-
import { Badge } from '@/components/ui/badge'
|
|
32
|
-
import { Button } from '@/components/ui/button'
|
|
33
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
34
|
-
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
|
35
|
-
import { Dialog, DialogClose, DialogContent, DialogTitle } from '@/components/ui/dialog'
|
|
36
|
-
import { Drawer, DrawerContent, DrawerTitle } from '@/components/ui/drawer'
|
|
37
|
-
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
|
|
38
|
-
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
39
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
40
|
-
import { Separator } from '@/components/ui/separator'
|
|
41
|
-
import { Sheet, SheetClose, SheetContent, SheetTitle } from '@/components/ui/sheet'
|
|
42
|
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
43
|
-
import {
|
|
44
|
-
besideLabel,
|
|
45
|
-
controlFor,
|
|
46
|
-
controls,
|
|
47
|
-
descriptionFor,
|
|
48
|
-
labelFor,
|
|
49
|
-
newTitleOf,
|
|
50
|
-
placeholderFor,
|
|
51
|
-
titleOf,
|
|
52
|
-
type ControlProps,
|
|
53
|
-
type Copy,
|
|
54
|
-
type FindApi,
|
|
55
|
-
type FoundPage,
|
|
56
|
-
} from './controls.tsx'
|
|
57
|
-
import { FieldShell, describedBy } from './field-shell.tsx'
|
|
58
|
-
import { idsOf, nameOfRow } from './relationship-control.tsx'
|
|
59
|
-
import { titleKey, useRelationshipTitles } from './relationship-cell.tsx'
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* What each place a screen names a component hands it, from `mithaq/admin`, with a React node where
|
|
63
|
-
* the props carry one. Everything else a component reads through the hooks `mithaq/admin/react`
|
|
64
|
-
* exports.
|
|
65
|
-
*/
|
|
66
|
-
export type Places = {
|
|
67
|
-
provider: ComponentType<ProviderProps<ReactNode>>
|
|
68
|
-
shell: ComponentType<ShellProps<readonly string[], ReactNode>>
|
|
69
|
-
page: ComponentType<PageProps>
|
|
70
|
-
layout: ComponentType<LayoutProps<readonly string[], ReactNode>>
|
|
71
|
-
component: ComponentType<ComponentProps>
|
|
72
|
-
control: ComponentType<ScreenControlProps>
|
|
73
|
-
cell: ComponentType<ScreenCellProps>
|
|
74
|
-
empty: ComponentType<EmptyProps>
|
|
75
|
-
menu: ComponentType<MenuProps<ReactNode>>
|
|
76
|
-
button: ComponentType<ButtonProps>
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** The components a screen names by path, keyed by the path `screen.json` prints. */
|
|
80
|
-
export type Refs = Readonly<Record<string, ComponentType<never>>>
|
|
81
|
-
|
|
82
|
-
/** A kayan's page declared as one tree, rather than drawn whole by one component. */
|
|
83
|
-
export type TreePage = Extract<PageClient, { page: FrameClient }>
|
|
84
|
-
|
|
85
|
-
/** What a form or a list draws a screen's page with, beside the adapted kayan. */
|
|
86
|
-
export type ScreenPage = {
|
|
87
|
-
refs: Refs
|
|
88
|
-
/** The screen's own kayan, which is what a component named by path is handed. */
|
|
89
|
-
kayan: ClientKayan
|
|
90
|
-
page: TreePage
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** A component the page names by path, typed for the place it sits in. */
|
|
94
|
-
export const refOf = <Place extends keyof Places>({
|
|
95
|
-
screen,
|
|
96
|
-
place: _place,
|
|
97
|
-
path,
|
|
98
|
-
}: {
|
|
99
|
-
screen: Pick<ScreenPage, 'refs'> | undefined
|
|
100
|
-
place: Place
|
|
101
|
-
path: string | undefined
|
|
102
|
-
}) =>
|
|
103
|
-
(screen && path && Object.hasOwn(screen.refs, path) ? screen.refs[path] : undefined) as Places[Place] | undefined
|
|
104
|
-
|
|
105
|
-
export type ListNode = NodeOf<'list'>['list']
|
|
106
|
-
/** A node of the kind it is, once `kindOf` has said which. */
|
|
107
|
-
const ofKind = <Kind extends NodeKind,>(node: NodeClient, kind: Kind) => (kindOf(node) === kind ? (node as NodeOf<Kind>) : undefined)
|
|
108
|
-
/** What a node holds, off its kind's entry in the one map. */
|
|
109
|
-
const inside = (node: NodeClient): readonly NodeClient[] => (nodeMap[kindOf(node)!].children as (node: NodeClient) => readonly NodeClient[])(node)
|
|
110
|
-
/** The first node some nodes hold at any depth that `is` picks. */
|
|
111
|
-
const first = <Found,>(nodes: readonly NodeClient[], is: (node: NodeClient) => Found | undefined): Found | undefined => {
|
|
112
|
-
for (const node of nodes) {
|
|
113
|
-
const found = is(node) ?? first(inside(node), is)
|
|
114
|
-
if (found !== undefined) return found
|
|
115
|
-
}
|
|
116
|
-
return undefined
|
|
117
|
-
}
|
|
118
|
-
/** The first list node of some nodes, wherever it sits, that lists the page's own kayan. */
|
|
119
|
-
export const listIn = (nodes: readonly NodeClient[]): ListNode | undefined =>
|
|
120
|
-
first(nodes, (node) => {
|
|
121
|
-
const list = ofKind(node, 'list')?.list
|
|
122
|
-
return list && !list.of ? list : undefined
|
|
123
|
-
})
|
|
124
|
-
/** Every list node some nodes hold that lists another kayan scoped to the page's row, wherever each sits. */
|
|
125
|
-
export const relatedIn = (nodes: readonly NodeClient[], scope?: Row): ListNode[] =>
|
|
126
|
-
nodes.flatMap((node) => {
|
|
127
|
-
// A node its `when` hides is not drawn, so nothing under it is read either: the read would only be
|
|
128
|
-
// refused. A tab that hides every node it holds is the tab strip's to leave out, and its nodes are
|
|
129
|
-
// each hidden here.
|
|
130
|
-
if (scope && !shown(node, scope)) return []
|
|
131
|
-
const list = ofKind(node, 'list')?.list
|
|
132
|
-
return list ? (list.of ? [list] : []) : relatedIn(inside(node), scope)
|
|
133
|
-
})
|
|
134
|
-
/** The first form node, wherever it sits: a page that holds one is drawn by a form. */
|
|
135
|
-
const formNodeIn = (nodes: readonly NodeClient[]): NodeOf<'form'> | undefined => first(nodes, (node) => ofKind(node, 'form'))
|
|
136
|
-
/** The first form node's nodes, wherever it sits. */
|
|
137
|
-
export const formIn = (nodes: readonly NodeClient[]): NodeClient[] | undefined => formNodeIn(nodes)?.form
|
|
138
|
-
/**
|
|
139
|
-
* Whether a page is drawn as the row's values rather than a form: it holds no form, or the viewer may update the row
|
|
140
|
-
* nowhere, where boxes refusing every keystroke would only pretend. A project component's form decides itself.
|
|
141
|
-
*/
|
|
142
|
-
export const readsAsValues = ({
|
|
143
|
-
page,
|
|
144
|
-
value,
|
|
145
|
-
operations,
|
|
146
|
-
}: {
|
|
147
|
-
page: TreePage
|
|
148
|
-
value: Row | null | undefined
|
|
149
|
-
operations: Readonly<Record<string, boolean>>
|
|
150
|
-
}): boolean => {
|
|
151
|
-
const form = formNodeIn(nodesOf(page))
|
|
152
|
-
return !form || (!!value && operations.update !== true && !form.component)
|
|
153
|
-
}
|
|
154
|
-
/** Whether some nodes draw a node of this kind over the scope: the node and every node holding it must be shown. */
|
|
155
|
-
export const drawsKind = ({ nodes, kind, scope }: { nodes: readonly NodeClient[]; kind: NodeKind; scope: Row }): boolean =>
|
|
156
|
-
nodes.some((node) => shown(node, scope) && (kindOf(node) === kind || drawsKind({ nodes: inside(node), kind, scope })))
|
|
157
|
-
/** Every node of a page's tree, region by region, from its shell down. */
|
|
158
|
-
export const nodesOf = (page: TreePage): NodeClient[] => regionsIn(page.page).flatMap(([, nodes]) => nodes)
|
|
159
|
-
/** The list node a screen's page draws, which is what a list page holds exactly one of. */
|
|
160
|
-
export const listOf = (screen: { page: TreePage } | undefined): ListNode | undefined => screen && listIn(nodesOf(screen.page))
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Whether a nav entry is the page the viewer is on: its kayan is the page's own, on every page of that
|
|
164
|
-
* kayan. Known on the server, so the first paint marks it.
|
|
165
|
-
*/
|
|
166
|
-
export const currentOf = (entry: { kayan?: string }, kayan: string | undefined): 'page' | undefined =>
|
|
167
|
-
entry.kayan !== undefined && entry.kayan === kayan ? 'page' : undefined
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* What one row is called by its kayan's own title field, in the viewer's
|
|
171
|
-
* locale, or nothing where the kayan declares none, the row holds none, or
|
|
172
|
-
* the title is a relationship that has to be read first; never the row's id.
|
|
173
|
-
*/
|
|
174
|
-
export const rowTitle = ({ kayan, row, locale }: { kayan: ClientKayan; row: Row; locale: string }): string | undefined => {
|
|
175
|
-
const field = kayan.fields.find((one) => one.name === kayan.useAsTitle && one.name !== 'id')
|
|
176
|
-
const held = field ? row[field.name] : undefined
|
|
177
|
-
if (!field || field.kind === 'relationship' || held === null || held === undefined || held === '') return undefined
|
|
178
|
-
const name = nameOfRow({ row, title: field.name, locale, kind: field.kind })
|
|
179
|
-
return name && name !== String(row.id ?? '') ? name : undefined
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Whether a row's title is empty: the kayan is titled by a field the reader may read, and its value (in the viewer's
|
|
184
|
-
* locale if localized) is nothing or blank. A draft may hold no title, and a snapshot of one may lack the key.
|
|
185
|
-
*/
|
|
186
|
-
export const isUntitled = ({ kayan, row, locale }: { kayan: ClientKayan; row: Row; locale: string }): boolean => {
|
|
187
|
-
const field = kayan.fields.find((one) => one.name === kayan.useAsTitle && one.name !== 'id')
|
|
188
|
-
if (!field || field.kind === 'relationship') return false
|
|
189
|
-
const held = row[field.name]
|
|
190
|
-
const text = held !== null && typeof held === 'object' && !(held instanceof Date) ? (held as Row)[locale] : held
|
|
191
|
-
return text === null || text === undefined || (typeof text === 'string' && text.trim() === '')
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* What a button's dialog says it runs on: the row's title (or "this row"), in the operation's `.warning` sentence.
|
|
196
|
-
* The title is isolated with U+2068/U+2069 so an email or digits in an Arabic sentence keep their order.
|
|
197
|
-
*/
|
|
198
|
-
export const rowWarning = ({ kayan, row, operation, copy: { t, locale } }: { kayan: ClientKayan; row: Row; operation?: string; copy: Copy }): string => {
|
|
199
|
-
const name = rowTitle({ kayan, row, locale })
|
|
200
|
-
if (!name) return t('admin.rowWarning', 'This applies to this row.') ?? 'This applies to this row.'
|
|
201
|
-
const sentence = (operation && t(`${kayan.name}.operations.${operation}.warning`)) || (t('admin.rowNamedWarning', 'This applies to {name}.') ?? 'This applies to {name}.')
|
|
202
|
-
return sentence.replace('{name}', () => `\u2068${name}\u2069`)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* A page's title read across its titling relationship: the related row's title, or the kind's name until it answers,
|
|
207
|
-
* on every server render, and for a row the viewer may not read.
|
|
208
|
-
*/
|
|
209
|
-
function RelatedTitle({ Heading, field, row, locale, fallback }: { Heading: 'h1' | 'h2'; field: ClientField; row: Row; locale: string; fallback: string }): ReactElement {
|
|
210
|
-
const state = use(ScreenContext)
|
|
211
|
-
const target = field.relationTo ?? ''
|
|
212
|
-
const api = useMemo<FindApi | undefined>(() => (state ? { [target]: { find: (input) => state.client({ kayan: target, operation: 'find', input }) as Promise<FoundPage> } } : undefined), [state?.client, target])
|
|
213
|
-
const titles = useRelationshipTitles({ fields: [field], docs: [row], context: { api, client: Object.values(state?.screen.kayan ?? {}), locale } })
|
|
214
|
-
const id = idsOf(row[field.name])[0]
|
|
215
|
-
const name = id === undefined ? undefined : titles.get(titleKey(target, id))
|
|
216
|
-
const title = typeof name === 'string' && name !== id ? name : fallback
|
|
217
|
-
return <Heading data-node="title" title={title} className="text-2xl font-bold tracking-tight">{title}</Heading>
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** The screen's nav as the host provides it: one link per entry under the screen's path. */
|
|
221
|
-
export function NavLinks(): ReactElement {
|
|
222
|
-
const screen = useScreen()
|
|
223
|
-
const copy = useLocale()
|
|
224
|
-
const current = use(ScreenContext)?.kayan?.name
|
|
225
|
-
return (
|
|
226
|
-
<ul className="flex flex-col gap-1">
|
|
227
|
-
{screen.nav.map((entry) => (
|
|
228
|
-
<li key={`${entry.page}${entry.path}`}>
|
|
229
|
-
<a href={`${screen.path}${entry.path}`} aria-current={currentOf(entry, current)} className="text-sm">
|
|
230
|
-
{entry.label?.[copy.locale] ?? titleOf({ kayan: { name: entry.kayan ?? entry.page }, copy, many: !screen.kayan[entry.kayan ?? '']?.single })}
|
|
231
|
-
</a>
|
|
232
|
-
</li>
|
|
233
|
-
))}
|
|
234
|
-
</ul>
|
|
235
|
-
)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* A button's `confirm`, asked in the app's own alert dialog in the page's locale rather than the
|
|
240
|
-
* browser's. `ask` answers whether the person said yes, at once for a button that carries no
|
|
241
|
-
* confirm, and `dialog` is the question, drawn beside the button that asks it.
|
|
242
|
-
*/
|
|
243
|
-
export const useConfirm = ({
|
|
244
|
-
confirm,
|
|
245
|
-
confirmLabel,
|
|
246
|
-
description,
|
|
247
|
-
copy: { t, locale },
|
|
248
|
-
}: {
|
|
249
|
-
confirm: ButtonClient['confirm']
|
|
250
|
-
confirmLabel: string
|
|
251
|
-
/** What the caller says the button runs on, such as a bulk's count of ticked rows, before the confirm's own body. */
|
|
252
|
-
description?: string
|
|
253
|
-
copy: Copy
|
|
254
|
-
}) => {
|
|
255
|
-
const [answer, setAnswer] = useState<(yes: boolean) => void>()
|
|
256
|
-
const settle = (yes: boolean) => {
|
|
257
|
-
answer?.(yes)
|
|
258
|
-
setAnswer(undefined)
|
|
259
|
-
}
|
|
260
|
-
const heading = confirm?.title[locale]
|
|
261
|
-
const body = [description, confirm?.body[locale]].filter(Boolean).join(' ')
|
|
262
|
-
const onConfirm = () => settle(true)
|
|
263
|
-
const onCancel = () => settle(false)
|
|
264
|
-
const ask = () => (confirm ? new Promise<boolean>((resolve) => setAnswer(() => resolve)) : Promise.resolve(true))
|
|
265
|
-
const dialog = confirm ? (
|
|
266
|
-
<AlertDialog open={answer !== undefined} onOpenChange={(open: boolean) => (open ? undefined : onCancel())}>
|
|
267
|
-
<AlertDialogContent>
|
|
268
|
-
<AlertDialogHeader>
|
|
269
|
-
<AlertDialogTitle>{heading}</AlertDialogTitle>
|
|
270
|
-
<AlertDialogDescription>{body}</AlertDialogDescription>
|
|
271
|
-
</AlertDialogHeader>
|
|
272
|
-
<AlertDialogFooter className="max-sm:[&_button]:h-11">
|
|
273
|
-
<AlertDialogCancel>{t('admin.cancel', 'Cancel') ?? 'Cancel'}</AlertDialogCancel>
|
|
274
|
-
<AlertDialogAction data-button="confirm-operation" onClick={onConfirm}>
|
|
275
|
-
{confirmLabel}
|
|
276
|
-
</AlertDialogAction>
|
|
277
|
-
</AlertDialogFooter>
|
|
278
|
-
</AlertDialogContent>
|
|
279
|
-
</AlertDialog>
|
|
280
|
-
) : null
|
|
281
|
-
return { ask, dialog }
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* A project's own button component, handed a run that asks the button's
|
|
286
|
-
* confirm first, so no component can run an operation past its question.
|
|
287
|
-
*/
|
|
288
|
-
export function OwnButton({
|
|
289
|
-
Own,
|
|
290
|
-
busy,
|
|
291
|
-
call,
|
|
292
|
-
copy,
|
|
293
|
-
operation,
|
|
294
|
-
}: {
|
|
295
|
-
Own: Places['button']
|
|
296
|
-
busy: boolean
|
|
297
|
-
call: (input: Row) => Promise<void>
|
|
298
|
-
copy: Copy
|
|
299
|
-
operation: Pick<PageButton, 'name' | 'label' | 'confirm'>
|
|
300
|
-
}): ReactElement {
|
|
301
|
-
const { ask, dialog } = useConfirm({ confirm: operation.confirm, confirmLabel: operation.label[copy.locale] ?? operation.name, copy })
|
|
302
|
-
return (
|
|
303
|
-
<>
|
|
304
|
-
<Own operation={operation.name} busy={busy} run={async (input: Row) => void ((await ask()) && (await call(input)))} />
|
|
305
|
-
{dialog}
|
|
306
|
-
</>
|
|
307
|
-
)
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* A button that runs its operation where its node sits, after its confirm.
|
|
312
|
-
* It is what a node draws when it asks for no values: a form draws a dialog
|
|
313
|
-
* for an operation that does.
|
|
314
|
-
*/
|
|
315
|
-
export function NodeButton({ node, copy, busy, run, description }: { node: Pick<ButtonClient, 'label' | 'confirm'>; copy: Copy; busy: boolean; run: () => void; description?: string }): ReactElement {
|
|
316
|
-
const label = node.label[copy.locale]
|
|
317
|
-
const { ask, dialog } = useConfirm({ confirm: node.confirm, confirmLabel: label ?? '', description, copy })
|
|
318
|
-
return (
|
|
319
|
-
<>
|
|
320
|
-
<Button type="button" variant="outline" className="max-sm:h-11" disabled={busy} onClick={() => void ask().then((yes) => yes && run())}>
|
|
321
|
-
{label}
|
|
322
|
-
</Button>
|
|
323
|
-
{dialog}
|
|
324
|
-
</>
|
|
325
|
-
)
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* A bulk update's box for a yes-or-no field, which has three answers where a checkbox has two: leave
|
|
330
|
-
* each row's value as it is, set it, or clear it. The two values read in the field's own words, and
|
|
331
|
-
* leaving it as it is sends nothing.
|
|
332
|
-
*/
|
|
333
|
-
export function BulkChoice({
|
|
334
|
-
copy: { t },
|
|
335
|
-
describedBy,
|
|
336
|
-
error,
|
|
337
|
-
field,
|
|
338
|
-
id,
|
|
339
|
-
kayan,
|
|
340
|
-
onChange,
|
|
341
|
-
readOnly,
|
|
342
|
-
value,
|
|
343
|
-
}: {
|
|
344
|
-
copy: Copy
|
|
345
|
-
describedBy?: string
|
|
346
|
-
/** A refusal of this box, which marks the trigger invalid as every control marks itself. */
|
|
347
|
-
error?: string
|
|
348
|
-
field: ClientField
|
|
349
|
-
id: string
|
|
350
|
-
kayan: ClientKayan
|
|
351
|
-
onChange: (value: boolean | undefined) => void
|
|
352
|
-
readOnly: boolean
|
|
353
|
-
value: unknown
|
|
354
|
-
}): ReactElement {
|
|
355
|
-
const word = (held: boolean) => t(`${kayan.name}.${field.name}.${held}`) ?? (held ? t('admin.yes', 'Yes') : t('admin.no', 'No')) ?? String(held)
|
|
356
|
-
const keep = t('admin.leaveAsIs', 'Leave as is') ?? 'Leave as is'
|
|
357
|
-
const current = typeof value === 'boolean' ? String(value) : 'keep'
|
|
358
|
-
return (
|
|
359
|
-
<Select value={current} onValueChange={(next: string | null) => onChange(next === 'true' ? true : next === 'false' ? false : undefined)} disabled={readOnly}>
|
|
360
|
-
<SelectTrigger id={id} className="w-full" aria-describedby={describedBy} aria-invalid={error ? true : undefined}>
|
|
361
|
-
<SelectValue>{current === 'keep' ? keep : word(current === 'true')}</SelectValue>
|
|
362
|
-
</SelectTrigger>
|
|
363
|
-
<SelectContent>
|
|
364
|
-
<SelectItem value="keep">{keep}</SelectItem>
|
|
365
|
-
<SelectItem value="true">{word(true)}</SelectItem>
|
|
366
|
-
<SelectItem value="false">{word(false)}</SelectItem>
|
|
367
|
-
</SelectContent>
|
|
368
|
-
</Select>
|
|
369
|
-
)
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/** One button a page draws where its node sits: the operation, and what the node says about it. */
|
|
373
|
-
export type PageButton = ClientKayan['operations'][number] & Pick<ButtonClient, 'label' | 'component' | 'confirm'>
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* The button a node draws for this actor, when the kayan declares its
|
|
377
|
-
* operation and the report allows it by its own name, with the fields its
|
|
378
|
-
* dialog asks for cut to the ones the node names.
|
|
379
|
-
*/
|
|
380
|
-
export const pageButtonOf = ({
|
|
381
|
-
kayan,
|
|
382
|
-
node,
|
|
383
|
-
allowed,
|
|
384
|
-
}: {
|
|
385
|
-
kayan: ClientKayan
|
|
386
|
-
node: ButtonClient
|
|
387
|
-
allowed: Readonly<Record<string, boolean>> | undefined
|
|
388
|
-
}): PageButton | undefined => {
|
|
389
|
-
const operation = kayan.operations.find((one) => one.name === node.button)
|
|
390
|
-
if (!operation || allowed?.[operation.name] !== true) return undefined
|
|
391
|
-
const fields = operation.fields?.filter((one) => !node.fields || node.fields.includes(one.name))
|
|
392
|
-
return { ...operation, ...(fields && { fields }), label: node.label, ...(node.component && { component: node.component }), ...(node.confirm && { confirm: node.confirm }) }
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/** A named group's label and description, resolved the way a field's are. */
|
|
396
|
-
export const groupCopy = ({
|
|
397
|
-
kayan,
|
|
398
|
-
name,
|
|
399
|
-
group,
|
|
400
|
-
copy: { t, locale },
|
|
401
|
-
}: {
|
|
402
|
-
kayan: ClientKayan
|
|
403
|
-
name: string
|
|
404
|
-
group: { label?: Readonly<Record<string, string>>; description?: Readonly<Record<string, string>> }
|
|
405
|
-
copy: Copy
|
|
406
|
-
}) => ({
|
|
407
|
-
label: (typeof group.label === 'object' ? group.label[locale] : undefined) ?? t(`${kayan.name}.layout.${name}`, name) ?? name,
|
|
408
|
-
description:
|
|
409
|
-
(typeof group.description === 'object' ? group.description[locale] : undefined)
|
|
410
|
-
?? t(`${kayan.name}.layout.${name}.description`),
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
/** One node of a screen's page. */
|
|
414
|
-
export type Node = NodeClient
|
|
415
|
-
|
|
416
|
-
/** Every field name a tree mentions, at any depth, of the leaves `keep` keeps. */
|
|
417
|
-
export const leavesOf = (nodes: readonly Node[], keep: (leaf: LeafClient) => boolean = () => true): readonly string[] =>
|
|
418
|
-
nodes.flatMap((node): readonly string[] => {
|
|
419
|
-
const leaf = ofKind(node, 'field')
|
|
420
|
-
return leaf ? (keep(leaf) ? [leaf.field] : []) : leavesOf(inside(node), keep)
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* One tab strip. `keepMounted` renders every panel, hiding the inactive ones, so a field on a tab nobody opened
|
|
425
|
-
* still holds its value; base-ui unmounts a hidden panel otherwise.
|
|
426
|
-
*/
|
|
427
|
-
function TabGroup({
|
|
428
|
-
line,
|
|
429
|
-
tabs,
|
|
430
|
-
}: {
|
|
431
|
-
line: boolean
|
|
432
|
-
tabs: readonly { key: string; label: string; description?: string; invalid: number; invalidLabel: string; body: ReactNode }[]
|
|
433
|
-
}): ReactElement {
|
|
434
|
-
return (
|
|
435
|
-
<Tabs defaultValue={tabs[0]?.key} data-node="tabs" className={line ? 'gap-4' : 'overflow-hidden rounded-lg border'}>
|
|
436
|
-
<TabsList variant={line ? 'line' : 'default'} className={line ? 'w-full justify-start max-sm:min-h-12' : 'm-1 max-sm:min-h-12'}>
|
|
437
|
-
{tabs.map((tab) => (
|
|
438
|
-
<TabsTrigger key={tab.key} value={tab.key} className={line ? 'flex-none max-sm:min-h-11' : 'max-sm:min-h-11'}>
|
|
439
|
-
{tab.label}
|
|
440
|
-
{/* How many of this tab's fields the refusal named, so a save refused over a field on a tab
|
|
441
|
-
nobody opened says so on the tab. */}
|
|
442
|
-
{tab.invalid > 0 ? (
|
|
443
|
-
<Badge variant="destructive" aria-label={tab.invalidLabel} title={tab.invalidLabel} data-mithaq-invalid={tab.invalid}>
|
|
444
|
-
{tab.invalid}
|
|
445
|
-
</Badge>
|
|
446
|
-
) : null}
|
|
447
|
-
</TabsTrigger>
|
|
448
|
-
))}
|
|
449
|
-
</TabsList>
|
|
450
|
-
{tabs.map((tab) => (
|
|
451
|
-
<TabsContent key={tab.key} value={tab.key} keepMounted className={line ? 'flex flex-col gap-4' : 'flex flex-col gap-4 p-4'}>
|
|
452
|
-
{tab.description ? <p className="text-sm text-muted-foreground">{tab.description}</p> : null}
|
|
453
|
-
{tab.body}
|
|
454
|
-
</TabsContent>
|
|
455
|
-
))}
|
|
456
|
-
</Tabs>
|
|
457
|
-
)
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/** What a tree is drawn with: the form's own answer for each leaf and component, and the frame each layout is. */
|
|
461
|
-
export type Draw = {
|
|
462
|
-
kayan: ClientKayan
|
|
463
|
-
copy: Copy
|
|
464
|
-
/** The refusal per field, which a tab counts over the fields it holds. */
|
|
465
|
-
issues: Readonly<Record<string, string>>
|
|
466
|
-
control: (leaf: string | LeafClient) => ReactNode
|
|
467
|
-
component: (path: string, index: number) => ReactNode
|
|
468
|
-
/** A layout nested in a region, handed its own regions drawn. */
|
|
469
|
-
layout: (args: { path: string; regions: Record<string, ReactNode>; index: number }) => ReactNode
|
|
470
|
-
/** A list node, the page's own or one of another kayan, and the component that draws it in place of the registry's list. */
|
|
471
|
-
list?: (args: { node: ListNode; index: number; component?: string }) => ReactNode
|
|
472
|
-
/** A button node, which only what holds a row to run it over draws. */
|
|
473
|
-
button?: (node: ButtonClient, index: number) => ReactNode
|
|
474
|
-
/** The derived nav, which the host holds. */
|
|
475
|
-
nav?: () => ReactNode
|
|
476
|
-
/** What a form node draws around what it holds. */
|
|
477
|
-
form?: (children: ReactNode) => ReactNode
|
|
478
|
-
/** The form's controls, which only a form draws: where the row stands, its last save and its buttons. */
|
|
479
|
-
controls?: () => ReactNode
|
|
480
|
-
/**
|
|
481
|
-
* What a node's `when` reads: the form's values by field name, the row the
|
|
482
|
-
* page draws as `record` and the access report as `$user`, which `scopeOf`
|
|
483
|
-
* puts together.
|
|
484
|
-
*/
|
|
485
|
-
scope: Row
|
|
486
|
-
/**
|
|
487
|
-
* Whether the nodes drawn sit inside a card: a section, an accordion, a pill
|
|
488
|
-
* tab strip, or the card a run of loose fields is drawn on. A tab strip reads
|
|
489
|
-
* it to pick the pill strip or the line bar.
|
|
490
|
-
*/
|
|
491
|
-
inCard?: boolean
|
|
492
|
-
/**
|
|
493
|
-
* Whether the page lists the kayan's own rows, where its title names the kayan by its plural:
|
|
494
|
-
* "Courses" over the list, "Course" over one of them. A list of another kayan on a record page
|
|
495
|
-
* leaves it the singular.
|
|
496
|
-
*/
|
|
497
|
-
many?: boolean
|
|
498
|
-
/**
|
|
499
|
-
* Whether the page opens over its list in a dialog, a sheet or a drawer, where its title is an h2
|
|
500
|
-
* so the list's h1 behind it stays the page's one.
|
|
501
|
-
*/
|
|
502
|
-
framed?: boolean
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
/**
|
|
506
|
-
* What a `when` reads, from the values a form holds, the row the page draws and the actor's access
|
|
507
|
-
* report, and the kind of each field of the kayan, which a decimal is compared by. No `when` can name
|
|
508
|
-
* `$kinds`: the build refuses a key that is no field and not `$user`.
|
|
509
|
-
*/
|
|
510
|
-
export const scopeOf = ({ values, record, access, kayan }: { values: Row; record: Row | null | undefined; access: unknown; kayan: ClientKayan }): Row =>
|
|
511
|
-
({ ...values, record: record ?? null, $user: access ?? null, $kinds: Object.fromEntries(kayan.fields.map((field) => [field.name, field.kind])) })
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Whether a node is drawn: it carries no `when`, or its `when` holds over the scope, a dotted key
|
|
515
|
-
* read one segment at a time. It decides what is shown and nothing else: the server still refuses
|
|
516
|
-
* what the actor may not do.
|
|
517
|
-
*/
|
|
518
|
-
export const shown = (node: NodeClient, scope: Row) =>
|
|
519
|
-
typeof node !== 'object' || !node.when
|
|
520
|
-
|| matchesWhere({
|
|
521
|
-
where: node.when,
|
|
522
|
-
valueOf: (key) => key.split('.').reduce<unknown>((value, part) => (value && typeof value === 'object' ? (value as Row)[part] : undefined), scope),
|
|
523
|
-
// A field is named alone, as `record.<field>`, or with a locale after it.
|
|
524
|
-
kindOf: (key) => {
|
|
525
|
-
const [head, next] = key.split('.')
|
|
526
|
-
return (scope.$kinds as Record<string, string> | undefined)?.[head === 'record' ? next! : head!]
|
|
527
|
-
},
|
|
528
|
-
})
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Every kind of node, how it draws and what it holds: the one map every walk of a tree goes through.
|
|
532
|
-
* TypeScript refuses it with a kind left out. The set is closed: a project draws its own thing with a
|
|
533
|
-
* `component` node, and changes how a kind draws by editing its copy of this map.
|
|
534
|
-
*/
|
|
535
|
-
export type NodeMap = {
|
|
536
|
-
[Kind in NodeKind]: {
|
|
537
|
-
/** The node drawn; `inRow` when it sits in a row, where a section takes its width. */
|
|
538
|
-
draw: (args: { node: NodeOf<Kind>; draw: Draw; index: number; inRow: boolean }) => ReactNode
|
|
539
|
-
/** The nodes it holds, which a walk descends into. */
|
|
540
|
-
children: (node: NodeOf<Kind>) => readonly NodeClient[]
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const none = () => []
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* How each node kind is drawn. A list is drawn by the host that holds its rows, and by nothing where no host hands one.
|
|
548
|
-
* Every node carries its kind as `data-node`, so a served page says what it drew.
|
|
549
|
-
*/
|
|
550
|
-
export const nodeMap: NodeMap = {
|
|
551
|
-
layout: {
|
|
552
|
-
draw: ({ node, draw, index }) => <div key={`layout-${index}`} data-node="layout" className="contents">{draw.layout({ path: node.layout, regions: regionsOf(node, draw), index })}</div>,
|
|
553
|
-
children: (node) => regionsIn(node).flatMap(([, nodes]) => nodes),
|
|
554
|
-
},
|
|
555
|
-
field: { draw: ({ node, draw }) => draw.control(node), children: none },
|
|
556
|
-
form: {
|
|
557
|
-
draw: ({ node, draw, index }) => {
|
|
558
|
-
// A form node that names a component is drawn by it in place of its leaves, and the form still
|
|
559
|
-
// holds, checks and saves the values.
|
|
560
|
-
const held = node.component ? draw.component(node.component, index) : carded(node.form, draw)
|
|
561
|
-
return <div key={`form-${index}`} data-node="form" className="flex flex-col gap-4">{draw.form ? draw.form(held) : held}</div>
|
|
562
|
-
},
|
|
563
|
-
children: (node) => node.form,
|
|
564
|
-
},
|
|
565
|
-
list: {
|
|
566
|
-
draw: ({ node, draw, index }) => <div key={`list-${index}`} data-node="list" className="contents">{draw.list?.({ node: node.list, index, component: node.component })}</div>,
|
|
567
|
-
children: none,
|
|
568
|
-
},
|
|
569
|
-
button: { draw: ({ node, draw, index }) => <div key={`button-${index}`} data-node="button" className="contents">{draw.button?.(node, index)}</div>, children: none },
|
|
570
|
-
title: {
|
|
571
|
-
// The whole title is its tooltip too, as a header may cut it short on a phone. The title is the row
|
|
572
|
-
// as read, not the value being typed, so it changes once a save lands.
|
|
573
|
-
draw: ({ draw: { kayan, copy, many, framed, form, scope }, index }) => {
|
|
574
|
-
const Heading = framed ? 'h2' : 'h1'
|
|
575
|
-
const record = scope.record as Row | null | undefined
|
|
576
|
-
const field = kayan.fields.find((one) => one.name === kayan.useAsTitle && one.name !== 'id')
|
|
577
|
-
// A global with no saved row yet is still its one page, so it keeps its name.
|
|
578
|
-
const fallback = !many && form && !record && !kayan.single ? newTitleOf({ kayan, copy }) : titleOf({ kayan, copy, many })
|
|
579
|
-
if (!many && record && field?.kind === 'relationship') return <RelatedTitle key={`title-${index}`} Heading={Heading} field={field} row={record} locale={copy.locale} fallback={fallback} />
|
|
580
|
-
const untitled = !many && !!record && isUntitled({ kayan, row: record, locale: copy.locale })
|
|
581
|
-
const title = untitled ? (copy.t('admin.untitled', 'Untitled') ?? 'Untitled') : (!many && record ? rowTitle({ kayan, row: record, locale: copy.locale }) : undefined) ?? fallback
|
|
582
|
-
return <Heading key={`title-${index}`} data-node="title" title={title} className="text-2xl font-bold tracking-tight">{untitled ? <span className="text-muted-foreground">{title}</span> : title}</Heading>
|
|
583
|
-
},
|
|
584
|
-
children: none,
|
|
585
|
-
},
|
|
586
|
-
nav: {
|
|
587
|
-
// Nothing in a frame: the list's own shell behind it already draws the nav.
|
|
588
|
-
draw: ({ draw, index }) => !draw.framed && <nav key={`nav-${index}`} data-node="nav" aria-label={draw.copy.t('admin.navigation', 'Navigation')}>{draw.nav?.()}</nav>,
|
|
589
|
-
children: none,
|
|
590
|
-
},
|
|
591
|
-
// Drawn only by a form, which hands its controls; off a form page the node draws nothing.
|
|
592
|
-
controls: {
|
|
593
|
-
draw: ({ draw, index }) => draw.controls && <div key={`controls-${index}`} data-node="controls" className="contents">{draw.controls()}</div>,
|
|
594
|
-
children: none,
|
|
595
|
-
},
|
|
596
|
-
component: {
|
|
597
|
-
draw: ({ node, draw, index }) => <div key={`component-${index}`} data-node="component" className="contents">{draw.component(node.component, index)}</div>,
|
|
598
|
-
children: none,
|
|
599
|
-
},
|
|
600
|
-
row: {
|
|
601
|
-
draw: ({ node, draw, index }) =>
|
|
602
|
-
node.resizable ? (
|
|
603
|
-
<ResizablePanelGroup key={`row-${index}`} data-node="row" orientation="horizontal" className="rounded-lg border">
|
|
604
|
-
{drawNodes({ nodes: node.row, draw }).flatMap((child, at) => [
|
|
605
|
-
...(at ? [<ResizableHandle key={`handle-${at}`} withHandle />] : []),
|
|
606
|
-
<ResizablePanel key={`panel-${at}`} className="p-2">{child}</ResizablePanel>,
|
|
607
|
-
])}
|
|
608
|
-
</ResizablePanelGroup>
|
|
609
|
-
) : (
|
|
610
|
-
// `min-w-40` on a field alone so it wraps to its own line on a phone before it is too narrow to
|
|
611
|
-
// show its value: `flex-1` alone starts every field at no width, so the row never wraps. A button
|
|
612
|
-
// or a short select beside it is not forced to 160 pixels.
|
|
613
|
-
<div key={`row-${index}`} data-node="row" className="flex flex-wrap items-start gap-4 [&>[data-control]]:min-w-40 [&>[data-field-value]]:min-w-40 [&>*]:flex-1">
|
|
614
|
-
{drawNodes({ nodes: node.row, draw, inRow: true })}
|
|
615
|
-
</div>
|
|
616
|
-
),
|
|
617
|
-
children: (node) => node.row,
|
|
618
|
-
},
|
|
619
|
-
separator: { draw: ({ index }) => <Separator key={`separator-${index}`} data-node="separator" />, children: none },
|
|
620
|
-
tabs: {
|
|
621
|
-
draw: ({ node, draw, index }) => (
|
|
622
|
-
<TabGroup
|
|
623
|
-
key={`tabs-${index}`}
|
|
624
|
-
line={!draw.inCard}
|
|
625
|
-
// A tab whose every node its `when` hides is left out, so no actor opens a tab that holds
|
|
626
|
-
// nothing it may see.
|
|
627
|
-
tabs={node.tabs.filter((tab) => tab.fields.some((one) => shown(one, draw.scope))).map((tab) => {
|
|
628
|
-
// Counted over the fields this tab holds at any depth, so a
|
|
629
|
-
// refused field inside a row inside a tab is counted on the tab.
|
|
630
|
-
const invalid = leavesOf(tab.fields).filter((name) => draw.issues[name] !== undefined).length
|
|
631
|
-
return {
|
|
632
|
-
key: tab.tab,
|
|
633
|
-
...groupCopy({ kayan: draw.kayan, name: tab.tab, group: tab, copy: draw.copy }),
|
|
634
|
-
invalid,
|
|
635
|
-
invalidLabel: (
|
|
636
|
-
draw.copy.t('admin.invalidFields', 'Fields needing attention: {count}') ?? 'Fields needing attention: {count}'
|
|
637
|
-
).replace('{count}', String(invalid)),
|
|
638
|
-
body: <div className="flex flex-col gap-4">{carded(tab.fields, draw)}</div>,
|
|
639
|
-
}
|
|
640
|
-
})}
|
|
641
|
-
/>
|
|
642
|
-
),
|
|
643
|
-
children: (node) => node.tabs.flatMap((tab) => tab.fields),
|
|
644
|
-
},
|
|
645
|
-
accordion: {
|
|
646
|
-
draw: ({ node, draw, index }) => (
|
|
647
|
-
<Accordion data-layout-accordion="true" data-node="accordion" key={`accordion-${index}`} defaultValue={node.accordion[0] ? [node.accordion[0].item] : []} keepMounted className="overflow-hidden rounded-lg border">
|
|
648
|
-
{node.accordion.map((item) => {
|
|
649
|
-
const { label, description } = groupCopy({ kayan: draw.kayan, name: item.item, group: item, copy: draw.copy })
|
|
650
|
-
return <AccordionItem key={item.item} value={item.item} className="border-b last:border-b-0">
|
|
651
|
-
<AccordionHeader><AccordionTrigger>{label}<ChevronDown aria-hidden="true" className="size-4" /></AccordionTrigger></AccordionHeader>
|
|
652
|
-
<AccordionPanel className="flex flex-col gap-4 p-4">
|
|
653
|
-
{description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
|
|
654
|
-
{drawNodes({ nodes: item.fields, draw: { ...draw, inCard: true } })}
|
|
655
|
-
</AccordionPanel>
|
|
656
|
-
</AccordionItem>
|
|
657
|
-
})}
|
|
658
|
-
</Accordion>
|
|
659
|
-
),
|
|
660
|
-
children: (node) => node.accordion.flatMap((item) => item.fields),
|
|
661
|
-
},
|
|
662
|
-
section: {
|
|
663
|
-
draw: ({ node, draw, inRow }) => {
|
|
664
|
-
const { label, description } = groupCopy({ kayan: draw.kayan, name: node.section, group: node, copy: draw.copy })
|
|
665
|
-
const body = <CardContent className="flex flex-col gap-4">{drawNodes({ nodes: node.fields, draw: { ...draw, inCard: true } })}</CardContent>
|
|
666
|
-
// A section is a card that opens and closes. `keepMounted` on the panel so a field inside a closed
|
|
667
|
-
// section still renders and a refusal naming it is counted.
|
|
668
|
-
return (
|
|
669
|
-
<Collapsible key={node.section} data-node="section" defaultOpen={node.collapsed !== true} style={inRow ? { flexGrow: node.width ?? 1, flexBasis: 0 } : undefined}>
|
|
670
|
-
<Card className="overflow-visible rounded-lg">
|
|
671
|
-
<CardHeader>
|
|
672
|
-
<CardTitle>
|
|
673
|
-
<h2 className="text-base font-medium">
|
|
674
|
-
<CollapsibleTrigger className="group flex w-full cursor-pointer items-center justify-between gap-2 text-start max-sm:min-h-11">
|
|
675
|
-
{label}
|
|
676
|
-
<ChevronDown
|
|
677
|
-
aria-hidden="true"
|
|
678
|
-
className="text-muted-foreground size-4 transition-transform group-data-[panel-open]:rotate-180"
|
|
679
|
-
/>
|
|
680
|
-
</CollapsibleTrigger>
|
|
681
|
-
</h2>
|
|
682
|
-
</CardTitle>
|
|
683
|
-
{description ? <CardDescription>{description}</CardDescription> : null}
|
|
684
|
-
</CardHeader>
|
|
685
|
-
<CollapsibleContent keepMounted>
|
|
686
|
-
{node.scroll ? <ScrollArea style={{ height: node.scroll }}>{body}</ScrollArea> : body}
|
|
687
|
-
</CollapsibleContent>
|
|
688
|
-
</Card>
|
|
689
|
-
</Collapsible>
|
|
690
|
-
)
|
|
691
|
-
},
|
|
692
|
-
children: (node) => node.fields,
|
|
693
|
-
},
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
/**
|
|
697
|
-
* A form's nodes, with each run of fields that sits in no section drawn on a card, so a dark page
|
|
698
|
-
* reads as cards over the inset and not as boxes on black. A section, tabs and a component are drawn
|
|
699
|
-
* as they are.
|
|
700
|
-
*/
|
|
701
|
-
const loose = new Set(['field', 'row', 'separator'])
|
|
702
|
-
export const carded = (nodes: readonly Node[], draw: Draw): ReactNode[] => {
|
|
703
|
-
const drawn: ReactNode[] = []
|
|
704
|
-
let run: Node[] = []
|
|
705
|
-
const flush = () => {
|
|
706
|
-
const held = run.filter((one) => shown(one, draw.scope) && !blank(one, draw.kayan))
|
|
707
|
-
// A separator divides what a card holds, so a card holding nothing else is not drawn.
|
|
708
|
-
if (held.some((one) => kindOf(one) !== 'separator')) {
|
|
709
|
-
drawn.push(
|
|
710
|
-
<Card key={`card-${drawn.length}`} className="overflow-visible rounded-lg">
|
|
711
|
-
<CardContent className="flex flex-col gap-4">{drawNodes({ nodes: held, draw: { ...draw, inCard: true } })}</CardContent>
|
|
712
|
-
</Card>,
|
|
713
|
-
)
|
|
714
|
-
}
|
|
715
|
-
run = []
|
|
716
|
-
}
|
|
717
|
-
for (const one of nodes) {
|
|
718
|
-
if (loose.has(kindOf(one)!)) {
|
|
719
|
-
run.push(one)
|
|
720
|
-
continue
|
|
721
|
-
}
|
|
722
|
-
flush()
|
|
723
|
-
drawn.push(<Fragment key={`node-${drawn.length}`}>{drawNodes({ nodes: [one], draw })}</Fragment>)
|
|
724
|
-
}
|
|
725
|
-
flush()
|
|
726
|
-
return drawn
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
/**
|
|
730
|
-
* Whether a node would draw nothing but empty frames: a leaf naming a field the projection does not
|
|
731
|
-
* hold, which is a field the viewer may not read, or a row, section, tab strip or accordion holding
|
|
732
|
-
* only such leaves. Such a group is not drawn, rather than drawn as an empty card.
|
|
733
|
-
*/
|
|
734
|
-
const blank = (node: Node, kayan: ClientKayan): boolean => {
|
|
735
|
-
const kind = kindOf(node)
|
|
736
|
-
if (kind === 'field') return !kayan.fields.some((field) => field.name === (typeof node === 'string' ? node : (node as LeafClient).field))
|
|
737
|
-
return ['row', 'section', 'tabs', 'accordion'].includes(kind!) && inside(node).every((one) => blank(one, kayan))
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
/** One tree, one node at a time, each through its kind's entry in the one map. */
|
|
741
|
-
export const drawNodes = ({ nodes, draw, inRow = false }: { nodes: readonly Node[]; draw: Draw; inRow?: boolean }): ReactNode[] =>
|
|
742
|
-
nodes.map((node, index) =>
|
|
743
|
-
!shown(node, draw.scope) || blank(node, draw.kayan)
|
|
744
|
-
? null
|
|
745
|
-
: (nodeMap[kindOf(node)!].draw as (args: { node: Node; draw: Draw; index: number; inRow: boolean }) => ReactNode)({ node, draw, index, inRow }),
|
|
746
|
-
)
|
|
747
|
-
|
|
748
|
-
/**
|
|
749
|
-
* Whether a node draws anything: shown, not blank, a button its page draws (an operation the report
|
|
750
|
-
* allows), and a group holding at least one such node. A danger zone of buttons the viewer may not
|
|
751
|
-
* run is no card at all.
|
|
752
|
-
*/
|
|
753
|
-
const draws = (node: Node, draw: Draw): boolean => {
|
|
754
|
-
if (!shown(node, draw.scope) || blank(node, draw.kayan)) return false
|
|
755
|
-
const kind = kindOf(node)
|
|
756
|
-
if (kind === 'button') return (draw.button?.(node as ButtonClient, 0) ?? null) !== null
|
|
757
|
-
return !['row', 'section', 'tabs', 'accordion'].includes(kind!) || inside(node).some((one) => draws(one, draw))
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
* A shell's or a layout's regions, each drawn from its nodes, which is what its
|
|
762
|
-
* file is handed. A region none of whose nodes is drawn is handed as nothing,
|
|
763
|
-
* so a layout leaves out its frame, a danger zone's heading among them.
|
|
764
|
-
*/
|
|
765
|
-
export const regionsOf = (frame: RegionsClient, draw: Draw): Record<string, ReactNode> =>
|
|
766
|
-
Object.fromEntries(regionsIn(frame).map(([region, nodes]) => [region, nodes.some((node) => draws(node, draw)) ? drawNodes({ nodes, draw }) : undefined]))
|
|
767
|
-
|
|
768
|
-
/**
|
|
769
|
-
* The shell or the layout a page names, handed its regions drawn and the page
|
|
770
|
-
* it frames. A name this app has no component for is said where the frame
|
|
771
|
-
* would have been rather than drawn as a blank.
|
|
772
|
-
*/
|
|
773
|
-
export const frameOf = ({
|
|
774
|
-
screen,
|
|
775
|
-
t,
|
|
776
|
-
path,
|
|
777
|
-
regions,
|
|
778
|
-
key,
|
|
779
|
-
}: {
|
|
780
|
-
screen: ScreenPage
|
|
781
|
-
t: Translate
|
|
782
|
-
path: string
|
|
783
|
-
regions: Record<string, ReactNode>
|
|
784
|
-
key?: Key
|
|
785
|
-
}): ReactNode => {
|
|
786
|
-
const Frame = refOf({ screen, place: 'layout', path })
|
|
787
|
-
if (Frame) return <Frame key={key} regions={regions} page={screen.page} />
|
|
788
|
-
const missing = t('admin.missingLayout', 'No component is registered for layout "{layout}".') ?? 'No component is registered for layout "{layout}".'
|
|
789
|
-
return <p key={key} data-missing-layout={path} className="text-muted-foreground text-sm">{missing.replace('{layout}', path)}</p>
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
/**
|
|
793
|
-
* A page's whole tree: its shell handed each region drawn, or, where the page
|
|
794
|
-
* opens over the list and so inside the list's own shell, its regions alone.
|
|
795
|
-
*/
|
|
796
|
-
export const pageOf = (screen: ScreenPage, draw: Draw): ReactNode => {
|
|
797
|
-
const drawn = { ...draw, many: listOf(screen) !== undefined }
|
|
798
|
-
if (!screen.page.mode) return frameOf({ screen, t: draw.copy.t, path: screen.page.page.shell, regions: regionsOf(screen.page.page, drawn) })
|
|
799
|
-
// Over the list the page draws only itself, and a nav node at any depth draws nothing there.
|
|
800
|
-
return regionsIn(screen.page.page).map(([region, nodes]) => <Fragment key={region}>{drawNodes({ nodes, draw: { ...drawn, framed: true } })}</Fragment>)
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
/**
|
|
804
|
-
* One field's control in its shell, as the form draws a leaf. The leaf's copy, width and read-only win over the field's;
|
|
805
|
-
* a component named by path gets the props its place hands it, any other name is a registry control.
|
|
806
|
-
*/
|
|
807
|
-
export function LeafControl({
|
|
808
|
-
field,
|
|
809
|
-
kayan,
|
|
810
|
-
leaf,
|
|
811
|
-
screen,
|
|
812
|
-
copy,
|
|
813
|
-
error,
|
|
814
|
-
invalidLocales,
|
|
815
|
-
onBlur,
|
|
816
|
-
...rest
|
|
817
|
-
}: Omit<ControlProps, 'id' | 't' | 'locale' | 'placeholder' | 'describedBy' | 'client'> & {
|
|
818
|
-
leaf?: LeafClient
|
|
819
|
-
screen?: ScreenPage
|
|
820
|
-
copy: Copy
|
|
821
|
-
/** The locales of a localized field a refusal named by its path, `title.ar`, whose boxes alone are marked. */
|
|
822
|
-
invalidLocales?: readonly string[]
|
|
823
|
-
onBlur?: () => void
|
|
824
|
-
kayans?: readonly ClientKayan[]
|
|
825
|
-
api?: FindApi
|
|
826
|
-
}): ReactElement {
|
|
827
|
-
const { kayans, ...props } = rest
|
|
828
|
-
const { locale } = copy
|
|
829
|
-
const chosen = leaf?.component ?? field.component
|
|
830
|
-
const Ref = refOf({ screen, place: 'control', path: chosen })
|
|
831
|
-
const Control = (leaf?.component && controls[leaf.component]) || controlFor(field)
|
|
832
|
-
const inputId = `${kayan.name}-${field.name}`
|
|
833
|
-
const description = leaf?.description?.[locale] ?? descriptionFor({ kayan, field, copy })
|
|
834
|
-
const readOnly = props.readOnly || leaf?.readOnly === true
|
|
835
|
-
const placeholder = leaf?.placeholder?.[locale] ?? placeholderFor({ kayan, field, copy })
|
|
836
|
-
// A localized field is one box per locale under one label, the viewer's own first and carrying the
|
|
837
|
-
// field's id; a change to one box writes the whole object, which is how the store takes it.
|
|
838
|
-
if (field.rules.localized === true && !Ref) {
|
|
839
|
-
const held = (props.value && typeof props.value === 'object' ? props.value : {}) as Record<string, unknown>
|
|
840
|
-
const names = new Intl.DisplayNames([locale], { type: 'language' })
|
|
841
|
-
const order = [locale, ...(copy.locales ?? []).filter((one) => one !== locale)]
|
|
842
|
-
// Marks the locales a refusal's paths name; before any save, the empty boxes of a required field;
|
|
843
|
-
// any other refusal marks every box.
|
|
844
|
-
const blank = (one: string) => held[one] === undefined || held[one] === null || held[one] === ''
|
|
845
|
-
const marked = (one: string) =>
|
|
846
|
-
!error ? false
|
|
847
|
-
: invalidLocales?.length ? invalidLocales.includes(one)
|
|
848
|
-
: field.rules.required === true && order.some(blank) ? blank(one)
|
|
849
|
-
: true
|
|
850
|
-
const label = labelFor({ kayan, field, copy })
|
|
851
|
-
return (
|
|
852
|
-
<FieldShell id={inputId} label={label} description={description} error={error} onBlur={onBlur} control={chosen} width={leaf?.width ?? field.width}>
|
|
853
|
-
<div className="grid gap-2">
|
|
854
|
-
{order.map((one) => (
|
|
855
|
-
<div key={one} className="grid gap-1">
|
|
856
|
-
{/* The viewer's own box is named by the field's label, every other box by the label and its locale. */}
|
|
857
|
-
{one === locale ? (
|
|
858
|
-
<span id={`${inputId}-${one}-locale`} className="text-muted-foreground text-xs">{names.of(one)}</span>
|
|
859
|
-
) : (
|
|
860
|
-
<label id={`${inputId}-${one}-locale`} htmlFor={`${inputId}-${one}`} className="text-muted-foreground text-xs">
|
|
861
|
-
<span className="sr-only">{label} </span>
|
|
862
|
-
{names.of(one)}
|
|
863
|
-
</label>
|
|
864
|
-
)}
|
|
865
|
-
<Control
|
|
866
|
-
{...props}
|
|
867
|
-
field={field}
|
|
868
|
-
kayan={kayan}
|
|
869
|
-
client={kayans}
|
|
870
|
-
t={copy.t}
|
|
871
|
-
locale={locale}
|
|
872
|
-
locales={copy.locales}
|
|
873
|
-
id={one === locale ? inputId : `${inputId}-${one}`}
|
|
874
|
-
value={held[one]}
|
|
875
|
-
onChange={(next) => props.onChange({ ...held, [one]: next })}
|
|
876
|
-
readOnly={readOnly}
|
|
877
|
-
error={marked(one) ? error : undefined}
|
|
878
|
-
placeholder={placeholder}
|
|
879
|
-
describedBy={[`${inputId}-${one}-locale`, describedBy({ id: inputId, description, error })].filter(Boolean).join(' ')}
|
|
880
|
-
/>
|
|
881
|
-
</div>
|
|
882
|
-
))}
|
|
883
|
-
</div>
|
|
884
|
-
</FieldShell>
|
|
885
|
-
)
|
|
886
|
-
}
|
|
887
|
-
return (
|
|
888
|
-
<FieldShell
|
|
889
|
-
id={inputId}
|
|
890
|
-
label={labelFor({ kayan, field, copy })}
|
|
891
|
-
description={description}
|
|
892
|
-
error={error}
|
|
893
|
-
onBlur={onBlur}
|
|
894
|
-
beside={!Ref && besideLabel(Control)}
|
|
895
|
-
control={chosen}
|
|
896
|
-
width={leaf?.width ?? field.width}
|
|
897
|
-
>
|
|
898
|
-
{Ref && screen ? (
|
|
899
|
-
<Ref field={field} value={props.value} onChange={props.onChange} readOnly={readOnly} issue={error ?? null} />
|
|
900
|
-
) : (
|
|
901
|
-
<Control
|
|
902
|
-
{...props}
|
|
903
|
-
field={field}
|
|
904
|
-
kayan={kayan}
|
|
905
|
-
client={kayans}
|
|
906
|
-
t={copy.t}
|
|
907
|
-
locale={locale}
|
|
908
|
-
locales={copy.locales}
|
|
909
|
-
id={inputId}
|
|
910
|
-
readOnly={readOnly}
|
|
911
|
-
error={error}
|
|
912
|
-
placeholder={placeholder}
|
|
913
|
-
describedBy={describedBy({ id: inputId, description, error })}
|
|
914
|
-
/>
|
|
915
|
-
)}
|
|
916
|
-
</FieldShell>
|
|
917
|
-
)
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
/**
|
|
921
|
-
* A sheet's or a dialog's close button, an icon named in the viewer's language. The sheet or dialog
|
|
922
|
-
* that draws it passes `showCloseButton={false}`, so the primitive's English one is not drawn beside it.
|
|
923
|
-
*/
|
|
924
|
-
export function CloseButton({ Close, label }: { Close: typeof SheetClose | typeof DialogClose; label: string }): ReactElement {
|
|
925
|
-
return (
|
|
926
|
-
<Close render={<Button variant="ghost" size="icon-sm" className="absolute top-4 end-4 max-sm:top-2 max-sm:end-2 max-sm:size-11" />}>
|
|
927
|
-
<XIcon aria-hidden="true" />
|
|
928
|
-
<span className="sr-only">{label}</span>
|
|
929
|
-
</Close>
|
|
930
|
-
)
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* The open popups of shadcn's four modals. The sidebar's mobile sheet is a sheet whose popup the
|
|
935
|
-
* sidebar renames `data-slot="sidebar"`; the desktop sidebar has the same slot and is never `data-open`.
|
|
936
|
-
*/
|
|
937
|
-
const openModal = '[data-slot=dialog-content][data-open],[data-slot=alert-dialog-content][data-open],[data-slot=sheet-content][data-open],[data-slot=sidebar][data-open],[data-slot=drawer-popup][data-open]'
|
|
938
|
-
|
|
939
|
-
/**
|
|
940
|
-
* Makes everything outside the open modal `inert`, as `showModal()` does, so Tab never reaches the page behind it:
|
|
941
|
-
* Base UI's focus guards act only on focus events, which an unfocused document never receives.
|
|
942
|
-
*/
|
|
943
|
-
export const holdFocus = (body: HTMLElement): (() => void) => {
|
|
944
|
-
/** Everything this made inert given back, which is all it ever clears. */
|
|
945
|
-
const release = () => {
|
|
946
|
-
for (const one of body.querySelectorAll('[data-mithaq-inert]')) {
|
|
947
|
-
one.removeAttribute('inert')
|
|
948
|
-
one.removeAttribute('data-mithaq-inert')
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
// A live region, such as a list's count of ticked rows, keeps speaking while a modal is open: an
|
|
952
|
-
// element holding one is walked into rather than made inert whole.
|
|
953
|
-
const shut = (node: Element) => {
|
|
954
|
-
if (node.matches('[aria-live], script') || node.hasAttribute('inert')) return
|
|
955
|
-
if (node.querySelector('[aria-live]')) {
|
|
956
|
-
for (const child of node.children) shut(child)
|
|
957
|
-
return
|
|
958
|
-
}
|
|
959
|
-
node.setAttribute('inert', '')
|
|
960
|
-
node.setAttribute('data-mithaq-inert', '')
|
|
961
|
-
}
|
|
962
|
-
const sync = () => {
|
|
963
|
-
release()
|
|
964
|
-
const open = [...body.querySelectorAll(openModal)].at(-1)
|
|
965
|
-
if (!open) return
|
|
966
|
-
for (const child of body.children) if (child.hasAttribute('data-base-ui-inert') && !child.contains(open)) shut(child)
|
|
967
|
-
}
|
|
968
|
-
const observer = new MutationObserver(sync)
|
|
969
|
-
observer.observe(body, { childList: true, subtree: true, attributeFilter: ['data-open', 'data-base-ui-inert'] })
|
|
970
|
-
sync()
|
|
971
|
-
// The cleanup gives the page back whole even when the frame unmounts under an open modal.
|
|
972
|
-
return () => {
|
|
973
|
-
observer.disconnect()
|
|
974
|
-
release()
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
/**
|
|
979
|
-
* Focus on open: the first form field, or the popup itself on a touch so the phone's keyboard stays down.
|
|
980
|
-
* A route-opened frame gets no interaction type, so on a coarse pointer anything but the keyboard counts as touch.
|
|
981
|
-
*/
|
|
982
|
-
export const initialFocusIn = (popup: RefObject<HTMLElement | null>) => (type: string) =>
|
|
983
|
-
type === 'touch' || (type !== 'keyboard' && globalThis.matchMedia?.('(pointer: coarse)').matches)
|
|
984
|
-
? popup.current
|
|
985
|
-
: popup.current?.querySelector<HTMLElement>('form :is(input:not([type=hidden]), textarea, select, [contenteditable=true], [role=combobox]):not(:disabled, [aria-hidden=true], [tabindex="-1"])') ?? true
|
|
986
|
-
|
|
987
|
-
/**
|
|
988
|
-
* Where a create or an edit opens over the list, when its page names a mode:
|
|
989
|
-
* shadcn's dialog, sheet or drawer, open while it is drawn, and closing it is
|
|
990
|
-
* the host's navigation back to the list.
|
|
991
|
-
*/
|
|
992
|
-
export function RecordFrame({
|
|
993
|
-
mode,
|
|
994
|
-
title,
|
|
995
|
-
closeLabel,
|
|
996
|
-
onClose,
|
|
997
|
-
children,
|
|
998
|
-
}: {
|
|
999
|
-
mode: 'dialog' | 'drawer' | 'sheet'
|
|
1000
|
-
title: string
|
|
1001
|
-
/** What a screen reader names the close button by, in the viewer's language. */
|
|
1002
|
-
closeLabel: string
|
|
1003
|
-
onClose: () => void
|
|
1004
|
-
children?: ReactNode
|
|
1005
|
-
}): ReactElement {
|
|
1006
|
-
const change = (open: boolean) => {
|
|
1007
|
-
if (!open) onClose()
|
|
1008
|
-
}
|
|
1009
|
-
const popup = useRef<HTMLDivElement>(null)
|
|
1010
|
-
const frame =
|
|
1011
|
-
// The title is for a screen reader alone, as the page draws its own title node inside. The close
|
|
1012
|
-
// button is this file's own, as the primitive's is named in English.
|
|
1013
|
-
mode === 'sheet' ? <Sheet open onOpenChange={change}><SheetContent ref={popup} showCloseButton={false} initialFocus={initialFocusIn(popup)} className="gap-4 overflow-y-auto p-4 data-[side=left]:max-sm:w-full data-[side=right]:max-sm:w-full"><SheetTitle className="sr-only">{title}</SheetTitle>{children}<CloseButton Close={SheetClose} label={closeLabel} /></SheetContent></Sheet>
|
|
1014
|
-
: mode === 'drawer' ? <Drawer open onOpenChange={change}><DrawerContent><DrawerTitle className="sr-only">{title}</DrawerTitle><div className="flex min-h-0 flex-col gap-4 overflow-y-auto p-4">{children}</div></DrawerContent></Drawer>
|
|
1015
|
-
: <Dialog open onOpenChange={change}><DialogContent ref={popup} showCloseButton={false} initialFocus={initialFocusIn(popup)}><DialogTitle className="sr-only">{title}</DialogTitle>{children}<CloseButton Close={DialogClose} label={closeLabel} /></DialogContent></Dialog>
|
|
1016
|
-
// A server render draws nothing of the popup, which is a portal, so the mode is said here.
|
|
1017
|
-
return (
|
|
1018
|
-
<>
|
|
1019
|
-
<span hidden data-record-mode={mode} />
|
|
1020
|
-
{frame}
|
|
1021
|
-
</>
|
|
1022
|
-
)
|
|
1023
|
-
}
|
|
1024
|
-
// mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 d6e35be14e1300296cbe3b2d1330322351a5f45e94e6ae3340b8b6f5728cbc0f
|