From eee31e5b99c92f735fa0c835c5589d59498dc90a Mon Sep 17 00:00:00 2001 From: user Date: Sat, 28 Mar 2026 18:12:43 +0200 Subject: [PATCH] Replace front proxy with new SvelteKit frontend app - Remove the old Hono/Bun proxy server - Add the new `apps/frontend` SvelteKit scaffold and telemetry hook --- apps/front/old.server.ts | 151 --------- apps/front/package.json | 33 -- apps/front/src/core/utils.ts | 54 ---- apps/front/src/domains/links/router.ts | 13 - apps/front/src/domains/links/service.ts | 297 ------------------ apps/front/src/index.ts | 32 -- apps/front/tsconfig.json | 33 -- apps/front/view.html | 290 ----------------- apps/frontend/.gitignore | 23 ++ apps/frontend/.npmrc | 1 + apps/frontend/.prettierignore | 9 + apps/frontend/components.json | 16 + apps/frontend/package.json | 74 +++++ apps/frontend/src/app.d.ts | 20 ++ apps/frontend/src/app.html | 11 + apps/frontend/src/demo.spec.ts | 7 + apps/frontend/src/hooks.server.ts | 15 + .../src/instrumentation.server.ts} | 27 +- .../src/lib/components/app-sidebar.svelte | 35 +++ .../lib/components/atoms/button-text.svelte | 17 + .../src/lib/components/atoms/icon.svelte | 11 + .../src/lib/components/atoms/title.svelte | 61 ++++ .../molecules/max-width-wrapper.svelte | 11 + .../src/lib/components/nav-main.svelte | 81 +++++ .../src/lib/components/nav-user.svelte | 150 +++++++++ .../src/lib/components/team-switcher.svelte | 80 +++++ .../ui/accordion/accordion-content.svelte | 22 ++ .../ui/accordion/accordion-item.svelte | 17 + .../ui/accordion/accordion-trigger.svelte | 32 ++ .../components/ui/accordion/accordion.svelte | 16 + .../src/lib/components/ui/accordion/index.ts | 16 + .../alert-dialog/alert-dialog-action.svelte | 18 ++ .../alert-dialog/alert-dialog-cancel.svelte | 18 ++ .../alert-dialog/alert-dialog-content.svelte | 29 ++ .../alert-dialog-description.svelte | 17 + .../alert-dialog/alert-dialog-footer.svelte | 20 ++ .../alert-dialog/alert-dialog-header.svelte | 20 ++ .../alert-dialog/alert-dialog-overlay.svelte | 20 ++ .../alert-dialog/alert-dialog-portal.svelte | 7 + .../ui/alert-dialog/alert-dialog-title.svelte | 17 + .../alert-dialog/alert-dialog-trigger.svelte | 7 + .../ui/alert-dialog/alert-dialog.svelte | 7 + .../lib/components/ui/alert-dialog/index.ts | 37 +++ .../ui/alert/alert-description.svelte | 23 ++ .../components/ui/alert/alert-title.svelte | 20 ++ .../src/lib/components/ui/alert/alert.svelte | 44 +++ .../src/lib/components/ui/alert/index.ts | 14 + .../ui/aspect-ratio/aspect-ratio.svelte | 7 + .../lib/components/ui/aspect-ratio/index.ts | 3 + .../ui/avatar/avatar-fallback.svelte | 17 + .../components/ui/avatar/avatar-image.svelte | 17 + .../lib/components/ui/avatar/avatar.svelte | 19 ++ .../src/lib/components/ui/avatar/index.ts | 13 + .../src/lib/components/ui/badge/badge.svelte | 50 +++ .../src/lib/components/ui/badge/index.ts | 2 + .../ui/breadcrumb/breadcrumb-ellipsis.svelte | 23 ++ .../ui/breadcrumb/breadcrumb-item.svelte | 20 ++ .../ui/breadcrumb/breadcrumb-link.svelte | 31 ++ .../ui/breadcrumb/breadcrumb-list.svelte | 23 ++ .../ui/breadcrumb/breadcrumb-page.svelte | 23 ++ .../ui/breadcrumb/breadcrumb-separator.svelte | 27 ++ .../ui/breadcrumb/breadcrumb.svelte | 21 ++ .../src/lib/components/ui/breadcrumb/index.ts | 25 ++ .../button-group-separator.svelte | 20 ++ .../ui/button-group/button-group-text.svelte | 30 ++ .../ui/button-group/button-group.svelte | 46 +++ .../lib/components/ui/button-group/index.ts | 13 + .../lib/components/ui/button/button.svelte | 82 +++++ .../src/lib/components/ui/button/index.ts | 17 + .../ui/calendar/calendar-caption.svelte | 76 +++++ .../ui/calendar/calendar-cell.svelte | 19 ++ .../ui/calendar/calendar-day.svelte | 35 +++ .../ui/calendar/calendar-grid-body.svelte | 12 + .../ui/calendar/calendar-grid-head.svelte | 12 + .../ui/calendar/calendar-grid-row.svelte | 12 + .../ui/calendar/calendar-grid.svelte | 16 + .../ui/calendar/calendar-head-cell.svelte | 19 ++ .../ui/calendar/calendar-header.svelte | 19 ++ .../ui/calendar/calendar-heading.svelte | 16 + .../ui/calendar/calendar-month-select.svelte | 44 +++ .../ui/calendar/calendar-month.svelte | 15 + .../ui/calendar/calendar-months.svelte | 19 ++ .../ui/calendar/calendar-nav.svelte | 19 ++ .../ui/calendar/calendar-next-button.svelte | 31 ++ .../ui/calendar/calendar-prev-button.svelte | 31 ++ .../ui/calendar/calendar-year-select.svelte | 43 +++ .../components/ui/calendar/calendar.svelte | 115 +++++++ .../src/lib/components/ui/calendar/index.ts | 40 +++ .../lib/components/ui/card/card-action.svelte | 20 ++ .../components/ui/card/card-content.svelte | 15 + .../ui/card/card-description.svelte | 20 ++ .../lib/components/ui/card/card-footer.svelte | 20 ++ .../lib/components/ui/card/card-header.svelte | 23 ++ .../lib/components/ui/card/card-title.svelte | 20 ++ .../src/lib/components/ui/card/card.svelte | 23 ++ .../src/lib/components/ui/card/index.ts | 25 ++ .../ui/carousel/carousel-content.svelte | 43 +++ .../ui/carousel/carousel-item.svelte | 30 ++ .../ui/carousel/carousel-next.svelte | 38 +++ .../ui/carousel/carousel-previous.svelte | 38 +++ .../components/ui/carousel/carousel.svelte | 93 ++++++ .../src/lib/components/ui/carousel/context.ts | 58 ++++ .../src/lib/components/ui/carousel/index.ts | 19 ++ .../ui/chart/chart-container.svelte | 80 +++++ .../components/ui/chart/chart-style.svelte | 37 +++ .../components/ui/chart/chart-tooltip.svelte | 159 ++++++++++ .../lib/components/ui/chart/chart-utils.ts | 66 ++++ .../src/lib/components/ui/chart/index.ts | 6 + .../components/ui/checkbox/checkbox.svelte | 36 +++ .../src/lib/components/ui/checkbox/index.ts | 6 + .../ui/collapsible/collapsible-content.svelte | 7 + .../ui/collapsible/collapsible-trigger.svelte | 7 + .../ui/collapsible/collapsible.svelte | 11 + .../lib/components/ui/collapsible/index.ts | 13 + .../ui/command/command-dialog.svelte | 40 +++ .../ui/command/command-empty.svelte | 17 + .../ui/command/command-group.svelte | 32 ++ .../ui/command/command-input.svelte | 26 ++ .../components/ui/command/command-item.svelte | 20 ++ .../ui/command/command-link-item.svelte | 20 ++ .../components/ui/command/command-list.svelte | 17 + .../ui/command/command-loading.svelte | 7 + .../ui/command/command-separator.svelte | 17 + .../ui/command/command-shortcut.svelte | 20 ++ .../lib/components/ui/command/command.svelte | 28 ++ .../src/lib/components/ui/command/index.ts | 37 +++ .../context-menu-checkbox-item.svelte | 40 +++ .../context-menu/context-menu-content.svelte | 28 ++ .../context-menu-group-heading.svelte | 21 ++ .../ui/context-menu/context-menu-group.svelte | 7 + .../ui/context-menu/context-menu-item.svelte | 27 ++ .../ui/context-menu/context-menu-label.svelte | 24 ++ .../context-menu/context-menu-portal.svelte | 7 + .../context-menu-radio-group.svelte | 16 + .../context-menu-radio-item.svelte | 33 ++ .../context-menu-separator.svelte | 17 + .../context-menu/context-menu-shortcut.svelte | 20 ++ .../context-menu-sub-content.svelte | 20 ++ .../context-menu-sub-trigger.svelte | 29 ++ .../ui/context-menu/context-menu-sub.svelte | 7 + .../context-menu/context-menu-trigger.svelte | 7 + .../ui/context-menu/context-menu.svelte | 7 + .../lib/components/ui/context-menu/index.ts | 52 +++ .../ui/data-table/data-table.svelte.ts | 142 +++++++++ .../ui/data-table/flex-render.svelte | 40 +++ .../src/lib/components/ui/data-table/index.ts | 3 + .../ui/data-table/render-helpers.ts | 111 +++++++ .../components/ui/dialog/dialog-close.svelte | 7 + .../ui/dialog/dialog-content.svelte | 45 +++ .../ui/dialog/dialog-description.svelte | 17 + .../components/ui/dialog/dialog-footer.svelte | 20 ++ .../components/ui/dialog/dialog-header.svelte | 20 ++ .../ui/dialog/dialog-overlay.svelte | 20 ++ .../components/ui/dialog/dialog-portal.svelte | 7 + .../components/ui/dialog/dialog-title.svelte | 17 + .../ui/dialog/dialog-trigger.svelte | 7 + .../lib/components/ui/dialog/dialog.svelte | 7 + .../src/lib/components/ui/dialog/index.ts | 34 ++ .../components/ui/drawer/drawer-close.svelte | 7 + .../ui/drawer/drawer-content.svelte | 40 +++ .../ui/drawer/drawer-description.svelte | 17 + .../components/ui/drawer/drawer-footer.svelte | 20 ++ .../components/ui/drawer/drawer-header.svelte | 20 ++ .../components/ui/drawer/drawer-nested.svelte | 12 + .../ui/drawer/drawer-overlay.svelte | 20 ++ .../components/ui/drawer/drawer-portal.svelte | 7 + .../components/ui/drawer/drawer-title.svelte | 17 + .../ui/drawer/drawer-trigger.svelte | 7 + .../lib/components/ui/drawer/drawer.svelte | 12 + .../src/lib/components/ui/drawer/index.ts | 38 +++ .../dropdown-menu-checkbox-group.svelte | 16 + .../dropdown-menu-checkbox-item.svelte | 43 +++ .../dropdown-menu-content.svelte | 29 ++ .../dropdown-menu-group-heading.svelte | 22 ++ .../dropdown-menu/dropdown-menu-group.svelte | 7 + .../dropdown-menu/dropdown-menu-item.svelte | 27 ++ .../dropdown-menu/dropdown-menu-label.svelte | 24 ++ .../dropdown-menu/dropdown-menu-portal.svelte | 7 + .../dropdown-menu-radio-group.svelte | 16 + .../dropdown-menu-radio-item.svelte | 33 ++ .../dropdown-menu-separator.svelte | 17 + .../dropdown-menu-shortcut.svelte | 20 ++ .../dropdown-menu-sub-content.svelte | 20 ++ .../dropdown-menu-sub-trigger.svelte | 29 ++ .../ui/dropdown-menu/dropdown-menu-sub.svelte | 7 + .../dropdown-menu-trigger.svelte | 7 + .../ui/dropdown-menu/dropdown-menu.svelte | 7 + .../lib/components/ui/dropdown-menu/index.ts | 54 ++++ .../components/ui/empty/empty-content.svelte | 23 ++ .../ui/empty/empty-description.svelte | 23 ++ .../components/ui/empty/empty-header.svelte | 20 ++ .../components/ui/empty/empty-media.svelte | 41 +++ .../components/ui/empty/empty-title.svelte | 20 ++ .../src/lib/components/ui/empty/empty.svelte | 23 ++ .../src/lib/components/ui/empty/index.ts | 22 ++ .../components/ui/field/field-content.svelte | 20 ++ .../ui/field/field-description.svelte | 25 ++ .../components/ui/field/field-error.svelte | 58 ++++ .../components/ui/field/field-group.svelte | 23 ++ .../components/ui/field/field-label.svelte | 26 ++ .../components/ui/field/field-legend.svelte | 29 ++ .../ui/field/field-separator.svelte | 38 +++ .../lib/components/ui/field/field-set.svelte | 24 ++ .../components/ui/field/field-title.svelte | 23 ++ .../src/lib/components/ui/field/field.svelte | 53 ++++ .../src/lib/components/ui/field/index.ts | 33 ++ .../lib/components/ui/form/form-button.svelte | 7 + .../ui/form/form-description.svelte | 17 + .../ui/form/form-element-field.svelte | 24 ++ .../ui/form/form-field-errors.svelte | 30 ++ .../lib/components/ui/form/form-field.svelte | 29 ++ .../components/ui/form/form-fieldset.svelte | 15 + .../lib/components/ui/form/form-label.svelte | 24 ++ .../lib/components/ui/form/form-legend.svelte | 16 + .../src/lib/components/ui/form/index.ts | 33 ++ .../ui/hover-card/hover-card-content.svelte | 31 ++ .../ui/hover-card/hover-card-portal.svelte | 7 + .../ui/hover-card/hover-card-trigger.svelte | 7 + .../ui/hover-card/hover-card.svelte | 7 + .../src/lib/components/ui/hover-card/index.ts | 15 + .../lib/components/ui/input-group/index.ts | 22 ++ .../ui/input-group/input-group-addon.svelte | 55 ++++ .../ui/input-group/input-group-button.svelte | 49 +++ .../ui/input-group/input-group-input.svelte | 23 ++ .../ui/input-group/input-group-text.svelte | 22 ++ .../input-group/input-group-textarea.svelte | 23 ++ .../ui/input-group/input-group.svelte | 38 +++ .../src/lib/components/ui/input-otp/index.ts | 15 + .../ui/input-otp/input-otp-group.svelte | 20 ++ .../ui/input-otp/input-otp-separator.svelte | 19 ++ .../ui/input-otp/input-otp-slot.svelte | 31 ++ .../components/ui/input-otp/input-otp.svelte | 22 ++ .../src/lib/components/ui/input/index.ts | 7 + .../src/lib/components/ui/input/input.svelte | 52 +++ .../src/lib/components/ui/item/index.ts | 34 ++ .../components/ui/item/item-actions.svelte | 20 ++ .../components/ui/item/item-content.svelte | 20 ++ .../ui/item/item-description.svelte | 24 ++ .../lib/components/ui/item/item-footer.svelte | 20 ++ .../lib/components/ui/item/item-group.svelte | 21 ++ .../lib/components/ui/item/item-header.svelte | 20 ++ .../lib/components/ui/item/item-media.svelte | 42 +++ .../components/ui/item/item-separator.svelte | 19 ++ .../lib/components/ui/item/item-title.svelte | 20 ++ .../src/lib/components/ui/item/item.svelte | 60 ++++ .../src/lib/components/ui/kbd/index.ts | 10 + .../lib/components/ui/kbd/kbd-group.svelte | 20 ++ .../src/lib/components/ui/kbd/kbd.svelte | 25 ++ .../src/lib/components/ui/label/index.ts | 7 + .../src/lib/components/ui/label/label.svelte | 20 ++ .../src/lib/components/ui/menubar/index.ts | 55 ++++ .../ui/menubar/menubar-checkbox-item.svelte | 43 +++ .../ui/menubar/menubar-content.svelte | 35 +++ .../ui/menubar/menubar-group-heading.svelte | 22 ++ .../ui/menubar/menubar-group.svelte | 12 + .../components/ui/menubar/menubar-item.svelte | 27 ++ .../ui/menubar/menubar-label.svelte | 25 ++ .../components/ui/menubar/menubar-menu.svelte | 7 + .../ui/menubar/menubar-portal.svelte | 7 + .../ui/menubar/menubar-radio-group.svelte | 11 + .../ui/menubar/menubar-radio-item.svelte | 33 ++ .../ui/menubar/menubar-separator.svelte | 17 + .../ui/menubar/menubar-shortcut.svelte | 20 ++ .../ui/menubar/menubar-sub-content.svelte | 20 ++ .../ui/menubar/menubar-sub-trigger.svelte | 29 ++ .../components/ui/menubar/menubar-sub.svelte | 7 + .../ui/menubar/menubar-trigger.svelte | 20 ++ .../lib/components/ui/menubar/menubar.svelte | 20 ++ .../lib/components/ui/native-select/index.ts | 12 + .../native-select-opt-group.svelte | 14 + .../native-select/native-select-option.svelte | 14 + .../ui/native-select/native-select.svelte | 38 +++ .../components/ui/navigation-menu/index.ts | 28 ++ .../navigation-menu-content.svelte | 21 ++ .../navigation-menu-indicator.svelte | 22 ++ .../navigation-menu-item.svelte | 17 + .../navigation-menu-link.svelte | 20 ++ .../navigation-menu-list.svelte | 17 + .../navigation-menu-trigger.svelte | 34 ++ .../navigation-menu-viewport.svelte | 22 ++ .../ui/navigation-menu/navigation-menu.svelte | 32 ++ .../src/lib/components/ui/pagination/index.ts | 31 ++ .../ui/pagination/pagination-content.svelte | 20 ++ .../ui/pagination/pagination-ellipsis.svelte | 22 ++ .../ui/pagination/pagination-item.svelte | 14 + .../ui/pagination/pagination-link.svelte | 39 +++ .../pagination/pagination-next-button.svelte | 33 ++ .../ui/pagination/pagination-next.svelte | 29 ++ .../pagination/pagination-prev-button.svelte | 33 ++ .../ui/pagination/pagination-previous.svelte | 29 ++ .../ui/pagination/pagination.svelte | 28 ++ .../src/lib/components/ui/popover/index.ts | 19 ++ .../ui/popover/popover-close.svelte | 7 + .../ui/popover/popover-content.svelte | 31 ++ .../ui/popover/popover-portal.svelte | 7 + .../ui/popover/popover-trigger.svelte | 17 + .../lib/components/ui/popover/popover.svelte | 7 + .../src/lib/components/ui/progress/index.ts | 7 + .../components/ui/progress/progress.svelte | 27 ++ .../lib/components/ui/radio-group/index.ts | 10 + .../ui/radio-group/radio-group-item.svelte | 31 ++ .../ui/radio-group/radio-group.svelte | 19 ++ .../lib/components/ui/range-calendar/index.ts | 40 +++ .../range-calendar-caption.svelte | 76 +++++ .../range-calendar/range-calendar-cell.svelte | 19 ++ .../range-calendar/range-calendar-day.svelte | 39 +++ .../range-calendar-grid-body.svelte | 7 + .../range-calendar-grid-head.svelte | 7 + .../range-calendar-grid-row.svelte | 12 + .../range-calendar/range-calendar-grid.svelte | 16 + .../range-calendar-head-cell.svelte | 19 ++ .../range-calendar-header.svelte | 19 ++ .../range-calendar-heading.svelte | 16 + .../range-calendar-month-select.svelte | 44 +++ .../range-calendar-month.svelte | 15 + .../range-calendar-months.svelte | 19 ++ .../range-calendar/range-calendar-nav.svelte | 19 ++ .../range-calendar-next-button.svelte | 31 ++ .../range-calendar-prev-button.svelte | 31 ++ .../range-calendar-year-select.svelte | 43 +++ .../ui/range-calendar/range-calendar.svelte | 112 +++++++ .../src/lib/components/ui/resizable/index.ts | 13 + .../ui/resizable/resizable-handle.svelte | 30 ++ .../ui/resizable/resizable-pane-group.svelte | 20 ++ .../lib/components/ui/scroll-area/index.ts | 10 + .../scroll-area/scroll-area-scrollbar.svelte | 31 ++ .../ui/scroll-area/scroll-area.svelte | 43 +++ .../src/lib/components/ui/select/index.ts | 37 +++ .../ui/select/select-content.svelte | 45 +++ .../ui/select/select-group-heading.svelte | 21 ++ .../components/ui/select/select-group.svelte | 7 + .../components/ui/select/select-item.svelte | 38 +++ .../components/ui/select/select-label.svelte | 20 ++ .../components/ui/select/select-portal.svelte | 7 + .../select/select-scroll-down-button.svelte | 20 ++ .../ui/select/select-scroll-up-button.svelte | 20 ++ .../ui/select/select-separator.svelte | 18 ++ .../ui/select/select-trigger.svelte | 29 ++ .../lib/components/ui/select/select.svelte | 11 + .../src/lib/components/ui/separator/index.ts | 7 + .../components/ui/separator/separator.svelte | 21 ++ .../src/lib/components/ui/sheet/index.ts | 34 ++ .../components/ui/sheet/sheet-close.svelte | 7 + .../components/ui/sheet/sheet-content.svelte | 60 ++++ .../ui/sheet/sheet-description.svelte | 17 + .../components/ui/sheet/sheet-footer.svelte | 20 ++ .../components/ui/sheet/sheet-header.svelte | 20 ++ .../components/ui/sheet/sheet-overlay.svelte | 20 ++ .../components/ui/sheet/sheet-portal.svelte | 7 + .../components/ui/sheet/sheet-title.svelte | 17 + .../components/ui/sheet/sheet-trigger.svelte | 7 + .../src/lib/components/ui/sheet/sheet.svelte | 7 + .../lib/components/ui/sidebar/constants.ts | 6 + .../components/ui/sidebar/context.svelte.ts | 81 +++++ .../src/lib/components/ui/sidebar/index.ts | 75 +++++ .../ui/sidebar/sidebar-content.svelte | 24 ++ .../ui/sidebar/sidebar-footer.svelte | 21 ++ .../ui/sidebar/sidebar-group-action.svelte | 36 +++ .../ui/sidebar/sidebar-group-content.svelte | 21 ++ .../ui/sidebar/sidebar-group-label.svelte | 34 ++ .../ui/sidebar/sidebar-group.svelte | 21 ++ .../ui/sidebar/sidebar-header.svelte | 21 ++ .../ui/sidebar/sidebar-input.svelte | 21 ++ .../ui/sidebar/sidebar-inset.svelte | 24 ++ .../ui/sidebar/sidebar-menu-action.svelte | 43 +++ .../ui/sidebar/sidebar-menu-badge.svelte | 29 ++ .../ui/sidebar/sidebar-menu-button.svelte | 103 ++++++ .../ui/sidebar/sidebar-menu-item.svelte | 21 ++ .../ui/sidebar/sidebar-menu-skeleton.svelte | 36 +++ .../ui/sidebar/sidebar-menu-sub-button.svelte | 43 +++ .../ui/sidebar/sidebar-menu-sub-item.svelte | 21 ++ .../ui/sidebar/sidebar-menu-sub.svelte | 25 ++ .../components/ui/sidebar/sidebar-menu.svelte | 21 ++ .../ui/sidebar/sidebar-provider.svelte | 53 ++++ .../components/ui/sidebar/sidebar-rail.svelte | 36 +++ .../ui/sidebar/sidebar-separator.svelte | 19 ++ .../ui/sidebar/sidebar-trigger.svelte | 35 +++ .../lib/components/ui/sidebar/sidebar.svelte | 104 ++++++ .../src/lib/components/ui/skeleton/index.ts | 7 + .../components/ui/skeleton/skeleton.svelte | 17 + .../src/lib/components/ui/slider/index.ts | 7 + .../lib/components/ui/slider/slider.svelte | 52 +++ .../src/lib/components/ui/sonner/index.ts | 1 + .../lib/components/ui/sonner/sonner.svelte | 34 ++ .../src/lib/components/ui/spinner/index.ts | 1 + .../lib/components/ui/spinner/spinner.svelte | 14 + .../src/lib/components/ui/switch/index.ts | 7 + .../lib/components/ui/switch/switch.svelte | 29 ++ .../src/lib/components/ui/table/index.ts | 28 ++ .../lib/components/ui/table/table-body.svelte | 20 ++ .../components/ui/table/table-caption.svelte | 20 ++ .../lib/components/ui/table/table-cell.svelte | 23 ++ .../components/ui/table/table-footer.svelte | 20 ++ .../lib/components/ui/table/table-head.svelte | 23 ++ .../components/ui/table/table-header.svelte | 20 ++ .../lib/components/ui/table/table-row.svelte | 23 ++ .../src/lib/components/ui/table/table.svelte | 22 ++ .../src/lib/components/ui/tabs/index.ts | 16 + .../components/ui/tabs/tabs-content.svelte | 17 + .../lib/components/ui/tabs/tabs-list.svelte | 20 ++ .../components/ui/tabs/tabs-trigger.svelte | 20 ++ .../src/lib/components/ui/tabs/tabs.svelte | 19 ++ .../src/lib/components/ui/textarea/index.ts | 7 + .../components/ui/textarea/textarea.svelte | 23 ++ .../lib/components/ui/toggle-group/index.ts | 10 + .../ui/toggle-group/toggle-group-item.svelte | 35 +++ .../ui/toggle-group/toggle-group.svelte | 59 ++++ .../src/lib/components/ui/toggle/index.ts | 13 + .../lib/components/ui/toggle/toggle.svelte | 52 +++ .../src/lib/components/ui/tooltip/index.ts | 19 ++ .../ui/tooltip/tooltip-content.svelte | 52 +++ .../ui/tooltip/tooltip-portal.svelte | 7 + .../ui/tooltip/tooltip-provider.svelte | 7 + .../ui/tooltip/tooltip-trigger.svelte | 7 + .../lib/components/ui/tooltip/tooltip.svelte | 7 + apps/frontend/src/lib/core/constants.ts | 6 + apps/frontend/src/lib/core/server.utils.ts | 25 ++ apps/frontend/src/lib/domains/link/data.ts | 22 ++ .../src/lib/domains/link/link.remote.ts | 16 + .../lib/domains/link/orchestrator.service.ts | 63 ++++ apps/frontend/src/lib/domains/link/service.ts | 224 +++++++++++++ apps/frontend/src/lib/domains/link/utils.ts | 63 ++++ apps/frontend/src/lib/global.stores.ts | 14 + .../src/lib/hooks/is-mobile.svelte.ts | 9 + apps/frontend/src/lib/make-client.ts | 21 ++ apps/frontend/src/lib/utils.ts | 13 + apps/frontend/src/routes/+layout.svelte | 20 ++ apps/frontend/src/routes/+page.svelte | 7 + apps/frontend/src/routes/layout.css | 195 ++++++++++++ .../static/fonts/manrope-variable.ttf | Bin 0 -> 164936 bytes apps/frontend/static/images/avatar.png | Bin 0 -> 2011 bytes apps/frontend/static/robots.txt | 3 + apps/frontend/svelte.config.js | 20 ++ apps/frontend/tsconfig.json | 20 ++ apps/frontend/vite.config.ts | 46 +++ apps/main/src/lib/core/constants.ts | 3 +- .../{front.Dockerfile => frontend.Dockerfile} | 6 +- memory.log.md | 12 + pnpm-lock.yaml | 162 +++++++++- 439 files changed, 11494 insertions(+), 933 deletions(-) delete mode 100644 apps/front/old.server.ts delete mode 100644 apps/front/package.json delete mode 100644 apps/front/src/core/utils.ts delete mode 100644 apps/front/src/domains/links/router.ts delete mode 100644 apps/front/src/domains/links/service.ts delete mode 100644 apps/front/src/index.ts delete mode 100644 apps/front/tsconfig.json delete mode 100644 apps/front/view.html create mode 100644 apps/frontend/.gitignore create mode 100644 apps/frontend/.npmrc create mode 100644 apps/frontend/.prettierignore create mode 100644 apps/frontend/components.json create mode 100644 apps/frontend/package.json create mode 100644 apps/frontend/src/app.d.ts create mode 100644 apps/frontend/src/app.html create mode 100644 apps/frontend/src/demo.spec.ts create mode 100644 apps/frontend/src/hooks.server.ts rename apps/{front/src/instrumentation.ts => frontend/src/instrumentation.server.ts} (57%) create mode 100644 apps/frontend/src/lib/components/app-sidebar.svelte create mode 100644 apps/frontend/src/lib/components/atoms/button-text.svelte create mode 100644 apps/frontend/src/lib/components/atoms/icon.svelte create mode 100644 apps/frontend/src/lib/components/atoms/title.svelte create mode 100644 apps/frontend/src/lib/components/molecules/max-width-wrapper.svelte create mode 100644 apps/frontend/src/lib/components/nav-main.svelte create mode 100644 apps/frontend/src/lib/components/nav-user.svelte create mode 100644 apps/frontend/src/lib/components/team-switcher.svelte create mode 100644 apps/frontend/src/lib/components/ui/accordion/accordion-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/accordion/accordion-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/accordion/accordion-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/accordion/accordion.svelte create mode 100644 apps/frontend/src/lib/components/ui/accordion/index.ts create mode 100644 apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte create mode 100644 apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte create mode 100644 apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte create mode 100644 apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte create mode 100644 apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte create mode 100644 apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte create mode 100644 apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte create mode 100644 apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte create mode 100644 apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog.svelte create mode 100644 apps/frontend/src/lib/components/ui/alert-dialog/index.ts create mode 100644 apps/frontend/src/lib/components/ui/alert/alert-description.svelte create mode 100644 apps/frontend/src/lib/components/ui/alert/alert-title.svelte create mode 100644 apps/frontend/src/lib/components/ui/alert/alert.svelte create mode 100644 apps/frontend/src/lib/components/ui/alert/index.ts create mode 100644 apps/frontend/src/lib/components/ui/aspect-ratio/aspect-ratio.svelte create mode 100644 apps/frontend/src/lib/components/ui/aspect-ratio/index.ts create mode 100644 apps/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte create mode 100644 apps/frontend/src/lib/components/ui/avatar/avatar-image.svelte create mode 100644 apps/frontend/src/lib/components/ui/avatar/avatar.svelte create mode 100644 apps/frontend/src/lib/components/ui/avatar/index.ts create mode 100644 apps/frontend/src/lib/components/ui/badge/badge.svelte create mode 100644 apps/frontend/src/lib/components/ui/badge/index.ts create mode 100644 apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte create mode 100644 apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte create mode 100644 apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte create mode 100644 apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte create mode 100644 apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte create mode 100644 apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb.svelte create mode 100644 apps/frontend/src/lib/components/ui/breadcrumb/index.ts create mode 100644 apps/frontend/src/lib/components/ui/button-group/button-group-separator.svelte create mode 100644 apps/frontend/src/lib/components/ui/button-group/button-group-text.svelte create mode 100644 apps/frontend/src/lib/components/ui/button-group/button-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/button-group/index.ts create mode 100644 apps/frontend/src/lib/components/ui/button/button.svelte create mode 100644 apps/frontend/src/lib/components/ui/button/index.ts create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-caption.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-cell.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-day.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-grid-body.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-grid-head.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-grid-row.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-grid.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-head-cell.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-header.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-heading.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-month-select.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-month.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-months.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-nav.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-next-button.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-prev-button.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar-year-select.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/calendar.svelte create mode 100644 apps/frontend/src/lib/components/ui/calendar/index.ts create mode 100644 apps/frontend/src/lib/components/ui/card/card-action.svelte create mode 100644 apps/frontend/src/lib/components/ui/card/card-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/card/card-description.svelte create mode 100644 apps/frontend/src/lib/components/ui/card/card-footer.svelte create mode 100644 apps/frontend/src/lib/components/ui/card/card-header.svelte create mode 100644 apps/frontend/src/lib/components/ui/card/card-title.svelte create mode 100644 apps/frontend/src/lib/components/ui/card/card.svelte create mode 100644 apps/frontend/src/lib/components/ui/card/index.ts create mode 100644 apps/frontend/src/lib/components/ui/carousel/carousel-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/carousel/carousel-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/carousel/carousel-next.svelte create mode 100644 apps/frontend/src/lib/components/ui/carousel/carousel-previous.svelte create mode 100644 apps/frontend/src/lib/components/ui/carousel/carousel.svelte create mode 100644 apps/frontend/src/lib/components/ui/carousel/context.ts create mode 100644 apps/frontend/src/lib/components/ui/carousel/index.ts create mode 100644 apps/frontend/src/lib/components/ui/chart/chart-container.svelte create mode 100644 apps/frontend/src/lib/components/ui/chart/chart-style.svelte create mode 100644 apps/frontend/src/lib/components/ui/chart/chart-tooltip.svelte create mode 100644 apps/frontend/src/lib/components/ui/chart/chart-utils.ts create mode 100644 apps/frontend/src/lib/components/ui/chart/index.ts create mode 100644 apps/frontend/src/lib/components/ui/checkbox/checkbox.svelte create mode 100644 apps/frontend/src/lib/components/ui/checkbox/index.ts create mode 100644 apps/frontend/src/lib/components/ui/collapsible/collapsible-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/collapsible/collapsible-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/collapsible/collapsible.svelte create mode 100644 apps/frontend/src/lib/components/ui/collapsible/index.ts create mode 100644 apps/frontend/src/lib/components/ui/command/command-dialog.svelte create mode 100644 apps/frontend/src/lib/components/ui/command/command-empty.svelte create mode 100644 apps/frontend/src/lib/components/ui/command/command-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/command/command-input.svelte create mode 100644 apps/frontend/src/lib/components/ui/command/command-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/command/command-link-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/command/command-list.svelte create mode 100644 apps/frontend/src/lib/components/ui/command/command-loading.svelte create mode 100644 apps/frontend/src/lib/components/ui/command/command-separator.svelte create mode 100644 apps/frontend/src/lib/components/ui/command/command-shortcut.svelte create mode 100644 apps/frontend/src/lib/components/ui/command/command.svelte create mode 100644 apps/frontend/src/lib/components/ui/command/index.ts create mode 100644 apps/frontend/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/context-menu/context-menu-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/context-menu/context-menu-group-heading.svelte create mode 100644 apps/frontend/src/lib/components/ui/context-menu/context-menu-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/context-menu/context-menu-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/context-menu/context-menu-label.svelte create mode 100644 apps/frontend/src/lib/components/ui/context-menu/context-menu-portal.svelte create mode 100644 apps/frontend/src/lib/components/ui/context-menu/context-menu-radio-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/context-menu/context-menu-radio-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/context-menu/context-menu-separator.svelte create mode 100644 apps/frontend/src/lib/components/ui/context-menu/context-menu-shortcut.svelte create mode 100644 apps/frontend/src/lib/components/ui/context-menu/context-menu-sub-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/context-menu/context-menu-sub.svelte create mode 100644 apps/frontend/src/lib/components/ui/context-menu/context-menu-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/context-menu/context-menu.svelte create mode 100644 apps/frontend/src/lib/components/ui/context-menu/index.ts create mode 100644 apps/frontend/src/lib/components/ui/data-table/data-table.svelte.ts create mode 100644 apps/frontend/src/lib/components/ui/data-table/flex-render.svelte create mode 100644 apps/frontend/src/lib/components/ui/data-table/index.ts create mode 100644 apps/frontend/src/lib/components/ui/data-table/render-helpers.ts create mode 100644 apps/frontend/src/lib/components/ui/dialog/dialog-close.svelte create mode 100644 apps/frontend/src/lib/components/ui/dialog/dialog-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/dialog/dialog-description.svelte create mode 100644 apps/frontend/src/lib/components/ui/dialog/dialog-footer.svelte create mode 100644 apps/frontend/src/lib/components/ui/dialog/dialog-header.svelte create mode 100644 apps/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte create mode 100644 apps/frontend/src/lib/components/ui/dialog/dialog-portal.svelte create mode 100644 apps/frontend/src/lib/components/ui/dialog/dialog-title.svelte create mode 100644 apps/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/dialog/dialog.svelte create mode 100644 apps/frontend/src/lib/components/ui/dialog/index.ts create mode 100644 apps/frontend/src/lib/components/ui/drawer/drawer-close.svelte create mode 100644 apps/frontend/src/lib/components/ui/drawer/drawer-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/drawer/drawer-description.svelte create mode 100644 apps/frontend/src/lib/components/ui/drawer/drawer-footer.svelte create mode 100644 apps/frontend/src/lib/components/ui/drawer/drawer-header.svelte create mode 100644 apps/frontend/src/lib/components/ui/drawer/drawer-nested.svelte create mode 100644 apps/frontend/src/lib/components/ui/drawer/drawer-overlay.svelte create mode 100644 apps/frontend/src/lib/components/ui/drawer/drawer-portal.svelte create mode 100644 apps/frontend/src/lib/components/ui/drawer/drawer-title.svelte create mode 100644 apps/frontend/src/lib/components/ui/drawer/drawer-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/drawer/drawer.svelte create mode 100644 apps/frontend/src/lib/components/ui/drawer/index.ts create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte create mode 100644 apps/frontend/src/lib/components/ui/dropdown-menu/index.ts create mode 100644 apps/frontend/src/lib/components/ui/empty/empty-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/empty/empty-description.svelte create mode 100644 apps/frontend/src/lib/components/ui/empty/empty-header.svelte create mode 100644 apps/frontend/src/lib/components/ui/empty/empty-media.svelte create mode 100644 apps/frontend/src/lib/components/ui/empty/empty-title.svelte create mode 100644 apps/frontend/src/lib/components/ui/empty/empty.svelte create mode 100644 apps/frontend/src/lib/components/ui/empty/index.ts create mode 100644 apps/frontend/src/lib/components/ui/field/field-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/field/field-description.svelte create mode 100644 apps/frontend/src/lib/components/ui/field/field-error.svelte create mode 100644 apps/frontend/src/lib/components/ui/field/field-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/field/field-label.svelte create mode 100644 apps/frontend/src/lib/components/ui/field/field-legend.svelte create mode 100644 apps/frontend/src/lib/components/ui/field/field-separator.svelte create mode 100644 apps/frontend/src/lib/components/ui/field/field-set.svelte create mode 100644 apps/frontend/src/lib/components/ui/field/field-title.svelte create mode 100644 apps/frontend/src/lib/components/ui/field/field.svelte create mode 100644 apps/frontend/src/lib/components/ui/field/index.ts create mode 100644 apps/frontend/src/lib/components/ui/form/form-button.svelte create mode 100644 apps/frontend/src/lib/components/ui/form/form-description.svelte create mode 100644 apps/frontend/src/lib/components/ui/form/form-element-field.svelte create mode 100644 apps/frontend/src/lib/components/ui/form/form-field-errors.svelte create mode 100644 apps/frontend/src/lib/components/ui/form/form-field.svelte create mode 100644 apps/frontend/src/lib/components/ui/form/form-fieldset.svelte create mode 100644 apps/frontend/src/lib/components/ui/form/form-label.svelte create mode 100644 apps/frontend/src/lib/components/ui/form/form-legend.svelte create mode 100644 apps/frontend/src/lib/components/ui/form/index.ts create mode 100644 apps/frontend/src/lib/components/ui/hover-card/hover-card-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/hover-card/hover-card-portal.svelte create mode 100644 apps/frontend/src/lib/components/ui/hover-card/hover-card-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/hover-card/hover-card.svelte create mode 100644 apps/frontend/src/lib/components/ui/hover-card/index.ts create mode 100644 apps/frontend/src/lib/components/ui/input-group/index.ts create mode 100644 apps/frontend/src/lib/components/ui/input-group/input-group-addon.svelte create mode 100644 apps/frontend/src/lib/components/ui/input-group/input-group-button.svelte create mode 100644 apps/frontend/src/lib/components/ui/input-group/input-group-input.svelte create mode 100644 apps/frontend/src/lib/components/ui/input-group/input-group-text.svelte create mode 100644 apps/frontend/src/lib/components/ui/input-group/input-group-textarea.svelte create mode 100644 apps/frontend/src/lib/components/ui/input-group/input-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/input-otp/index.ts create mode 100644 apps/frontend/src/lib/components/ui/input-otp/input-otp-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/input-otp/input-otp-separator.svelte create mode 100644 apps/frontend/src/lib/components/ui/input-otp/input-otp-slot.svelte create mode 100644 apps/frontend/src/lib/components/ui/input-otp/input-otp.svelte create mode 100644 apps/frontend/src/lib/components/ui/input/index.ts create mode 100644 apps/frontend/src/lib/components/ui/input/input.svelte create mode 100644 apps/frontend/src/lib/components/ui/item/index.ts create mode 100644 apps/frontend/src/lib/components/ui/item/item-actions.svelte create mode 100644 apps/frontend/src/lib/components/ui/item/item-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/item/item-description.svelte create mode 100644 apps/frontend/src/lib/components/ui/item/item-footer.svelte create mode 100644 apps/frontend/src/lib/components/ui/item/item-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/item/item-header.svelte create mode 100644 apps/frontend/src/lib/components/ui/item/item-media.svelte create mode 100644 apps/frontend/src/lib/components/ui/item/item-separator.svelte create mode 100644 apps/frontend/src/lib/components/ui/item/item-title.svelte create mode 100644 apps/frontend/src/lib/components/ui/item/item.svelte create mode 100644 apps/frontend/src/lib/components/ui/kbd/index.ts create mode 100644 apps/frontend/src/lib/components/ui/kbd/kbd-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/kbd/kbd.svelte create mode 100644 apps/frontend/src/lib/components/ui/label/index.ts create mode 100644 apps/frontend/src/lib/components/ui/label/label.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/index.ts create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar-checkbox-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar-group-heading.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar-label.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar-menu.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar-portal.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar-radio-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar-radio-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar-separator.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar-shortcut.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar-sub-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar-sub-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar-sub.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/menubar/menubar.svelte create mode 100644 apps/frontend/src/lib/components/ui/native-select/index.ts create mode 100644 apps/frontend/src/lib/components/ui/native-select/native-select-opt-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/native-select/native-select-option.svelte create mode 100644 apps/frontend/src/lib/components/ui/native-select/native-select.svelte create mode 100644 apps/frontend/src/lib/components/ui/navigation-menu/index.ts create mode 100644 apps/frontend/src/lib/components/ui/navigation-menu/navigation-menu-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/navigation-menu/navigation-menu-indicator.svelte create mode 100644 apps/frontend/src/lib/components/ui/navigation-menu/navigation-menu-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/navigation-menu/navigation-menu-link.svelte create mode 100644 apps/frontend/src/lib/components/ui/navigation-menu/navigation-menu-list.svelte create mode 100644 apps/frontend/src/lib/components/ui/navigation-menu/navigation-menu-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/navigation-menu/navigation-menu-viewport.svelte create mode 100644 apps/frontend/src/lib/components/ui/navigation-menu/navigation-menu.svelte create mode 100644 apps/frontend/src/lib/components/ui/pagination/index.ts create mode 100644 apps/frontend/src/lib/components/ui/pagination/pagination-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/pagination/pagination-ellipsis.svelte create mode 100644 apps/frontend/src/lib/components/ui/pagination/pagination-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/pagination/pagination-link.svelte create mode 100644 apps/frontend/src/lib/components/ui/pagination/pagination-next-button.svelte create mode 100644 apps/frontend/src/lib/components/ui/pagination/pagination-next.svelte create mode 100644 apps/frontend/src/lib/components/ui/pagination/pagination-prev-button.svelte create mode 100644 apps/frontend/src/lib/components/ui/pagination/pagination-previous.svelte create mode 100644 apps/frontend/src/lib/components/ui/pagination/pagination.svelte create mode 100644 apps/frontend/src/lib/components/ui/popover/index.ts create mode 100644 apps/frontend/src/lib/components/ui/popover/popover-close.svelte create mode 100644 apps/frontend/src/lib/components/ui/popover/popover-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/popover/popover-portal.svelte create mode 100644 apps/frontend/src/lib/components/ui/popover/popover-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/popover/popover.svelte create mode 100644 apps/frontend/src/lib/components/ui/progress/index.ts create mode 100644 apps/frontend/src/lib/components/ui/progress/progress.svelte create mode 100644 apps/frontend/src/lib/components/ui/radio-group/index.ts create mode 100644 apps/frontend/src/lib/components/ui/radio-group/radio-group-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/radio-group/radio-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/index.ts create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-caption.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-cell.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-day.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-grid-body.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-grid-head.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-grid-row.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-grid.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-head-cell.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-header.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-heading.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-month-select.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-month.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-months.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-nav.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-next-button.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-prev-button.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar-year-select.svelte create mode 100644 apps/frontend/src/lib/components/ui/range-calendar/range-calendar.svelte create mode 100644 apps/frontend/src/lib/components/ui/resizable/index.ts create mode 100644 apps/frontend/src/lib/components/ui/resizable/resizable-handle.svelte create mode 100644 apps/frontend/src/lib/components/ui/resizable/resizable-pane-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/scroll-area/index.ts create mode 100644 apps/frontend/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte create mode 100644 apps/frontend/src/lib/components/ui/scroll-area/scroll-area.svelte create mode 100644 apps/frontend/src/lib/components/ui/select/index.ts create mode 100644 apps/frontend/src/lib/components/ui/select/select-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/select/select-group-heading.svelte create mode 100644 apps/frontend/src/lib/components/ui/select/select-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/select/select-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/select/select-label.svelte create mode 100644 apps/frontend/src/lib/components/ui/select/select-portal.svelte create mode 100644 apps/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte create mode 100644 apps/frontend/src/lib/components/ui/select/select-scroll-up-button.svelte create mode 100644 apps/frontend/src/lib/components/ui/select/select-separator.svelte create mode 100644 apps/frontend/src/lib/components/ui/select/select-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/select/select.svelte create mode 100644 apps/frontend/src/lib/components/ui/separator/index.ts create mode 100644 apps/frontend/src/lib/components/ui/separator/separator.svelte create mode 100644 apps/frontend/src/lib/components/ui/sheet/index.ts create mode 100644 apps/frontend/src/lib/components/ui/sheet/sheet-close.svelte create mode 100644 apps/frontend/src/lib/components/ui/sheet/sheet-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/sheet/sheet-description.svelte create mode 100644 apps/frontend/src/lib/components/ui/sheet/sheet-footer.svelte create mode 100644 apps/frontend/src/lib/components/ui/sheet/sheet-header.svelte create mode 100644 apps/frontend/src/lib/components/ui/sheet/sheet-overlay.svelte create mode 100644 apps/frontend/src/lib/components/ui/sheet/sheet-portal.svelte create mode 100644 apps/frontend/src/lib/components/ui/sheet/sheet-title.svelte create mode 100644 apps/frontend/src/lib/components/ui/sheet/sheet-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/sheet/sheet.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/constants.ts create mode 100644 apps/frontend/src/lib/components/ui/sidebar/context.svelte.ts create mode 100644 apps/frontend/src/lib/components/ui/sidebar/index.ts create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-footer.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-group-action.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-group-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-group-label.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-header.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-input.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-inset.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-menu-action.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-menu-button.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-menu-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-menu.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-provider.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-rail.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-separator.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/sidebar/sidebar.svelte create mode 100644 apps/frontend/src/lib/components/ui/skeleton/index.ts create mode 100644 apps/frontend/src/lib/components/ui/skeleton/skeleton.svelte create mode 100644 apps/frontend/src/lib/components/ui/slider/index.ts create mode 100644 apps/frontend/src/lib/components/ui/slider/slider.svelte create mode 100644 apps/frontend/src/lib/components/ui/sonner/index.ts create mode 100644 apps/frontend/src/lib/components/ui/sonner/sonner.svelte create mode 100644 apps/frontend/src/lib/components/ui/spinner/index.ts create mode 100644 apps/frontend/src/lib/components/ui/spinner/spinner.svelte create mode 100644 apps/frontend/src/lib/components/ui/switch/index.ts create mode 100644 apps/frontend/src/lib/components/ui/switch/switch.svelte create mode 100644 apps/frontend/src/lib/components/ui/table/index.ts create mode 100644 apps/frontend/src/lib/components/ui/table/table-body.svelte create mode 100644 apps/frontend/src/lib/components/ui/table/table-caption.svelte create mode 100644 apps/frontend/src/lib/components/ui/table/table-cell.svelte create mode 100644 apps/frontend/src/lib/components/ui/table/table-footer.svelte create mode 100644 apps/frontend/src/lib/components/ui/table/table-head.svelte create mode 100644 apps/frontend/src/lib/components/ui/table/table-header.svelte create mode 100644 apps/frontend/src/lib/components/ui/table/table-row.svelte create mode 100644 apps/frontend/src/lib/components/ui/table/table.svelte create mode 100644 apps/frontend/src/lib/components/ui/tabs/index.ts create mode 100644 apps/frontend/src/lib/components/ui/tabs/tabs-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/tabs/tabs-list.svelte create mode 100644 apps/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/tabs/tabs.svelte create mode 100644 apps/frontend/src/lib/components/ui/textarea/index.ts create mode 100644 apps/frontend/src/lib/components/ui/textarea/textarea.svelte create mode 100644 apps/frontend/src/lib/components/ui/toggle-group/index.ts create mode 100644 apps/frontend/src/lib/components/ui/toggle-group/toggle-group-item.svelte create mode 100644 apps/frontend/src/lib/components/ui/toggle-group/toggle-group.svelte create mode 100644 apps/frontend/src/lib/components/ui/toggle/index.ts create mode 100644 apps/frontend/src/lib/components/ui/toggle/toggle.svelte create mode 100644 apps/frontend/src/lib/components/ui/tooltip/index.ts create mode 100644 apps/frontend/src/lib/components/ui/tooltip/tooltip-content.svelte create mode 100644 apps/frontend/src/lib/components/ui/tooltip/tooltip-portal.svelte create mode 100644 apps/frontend/src/lib/components/ui/tooltip/tooltip-provider.svelte create mode 100644 apps/frontend/src/lib/components/ui/tooltip/tooltip-trigger.svelte create mode 100644 apps/frontend/src/lib/components/ui/tooltip/tooltip.svelte create mode 100644 apps/frontend/src/lib/core/constants.ts create mode 100644 apps/frontend/src/lib/core/server.utils.ts create mode 100644 apps/frontend/src/lib/domains/link/data.ts create mode 100644 apps/frontend/src/lib/domains/link/link.remote.ts create mode 100644 apps/frontend/src/lib/domains/link/orchestrator.service.ts create mode 100644 apps/frontend/src/lib/domains/link/service.ts create mode 100644 apps/frontend/src/lib/domains/link/utils.ts create mode 100644 apps/frontend/src/lib/global.stores.ts create mode 100644 apps/frontend/src/lib/hooks/is-mobile.svelte.ts create mode 100644 apps/frontend/src/lib/make-client.ts create mode 100644 apps/frontend/src/lib/utils.ts create mode 100644 apps/frontend/src/routes/+layout.svelte create mode 100644 apps/frontend/src/routes/+page.svelte create mode 100644 apps/frontend/src/routes/layout.css create mode 100644 apps/frontend/static/fonts/manrope-variable.ttf create mode 100644 apps/frontend/static/images/avatar.png create mode 100644 apps/frontend/static/robots.txt create mode 100644 apps/frontend/svelte.config.js create mode 100644 apps/frontend/tsconfig.json create mode 100644 apps/frontend/vite.config.ts rename dockerfiles/{front.Dockerfile => frontend.Dockerfile} (67%) diff --git a/apps/front/old.server.ts b/apps/front/old.server.ts deleted file mode 100644 index cc5d571..0000000 --- a/apps/front/old.server.ts +++ /dev/null @@ -1,151 +0,0 @@ -// @ts-nocheck -/** - * Mobile Proxy Test — Bun Reverse Proxy Server - * - * Sits on port 3000 (what ngrok tunnels) and: - * GET / -> serves our clean mobile viewer (view.html) - * Everything else -> reverse-proxied to ws-scrcpy on port 8000 - * - * WebSocket upgrades are proxied transparently so the ws-scrcpy - * player running inside our iframe can talk to the scrcpy server. - */ - -const WS_SCRCPY_ORIGIN = "http://localhost:8000"; -const PROXY_PORT = 3000; - -const viewHtml = await Bun.file( - new URL("./view.html", import.meta.url).pathname, -).text(); - -const server = Bun.serve({ - port: PROXY_PORT, - hostname: "0.0.0.0", - - async fetch(req, server) { - const url = new URL(req.url); - - // Serve our clean mobile viewer at root only - // (the iframe loads ws-scrcpy's own page via /?scrcpy=1) - if ( - (url.pathname === "/" || url.pathname === "/index.html") && - !url.searchParams.has("scrcpy") - ) { - return new Response(viewHtml, { - headers: { "Content-Type": "text/html; charset=utf-8" }, - }); - } - - // Upgrade WebSocket connections — proxy to ws-scrcpy - if (req.headers.get("upgrade")?.toLowerCase() === "websocket") { - const targetUrl = `${WS_SCRCPY_ORIGIN.replace("http", "ws")}${url.pathname}${url.search}`; - - // Use Bun's WebSocket upgrade to establish a client-side WS to ws-scrcpy - // and bridge the two connections - const success = server.upgrade(req, { - data: { targetUrl }, - }); - - if (success) return undefined; - return new Response("WebSocket upgrade failed", { status: 400 }); - } - - // Proxy all other HTTP requests to ws-scrcpy - const targetUrl = `${WS_SCRCPY_ORIGIN}${url.pathname}${url.search}`; - - try { - const proxyRes = await fetch(targetUrl, { - method: req.method, - headers: req.headers, - body: - req.method !== "GET" && req.method !== "HEAD" - ? req.body - : undefined, - }); - - // Clone response with CORS headers stripped / adjusted - const headers = new Headers(proxyRes.headers); - headers.delete("content-encoding"); // Bun handles this - - return new Response(proxyRes.body, { - status: proxyRes.status, - statusText: proxyRes.statusText, - headers, - }); - } catch (err) { - console.error(`Proxy error for ${url.pathname}:`, err); - return new Response( - "ws-scrcpy not reachable — is it running on port 8000?", - { - status: 502, - }, - ); - } - }, - - websocket: { - async open(ws) { - const { targetUrl } = ws.data as { targetUrl: string }; - - // Open a WebSocket connection to ws-scrcpy - const upstream = new WebSocket(targetUrl); - - // Store upstream reference on ws data for cleanup - (ws.data as any).upstream = upstream; - - upstream.binaryType = "arraybuffer"; - - upstream.onopen = () => { - // Connection established, nothing extra needed - }; - - upstream.onmessage = (event) => { - try { - if (event.data instanceof ArrayBuffer) { - ws.sendBinary(new Uint8Array(event.data)); - } else { - ws.sendText(event.data); - } - } catch { - // Client disconnected - } - }; - - upstream.onclose = () => { - ws.close(); - }; - - upstream.onerror = (err) => { - console.error("Upstream WS error:", err); - ws.close(); - }; - }, - - message(ws, message) { - const upstream = (ws.data as any).upstream as WebSocket | undefined; - if (!upstream || upstream.readyState !== WebSocket.OPEN) return; - - try { - if (typeof message === "string") { - upstream.send(message); - } else { - upstream.send(message); - } - } catch { - // Upstream disconnected - } - }, - - close(ws) { - const upstream = (ws.data as any).upstream as WebSocket | undefined; - if (upstream && upstream.readyState === WebSocket.OPEN) { - upstream.close(); - } - }, - }, -}); - -console.log(`\n Mobile Proxy running on http://localhost:${server.port}`); -console.log(` Proxying to ws-scrcpy at ${WS_SCRCPY_ORIGIN}`); -console.log( - `\n Point ngrok at port ${server.port}, then open the ngrok URL on the user's phone.\n`, -); diff --git a/apps/front/package.json b/apps/front/package.json deleted file mode 100644 index be52692..0000000 --- a/apps/front/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@apps/front", - "type": "module", - "scripts": { - "dev": "PORT=3001 tsx watch src/index.ts", - "build": "tsc", - "prod": "HOST=0.0.0.0 PORT=3000 tsx src/index.ts" - }, - "dependencies": { - "@hono/node-server": "^1.19.9", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/auto-instrumentations-node": "^0.70.1", - "@opentelemetry/exporter-logs-otlp-proto": "^0.212.0", - "@opentelemetry/exporter-metrics-otlp-proto": "^0.212.0", - "@opentelemetry/exporter-trace-otlp-proto": "^0.212.0", - "@opentelemetry/sdk-logs": "^0.212.0", - "@opentelemetry/sdk-metrics": "^2.1.0", - "@opentelemetry/sdk-node": "^0.212.0", - "@pkg/db": "workspace:*", - "@pkg/logger": "workspace:*", - "@pkg/logic": "workspace:*", - "@pkg/result": "workspace:*", - "@pkg/settings": "workspace:*", - "hono": "^4.12.8", - "import-in-the-middle": "^3.0.0", - "valibot": "^1.2.0" - }, - "devDependencies": { - "@types/node": "^25.3.2", - "tsx": "^4.21.0", - "typescript": "^5.9.3" - } -} diff --git a/apps/front/src/core/utils.ts b/apps/front/src/core/utils.ts deleted file mode 100644 index 209b471..0000000 --- a/apps/front/src/core/utils.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { FlowExecCtx } from "@pkg/logic/core/flow.execution.context"; -import { ERROR_CODES, errorStatusMap, type Err } from "@pkg/result"; -import { randomUUID } from "node:crypto"; -import type { ContentfulStatusCode } from "hono/utils/http-status"; - -export function buildFlowExecCtx(): FlowExecCtx { - return { flowId: randomUUID() }; -} - -export function normalizeBaseUrl(url: string): string { - return url.endsWith("/") ? url.slice(0, -1) : url; -} - -export function createAppError( - fctx: FlowExecCtx, - code: string, - message: string, - description: string, - detail: string, - actionable?: boolean, -): Err { - return { - flowId: fctx.flowId, - code, - message, - description, - detail, - actionable, - }; -} - -export function jsonError(error: Err, status?: number) { - return { - body: { - data: null, - error: { - ...error, - }, - }, - status: - status || - errorStatusMap[error.code] || - errorStatusMap[ERROR_CODES.INTERNAL_SERVER_ERROR] || - 500, - }; -} - -export function toStatusCode(status: number): ContentfulStatusCode { - return status as ContentfulStatusCode; -} - -export function isErrPayload(value: unknown): value is Err { - return !!value && typeof value === "object" && "code" in value; -} diff --git a/apps/front/src/domains/links/router.ts b/apps/front/src/domains/links/router.ts deleted file mode 100644 index 91b6a3b..0000000 --- a/apps/front/src/domains/links/router.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Hono } from "hono"; -import { buildFlowExecCtx, toStatusCode } from "../../core/utils"; -import { prepareLink, resolveLink } from "./service"; - -export const linksRouter = new Hono() - .get("/links/:token/resolve", async (c) => { - const response = await resolveLink(buildFlowExecCtx(), c.req.param("token")); - return c.json(response.body, toStatusCode(response.status)); - }) - .post("/links/:token/prepare", async (c) => { - const response = await prepareLink(buildFlowExecCtx(), c.req.param("token")); - return c.json(response.body, toStatusCode(response.status)); - }); diff --git a/apps/front/src/domains/links/service.ts b/apps/front/src/domains/links/service.ts deleted file mode 100644 index edbb37d..0000000 --- a/apps/front/src/domains/links/service.ts +++ /dev/null @@ -1,297 +0,0 @@ -import { getLinkController } from "@pkg/logic/domains/link/controller"; -import { DeviceStatus } from "@pkg/logic/domains/device/data"; -import type { FlowExecCtx } from "@pkg/logic/core/flow.execution.context"; -import { formatErrorDetail, logDomainEvent } from "@pkg/logger"; -import { ERROR_CODES, type Err } from "@pkg/result"; -import { settings } from "@pkg/settings"; -import { - createAppError, - isErrPayload, - jsonError, - normalizeBaseUrl, -} from "../../core/utils"; - -const linkController = getLinkController(); - -type OrchestratorPreparePayload = { - deviceId: number; - packageName: string; - linkToken: string; -}; - -type OrchestratorPrepareResponse = { - data: { - deviceId: number; - containerId: string; - packageName: string; - serial: string; - status: string; - }; - error: null; -}; - -async function callOrchestratorPrepare( - fctx: FlowExecCtx, - payload: OrchestratorPreparePayload, -): Promise { - const response = await fetch( - `${normalizeBaseUrl(settings.orchestratorApiUrl)}/internal/sessions/prepare`, - { - method: "POST", - headers: { - "content-type": "application/json", - "x-internal-api-key": settings.internalApiKey, - "x-flow-id": fctx.flowId, - }, - body: JSON.stringify(payload), - }, - ); - - let body: unknown = null; - try { - body = await response.json(); - } catch (error) { - throw createAppError( - fctx, - ERROR_CODES.EXTERNAL_SERVICE_ERROR, - "Invalid orchestrator response", - "The orchestrator returned a response that could not be parsed", - formatErrorDetail(error), - ); - } - - if (!response.ok && body && typeof body === "object" && "error" in body) { - const payloadError = (body as { error?: unknown }).error; - if (isErrPayload(payloadError)) { - throw payloadError; - } - } - - if ( - !body || - typeof body !== "object" || - !("data" in body) || - ("error" in body && body.error) - ) { - throw createAppError( - fctx, - ERROR_CODES.EXTERNAL_SERVICE_ERROR, - "Unexpected orchestrator response", - "The orchestrator response was missing the expected data payload", - JSON.stringify(body), - ); - } - - return body as OrchestratorPrepareResponse; -} - -export async function resolveLink(fctx: FlowExecCtx, token: string) { - logDomainEvent({ - event: "front.link_resolve.started", - fctx, - meta: { token }, - }); - - const linkResult = await linkController.validate(fctx, token); - if (linkResult.isErr()) { - logDomainEvent({ - level: "warn", - event: "front.link_resolve.rejected", - fctx, - error: linkResult.error, - meta: { token }, - }); - - return jsonError(linkResult.error); - } - - const link = linkResult.value; - if (!link.device) { - return jsonError( - createAppError( - fctx, - ERROR_CODES.NOT_ALLOWED, - "Link is not assigned to a device", - "This link cannot start a session because no device is assigned", - `token=${token}`, - true, - ), - ); - } - - if (!link.supportedApp) { - return jsonError( - createAppError( - fctx, - ERROR_CODES.NOT_ALLOWED, - "Link is not assigned to an app", - "This link cannot start a session because no app is assigned", - `token=${token}`, - true, - ), - ); - } - - return { - body: { - data: { - link: { - id: link.id, - token: link.token, - status: link.status, - expiresAt: link.expiresAt, - }, - device: { - id: link.device.id, - title: link.device.title, - status: link.device.status, - inUse: link.device.inUse, - isAvailable: - link.device.status === DeviceStatus.ONLINE && !link.device.inUse, - }, - supportedApp: { - id: link.supportedApp.id, - title: link.supportedApp.title, - packageName: link.supportedApp.packageName, - }, - }, - error: null, - }, - status: 200, - }; -} - -export async function prepareLink(fctx: FlowExecCtx, token: string) { - logDomainEvent({ - event: "front.link_prepare.started", - fctx, - meta: { token }, - }); - - const linkResult = await linkController.validate(fctx, token); - if (linkResult.isErr()) { - logDomainEvent({ - level: "warn", - event: "front.link_prepare.rejected", - fctx, - error: linkResult.error, - meta: { token }, - }); - - return jsonError(linkResult.error); - } - - const link = linkResult.value; - if (!link.device) { - return jsonError( - createAppError( - fctx, - ERROR_CODES.NOT_ALLOWED, - "Link is not assigned to a device", - "This link cannot start a session because no device is assigned", - `token=${token}`, - true, - ), - ); - } - - if (!link.supportedApp) { - return jsonError( - createAppError( - fctx, - ERROR_CODES.NOT_ALLOWED, - "Link is not assigned to an app", - "This link cannot start a session because no app is assigned", - `token=${token}`, - true, - ), - ); - } - - if (link.device.status !== DeviceStatus.ONLINE || link.device.inUse) { - return jsonError( - createAppError( - fctx, - ERROR_CODES.NOT_ALLOWED, - "Device is not available", - "The assigned device is currently busy or offline", - `deviceId=${link.device.id} status=${link.device.status} inUse=${link.device.inUse}`, - true, - ), - ); - } - - try { - const orchestratorResponse = await callOrchestratorPrepare(fctx, { - deviceId: link.device.id, - packageName: link.supportedApp.packageName, - linkToken: link.token, - }); - - logDomainEvent({ - event: "front.link_prepare.succeeded", - fctx, - meta: { - token, - deviceId: link.device.id, - packageName: link.supportedApp.packageName, - }, - }); - - return { - body: { - data: { - link: { - id: link.id, - token: link.token, - }, - device: { - id: link.device.id, - title: link.device.title, - host: link.device.host, - wsPort: link.device.wsPort, - }, - supportedApp: { - id: link.supportedApp.id, - title: link.supportedApp.title, - packageName: link.supportedApp.packageName, - }, - session: orchestratorResponse.data, - }, - error: null, - }, - status: 200, - }; - } catch (error) { - const err: Err = isErrPayload(error) - ? error - : createAppError( - fctx, - ERROR_CODES.EXTERNAL_SERVICE_ERROR, - "Failed to prepare session", - "The front server could not prepare the assigned Android session", - formatErrorDetail(error), - ); - - logDomainEvent({ - level: "error", - event: "front.link_prepare.failed", - fctx, - error: err, - meta: { - token, - orchestratorUrl: settings.orchestratorApiUrl, - }, - }); - - return jsonError( - err.flowId - ? err - : { - ...err, - flowId: fctx.flowId, - }, - err.code === ERROR_CODES.EXTERNAL_SERVICE_ERROR ? 502 : undefined, - ); - } -} diff --git a/apps/front/src/index.ts b/apps/front/src/index.ts deleted file mode 100644 index b766601..0000000 --- a/apps/front/src/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import "./instrumentation.js"; - -import { createHttpTelemetryMiddleware } from "@pkg/logic/core/http.telemetry"; -import { serve } from "@hono/node-server"; -import { Hono } from "hono"; -import { linksRouter } from "./domains/links/router"; - -const app = new Hono().use("*", createHttpTelemetryMiddleware("front")); - -const host = process.env.HOST || "0.0.0.0"; -const port = Number(process.env.PORT || "3000"); - -app.get("/health", (c) => { - return c.json({ ok: true }); -}); - -app.get("/ping", (c) => { - return c.text("pong"); -}); - -app.route("/", linksRouter); - -serve( - { - fetch: app.fetch, - port, - hostname: host, - }, - (info) => { - console.log(`Server is running on http://${host}:${info.port}`); - }, -); diff --git a/apps/front/tsconfig.json b/apps/front/tsconfig.json deleted file mode 100644 index fce7dfe..0000000 --- a/apps/front/tsconfig.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "verbatimModuleSyntax": false, - "skipLibCheck": true, - "types": [ - "node" - ], - "baseUrl": ".", - "paths": { - "@pkg/logic": ["../../packages/logic"], - "@pkg/logic/*": ["../../packages/logic/*"], - "@pkg/db": ["../../packages/db"], - "@pkg/db/*": ["../../packages/db/*"], - "@pkg/logger": ["../../packages/logger"], - "@pkg/logger/*": ["../../packages/logger/*"], - "@pkg/result": ["../../packages/result"], - "@pkg/result/*": ["../../packages/result/*"], - "@pkg/settings": ["../../packages/settings"], - "@pkg/settings/*": ["../../packages/settings/*"], - "@/*": ["../../packages/logic/*"], - "@core/*": ["../../packages/logic/core/*"], - "@domains/*": ["../../packages/logic/domains/*"] - }, - "jsx": "react-jsx", - "jsxImportSource": "hono/jsx", - "outDir": "./dist" - }, - "exclude": ["node_modules"] -} diff --git a/apps/front/view.html b/apps/front/view.html deleted file mode 100644 index 03e7f6c..0000000 --- a/apps/front/view.html +++ /dev/null @@ -1,290 +0,0 @@ - - - - - - - - - - Loading... - - - -
-
- Connecting... -
- - - - - - diff --git a/apps/frontend/.gitignore b/apps/frontend/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/apps/frontend/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/apps/frontend/.npmrc b/apps/frontend/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/apps/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/apps/frontend/.prettierignore b/apps/frontend/.prettierignore new file mode 100644 index 0000000..7d74fe2 --- /dev/null +++ b/apps/frontend/.prettierignore @@ -0,0 +1,9 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb + +# Miscellaneous +/static/ diff --git a/apps/frontend/components.json b/apps/frontend/components.json new file mode 100644 index 0000000..1e6a638 --- /dev/null +++ b/apps/frontend/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src/routes/layout.css", + "baseColor": "neutral" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry" +} diff --git a/apps/frontend/package.json b/apps/frontend/package.json new file mode 100644 index 0000000..b28c10f --- /dev/null +++ b/apps/frontend/package.json @@ -0,0 +1,74 @@ +{ + "name": "@apps/frontend", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev --port 5173", + "build": "NODE_ENV=build vite build", + "prod": "HOST=0.0.0.0 PORT=3000 node ./build/index.js", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check .", + "test:unit": "vitest" + }, + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/auto-instrumentations-node": "^0.70.1", + "@opentelemetry/exporter-logs-otlp-proto": "^0.212.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.212.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.212.0", + "@opentelemetry/sdk-logs": "^0.212.0", + "@opentelemetry/sdk-node": "^0.212.0", + "@pkg/db": "workspace:*", + "@pkg/keystore": "workspace:*", + "@pkg/logger": "workspace:*", + "@pkg/logic": "workspace:*", + "@pkg/result": "workspace:*", + "@pkg/settings": "workspace:*", + "date-fns": "^4.1.0", + "import-in-the-middle": "^3.0.0", + "nanoid": "^5.1.6", + "neverthrow": "^8.2.0", + "valibot": "^1.2.0" + }, + "devDependencies": { + "@iconify/json": "^2.2.434", + "@internationalized/date": "^3.10.0", + "@lucide/svelte": "^0.561.0", + "@sveltejs/adapter-node": "^5.5.4", + "@sveltejs/kit": "^2.53.4", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/table-core": "^8.21.3", + "@types/qrcode": "^1.5.6", + "bits-ui": "^2.14.4", + "clsx": "^2.1.1", + "embla-carousel-svelte": "^8.6.0", + "formsnap": "^2.0.1", + "layerchart": "2.0.0-next.43", + "mode-watcher": "^1.1.0", + "paneforge": "^1.0.2", + "prettier": "^3.7.4", + "prettier-plugin-svelte": "^3.4.0", + "prettier-plugin-tailwindcss": "^0.7.2", + "svelte": "^5.53.6", + "svelte-check": "^4.4.4", + "svelte-sonner": "^1.0.7", + "sveltekit-superforms": "^2.30.0", + "tailwind-merge": "^3.4.0", + "tailwind-variants": "^3.2.2", + "tailwindcss": "^4.1.18", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.3", + "unplugin-icons": "^23.0.1", + "vaul-svelte": "^1.0.0-next.7", + "vite": "^7.2.6", + "vitest": "^4.0.15" + } +} diff --git a/apps/frontend/src/app.d.ts b/apps/frontend/src/app.d.ts new file mode 100644 index 0000000..7ded0fe --- /dev/null +++ b/apps/frontend/src/app.d.ts @@ -0,0 +1,20 @@ +import type { Session, User } from "@pkg/logic/domains/user/data"; +import "unplugin-icons/types/svelte"; + +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + interface Locals { + flowId?: string; + session?: Session; + user?: User; + } + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/apps/frontend/src/app.html b/apps/frontend/src/app.html new file mode 100644 index 0000000..f273cc5 --- /dev/null +++ b/apps/frontend/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/apps/frontend/src/demo.spec.ts b/apps/frontend/src/demo.spec.ts new file mode 100644 index 0000000..e07cbbd --- /dev/null +++ b/apps/frontend/src/demo.spec.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('sum test', () => { + it('adds 1 + 2 to equal 3', () => { + expect(1 + 2).toBe(3); + }); +}); diff --git a/apps/frontend/src/hooks.server.ts b/apps/frontend/src/hooks.server.ts new file mode 100644 index 0000000..d18086b --- /dev/null +++ b/apps/frontend/src/hooks.server.ts @@ -0,0 +1,15 @@ +import type { Handle, HandleServerError } from "@sveltejs/kit"; + +export const handleError: HandleServerError = async ({ error, event }) => { + console.log("[-] Running error middleware for : ", event.url.pathname); + console.log(error); + return { message: (error as Error).message ?? "Internal Server Error" }; +}; + +export const middleware: Handle = async ({ event, resolve }) => { + event.locals.flowId ||= crypto.randomUUID(); + console.log("[+] Running middleware for : ", event.url.pathname); + return resolve(event); +}; + +export const handle = middleware; diff --git a/apps/front/src/instrumentation.ts b/apps/frontend/src/instrumentation.server.ts similarity index 57% rename from apps/front/src/instrumentation.ts rename to apps/frontend/src/instrumentation.server.ts index 6b3ec48..4184ea0 100644 --- a/apps/front/src/instrumentation.ts +++ b/apps/frontend/src/instrumentation.server.ts @@ -1,8 +1,6 @@ import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; -import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto"; -import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; -import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; import { createAddHookMessageChannel } from "import-in-the-middle"; import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs"; import { NodeSDK } from "@opentelemetry/sdk-node"; @@ -12,23 +10,9 @@ import { register } from "node:module"; const { registerOptions } = createAddHookMessageChannel(); register("import-in-the-middle/hook.mjs", import.meta.url, registerOptions); -const normalizedEndpoint = settings.otelExporterOtlpHttpEndpoint.startsWith( - "http", -) - ? settings.otelExporterOtlpHttpEndpoint - : `http://${settings.otelExporterOtlpHttpEndpoint}`; - -if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) { - process.env.OTEL_EXPORTER_OTLP_ENDPOINT = normalizedEndpoint; -} - const sdk = new NodeSDK({ - serviceName: `${settings.otelServiceName}-processor`, + serviceName: settings.otelServiceName || settings.appName, traceExporter: new OTLPTraceExporter(), - metricReader: new PeriodicExportingMetricReader({ - exporter: new OTLPMetricExporter(), - exportIntervalMillis: 10_000, - }), logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter())], instrumentations: [ getNodeAutoInstrumentations({ @@ -41,10 +25,3 @@ const sdk = new NodeSDK({ }); sdk.start(); - -const shutdown = () => { - void sdk.shutdown(); -}; - -process.on("SIGTERM", shutdown); -process.on("SIGINT", shutdown); diff --git a/apps/frontend/src/lib/components/app-sidebar.svelte b/apps/frontend/src/lib/components/app-sidebar.svelte new file mode 100644 index 0000000..f0ed540 --- /dev/null +++ b/apps/frontend/src/lib/components/app-sidebar.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/apps/frontend/src/lib/components/atoms/button-text.svelte b/apps/frontend/src/lib/components/atoms/button-text.svelte new file mode 100644 index 0000000..207d697 --- /dev/null +++ b/apps/frontend/src/lib/components/atoms/button-text.svelte @@ -0,0 +1,17 @@ + + +{#if loading} + + {loadingText} +{:else} + {text} +{/if} diff --git a/apps/frontend/src/lib/components/atoms/icon.svelte b/apps/frontend/src/lib/components/atoms/icon.svelte new file mode 100644 index 0000000..794c944 --- /dev/null +++ b/apps/frontend/src/lib/components/atoms/icon.svelte @@ -0,0 +1,11 @@ + + + diff --git a/apps/frontend/src/lib/components/atoms/title.svelte b/apps/frontend/src/lib/components/atoms/title.svelte new file mode 100644 index 0000000..ed17511 --- /dev/null +++ b/apps/frontend/src/lib/components/atoms/title.svelte @@ -0,0 +1,61 @@ + + + + {@render children?.()} + diff --git a/apps/frontend/src/lib/components/molecules/max-width-wrapper.svelte b/apps/frontend/src/lib/components/molecules/max-width-wrapper.svelte new file mode 100644 index 0000000..e3ec428 --- /dev/null +++ b/apps/frontend/src/lib/components/molecules/max-width-wrapper.svelte @@ -0,0 +1,11 @@ + + +
+ {@render children()} +
diff --git a/apps/frontend/src/lib/components/nav-main.svelte b/apps/frontend/src/lib/components/nav-main.svelte new file mode 100644 index 0000000..5968981 --- /dev/null +++ b/apps/frontend/src/lib/components/nav-main.svelte @@ -0,0 +1,81 @@ + + + + + {#each items as mainItem (mainItem.title)} + + + +
+ + + {#snippet child({ props })} + + {#if mainItem.icon} + + {/if} + {mainItem.title} + + {/snippet} + + + + {#if mainItem.items && sidebar.open} + + {#snippet child({ props })} +
+ + Toggle {mainItem.title} submenu +
+ {/snippet} +
+ {/if} +
+ + + + {#if mainItem.items} + + {#each mainItem.items as subItem (subItem.title)} + + + {#snippet child({ props })} + + {subItem.title} + + {/snippet} + + + {/each} + + {/if} + +
+
+ {/each} +
+
diff --git a/apps/frontend/src/lib/components/nav-user.svelte b/apps/frontend/src/lib/components/nav-user.svelte new file mode 100644 index 0000000..0a9e96a --- /dev/null +++ b/apps/frontend/src/lib/components/nav-user.svelte @@ -0,0 +1,150 @@ + + + + + + + {#snippet child({ props })} + + + + + {user.name.slice(0, 2).toUpperCase()} + + +
+ {user.name} + {user.email} +
+ +
+ {/snippet} +
+ + +
+ + + + {user.name.slice(0, 2).toUpperCase()} + + +
+ {user.name} + {user.email} +
+
+
+ + + + {#each secondaryNavTree as item (item.title)} + + + + {item.title} + + + {/each} + + + + + + + + + + Theme + + + setMode("light")}> + + Light + + setMode("dark")}> + + Dark + + resetMode()}> + + System + + + + + + logoutUser()}> + + Log out + +
+
+
+
diff --git a/apps/frontend/src/lib/components/team-switcher.svelte b/apps/frontend/src/lib/components/team-switcher.svelte new file mode 100644 index 0000000..4981212 --- /dev/null +++ b/apps/frontend/src/lib/components/team-switcher.svelte @@ -0,0 +1,80 @@ + + + + + + + {#snippet child({ props })} + +
+ +
+
+ + {activeTeam.name} + + {activeTeam.plan} +
+ +
+ {/snippet} +
+ + Teams + {#each teams as team, index (team.name)} + (activeTeam = team)} + class="gap-2 p-2" + > +
+ +
+ {team.name} + > +
+ {/each} + + +
+ +
+
+ Coming soon +
+
+
+
+
+
diff --git a/apps/frontend/src/lib/components/ui/accordion/accordion-content.svelte b/apps/frontend/src/lib/components/ui/accordion/accordion-content.svelte new file mode 100644 index 0000000..559db3d --- /dev/null +++ b/apps/frontend/src/lib/components/ui/accordion/accordion-content.svelte @@ -0,0 +1,22 @@ + + + +
+ {@render children?.()} +
+
diff --git a/apps/frontend/src/lib/components/ui/accordion/accordion-item.svelte b/apps/frontend/src/lib/components/ui/accordion/accordion-item.svelte new file mode 100644 index 0000000..780545c --- /dev/null +++ b/apps/frontend/src/lib/components/ui/accordion/accordion-item.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/accordion/accordion-trigger.svelte b/apps/frontend/src/lib/components/ui/accordion/accordion-trigger.svelte new file mode 100644 index 0000000..c46c246 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/accordion/accordion-trigger.svelte @@ -0,0 +1,32 @@ + + + + svg]:rotate-180", + className + )} + {...restProps} + > + {@render children?.()} + + + diff --git a/apps/frontend/src/lib/components/ui/accordion/accordion.svelte b/apps/frontend/src/lib/components/ui/accordion/accordion.svelte new file mode 100644 index 0000000..117ee37 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/accordion/accordion.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/accordion/index.ts b/apps/frontend/src/lib/components/ui/accordion/index.ts new file mode 100644 index 0000000..ac343a1 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/accordion/index.ts @@ -0,0 +1,16 @@ +import Root from "./accordion.svelte"; +import Content from "./accordion-content.svelte"; +import Item from "./accordion-item.svelte"; +import Trigger from "./accordion-trigger.svelte"; + +export { + Root, + Content, + Item, + Trigger, + // + Root as Accordion, + Content as AccordionContent, + Item as AccordionItem, + Trigger as AccordionTrigger, +}; diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte new file mode 100644 index 0000000..a005691 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte @@ -0,0 +1,18 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte new file mode 100644 index 0000000..a7b0cf7 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte @@ -0,0 +1,18 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte new file mode 100644 index 0000000..00bdd9c --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte @@ -0,0 +1,29 @@ + + + + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte new file mode 100644 index 0000000..2ec67dc --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte new file mode 100644 index 0000000..f78b97a --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte new file mode 100644 index 0000000..1835d91 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte new file mode 100644 index 0000000..a64ee76 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte new file mode 100644 index 0000000..f0a19a8 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte new file mode 100644 index 0000000..7ef2b5f --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte new file mode 100644 index 0000000..b22d1d5 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog.svelte b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog.svelte new file mode 100644 index 0000000..7ea78bb --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/alert-dialog.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/alert-dialog/index.ts b/apps/frontend/src/lib/components/ui/alert-dialog/index.ts new file mode 100644 index 0000000..269538e --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert-dialog/index.ts @@ -0,0 +1,37 @@ +import Root from "./alert-dialog.svelte"; +import Portal from "./alert-dialog-portal.svelte"; +import Trigger from "./alert-dialog-trigger.svelte"; +import Title from "./alert-dialog-title.svelte"; +import Action from "./alert-dialog-action.svelte"; +import Cancel from "./alert-dialog-cancel.svelte"; +import Footer from "./alert-dialog-footer.svelte"; +import Header from "./alert-dialog-header.svelte"; +import Overlay from "./alert-dialog-overlay.svelte"; +import Content from "./alert-dialog-content.svelte"; +import Description from "./alert-dialog-description.svelte"; + +export { + Root, + Title, + Action, + Cancel, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + // + Root as AlertDialog, + Title as AlertDialogTitle, + Action as AlertDialogAction, + Cancel as AlertDialogCancel, + Portal as AlertDialogPortal, + Footer as AlertDialogFooter, + Header as AlertDialogHeader, + Trigger as AlertDialogTrigger, + Overlay as AlertDialogOverlay, + Content as AlertDialogContent, + Description as AlertDialogDescription, +}; diff --git a/apps/frontend/src/lib/components/ui/alert/alert-description.svelte b/apps/frontend/src/lib/components/ui/alert/alert-description.svelte new file mode 100644 index 0000000..8b56aed --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert/alert-description.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/apps/frontend/src/lib/components/ui/alert/alert-title.svelte b/apps/frontend/src/lib/components/ui/alert/alert-title.svelte new file mode 100644 index 0000000..77e45ad --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert/alert-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/frontend/src/lib/components/ui/alert/alert.svelte b/apps/frontend/src/lib/components/ui/alert/alert.svelte new file mode 100644 index 0000000..2b2eff9 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert/alert.svelte @@ -0,0 +1,44 @@ + + + + + diff --git a/apps/frontend/src/lib/components/ui/alert/index.ts b/apps/frontend/src/lib/components/ui/alert/index.ts new file mode 100644 index 0000000..97e21b4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/alert/index.ts @@ -0,0 +1,14 @@ +import Root from "./alert.svelte"; +import Description from "./alert-description.svelte"; +import Title from "./alert-title.svelte"; +export { alertVariants, type AlertVariant } from "./alert.svelte"; + +export { + Root, + Description, + Title, + // + Root as Alert, + Description as AlertDescription, + Title as AlertTitle, +}; diff --git a/apps/frontend/src/lib/components/ui/aspect-ratio/aspect-ratio.svelte b/apps/frontend/src/lib/components/ui/aspect-ratio/aspect-ratio.svelte new file mode 100644 index 0000000..815aab0 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/aspect-ratio/aspect-ratio.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/aspect-ratio/index.ts b/apps/frontend/src/lib/components/ui/aspect-ratio/index.ts new file mode 100644 index 0000000..985c75f --- /dev/null +++ b/apps/frontend/src/lib/components/ui/aspect-ratio/index.ts @@ -0,0 +1,3 @@ +import Root from "./aspect-ratio.svelte"; + +export { Root, Root as AspectRatio }; diff --git a/apps/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte b/apps/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte new file mode 100644 index 0000000..249d4a4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/avatar/avatar-image.svelte b/apps/frontend/src/lib/components/ui/avatar/avatar-image.svelte new file mode 100644 index 0000000..2bb9db4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/avatar/avatar-image.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/avatar/avatar.svelte b/apps/frontend/src/lib/components/ui/avatar/avatar.svelte new file mode 100644 index 0000000..e37214d --- /dev/null +++ b/apps/frontend/src/lib/components/ui/avatar/avatar.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/avatar/index.ts b/apps/frontend/src/lib/components/ui/avatar/index.ts new file mode 100644 index 0000000..d06457b --- /dev/null +++ b/apps/frontend/src/lib/components/ui/avatar/index.ts @@ -0,0 +1,13 @@ +import Root from "./avatar.svelte"; +import Image from "./avatar-image.svelte"; +import Fallback from "./avatar-fallback.svelte"; + +export { + Root, + Image, + Fallback, + // + Root as Avatar, + Image as AvatarImage, + Fallback as AvatarFallback, +}; diff --git a/apps/frontend/src/lib/components/ui/badge/badge.svelte b/apps/frontend/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 0000000..e3164ba --- /dev/null +++ b/apps/frontend/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,50 @@ + + + + + + {@render children?.()} + diff --git a/apps/frontend/src/lib/components/ui/badge/index.ts b/apps/frontend/src/lib/components/ui/badge/index.ts new file mode 100644 index 0000000..64e0aa9 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from "./badge.svelte"; +export { badgeVariants, type BadgeVariant } from "./badge.svelte"; diff --git a/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte new file mode 100644 index 0000000..a178cf5 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte @@ -0,0 +1,23 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte new file mode 100644 index 0000000..1a84c4c --- /dev/null +++ b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte @@ -0,0 +1,20 @@ + + +
  • + {@render children?.()} +
  • diff --git a/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte new file mode 100644 index 0000000..e6bc17d --- /dev/null +++ b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte @@ -0,0 +1,31 @@ + + +{#if child} + {@render child({ props: attrs })} +{:else} + + {@render children?.()} + +{/if} diff --git a/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte new file mode 100644 index 0000000..1272a37 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte @@ -0,0 +1,23 @@ + + +
      + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte new file mode 100644 index 0000000..5fb6979 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte new file mode 100644 index 0000000..84106a1 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte @@ -0,0 +1,27 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb.svelte b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb.svelte new file mode 100644 index 0000000..8f8a3e6 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/breadcrumb/breadcrumb.svelte @@ -0,0 +1,21 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/breadcrumb/index.ts b/apps/frontend/src/lib/components/ui/breadcrumb/index.ts new file mode 100644 index 0000000..dc914ec --- /dev/null +++ b/apps/frontend/src/lib/components/ui/breadcrumb/index.ts @@ -0,0 +1,25 @@ +import Root from "./breadcrumb.svelte"; +import Ellipsis from "./breadcrumb-ellipsis.svelte"; +import Item from "./breadcrumb-item.svelte"; +import Separator from "./breadcrumb-separator.svelte"; +import Link from "./breadcrumb-link.svelte"; +import List from "./breadcrumb-list.svelte"; +import Page from "./breadcrumb-page.svelte"; + +export { + Root, + Ellipsis, + Item, + Separator, + Link, + List, + Page, + // + Root as Breadcrumb, + Ellipsis as BreadcrumbEllipsis, + Item as BreadcrumbItem, + Separator as BreadcrumbSeparator, + Link as BreadcrumbLink, + List as BreadcrumbList, + Page as BreadcrumbPage, +}; diff --git a/apps/frontend/src/lib/components/ui/button-group/button-group-separator.svelte b/apps/frontend/src/lib/components/ui/button-group/button-group-separator.svelte new file mode 100644 index 0000000..86ff8ae --- /dev/null +++ b/apps/frontend/src/lib/components/ui/button-group/button-group-separator.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/button-group/button-group-text.svelte b/apps/frontend/src/lib/components/ui/button-group/button-group-text.svelte new file mode 100644 index 0000000..1be72bb --- /dev/null +++ b/apps/frontend/src/lib/components/ui/button-group/button-group-text.svelte @@ -0,0 +1,30 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
    + {@render mergedProps.children?.()} +
    +{/if} diff --git a/apps/frontend/src/lib/components/ui/button-group/button-group.svelte b/apps/frontend/src/lib/components/ui/button-group/button-group.svelte new file mode 100644 index 0000000..34c8d79 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/button-group/button-group.svelte @@ -0,0 +1,46 @@ + + + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/button-group/index.ts b/apps/frontend/src/lib/components/ui/button-group/index.ts new file mode 100644 index 0000000..7f706e3 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/button-group/index.ts @@ -0,0 +1,13 @@ +import Root from "./button-group.svelte"; +import Text from "./button-group-text.svelte"; +import Separator from "./button-group-separator.svelte"; + +export { + Root, + Text, + Separator, + // + Root as ButtonGroup, + Text as ButtonGroupText, + Separator as ButtonGroupSeparator, +}; diff --git a/apps/frontend/src/lib/components/ui/button/button.svelte b/apps/frontend/src/lib/components/ui/button/button.svelte new file mode 100644 index 0000000..a8296ae --- /dev/null +++ b/apps/frontend/src/lib/components/ui/button/button.svelte @@ -0,0 +1,82 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/apps/frontend/src/lib/components/ui/button/index.ts b/apps/frontend/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000..fb585d7 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/button/index.ts @@ -0,0 +1,17 @@ +import Root, { + type ButtonProps, + type ButtonSize, + type ButtonVariant, + buttonVariants, +} from "./button.svelte"; + +export { + Root, + type ButtonProps as Props, + // + Root as Button, + buttonVariants, + type ButtonProps, + type ButtonSize, + type ButtonVariant, +}; diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-caption.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-caption.svelte new file mode 100644 index 0000000..5c93037 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-caption.svelte @@ -0,0 +1,76 @@ + + +{#snippet MonthSelect()} + { + if (!placeholder) return; + const v = Number.parseInt(e.currentTarget.value); + const newPlaceholder = placeholder.set({ month: v }); + placeholder = newPlaceholder.subtract({ months: monthIndex }); + }} + /> +{/snippet} + +{#snippet YearSelect()} + +{/snippet} + +{#if captionLayout === "dropdown"} + {@render MonthSelect()} + {@render YearSelect()} +{:else if captionLayout === "dropdown-months"} + {@render MonthSelect()} + {#if placeholder} + {formatYear(placeholder)} + {/if} +{:else if captionLayout === "dropdown-years"} + {#if placeholder} + {formatMonth(placeholder)} + {/if} + {@render YearSelect()} +{:else} + {formatMonth(month)} {formatYear(month)} +{/if} diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-cell.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-cell.svelte new file mode 100644 index 0000000..4cdb548 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-cell.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-day.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-day.svelte new file mode 100644 index 0000000..19d7bde --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-day.svelte @@ -0,0 +1,35 @@ + + +span]:text-xs [&>span]:opacity-70", + className + )} + {...restProps} +/> diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-grid-body.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-grid-body.svelte new file mode 100644 index 0000000..8cd86de --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-grid-body.svelte @@ -0,0 +1,12 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-grid-head.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-grid-head.svelte new file mode 100644 index 0000000..333edc4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-grid-head.svelte @@ -0,0 +1,12 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-grid-row.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-grid-row.svelte new file mode 100644 index 0000000..9032236 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-grid-row.svelte @@ -0,0 +1,12 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-grid.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-grid.svelte new file mode 100644 index 0000000..e0c8627 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-grid.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-head-cell.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-head-cell.svelte new file mode 100644 index 0000000..131807e --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-head-cell.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-header.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-header.svelte new file mode 100644 index 0000000..c39e955 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-header.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-heading.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-heading.svelte new file mode 100644 index 0000000..a9b9810 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-heading.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-month-select.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-month-select.svelte new file mode 100644 index 0000000..8d88deb --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-month-select.svelte @@ -0,0 +1,44 @@ + + + + + {#snippet child({ props, monthItems, selectedMonthItem })} + + + {/snippet} + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-month.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-month.svelte new file mode 100644 index 0000000..e747fae --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-month.svelte @@ -0,0 +1,15 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-months.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-months.svelte new file mode 100644 index 0000000..f717a9d --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-months.svelte @@ -0,0 +1,19 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-nav.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-nav.svelte new file mode 100644 index 0000000..27f33d7 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-nav.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-next-button.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-next-button.svelte new file mode 100644 index 0000000..5c5a78d --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-next-button.svelte @@ -0,0 +1,31 @@ + + +{#snippet Fallback()} + +{/snippet} + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-prev-button.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-prev-button.svelte new file mode 100644 index 0000000..33cfd63 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-prev-button.svelte @@ -0,0 +1,31 @@ + + +{#snippet Fallback()} + +{/snippet} + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar-year-select.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar-year-select.svelte new file mode 100644 index 0000000..226efdf --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar-year-select.svelte @@ -0,0 +1,43 @@ + + + + + {#snippet child({ props, yearItems, selectedYearItem })} + + + {/snippet} + + diff --git a/apps/frontend/src/lib/components/ui/calendar/calendar.svelte b/apps/frontend/src/lib/components/ui/calendar/calendar.svelte new file mode 100644 index 0000000..29b6fff --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/calendar.svelte @@ -0,0 +1,115 @@ + + + + + {#snippet children({ months, weekdays })} + + + + + + {#each months as month, monthIndex (month)} + + + + + + + + {#each weekdays as weekday (weekday)} + + {weekday.slice(0, 2)} + + {/each} + + + + {#each month.weeks as weekDates (weekDates)} + + {#each weekDates as date (date)} + + {#if day} + {@render day({ + day: date, + outsideMonth: !isEqualMonth(date, month.value), + })} + {:else} + + {/if} + + {/each} + + {/each} + + + + {/each} + + {/snippet} + diff --git a/apps/frontend/src/lib/components/ui/calendar/index.ts b/apps/frontend/src/lib/components/ui/calendar/index.ts new file mode 100644 index 0000000..f3a16d2 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/calendar/index.ts @@ -0,0 +1,40 @@ +import Root from "./calendar.svelte"; +import Cell from "./calendar-cell.svelte"; +import Day from "./calendar-day.svelte"; +import Grid from "./calendar-grid.svelte"; +import Header from "./calendar-header.svelte"; +import Months from "./calendar-months.svelte"; +import GridRow from "./calendar-grid-row.svelte"; +import Heading from "./calendar-heading.svelte"; +import GridBody from "./calendar-grid-body.svelte"; +import GridHead from "./calendar-grid-head.svelte"; +import HeadCell from "./calendar-head-cell.svelte"; +import NextButton from "./calendar-next-button.svelte"; +import PrevButton from "./calendar-prev-button.svelte"; +import MonthSelect from "./calendar-month-select.svelte"; +import YearSelect from "./calendar-year-select.svelte"; +import Month from "./calendar-month.svelte"; +import Nav from "./calendar-nav.svelte"; +import Caption from "./calendar-caption.svelte"; + +export { + Day, + Cell, + Grid, + Header, + Months, + GridRow, + Heading, + GridBody, + GridHead, + HeadCell, + NextButton, + PrevButton, + Nav, + Month, + YearSelect, + MonthSelect, + Caption, + // + Root as Calendar, +}; diff --git a/apps/frontend/src/lib/components/ui/card/card-action.svelte b/apps/frontend/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 0000000..cc36c56 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/card/card-content.svelte b/apps/frontend/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 0000000..bc90b83 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,15 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/card/card-description.svelte b/apps/frontend/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 0000000..9b20ac7 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

    + {@render children?.()} +

    diff --git a/apps/frontend/src/lib/components/ui/card/card-footer.svelte b/apps/frontend/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 0000000..2d4d0f2 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/card/card-header.svelte b/apps/frontend/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 0000000..2501788 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/card/card-title.svelte b/apps/frontend/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 0000000..7447231 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/card/card.svelte b/apps/frontend/src/lib/components/ui/card/card.svelte new file mode 100644 index 0000000..99448cc --- /dev/null +++ b/apps/frontend/src/lib/components/ui/card/card.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/card/index.ts b/apps/frontend/src/lib/components/ui/card/index.ts new file mode 100644 index 0000000..4d3fce4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/card/index.ts @@ -0,0 +1,25 @@ +import Root from "./card.svelte"; +import Content from "./card-content.svelte"; +import Description from "./card-description.svelte"; +import Footer from "./card-footer.svelte"; +import Header from "./card-header.svelte"; +import Title from "./card-title.svelte"; +import Action from "./card-action.svelte"; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + Action, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, + Action as CardAction, +}; diff --git a/apps/frontend/src/lib/components/ui/carousel/carousel-content.svelte b/apps/frontend/src/lib/components/ui/carousel/carousel-content.svelte new file mode 100644 index 0000000..84c71f8 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/carousel/carousel-content.svelte @@ -0,0 +1,43 @@ + + +
    +
    + {@render children?.()} +
    +
    diff --git a/apps/frontend/src/lib/components/ui/carousel/carousel-item.svelte b/apps/frontend/src/lib/components/ui/carousel/carousel-item.svelte new file mode 100644 index 0000000..ebf1649 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/carousel/carousel-item.svelte @@ -0,0 +1,30 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/carousel/carousel-next.svelte b/apps/frontend/src/lib/components/ui/carousel/carousel-next.svelte new file mode 100644 index 0000000..1aaa1f4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/carousel/carousel-next.svelte @@ -0,0 +1,38 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/carousel/carousel-previous.svelte b/apps/frontend/src/lib/components/ui/carousel/carousel-previous.svelte new file mode 100644 index 0000000..dafe4fd --- /dev/null +++ b/apps/frontend/src/lib/components/ui/carousel/carousel-previous.svelte @@ -0,0 +1,38 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/carousel/carousel.svelte b/apps/frontend/src/lib/components/ui/carousel/carousel.svelte new file mode 100644 index 0000000..0492805 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/carousel/carousel.svelte @@ -0,0 +1,93 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/carousel/context.ts b/apps/frontend/src/lib/components/ui/carousel/context.ts new file mode 100644 index 0000000..a5fd74f --- /dev/null +++ b/apps/frontend/src/lib/components/ui/carousel/context.ts @@ -0,0 +1,58 @@ +import type { WithElementRef } from "$lib/utils.js"; +import type { + EmblaCarouselSvelteType, + default as emblaCarouselSvelte, +} from "embla-carousel-svelte"; +import { getContext, hasContext, setContext } from "svelte"; +import type { HTMLAttributes } from "svelte/elements"; + +export type CarouselAPI = + NonNullable["on:emblaInit"]> extends ( + evt: CustomEvent + ) => void + ? CarouselAPI + : never; + +type EmblaCarouselConfig = NonNullable[1]>; + +export type CarouselOptions = EmblaCarouselConfig["options"]; +export type CarouselPlugins = EmblaCarouselConfig["plugins"]; + +//// + +export type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugins; + setApi?: (api: CarouselAPI | undefined) => void; + orientation?: "horizontal" | "vertical"; +} & WithElementRef>; + +const EMBLA_CAROUSEL_CONTEXT = Symbol("EMBLA_CAROUSEL_CONTEXT"); + +export type EmblaContext = { + api: CarouselAPI | undefined; + orientation: "horizontal" | "vertical"; + scrollNext: () => void; + scrollPrev: () => void; + canScrollNext: boolean; + canScrollPrev: boolean; + handleKeyDown: (e: KeyboardEvent) => void; + options: CarouselOptions; + plugins: CarouselPlugins; + onInit: (e: CustomEvent) => void; + scrollTo: (index: number, jump?: boolean) => void; + scrollSnaps: number[]; + selectedIndex: number; +}; + +export function setEmblaContext(config: EmblaContext): EmblaContext { + setContext(EMBLA_CAROUSEL_CONTEXT, config); + return config; +} + +export function getEmblaContext(name = "This component") { + if (!hasContext(EMBLA_CAROUSEL_CONTEXT)) { + throw new Error(`${name} must be used within a component`); + } + return getContext>(EMBLA_CAROUSEL_CONTEXT); +} diff --git a/apps/frontend/src/lib/components/ui/carousel/index.ts b/apps/frontend/src/lib/components/ui/carousel/index.ts new file mode 100644 index 0000000..957fc74 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/carousel/index.ts @@ -0,0 +1,19 @@ +import Root from "./carousel.svelte"; +import Content from "./carousel-content.svelte"; +import Item from "./carousel-item.svelte"; +import Previous from "./carousel-previous.svelte"; +import Next from "./carousel-next.svelte"; + +export { + Root, + Content, + Item, + Previous, + Next, + // + Root as Carousel, + Content as CarouselContent, + Item as CarouselItem, + Previous as CarouselPrevious, + Next as CarouselNext, +}; diff --git a/apps/frontend/src/lib/components/ui/chart/chart-container.svelte b/apps/frontend/src/lib/components/ui/chart/chart-container.svelte new file mode 100644 index 0000000..36c0000 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/chart/chart-container.svelte @@ -0,0 +1,80 @@ + + +
    + + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/chart/chart-style.svelte b/apps/frontend/src/lib/components/ui/chart/chart-style.svelte new file mode 100644 index 0000000..864ecc3 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/chart/chart-style.svelte @@ -0,0 +1,37 @@ + + +{#if themeContents} + {#key id} + + {themeContents} + + {/key} +{/if} diff --git a/apps/frontend/src/lib/components/ui/chart/chart-tooltip.svelte b/apps/frontend/src/lib/components/ui/chart/chart-tooltip.svelte new file mode 100644 index 0000000..6eb66ff --- /dev/null +++ b/apps/frontend/src/lib/components/ui/chart/chart-tooltip.svelte @@ -0,0 +1,159 @@ + + +{#snippet TooltipLabel()} + {#if formattedLabel} +
    + {#if typeof formattedLabel === "function"} + {@render formattedLabel()} + {:else} + {formattedLabel} + {/if} +
    + {/if} +{/snippet} + + +
    + {#if !nestLabel} + {@render TooltipLabel()} + {/if} +
    + {#each tooltipCtx.payload as item, i (item.key + i)} + {@const key = `${nameKey || item.key || item.name || "value"}`} + {@const itemConfig = getPayloadConfigFromPayload(chart.config, item, key)} + {@const indicatorColor = color || item.payload?.color || item.color} +
    svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5", + indicator === "dot" && "items-center" + )} + > + {#if formatter && item.value !== undefined && item.name} + {@render formatter({ + value: item.value, + name: item.name, + item, + index: i, + payload: tooltipCtx.payload, + })} + {:else} + {#if itemConfig?.icon} + + {:else if !hideIndicator} +
    + {/if} +
    +
    + {#if nestLabel} + {@render TooltipLabel()} + {/if} + + {itemConfig?.label || item.name} + +
    + {#if item.value !== undefined} + + {item.value.toLocaleString()} + + {/if} +
    + {/if} +
    + {/each} +
    +
    +
    diff --git a/apps/frontend/src/lib/components/ui/chart/chart-utils.ts b/apps/frontend/src/lib/components/ui/chart/chart-utils.ts new file mode 100644 index 0000000..2decbbf --- /dev/null +++ b/apps/frontend/src/lib/components/ui/chart/chart-utils.ts @@ -0,0 +1,66 @@ +import type { Tooltip } from "layerchart"; +import { getContext, setContext, type Component, type ComponentProps, type Snippet } from "svelte"; + +export const THEMES = { light: "", dark: ".dark" } as const; + +export type ChartConfig = { + [k in string]: { + label?: string; + icon?: Component; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ); +}; + +export type ExtractSnippetParams = T extends Snippet<[infer P]> ? P : never; + +export type TooltipPayload = ExtractSnippetParams< + ComponentProps["children"] +>["payload"][number]; + +// Helper to extract item config from a payload. +export function getPayloadConfigFromPayload( + config: ChartConfig, + payload: TooltipPayload, + key: string +) { + if (typeof payload !== "object" || payload === null) return undefined; + + const payloadPayload = + "payload" in payload && typeof payload.payload === "object" && payload.payload !== null + ? payload.payload + : undefined; + + let configLabelKey: string = key; + + if (payload.key === key) { + configLabelKey = payload.key; + } else if (payload.name === key) { + configLabelKey = payload.name; + } else if (key in payload && typeof payload[key as keyof typeof payload] === "string") { + configLabelKey = payload[key as keyof typeof payload] as string; + } else if ( + payloadPayload !== undefined && + key in payloadPayload && + typeof payloadPayload[key as keyof typeof payloadPayload] === "string" + ) { + configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string; + } + + return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]; +} + +type ChartContextValue = { + config: ChartConfig; +}; + +const chartContextKey = Symbol("chart-context"); + +export function setChartContext(value: ChartContextValue) { + return setContext(chartContextKey, value); +} + +export function useChart() { + return getContext(chartContextKey); +} diff --git a/apps/frontend/src/lib/components/ui/chart/index.ts b/apps/frontend/src/lib/components/ui/chart/index.ts new file mode 100644 index 0000000..f22375e --- /dev/null +++ b/apps/frontend/src/lib/components/ui/chart/index.ts @@ -0,0 +1,6 @@ +import ChartContainer from "./chart-container.svelte"; +import ChartTooltip from "./chart-tooltip.svelte"; + +export { getPayloadConfigFromPayload, type ChartConfig } from "./chart-utils.js"; + +export { ChartContainer, ChartTooltip, ChartContainer as Container, ChartTooltip as Tooltip }; diff --git a/apps/frontend/src/lib/components/ui/checkbox/checkbox.svelte b/apps/frontend/src/lib/components/ui/checkbox/checkbox.svelte new file mode 100644 index 0000000..0a2b010 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/checkbox/checkbox.svelte @@ -0,0 +1,36 @@ + + + + {#snippet children({ checked, indeterminate })} +
    + {#if checked} + + {:else if indeterminate} + + {/if} +
    + {/snippet} +
    diff --git a/apps/frontend/src/lib/components/ui/checkbox/index.ts b/apps/frontend/src/lib/components/ui/checkbox/index.ts new file mode 100644 index 0000000..6d92d94 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/checkbox/index.ts @@ -0,0 +1,6 @@ +import Root from "./checkbox.svelte"; +export { + Root, + // + Root as Checkbox, +}; diff --git a/apps/frontend/src/lib/components/ui/collapsible/collapsible-content.svelte b/apps/frontend/src/lib/components/ui/collapsible/collapsible-content.svelte new file mode 100644 index 0000000..bdabb55 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/collapsible/collapsible-content.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/collapsible/collapsible-trigger.svelte b/apps/frontend/src/lib/components/ui/collapsible/collapsible-trigger.svelte new file mode 100644 index 0000000..ece7ad6 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/collapsible/collapsible-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/collapsible/collapsible.svelte b/apps/frontend/src/lib/components/ui/collapsible/collapsible.svelte new file mode 100644 index 0000000..39cdd4e --- /dev/null +++ b/apps/frontend/src/lib/components/ui/collapsible/collapsible.svelte @@ -0,0 +1,11 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/collapsible/index.ts b/apps/frontend/src/lib/components/ui/collapsible/index.ts new file mode 100644 index 0000000..169b479 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/collapsible/index.ts @@ -0,0 +1,13 @@ +import Root from "./collapsible.svelte"; +import Trigger from "./collapsible-trigger.svelte"; +import Content from "./collapsible-content.svelte"; + +export { + Root, + Content, + Trigger, + // + Root as Collapsible, + Content as CollapsibleContent, + Trigger as CollapsibleTrigger, +}; diff --git a/apps/frontend/src/lib/components/ui/command/command-dialog.svelte b/apps/frontend/src/lib/components/ui/command/command-dialog.svelte new file mode 100644 index 0000000..4bdb740 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-dialog.svelte @@ -0,0 +1,40 @@ + + + + + {title} + {description} + + + + + diff --git a/apps/frontend/src/lib/components/ui/command/command-empty.svelte b/apps/frontend/src/lib/components/ui/command/command-empty.svelte new file mode 100644 index 0000000..6726cd8 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-empty.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/command/command-group.svelte b/apps/frontend/src/lib/components/ui/command/command-group.svelte new file mode 100644 index 0000000..104f817 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-group.svelte @@ -0,0 +1,32 @@ + + + + {#if heading} + + {heading} + + {/if} + + diff --git a/apps/frontend/src/lib/components/ui/command/command-input.svelte b/apps/frontend/src/lib/components/ui/command/command-input.svelte new file mode 100644 index 0000000..28d3dcf --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-input.svelte @@ -0,0 +1,26 @@ + + +
    + + +
    diff --git a/apps/frontend/src/lib/components/ui/command/command-item.svelte b/apps/frontend/src/lib/components/ui/command/command-item.svelte new file mode 100644 index 0000000..5833416 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-item.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/command/command-link-item.svelte b/apps/frontend/src/lib/components/ui/command/command-link-item.svelte new file mode 100644 index 0000000..ada6d2c --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-link-item.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/command/command-list.svelte b/apps/frontend/src/lib/components/ui/command/command-list.svelte new file mode 100644 index 0000000..2d3a01a --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-list.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/command/command-loading.svelte b/apps/frontend/src/lib/components/ui/command/command-loading.svelte new file mode 100644 index 0000000..19dd298 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-loading.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/command/command-separator.svelte b/apps/frontend/src/lib/components/ui/command/command-separator.svelte new file mode 100644 index 0000000..35c4c95 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/command/command-shortcut.svelte b/apps/frontend/src/lib/components/ui/command/command-shortcut.svelte new file mode 100644 index 0000000..f3d6928 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/apps/frontend/src/lib/components/ui/command/command.svelte b/apps/frontend/src/lib/components/ui/command/command.svelte new file mode 100644 index 0000000..a1581f1 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/command.svelte @@ -0,0 +1,28 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/command/index.ts b/apps/frontend/src/lib/components/ui/command/index.ts new file mode 100644 index 0000000..5435fbe --- /dev/null +++ b/apps/frontend/src/lib/components/ui/command/index.ts @@ -0,0 +1,37 @@ +import Root from "./command.svelte"; +import Loading from "./command-loading.svelte"; +import Dialog from "./command-dialog.svelte"; +import Empty from "./command-empty.svelte"; +import Group from "./command-group.svelte"; +import Item from "./command-item.svelte"; +import Input from "./command-input.svelte"; +import List from "./command-list.svelte"; +import Separator from "./command-separator.svelte"; +import Shortcut from "./command-shortcut.svelte"; +import LinkItem from "./command-link-item.svelte"; + +export { + Root, + Dialog, + Empty, + Group, + Item, + LinkItem, + Input, + List, + Separator, + Shortcut, + Loading, + // + Root as Command, + Dialog as CommandDialog, + Empty as CommandEmpty, + Group as CommandGroup, + Item as CommandItem, + LinkItem as CommandLinkItem, + Input as CommandInput, + List as CommandList, + Separator as CommandSeparator, + Shortcut as CommandShortcut, + Loading as CommandLoading, +}; diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte new file mode 100644 index 0000000..f3b6db3 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte @@ -0,0 +1,40 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-content.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-content.svelte new file mode 100644 index 0000000..20b516d --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-content.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-group-heading.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-group-heading.svelte new file mode 100644 index 0000000..2cb6207 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-group-heading.svelte @@ -0,0 +1,21 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-group.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-group.svelte new file mode 100644 index 0000000..c7c1e06 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-item.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-item.svelte new file mode 100644 index 0000000..4e8d224 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-label.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-label.svelte new file mode 100644 index 0000000..5e96107 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-label.svelte @@ -0,0 +1,24 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-portal.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-portal.svelte new file mode 100644 index 0000000..96b1e3e --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-radio-group.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-radio-group.svelte new file mode 100644 index 0000000..964cb55 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-radio-item.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-radio-item.svelte new file mode 100644 index 0000000..0141b14 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-radio-item.svelte @@ -0,0 +1,33 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-separator.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-separator.svelte new file mode 100644 index 0000000..7f5b237 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-shortcut.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-shortcut.svelte new file mode 100644 index 0000000..6181881 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub-content.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub-content.svelte new file mode 100644 index 0000000..2b6ca47 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte new file mode 100644 index 0000000..38d74eb --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub.svelte new file mode 100644 index 0000000..a03827b --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-sub.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu-trigger.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu-trigger.svelte new file mode 100644 index 0000000..3efa857 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/context-menu.svelte b/apps/frontend/src/lib/components/ui/context-menu/context-menu.svelte new file mode 100644 index 0000000..cfaefb3 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/context-menu.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/context-menu/index.ts b/apps/frontend/src/lib/components/ui/context-menu/index.ts new file mode 100644 index 0000000..cbeaee1 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/context-menu/index.ts @@ -0,0 +1,52 @@ +import Root from "./context-menu.svelte"; +import Sub from "./context-menu-sub.svelte"; +import Portal from "./context-menu-portal.svelte"; +import Trigger from "./context-menu-trigger.svelte"; +import Group from "./context-menu-group.svelte"; +import RadioGroup from "./context-menu-radio-group.svelte"; +import Item from "./context-menu-item.svelte"; +import GroupHeading from "./context-menu-group-heading.svelte"; +import Content from "./context-menu-content.svelte"; +import Shortcut from "./context-menu-shortcut.svelte"; +import RadioItem from "./context-menu-radio-item.svelte"; +import Separator from "./context-menu-separator.svelte"; +import SubContent from "./context-menu-sub-content.svelte"; +import SubTrigger from "./context-menu-sub-trigger.svelte"; +import CheckboxItem from "./context-menu-checkbox-item.svelte"; +import Label from "./context-menu-label.svelte"; + +export { + Root, + Sub, + Portal, + Item, + GroupHeading, + Label, + Group, + Trigger, + Content, + Shortcut, + Separator, + RadioItem, + SubContent, + SubTrigger, + RadioGroup, + CheckboxItem, + // + Root as ContextMenu, + Sub as ContextMenuSub, + Portal as ContextMenuPortal, + Item as ContextMenuItem, + GroupHeading as ContextMenuGroupHeading, + Group as ContextMenuGroup, + Content as ContextMenuContent, + Trigger as ContextMenuTrigger, + Shortcut as ContextMenuShortcut, + RadioItem as ContextMenuRadioItem, + Separator as ContextMenuSeparator, + RadioGroup as ContextMenuRadioGroup, + SubContent as ContextMenuSubContent, + SubTrigger as ContextMenuSubTrigger, + CheckboxItem as ContextMenuCheckboxItem, + Label as ContextMenuLabel, +}; diff --git a/apps/frontend/src/lib/components/ui/data-table/data-table.svelte.ts b/apps/frontend/src/lib/components/ui/data-table/data-table.svelte.ts new file mode 100644 index 0000000..5b7985e --- /dev/null +++ b/apps/frontend/src/lib/components/ui/data-table/data-table.svelte.ts @@ -0,0 +1,142 @@ +import { + type RowData, + type TableOptions, + type TableOptionsResolved, + type TableState, + createTable, +} from "@tanstack/table-core"; + +/** + * Creates a reactive TanStack table object for Svelte. + * @param options Table options to create the table with. + * @returns A reactive table object. + * @example + * ```svelte + * + * + * + * + * {#each table.getHeaderGroups() as headerGroup} + * + * {#each headerGroup.headers as header} + * + * {/each} + * + * {/each} + * + * + *
    + * + *
    + * ``` + */ +export function createSvelteTable(options: TableOptions) { + const resolvedOptions: TableOptionsResolved = mergeObjects( + { + state: {}, + onStateChange() {}, + renderFallbackValue: null, + mergeOptions: ( + defaultOptions: TableOptions, + options: Partial> + ) => { + return mergeObjects(defaultOptions, options); + }, + }, + options + ); + + const table = createTable(resolvedOptions); + let state = $state>(table.initialState); + + function updateOptions() { + table.setOptions((prev) => { + return mergeObjects(prev, options, { + state: mergeObjects(state, options.state || {}), + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onStateChange: (updater: any) => { + if (updater instanceof Function) state = updater(state); + else state = mergeObjects(state, updater); + + options.onStateChange?.(updater); + }, + }); + }); + } + + updateOptions(); + + $effect.pre(() => { + updateOptions(); + }); + + return table; +} + +type MaybeThunk = T | (() => T | null | undefined); +type Intersection = (T extends [infer H, ...infer R] + ? H & Intersection + : unknown) & {}; + +/** + * Lazily merges several objects (or thunks) while preserving + * getter semantics from every source. + * + * Proxy-based to avoid known WebKit recursion issue. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function mergeObjects[]>( + ...sources: Sources +): Intersection<{ [K in keyof Sources]: Sources[K] }> { + const resolve = (src: MaybeThunk): T | undefined => + typeof src === "function" ? (src() ?? undefined) : src; + + const findSourceWithKey = (key: PropertyKey) => { + for (let i = sources.length - 1; i >= 0; i--) { + const obj = resolve(sources[i]); + if (obj && key in obj) return obj; + } + return undefined; + }; + + return new Proxy(Object.create(null), { + get(_, key) { + const src = findSourceWithKey(key); + + return src?.[key as never]; + }, + + has(_, key) { + return !!findSourceWithKey(key); + }, + + ownKeys(): (string | symbol)[] { + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const all = new Set(); + for (const s of sources) { + const obj = resolve(s); + if (obj) { + for (const k of Reflect.ownKeys(obj) as (string | symbol)[]) { + all.add(k); + } + } + } + return [...all]; + }, + + getOwnPropertyDescriptor(_, key) { + const src = findSourceWithKey(key); + if (!src) return undefined; + return { + configurable: true, + enumerable: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: (src as any)[key], + writable: true, + }; + }, + }) as Intersection<{ [K in keyof Sources]: Sources[K] }>; +} diff --git a/apps/frontend/src/lib/components/ui/data-table/flex-render.svelte b/apps/frontend/src/lib/components/ui/data-table/flex-render.svelte new file mode 100644 index 0000000..ac82a58 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/data-table/flex-render.svelte @@ -0,0 +1,40 @@ + + +{#if typeof content === "string"} + {content} +{:else if content instanceof Function} + + + {@const result = content(context as any)} + {#if result instanceof RenderComponentConfig} + {@const { component: Component, props } = result} + + {:else if result instanceof RenderSnippetConfig} + {@const { snippet, params } = result} + {@render snippet({ ...params, attach })} + {:else} + {result} + {/if} +{/if} diff --git a/apps/frontend/src/lib/components/ui/data-table/index.ts b/apps/frontend/src/lib/components/ui/data-table/index.ts new file mode 100644 index 0000000..5f4e77e --- /dev/null +++ b/apps/frontend/src/lib/components/ui/data-table/index.ts @@ -0,0 +1,3 @@ +export { default as FlexRender } from "./flex-render.svelte"; +export { renderComponent, renderSnippet } from "./render-helpers.js"; +export { createSvelteTable } from "./data-table.svelte.js"; diff --git a/apps/frontend/src/lib/components/ui/data-table/render-helpers.ts b/apps/frontend/src/lib/components/ui/data-table/render-helpers.ts new file mode 100644 index 0000000..fa036d6 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/data-table/render-helpers.ts @@ -0,0 +1,111 @@ +import type { Component, ComponentProps, Snippet } from "svelte"; + +/** + * A helper class to make it easy to identify Svelte components in + * `columnDef.cell` and `columnDef.header` properties. + * + * > NOTE: This class should only be used internally by the adapter. If you're + * reading this and you don't know what this is for, you probably don't need it. + * + * @example + * ```svelte + * {@const result = content(context as any)} + * {#if result instanceof RenderComponentConfig} + * {@const { component: Component, props } = result} + * + * {/if} + * ``` + */ +export class RenderComponentConfig { + component: TComponent; + props: ComponentProps | Record; + constructor( + component: TComponent, + props: ComponentProps | Record = {} + ) { + this.component = component; + this.props = props; + } +} + +/** + * A helper class to make it easy to identify Svelte Snippets in `columnDef.cell` and `columnDef.header` properties. + * + * > NOTE: This class should only be used internally by the adapter. If you're + * reading this and you don't know what this is for, you probably don't need it. + * + * @example + * ```svelte + * {@const result = content(context as any)} + * {#if result instanceof RenderSnippetConfig} + * {@const { snippet, params } = result} + * {@render snippet(params)} + * {/if} + * ``` + */ +export class RenderSnippetConfig { + snippet: Snippet<[TProps]>; + params: TProps; + constructor(snippet: Snippet<[TProps]>, params: TProps) { + this.snippet = snippet; + this.params = params; + } +} + +/** + * A helper function to help create cells from Svelte components through ColumnDef's `cell` and `header` properties. + * + * This is only to be used with Svelte Components - use `renderSnippet` for Svelte Snippets. + * + * @param component A Svelte component + * @param props The props to pass to `component` + * @returns A `RenderComponentConfig` object that helps svelte-table know how to render the header/cell component. + * @example + * ```ts + * // +page.svelte + * const defaultColumns = [ + * columnHelper.accessor('name', { + * header: header => renderComponent(SortHeader, { label: 'Name', header }), + * }), + * columnHelper.accessor('state', { + * header: header => renderComponent(SortHeader, { label: 'State', header }), + * }), + * ] + * ``` + * @see {@link https://tanstack.com/table/latest/docs/guide/column-defs} + */ +export function renderComponent< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends Component, + Props extends ComponentProps, +>(component: T, props: Props = {} as Props) { + return new RenderComponentConfig(component, props); +} + +/** + * A helper function to help create cells from Svelte Snippets through ColumnDef's `cell` and `header` properties. + * + * The snippet must only take one parameter. + * + * This is only to be used with Snippets - use `renderComponent` for Svelte Components. + * + * @param snippet + * @param params + * @returns - A `RenderSnippetConfig` object that helps svelte-table know how to render the header/cell snippet. + * @example + * ```ts + * // +page.svelte + * const defaultColumns = [ + * columnHelper.accessor('name', { + * cell: cell => renderSnippet(nameSnippet, { name: cell.row.name }), + * }), + * columnHelper.accessor('state', { + * cell: cell => renderSnippet(stateSnippet, { state: cell.row.state }), + * }), + * ] + * ``` + * @see {@link https://tanstack.com/table/latest/docs/guide/column-defs} + */ +export function renderSnippet(snippet: Snippet<[TProps]>, params: TProps = {} as TProps) { + return new RenderSnippetConfig(snippet, params); +} diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-close.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-close.svelte new file mode 100644 index 0000000..840b2f6 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-content.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 0000000..ae1a03f --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,45 @@ + + + + + + {@render children?.()} + {#if showCloseButton} + + + Close + + {/if} + + diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-description.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 0000000..3845023 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-footer.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 0000000..e7ff446 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-header.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 0000000..4e5c447 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 0000000..f81ad83 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-portal.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-portal.svelte new file mode 100644 index 0000000..ccfa79c --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-title.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 0000000..e4d4b34 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte new file mode 100644 index 0000000..9d1e801 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dialog/dialog.svelte b/apps/frontend/src/lib/components/ui/dialog/dialog.svelte new file mode 100644 index 0000000..211672c --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/dialog.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dialog/index.ts b/apps/frontend/src/lib/components/ui/dialog/index.ts new file mode 100644 index 0000000..076cef5 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,34 @@ +import Root from "./dialog.svelte"; +import Portal from "./dialog-portal.svelte"; +import Title from "./dialog-title.svelte"; +import Footer from "./dialog-footer.svelte"; +import Header from "./dialog-header.svelte"; +import Overlay from "./dialog-overlay.svelte"; +import Content from "./dialog-content.svelte"; +import Description from "./dialog-description.svelte"; +import Trigger from "./dialog-trigger.svelte"; +import Close from "./dialog-close.svelte"; + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose, +}; diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-close.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-close.svelte new file mode 100644 index 0000000..95c2479 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-content.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-content.svelte new file mode 100644 index 0000000..6bb01db --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-content.svelte @@ -0,0 +1,40 @@ + + + + + + + {@render children?.()} + + diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-description.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-description.svelte new file mode 100644 index 0000000..2763a1a --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-footer.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-footer.svelte new file mode 100644 index 0000000..1691f58 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-footer.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-header.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-header.svelte new file mode 100644 index 0000000..65d2de5 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-header.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-nested.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-nested.svelte new file mode 100644 index 0000000..834af94 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-nested.svelte @@ -0,0 +1,12 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-overlay.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-overlay.svelte new file mode 100644 index 0000000..53f78a2 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-portal.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-portal.svelte new file mode 100644 index 0000000..5a0dd74 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-title.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-title.svelte new file mode 100644 index 0000000..a2e5761 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer-trigger.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer-trigger.svelte new file mode 100644 index 0000000..f1877d8 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/drawer/drawer.svelte b/apps/frontend/src/lib/components/ui/drawer/drawer.svelte new file mode 100644 index 0000000..0cb57ff --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/drawer.svelte @@ -0,0 +1,12 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/drawer/index.ts b/apps/frontend/src/lib/components/ui/drawer/index.ts new file mode 100644 index 0000000..1656cac --- /dev/null +++ b/apps/frontend/src/lib/components/ui/drawer/index.ts @@ -0,0 +1,38 @@ +import Root from "./drawer.svelte"; +import Content from "./drawer-content.svelte"; +import Description from "./drawer-description.svelte"; +import Overlay from "./drawer-overlay.svelte"; +import Footer from "./drawer-footer.svelte"; +import Header from "./drawer-header.svelte"; +import Title from "./drawer-title.svelte"; +import NestedRoot from "./drawer-nested.svelte"; +import Close from "./drawer-close.svelte"; +import Trigger from "./drawer-trigger.svelte"; +import Portal from "./drawer-portal.svelte"; + +export { + Root, + NestedRoot, + Content, + Description, + Overlay, + Footer, + Header, + Title, + Trigger, + Portal, + Close, + + // + Root as Drawer, + NestedRoot as DrawerNestedRoot, + Content as DrawerContent, + Description as DrawerDescription, + Overlay as DrawerOverlay, + Footer as DrawerFooter, + Header as DrawerHeader, + Title as DrawerTitle, + Trigger as DrawerTrigger, + Portal as DrawerPortal, + Close as DrawerClose, +}; diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte new file mode 100644 index 0000000..e0e1971 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000..6d9ef85 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,43 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000..1e96782 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte new file mode 100644 index 0000000..433540f --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -0,0 +1,22 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte new file mode 100644 index 0000000..aca1f7b --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000..04cd110 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000..9681c2b --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,24 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte new file mode 100644 index 0000000..274cfef --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000..189aef4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000..ce2ad09 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,33 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000..90f1b6f --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000..7c6e9c6 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000..3f06dc4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000..5f49d01 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte new file mode 100644 index 0000000..f044581 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte new file mode 100644 index 0000000..cb05344 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte new file mode 100644 index 0000000..cb4bc62 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/dropdown-menu/index.ts b/apps/frontend/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..7850c6a --- /dev/null +++ b/apps/frontend/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,54 @@ +import Root from "./dropdown-menu.svelte"; +import Sub from "./dropdown-menu-sub.svelte"; +import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte"; +import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; +import Content from "./dropdown-menu-content.svelte"; +import Group from "./dropdown-menu-group.svelte"; +import Item from "./dropdown-menu-item.svelte"; +import Label from "./dropdown-menu-label.svelte"; +import RadioGroup from "./dropdown-menu-radio-group.svelte"; +import RadioItem from "./dropdown-menu-radio-item.svelte"; +import Separator from "./dropdown-menu-separator.svelte"; +import Shortcut from "./dropdown-menu-shortcut.svelte"; +import Trigger from "./dropdown-menu-trigger.svelte"; +import SubContent from "./dropdown-menu-sub-content.svelte"; +import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; +import GroupHeading from "./dropdown-menu-group-heading.svelte"; +import Portal from "./dropdown-menu-portal.svelte"; + +export { + CheckboxGroup, + CheckboxItem, + Content, + Portal, + Root as DropdownMenu, + CheckboxGroup as DropdownMenuCheckboxGroup, + CheckboxItem as DropdownMenuCheckboxItem, + Content as DropdownMenuContent, + Portal as DropdownMenuPortal, + Group as DropdownMenuGroup, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + RadioGroup as DropdownMenuRadioGroup, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + Shortcut as DropdownMenuShortcut, + Sub as DropdownMenuSub, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + Trigger as DropdownMenuTrigger, + GroupHeading as DropdownMenuGroupHeading, + Group, + GroupHeading, + Item, + Label, + RadioGroup, + RadioItem, + Root, + Separator, + Shortcut, + Sub, + SubContent, + SubTrigger, + Trigger, +}; diff --git a/apps/frontend/src/lib/components/ui/empty/empty-content.svelte b/apps/frontend/src/lib/components/ui/empty/empty-content.svelte new file mode 100644 index 0000000..f5a9c68 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/empty/empty-content.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/empty/empty-description.svelte b/apps/frontend/src/lib/components/ui/empty/empty-description.svelte new file mode 100644 index 0000000..85a866c --- /dev/null +++ b/apps/frontend/src/lib/components/ui/empty/empty-description.svelte @@ -0,0 +1,23 @@ + + +
    a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...restProps} +> + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/empty/empty-header.svelte b/apps/frontend/src/lib/components/ui/empty/empty-header.svelte new file mode 100644 index 0000000..296eaf8 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/empty/empty-header.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/empty/empty-media.svelte b/apps/frontend/src/lib/components/ui/empty/empty-media.svelte new file mode 100644 index 0000000..0b4e45d --- /dev/null +++ b/apps/frontend/src/lib/components/ui/empty/empty-media.svelte @@ -0,0 +1,41 @@ + + + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/empty/empty-title.svelte b/apps/frontend/src/lib/components/ui/empty/empty-title.svelte new file mode 100644 index 0000000..8c237aa --- /dev/null +++ b/apps/frontend/src/lib/components/ui/empty/empty-title.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/empty/empty.svelte b/apps/frontend/src/lib/components/ui/empty/empty.svelte new file mode 100644 index 0000000..4ccf060 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/empty/empty.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/empty/index.ts b/apps/frontend/src/lib/components/ui/empty/index.ts new file mode 100644 index 0000000..ae4c106 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/empty/index.ts @@ -0,0 +1,22 @@ +import Root from "./empty.svelte"; +import Header from "./empty-header.svelte"; +import Media from "./empty-media.svelte"; +import Title from "./empty-title.svelte"; +import Description from "./empty-description.svelte"; +import Content from "./empty-content.svelte"; + +export { + Root, + Header, + Media, + Title, + Description, + Content, + // + Root as Empty, + Header as EmptyHeader, + Media as EmptyMedia, + Title as EmptyTitle, + Description as EmptyDescription, + Content as EmptyContent, +}; diff --git a/apps/frontend/src/lib/components/ui/field/field-content.svelte b/apps/frontend/src/lib/components/ui/field/field-content.svelte new file mode 100644 index 0000000..1b6535b --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-content.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/field/field-description.svelte b/apps/frontend/src/lib/components/ui/field/field-description.svelte new file mode 100644 index 0000000..a0c9f06 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-description.svelte @@ -0,0 +1,25 @@ + + +

    a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...restProps} +> + {@render children?.()} +

    diff --git a/apps/frontend/src/lib/components/ui/field/field-error.svelte b/apps/frontend/src/lib/components/ui/field/field-error.svelte new file mode 100644 index 0000000..1d5cc5f --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-error.svelte @@ -0,0 +1,58 @@ + + +{#if hasContent} + +{/if} diff --git a/apps/frontend/src/lib/components/ui/field/field-group.svelte b/apps/frontend/src/lib/components/ui/field/field-group.svelte new file mode 100644 index 0000000..e685427 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-group.svelte @@ -0,0 +1,23 @@ + + +
    [data-slot=field-group]]:gap-4", + className + )} + {...restProps} +> + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/field/field-label.svelte b/apps/frontend/src/lib/components/ui/field/field-label.svelte new file mode 100644 index 0000000..2ee431a --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-label.svelte @@ -0,0 +1,26 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/field/field-legend.svelte b/apps/frontend/src/lib/components/ui/field/field-legend.svelte new file mode 100644 index 0000000..3f1c50f --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-legend.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + diff --git a/apps/frontend/src/lib/components/ui/field/field-separator.svelte b/apps/frontend/src/lib/components/ui/field/field-separator.svelte new file mode 100644 index 0000000..12bcb77 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-separator.svelte @@ -0,0 +1,38 @@ + + +
    + + {#if children} + + {@render children()} + + {/if} +
    diff --git a/apps/frontend/src/lib/components/ui/field/field-set.svelte b/apps/frontend/src/lib/components/ui/field/field-set.svelte new file mode 100644 index 0000000..1d8e233 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-set.svelte @@ -0,0 +1,24 @@ + + +
    [data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className + )} + {...restProps} +> + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/field/field-title.svelte b/apps/frontend/src/lib/components/ui/field/field-title.svelte new file mode 100644 index 0000000..5906044 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field-title.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/field/field.svelte b/apps/frontend/src/lib/components/ui/field/field.svelte new file mode 100644 index 0000000..3284203 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/field.svelte @@ -0,0 +1,53 @@ + + + + +
    + {@render children?.()} +
    diff --git a/apps/frontend/src/lib/components/ui/field/index.ts b/apps/frontend/src/lib/components/ui/field/index.ts new file mode 100644 index 0000000..a644a95 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/field/index.ts @@ -0,0 +1,33 @@ +import Field from "./field.svelte"; +import Set from "./field-set.svelte"; +import Legend from "./field-legend.svelte"; +import Group from "./field-group.svelte"; +import Content from "./field-content.svelte"; +import Label from "./field-label.svelte"; +import Title from "./field-title.svelte"; +import Description from "./field-description.svelte"; +import Separator from "./field-separator.svelte"; +import Error from "./field-error.svelte"; + +export { + Field, + Set, + Legend, + Group, + Content, + Label, + Title, + Description, + Separator, + Error, + // + Set as FieldSet, + Legend as FieldLegend, + Group as FieldGroup, + Content as FieldContent, + Label as FieldLabel, + Title as FieldTitle, + Description as FieldDescription, + Separator as FieldSeparator, + Error as FieldError, +}; diff --git a/apps/frontend/src/lib/components/ui/form/form-button.svelte b/apps/frontend/src/lib/components/ui/form/form-button.svelte new file mode 100644 index 0000000..48d3936 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/form/form-button.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/input-group/input-group-input.svelte b/apps/frontend/src/lib/components/ui/input-group/input-group-input.svelte new file mode 100644 index 0000000..ded2655 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/input-group/input-group-input.svelte @@ -0,0 +1,23 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/input-group/input-group-text.svelte b/apps/frontend/src/lib/components/ui/input-group/input-group-text.svelte new file mode 100644 index 0000000..9c43dc4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/input-group/input-group-text.svelte @@ -0,0 +1,22 @@ + + + + {@render children?.()} + diff --git a/apps/frontend/src/lib/components/ui/input-group/input-group-textarea.svelte b/apps/frontend/src/lib/components/ui/input-group/input-group-textarea.svelte new file mode 100644 index 0000000..91850ff --- /dev/null +++ b/apps/frontend/src/lib/components/ui/input-group/input-group-textarea.svelte @@ -0,0 +1,23 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/toggle-group/index.ts b/apps/frontend/src/lib/components/ui/toggle-group/index.ts new file mode 100644 index 0000000..12b14b9 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/toggle-group/index.ts @@ -0,0 +1,10 @@ +import Root from "./toggle-group.svelte"; +import Item from "./toggle-group-item.svelte"; + +export { + Root, + Item, + // + Root as ToggleGroup, + Item as ToggleGroupItem, +}; diff --git a/apps/frontend/src/lib/components/ui/toggle-group/toggle-group-item.svelte b/apps/frontend/src/lib/components/ui/toggle-group/toggle-group-item.svelte new file mode 100644 index 0000000..6d60b52 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/toggle-group/toggle-group-item.svelte @@ -0,0 +1,35 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/toggle-group/toggle-group.svelte b/apps/frontend/src/lib/components/ui/toggle-group/toggle-group.svelte new file mode 100644 index 0000000..106561c --- /dev/null +++ b/apps/frontend/src/lib/components/ui/toggle-group/toggle-group.svelte @@ -0,0 +1,59 @@ + + + + + + diff --git a/apps/frontend/src/lib/components/ui/toggle/index.ts b/apps/frontend/src/lib/components/ui/toggle/index.ts new file mode 100644 index 0000000..8cb2936 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/toggle/index.ts @@ -0,0 +1,13 @@ +import Root from "./toggle.svelte"; +export { + toggleVariants, + type ToggleSize, + type ToggleVariant, + type ToggleVariants, +} from "./toggle.svelte"; + +export { + Root, + // + Root as Toggle, +}; diff --git a/apps/frontend/src/lib/components/ui/toggle/toggle.svelte b/apps/frontend/src/lib/components/ui/toggle/toggle.svelte new file mode 100644 index 0000000..56eb86b --- /dev/null +++ b/apps/frontend/src/lib/components/ui/toggle/toggle.svelte @@ -0,0 +1,52 @@ + + + + + diff --git a/apps/frontend/src/lib/components/ui/tooltip/index.ts b/apps/frontend/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 0000000..1718604 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,19 @@ +import Root from "./tooltip.svelte"; +import Trigger from "./tooltip-trigger.svelte"; +import Content from "./tooltip-content.svelte"; +import Provider from "./tooltip-provider.svelte"; +import Portal from "./tooltip-portal.svelte"; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal, +}; diff --git a/apps/frontend/src/lib/components/ui/tooltip/tooltip-content.svelte b/apps/frontend/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 0000000..788ec34 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,52 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
    + {/snippet} +
    +
    +
    diff --git a/apps/frontend/src/lib/components/ui/tooltip/tooltip-portal.svelte b/apps/frontend/src/lib/components/ui/tooltip/tooltip-portal.svelte new file mode 100644 index 0000000..d234f7d --- /dev/null +++ b/apps/frontend/src/lib/components/ui/tooltip/tooltip-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/tooltip/tooltip-provider.svelte b/apps/frontend/src/lib/components/ui/tooltip/tooltip-provider.svelte new file mode 100644 index 0000000..8150bef --- /dev/null +++ b/apps/frontend/src/lib/components/ui/tooltip/tooltip-provider.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/apps/frontend/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 0000000..1acdaa4 --- /dev/null +++ b/apps/frontend/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/components/ui/tooltip/tooltip.svelte b/apps/frontend/src/lib/components/ui/tooltip/tooltip.svelte new file mode 100644 index 0000000..0b0f9ce --- /dev/null +++ b/apps/frontend/src/lib/components/ui/tooltip/tooltip.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/frontend/src/lib/core/constants.ts b/apps/frontend/src/lib/core/constants.ts new file mode 100644 index 0000000..9d1cd2f --- /dev/null +++ b/apps/frontend/src/lib/core/constants.ts @@ -0,0 +1,6 @@ +import { PUBLIC_WS_SCRCPY_SVC_URL } from "$env/static/public"; + +export const WS_SCRCPY_URL = PUBLIC_WS_SCRCPY_SVC_URL; + +export const TRANSITION_COLORS = "transition-colors duration-150 ease-in-out"; +export const TRANSITION_ALL = "transition-all duration-150 ease-in-out"; diff --git a/apps/frontend/src/lib/core/server.utils.ts b/apps/frontend/src/lib/core/server.utils.ts new file mode 100644 index 0000000..450c7dd --- /dev/null +++ b/apps/frontend/src/lib/core/server.utils.ts @@ -0,0 +1,25 @@ +import type { FlowExecCtx } from "@pkg/logic/core/flow.execution.context"; +import type { Err } from "@pkg/result"; + +export async function getFlowExecCtxForRemoteFuncs( + locals: App.Locals, +): Promise { + return { + flowId: locals.flowId || crypto.randomUUID(), + userId: locals.user?.id, + sessionId: locals.session?.id, + }; +} + +export function unauthorized(fctx: FlowExecCtx) { + return { + data: null, + error: { + flowId: fctx.flowId, + code: "UNAUTHORIZED", + message: "User not authenticated", + description: "Please log in", + detail: "No user ID found in session", + } as Err, + }; +} diff --git a/apps/frontend/src/lib/domains/link/data.ts b/apps/frontend/src/lib/domains/link/data.ts new file mode 100644 index 0000000..481aa5e --- /dev/null +++ b/apps/frontend/src/lib/domains/link/data.ts @@ -0,0 +1,22 @@ +import * as v from "valibot"; + +export const tokenSchema = v.object({ + token: v.pipe(v.string(), v.minLength(1)), +}); + +export type OrchestratorPreparePayload = { + deviceId: number; + packageName: string; + linkToken: string; +}; + +export type OrchestratorPrepareResponse = { + data: { + deviceId: number; + containerId: string; + packageName: string; + serial: string; + status: string; + }; + error: null; +}; diff --git a/apps/frontend/src/lib/domains/link/link.remote.ts b/apps/frontend/src/lib/domains/link/link.remote.ts new file mode 100644 index 0000000..da016e4 --- /dev/null +++ b/apps/frontend/src/lib/domains/link/link.remote.ts @@ -0,0 +1,16 @@ +import { getFlowExecCtxForRemoteFuncs } from "$lib/core/server.utils"; +import { command, getRequestEvent, query } from "$app/server"; +import { prepareLinkFlow, resolveLinkFlow } from "./service"; +import { tokenSchema } from "./data"; + +export const resolveLinkSQ = query(tokenSchema, async ({ token }) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + return resolveLinkFlow(fctx, token); +}); + +export const prepareLinkSC = command(tokenSchema, async ({ token }) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + return prepareLinkFlow(fctx, token); +}); diff --git a/apps/frontend/src/lib/domains/link/orchestrator.service.ts b/apps/frontend/src/lib/domains/link/orchestrator.service.ts new file mode 100644 index 0000000..366b5c0 --- /dev/null +++ b/apps/frontend/src/lib/domains/link/orchestrator.service.ts @@ -0,0 +1,63 @@ +import type { + OrchestratorPreparePayload, + OrchestratorPrepareResponse, +} from "./data"; +import { createLinkFlowError, isErrPayload, normalizeBaseUrl } from "./utils"; +import { formatErrorDetail } from "@pkg/logger"; +import { ERROR_CODES } from "@pkg/result"; +import { settings } from "@pkg/settings"; + +export async function prepareOrchestratedSession( + flowId: string, + payload: OrchestratorPreparePayload, +): Promise { + const response = await fetch( + `${normalizeBaseUrl(settings.orchestratorApiUrl)}/internal/sessions/prepare`, + { + method: "POST", + headers: { + "content-type": "application/json", + "x-internal-api-key": settings.internalApiKey, + "x-flow-id": flowId, + }, + body: JSON.stringify(payload), + }, + ); + + let body: unknown = null; + try { + body = await response.json(); + } catch (error) { + throw createLinkFlowError( + flowId, + ERROR_CODES.EXTERNAL_SERVICE_ERROR, + "Invalid orchestrator response", + "The orchestrator returned a response that could not be parsed", + formatErrorDetail(error), + ); + } + + if (!response.ok && body && typeof body === "object" && "error" in body) { + const payloadError = (body as { error?: unknown }).error; + if (isErrPayload(payloadError)) { + throw payloadError; + } + } + + if ( + !body || + typeof body !== "object" || + !("data" in body) || + ("error" in body && body.error) + ) { + throw createLinkFlowError( + flowId, + ERROR_CODES.EXTERNAL_SERVICE_ERROR, + "Unexpected orchestrator response", + "The orchestrator response was missing the expected data payload", + JSON.stringify(body), + ); + } + + return body as OrchestratorPrepareResponse; +} diff --git a/apps/frontend/src/lib/domains/link/service.ts b/apps/frontend/src/lib/domains/link/service.ts new file mode 100644 index 0000000..873c8ab --- /dev/null +++ b/apps/frontend/src/lib/domains/link/service.ts @@ -0,0 +1,224 @@ +import { + createLinkFlowError, + isErrPayload, + missingDeviceError, + missingSupportedAppError, + unavailableDeviceError, +} from "./utils"; +import type { FlowExecCtx } from "@pkg/logic/core/flow.execution.context"; +import { getLinkController } from "@pkg/logic/domains/link/controller"; +import { prepareOrchestratedSession } from "./orchestrator.service"; +import { formatErrorDetail, logDomainEvent } from "@pkg/logger"; +import { DeviceStatus } from "@pkg/logic/domains/device/data"; +import { ERROR_CODES, type Err } from "@pkg/result"; +import { settings } from "@pkg/settings"; + +const lc = getLinkController(); + +type LinkSessionShape = { + id: number; + token: string; + status: string; + expiresAt: Date | null; + device: { + id: number; + title: string; + status: string; + inUse: boolean; + host: string; + wsPort: string; + }; + supportedApp: { + id: number; + title: string; + packageName: string; + }; +}; + +async function getPreparedLinkContext(fctx: FlowExecCtx, token: string) { + const linkResult = await lc.validate(fctx, token); + if (linkResult.isErr()) { + return { data: null, error: linkResult.error } as const; + } + + const link = linkResult.value; + + if (!link.device) { + return { + data: null, + error: missingDeviceError(fctx.flowId, token), + } as const; + } + + if (!link.supportedApp) { + return { + data: null, + error: missingSupportedAppError(fctx.flowId, token), + } as const; + } + + return { + data: { + id: link.id, + token: link.token, + status: link.status, + expiresAt: link.expiresAt, + device: { + id: link.device.id, + title: link.device.title, + status: link.device.status, + inUse: link.device.inUse, + host: link.device.host, + wsPort: link.device.wsPort, + }, + supportedApp: { + id: link.supportedApp.id, + title: link.supportedApp.title, + packageName: link.supportedApp.packageName, + }, + } satisfies LinkSessionShape, + error: null, + } as const; +} + +export async function resolveLinkFlow(fctx: FlowExecCtx, token: string) { + logDomainEvent({ + event: "front.link_resolve.started", + fctx, + meta: { token }, + }); + + const prepared = await getPreparedLinkContext(fctx, token); + if (prepared.error) { + logDomainEvent({ + level: "warn", + event: "front.link_resolve.rejected", + fctx, + error: prepared.error, + meta: { token }, + }); + + return { data: null, error: prepared.error }; + } + + const link = prepared.data; + + return { + data: { + link: { + id: link.id, + token: link.token, + status: link.status, + expiresAt: link.expiresAt, + }, + device: { + id: link.device.id, + title: link.device.title, + status: link.device.status, + inUse: link.device.inUse, + isAvailable: + link.device.status === DeviceStatus.ONLINE && + !link.device.inUse, + }, + supportedApp: link.supportedApp, + }, + error: null, + }; +} + +export async function prepareLinkFlow(fctx: FlowExecCtx, token: string) { + logDomainEvent({ + event: "front.link_prepare.started", + fctx, + meta: { token }, + }); + + const prepared = await getPreparedLinkContext(fctx, token); + if (prepared.error) { + logDomainEvent({ + level: "warn", + event: "front.link_prepare.rejected", + fctx, + error: prepared.error, + meta: { token }, + }); + + return { data: null, error: prepared.error }; + } + + const link = prepared.data; + + if (link.device.status !== DeviceStatus.ONLINE || link.device.inUse) { + return { + data: null, + error: unavailableDeviceError(fctx.flowId, link.device), + }; + } + + try { + const session = await prepareOrchestratedSession(fctx.flowId, { + deviceId: link.device.id, + packageName: link.supportedApp.packageName, + linkToken: link.token, + }); + + logDomainEvent({ + event: "front.link_prepare.succeeded", + fctx, + meta: { + token, + deviceId: link.device.id, + packageName: link.supportedApp.packageName, + }, + }); + + return { + data: { + link: { + id: link.id, + token: link.token, + }, + device: { + id: link.device.id, + title: link.device.title, + host: link.device.host, + wsPort: link.device.wsPort, + }, + supportedApp: link.supportedApp, + session: session.data, + }, + error: null, + }; + } catch (error) { + const err: Err = isErrPayload(error) + ? error + : createLinkFlowError( + fctx.flowId, + ERROR_CODES.EXTERNAL_SERVICE_ERROR, + "Failed to prepare session", + "The front server could not prepare the assigned Android session", + formatErrorDetail(error), + ); + + logDomainEvent({ + level: "error", + event: "front.link_prepare.failed", + fctx, + error: err, + meta: { + token, + orchestratorUrl: settings.orchestratorApiUrl, + }, + }); + + return { + data: null, + error: err.flowId + ? err + : { + ...err, + flowId: fctx.flowId, + }, + }; + } +} diff --git a/apps/frontend/src/lib/domains/link/utils.ts b/apps/frontend/src/lib/domains/link/utils.ts new file mode 100644 index 0000000..d491775 --- /dev/null +++ b/apps/frontend/src/lib/domains/link/utils.ts @@ -0,0 +1,63 @@ +import { ERROR_CODES, type Err } from "@pkg/result"; + +export function normalizeBaseUrl(url: string): string { + return url.endsWith("/") ? url.slice(0, -1) : url; +} + +export function createLinkFlowError( + flowId: string, + code: string, + message: string, + description: string, + detail: string, + actionable?: boolean, +): Err { + return { + flowId, + code, + message, + description, + detail, + actionable, + }; +} + +export function isErrPayload(value: unknown): value is Err { + return !!value && typeof value === "object" && "code" in value; +} + +export function missingDeviceError(flowId: string, token: string): Err { + return createLinkFlowError( + flowId, + ERROR_CODES.NOT_ALLOWED, + "Link is not assigned to a device", + "This link cannot start a session because no device is assigned", + `token=${token}`, + true, + ); +} + +export function missingSupportedAppError(flowId: string, token: string): Err { + return createLinkFlowError( + flowId, + ERROR_CODES.NOT_ALLOWED, + "Link is not assigned to an app", + "This link cannot start a session because no app is assigned", + `token=${token}`, + true, + ); +} + +export function unavailableDeviceError( + flowId: string, + device: { id: number; status: string; inUse: boolean }, +): Err { + return createLinkFlowError( + flowId, + ERROR_CODES.NOT_ALLOWED, + "Device is not available", + "The assigned device is currently busy or offline", + `deviceId=${device.id} status=${device.status} inUse=${device.inUse}`, + true, + ); +} diff --git a/apps/frontend/src/lib/global.stores.ts b/apps/frontend/src/lib/global.stores.ts new file mode 100644 index 0000000..88b4e38 --- /dev/null +++ b/apps/frontend/src/lib/global.stores.ts @@ -0,0 +1,14 @@ +import type { Session, User } from "@pkg/logic/domains/user/data"; +import type { AppSidebarItem } from "./core/constants"; +import { writable } from "svelte/store"; +import type { Router } from "$lib/api"; +import type { hc } from "hono/client"; + +export const breadcrumbs = writable>([ + { title: "Dashboard", url: "/dashboard" }, +]); + +export const apiClient = writable>>(undefined); + +export const user = writable(undefined); +export const session = writable(undefined); diff --git a/apps/frontend/src/lib/hooks/is-mobile.svelte.ts b/apps/frontend/src/lib/hooks/is-mobile.svelte.ts new file mode 100644 index 0000000..4829c00 --- /dev/null +++ b/apps/frontend/src/lib/hooks/is-mobile.svelte.ts @@ -0,0 +1,9 @@ +import { MediaQuery } from "svelte/reactivity"; + +const DEFAULT_MOBILE_BREAKPOINT = 768; + +export class IsMobile extends MediaQuery { + constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { + super(`max-width: ${breakpoint - 1}px`); + } +} diff --git a/apps/frontend/src/lib/make-client.ts b/apps/frontend/src/lib/make-client.ts new file mode 100644 index 0000000..999475d --- /dev/null +++ b/apps/frontend/src/lib/make-client.ts @@ -0,0 +1,21 @@ +import type { Router } from "$lib/api"; +import { hc } from "hono/client"; + +let browserClient: ReturnType>; + +export const makeClient = (fetch: Window["fetch"]) => { + const isBrowser = typeof window !== "undefined"; + const origin = isBrowser ? window.location.origin : ""; + + if (isBrowser && browserClient) { + return browserClient; + } + + const client = hc(origin + "/api/v1", { fetch }); + + if (isBrowser) { + browserClient = client; + } + + return client; +}; diff --git a/apps/frontend/src/lib/utils.ts b/apps/frontend/src/lib/utils.ts new file mode 100644 index 0000000..55b3a91 --- /dev/null +++ b/apps/frontend/src/lib/utils.ts @@ -0,0 +1,13 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChild = T extends { child?: any } ? Omit : T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { ref?: U | null }; diff --git a/apps/frontend/src/routes/+layout.svelte b/apps/frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..a374c05 --- /dev/null +++ b/apps/frontend/src/routes/+layout.svelte @@ -0,0 +1,20 @@ + + + + {$breadcrumbs[$breadcrumbs.length - 1]?.title ?? "Dashboard"} + + + + + + + +{@render children()} diff --git a/apps/frontend/src/routes/+page.svelte b/apps/frontend/src/routes/+page.svelte new file mode 100644 index 0000000..f4d7b91 --- /dev/null +++ b/apps/frontend/src/routes/+page.svelte @@ -0,0 +1,7 @@ + + +base page to show the loading state diff --git a/apps/frontend/src/routes/layout.css b/apps/frontend/src/routes/layout.css new file mode 100644 index 0000000..e245be5 --- /dev/null +++ b/apps/frontend/src/routes/layout.css @@ -0,0 +1,195 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@plugin "@tailwindcss/forms"; +@plugin "@tailwindcss/typography"; + +@font-face { + font-family: "Manrope"; + src: url("/fonts/manrope-variable.ttf") format("truetype"); +} + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(0.994 0 0); + --foreground: oklch(0 0 0); + + --card: oklch(0.994 0 0); + --card-foreground: oklch(0 0 0); + + --popover: oklch(0.991 0 0); + --popover-foreground: oklch(0 0 0); + + /* --- main theme: teal --- */ + --primary: oklch(0.6 0.15 180); /* medium teal */ + --primary-foreground: oklch(0.99 0 0); + + --secondary: oklch(0.93 0.04 178); /* soft pale teal */ + --secondary-foreground: oklch(0.25 0.03 180); + + --muted: oklch(0.96 0.01 175); + --muted-foreground: oklch(0.4 0.01 178); + + --accent: oklch(0.86 0.07 175); /* teal accent */ + --accent-foreground: oklch(0.5 0.12 180); + + --destructive: oklch(0.63 0.18 25); + --destructive-foreground: oklch(1 0 0); + + --border: oklch(0.92 0.02 178); + --input: oklch(0.94 0 0); + --ring: oklch(0.6 0.15 180); + + /* charts — variety within teal spectrum */ + --chart-1: oklch(0.7 0.13 175); + --chart-2: oklch(0.6 0.15 180); + --chart-3: oklch(0.72 0.14 165); /* slightly more green-teal */ + --chart-4: oklch(0.65 0.12 190); /* slightly bluer teal */ + --chart-5: oklch(0.76 0.09 182); + + --sidebar: oklch(0.97 0.01 178); + --sidebar-foreground: oklch(0 0 0); + --sidebar-primary: oklch(0.6 0.15 180); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.92 0.02 178); + --sidebar-accent-foreground: oklch(0.2 0.02 180); + --sidebar-border: oklch(0.92 0.02 178); + --sidebar-ring: oklch(0.6 0.15 180); + + --font-sans: Plus Jakarta Sans, sans-serif; + --font-serif: Lora, serif; + --font-mono: IBM Plex Mono, monospace; + + --radius: 0.69rem; + + --shadow-2xs: 0px 2px 3px 0px hsl(0 0% 0% / 0.08); + --shadow-xs: 0px 2px 3px 0px hsl(0 0% 0% / 0.08); + --shadow-sm: + 0px 2px 3px 0px hsl(0 0% 0% / 0.16), + 0px 1px 2px -1px hsl(0 0% 0% / 0.16); + --shadow: + 0px 2px 3px 0px hsl(0 0% 0% / 0.16), + 0px 1px 2px -1px hsl(0 0% 0% / 0.16); + --shadow-md: + 0px 2px 3px 0px hsl(0 0% 0% / 0.16), + 0px 2px 4px -1px hsl(0 0% 0% / 0.16); + --shadow-lg: + 0px 2px 3px 0px hsl(0 0% 0% / 0.16), + 0px 4px 6px -1px hsl(0 0% 0% / 0.16); + --shadow-xl: + 0px 2px 3px 0px hsl(0 0% 0% / 0.16), + 0px 8px 10px -1px hsl(0 0% 0% / 0.16); + --shadow-2xl: 0px 2px 3px 0px hsl(0 0% 0% / 0.4); + + --tracking-normal: -0.025em; + --spacing: 0.27rem; +} + +.dark { + --background: oklch(0.23 0.01 178); + --foreground: oklch(0.95 0 0); + + --card: oklch(0.25 0.015 178); + --card-foreground: oklch(0.95 0 0); + + --popover: oklch(0.25 0.015 178); + --popover-foreground: oklch(0.95 0 0); + + --primary: oklch(0.56 0.13 180); + --primary-foreground: oklch(0.97 0 0); + + --secondary: oklch(0.35 0.03 180); + --secondary-foreground: oklch(0.92 0 0); + + --muted: oklch(0.33 0.02 178); + --muted-foreground: oklch(0.7 0.01 178); + + --accent: oklch(0.44 0.08 178); + --accent-foreground: oklch(0.88 0.08 180); + + --destructive: oklch(0.7 0.17 25); + --destructive-foreground: oklch(1 0 0); + + --border: oklch(0.34 0.02 178); + --input: oklch(0.34 0.02 178); + --ring: oklch(0.56 0.13 180); + + --chart-1: oklch(0.68 0.12 175); + --chart-2: oklch(0.62 0.15 180); + --chart-3: oklch(0.7 0.11 165); + --chart-4: oklch(0.65 0.13 190); + --chart-5: oklch(0.72 0.09 182); + + --sidebar: oklch(0.2 0.01 178); + --sidebar-foreground: oklch(0.95 0 0); + --sidebar-primary: oklch(0.56 0.13 180); + --sidebar-primary-foreground: oklch(0.97 0 0); + --sidebar-accent: oklch(0.35 0.03 180); + --sidebar-accent-foreground: oklch(0.65 0.15 180); + --sidebar-border: oklch(0.34 0.02 178); + --sidebar-ring: oklch(0.65 0.15 180); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); + --font-serif: var(--font-serif); + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + + --shadow-2xs: var(--shadow-2xs); + --shadow-xs: var(--shadow-xs); + --shadow-sm: var(--shadow-sm); + --shadow: var(--shadow); + --shadow-md: var(--shadow-md); + --shadow-lg: var(--shadow-lg); + --shadow-xl: var(--shadow-xl); + --shadow-2xl: var(--shadow-2xl); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + font-family: "Manrope", sans-serif; + letter-spacing: var(--tracking-normal); + } +} diff --git a/apps/frontend/static/fonts/manrope-variable.ttf b/apps/frontend/static/fonts/manrope-variable.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f39ca39c968c9d921b9c9e6e737ebdd2c1447a15 GIT binary patch literal 164936 zcmdpf2YgjU+V{-dl3qz8gm5VWQbKwt0n$lA2&oW4NUxAaxPgFx0TB@q5LrZQh=>S? zh%AC4O+;iBkyT^`0a39bvWlz-lJ7syIXAg!5`1^RcfaqP-}%p+sn0w!^UR!ShjGSO zCxDjuj*5zo`F))JI%E6@Op`_>jZI~%ZsagF=1Im<7LQ6zk61nOm0pbX!Q+q^J1#us zYVkL4Z@+~x-EU)42l=m9S!c_b@PPl^)U@!l`g>nkAvN(!LJ%lS}fZRYP?|{83OXOUh>!zaD(`YsMFbGIq;J3pdiY z^rfArAF8`jlj9Kj3)5H}m!X{AxnY4+KkJtK4EzeKGnQ$1)iWVYadD>(QbqLRf;P1 zFn_TK?~T`5CiW#U!xWQ2YV4Rewx8ME5K3At(?o`W@n@vHcCcsc?cUDaDMgIJ;Jt<3Z`tcR#p&27!}cHF>|RC9Y4$h}m(BeMt36gr)dH@~U+&Q0cE zj4ivYn1ff1ZjejX>`T>L)07tElr1}@n(L73M^wEYxw2o?TcA{KRrQul>Ec$%pOvb) zHS=cERlN;zF;mssG6Nf`>g|{Z>!IrHp@Z;Zd90Gvuqsx~ir7FF&FYvD?|I1cDprpA z+R{aP=Be;s!OGh8HwN~Fu&;%ymSLvAt^_vah_|MdZ329iuu{;~nmQ9Ttz&PBt;zp* z#M3&gCi~`e$rz2`%TP85_cT@j+RE^5gx&{F8R+!^muf)WAU2l8umqL{I?G^Zz*`kK zYlJQpx?<>NQ0j_cqPQrBS^$bF;a*Z}00%0e79m6f+?PTpea9hO1HzX%R0&;E$ddA= z5C*_*9Ql^C$=D6d4;0GBU`AQ!d8 zh9Trwgciq!btud~Dx(IolG#K>B=Vq|&7@MJlwSk$2Niy72)HLpisW(g^cYwY+*h&5 zuq&X_6OK|L^FWp}S+4p*uawJaYRN27bEy{j<^x|<;HE6E&G|M6rskZx8?~Q7N=#=@ zuw!UN!uf3eH{mTti5X&p_(bESDbg&}Y}b6EboW><5nM7{b=3SI?sB#^~)ui3t5d(F<;F2t_LZi(GKyMNg0>|N~p+lSa^ z*jL*xv0rC@!2U}ITZdqWN{1I5zIN>IIK%OPlf6@w(?`yooim&tcmA={h)ydzediMH zvc=WQwa|6F>rbw?+^pRq-R8LMbGzjpI1rL9Z#U5XEPVM}%r-NsP=VZ?Xp6fif zdVc8nuP%3W$?NiHm$$q8=;Y@ee?)|&RcQ5b0xcj*t!+Jd5)2-*ip1=0G_b#iu z%I^BG_u$^o_VMoXOkdV_V&7eN_q%)H-8cKq==Vea^8TCppB~^cAZfre1I`R|8JIk9 z^}zK5zxC8H{5Tz-!cCn|0Vth{VxWD2D}hx z8CV&3H1OA;fk7ofvw{`|tqIy5^m@?2pwmHDf<1$Mg7box1#b?1FZg1JbBK3{f5?cC zi6PY?uY{Z%>^gYt;H85<9AX%fJmkS4Zx6K^nlSW{q2CP~HEhAKH-=pejR<`(^poLt z4L1%yHDbhwxg&OtxG^$eWX8zHMt&C7C2V9^dDs(S$HSe&Cx&l~u!u;D*bs3g(k(JF za%|+Ik$;Q49yKUxTGUHXUq^dIr$#>=eIaIG%#4_~M)e%^z^Fr`uE)m4J{h|)_RZL{ zaTalbad~kM#chf^6n8P+Cf+YTIo=q*IsUcy_v0^(wjVuU^x)A^qmxG8H+tFV(+NDm zDj^~vK4C(_)P(s7I}`RM98UOtjPsb>G1JGa8*^aHyJNmfbV>9|?3FkmaYSNFVq)UB z#EQf%-uTB0a z`Amu~B|4=rr98!$vM6PF%BfVJ)ZwY=sl}q{HU6>jpNv13;gk`X zF*>6#qddcyu{q=W2?Hm@Oqe?1#R(0WVVSX+Rhe~}^_hnzOw+uQuT~Ss$gcpwt_bb-YV==m|ply z;R}W53jbNuqbRFrX3@@~hGM7UoZ^ke=Zn8B{%7&^5?*3e;#A^U(xYTbiLqo($-Y1AGhJqS%?z5EIJ0EtyqQnU+&T03%pYdu&-(lx%X{v^ZYv zp8dw`4`!d4V>74AoL+MV&KWf)Y0iW>Q|8Q=vvAJJIWNw6ea_$Jd_A|*+@W(v&7CrL z#oQ<6?zk5{d!=`c{-*iG@r9y%N7>TX6W(#Hyn{OlZDf|&ZDcFZ`mA7~&2FZ?nN(?; zgg%7nR$|=4+PbCPN!+Gb7NAvIXm*#P9qZuTaFH!$OW}IDdaq|q_lsybO?NC(b^CiI zTibXaW@lU6hnaGxunyl1->_VkVRq-U!Q39Lc1*iBYqZIRU(-F8$-6CXO0(6tH{yn# zZvkeq_sG5CYcBd0?cKq!kx%1I6z77%HR>IK{=Mm5%H}uTrm+85-NBYL-sP;m#qEf% zA9)|=)43*Z|zHb&IuaxpOR8KDT9q*^_ijzD`G~ zB^RHh+$^M2EI?VGiU-gLE>lRwKtQB9w1H%SuZ9N>Fr|Qn`nyly#(DNpr&?r0guEcr{FRBBTr4hOObH zh+_|I5>a1wAjZLncP`TWF2yw)W*IO|Lu?r^mpPM#RIG-pciBJrQZiWr7vm7ZV7S#X>&jvi!G)o$7jta540QysEwdebB>R;e#24>sz$xJhW!91eL>M#wzJ_9^d?_YVky^2e8Dr&yIsEW*`O~GIX$@H zCc>4j1|UB9gyQMPjw7@vj&vaYI^-CmbTgFcLe>{%{n#$LSF=O7k7Cr|CfmU+*(w%B z*#sSd8-=&BZLez8mQc?@8vVSn2%tL_Edj9zQ?J*oBD~#!IsceJ(t3A^ZZr(C_oV;eO@w zD~x^q!{=Xpeje(lpTGb4uFp3>XLbJ7^PA4EW9wwq=`(CD<+FNF&72ol=xJfk?sWI1%8R=GUN%Lt2C=M zYw(7+Ffy5>mE)-`E-g|dFOv!#rIgH&?xsr{3cFBkm^2X&h(s}4q>E)@r+7=u5m{mq zD1Ao6i&7CM#);7)0pFj`iRVRxnP-Ak@a;sHgF$fyt258Titj3VAf0 z&4EPz3+l%csK4ux=J6s=yd)|x7Pt{3*!v+lkFqoDGj@S}#s0~DX1_o-+~Ayhpq=T$ zyYb$r6(jga9>t^i7?CZGiX5?7ED~eI!(xqCDxMO56K{);#9=-T+}t9{P+QK5cg2fh zxELWm5j(^oks{8CP(EJF#20joh!x4q3ANISxnb=OGLEyK6$ z9f$xMjKOM9U#!GB?uwc{05xY2kKvtpEXv~RkOrF|6W&0pcYt+;H1TF{vo4Ui-679< zK+gL?at(y!@n`>FccIk@Vdv0#o`tOV95VkrB-WQ0ANhifLfaLMam^U^BSwKPV$|d( zHkw_<=*2aRO#KRZ_b>GLu47E)CQE0x(5n27cB6q!K|45^cVd;?olWCjtd@H-Bk#&) za08pgd$O6l2dm@VSv|j--OKwzLiJ%Q`Ed3KB=-V7kUfgogynn)Tfv7xUWcPy9mSqu z&+|m~0#9Na_*hoR?bu=-!0zF_*gW1(oE9I8Gvd7X3?=?^ae-eE3-~|zPyA>83%@2@ z_`mqC!V?nDOE?R6(Mh;Lvbc)QJeTM3MLdhw^LzPxeji`J=RiK*&r5kRui-O!2A{w) z`9z-0b9g>4;Dx-1m+)!ah~7#qpTX<+Js3fm#pm&b`~kk4KLWY+C||`N<7@a6{7L>4 ze}-@7TliM~5`UR*P zNdK2GuD=zd$uDEZVhcuyK8BS2gaxoqSs!+c-HrCBAN!E?XD84ueZZp7-bJwQSsu4$ z1>BZRL_cg2<_@wkYmkFhC<`+RMd-Dba7R|komd%nX2slrJ-~h0gWR7z!~@yGJeV!z zA#54i^d&rqJjN3#6t0q_<|?%6rQd-!j5PUvcCU`$HE!mZf8q$bu6^a#{+hPx#(0? zo6DB8`)q4r*YaKUtN93FqQBYd=?n8D#08b(OGJ4ZKH@7IK81d%`h-Fqs+cwGgt}Q% z38{@on}zuCXwG6jrTLSuM{iFzpq*y{X!-X()-pWHBP>+A7&J{sym;3hhTi;H1G6?B z-B`GLlDV{ebcG1#-}Gn~GHVM@*0<%OorQSq!OK5-7PCU6tLed)FWS3*`(sgvFzdmK zV5n_9^)R2sl5`Q|j`Z^Vj-G9;-HQ##7a4ZTXBKFg1>VeRcvC5xt;(n@v$CAZGU{hu zM)|>pOY+fdMES&H@-HkYjHTiP%h${DtHjY-oeg^K^pRJ*=+|Uk)uF&2NbJD(m}4kJcM< z&HVX>h3el!zGmQAjQVZ_%~eS6i%6f8t+Jj%4r(3Mrx)s)OKa zbQ!EK$rvfaq$~+VI0#e9RI11CAr3scQoPGLb`f^vY>ECno}17wfqU>?`*g!4-Ro?L z<#gs^X^nTt2i zH}F}yQOey2R!1~T_mWoBC#qMHo|UW)`Z_&EK(wHR!k2uJ`ESa5sn3N!$T!&zNnT32 zD4)Sg{al#I{73#a%V)Tye1_aKDfba3+O@uV$XhWHYA}mJc}dcb!#6S7Hp;t= zpaVRmbb`Ogha~V-rfZ4zCd%mov}uqVnlE7{^H8SG7kaqW;ago>2X~T>%i-@V^9Q|k zx+j&q0IhYpt4bImtKn0zpy4BF-}uNdV6O!YGG2;v0qXaAaFYT#KLBR)q1NF!g}4vm zJsWn=<9S_sikCy(pgwD%uEZnlF5}rL^%|5#QHZ>8WQF2a#M74*YP|5whWU294}cx? z9d*YmZ&m9rusVxaJjE=D?}8jX&+3F1J0pDA8SNsrRs^yu^3hyDJb3tWNi$!siDID^ z=!1)+>LdNA?xY)yFZa;+vP?}ksNJA?K=oid+BF9a8a&#;C|e$i9%YA_n-OjGphpdX{Lnat}Pq#TM9=Bb*lIGJK0eaI+2mkVgGlq!}wvgou?jhghqd|SN5Ns{VdI9R@4#6tj-c; znEb$<41*sBU-4-6qg-U5T&&;)cxIr?pVPdD_Z@gP;wfiWLF<>&okoji0MfgS|AZ&P z@+MnLV-D7QyV%b(Q85W=d_lp?nsV-2o@cCNF5Jj0!ZV`{?6=62Gy^*|Iy}?Bc8(dF zU|=g##*Q(t8Z3h|u%xt<(FS~(FpL9%*9cSK%T>#CcyquPjuXCcIx!b~N4IuicUu=W zw{>BCTNhThbzym17y1@1GMI=Mm4Y<^sVNZ#q_TC|Y+Lwf?ZTpk3y5+G$F3O{oAb~u zFR$Fl^#u}HLEf|?_FG|PRRz0RT$5M8E|-^;ga`f9wer>B)w7PHmUYV)VD71L^~r?DkQX}*x?Lw+K!6^7*qiL{LN`#*DDDp6otHfj(k*Fb*>E^V4jWcV~TN1hxg14~6T1B86p$Aa$-_xer z`l|H_+B1HPT#6msm$M1wDL6&V;m%@h+`gjyx4+X&eeqi|zI zGRCyz=$whhQj8@|X%&mimr%X0-bMGD?yT;RZli9gu11%k3)S_|>9v=%$FXu`jdqdN zsLj_VVid5i)H+}{q zBO5U?Q;M(u5xf^hC9bgx>**XZdYxj zYO;A=+F)Fdb04VJ$VRTu6BsqS08>wx#FJ@va*1~?SBD}7gkb=3-diHO0ehL-Um|s$ zs(m+UBOZ{*yU9>dY7_8w2s$6=Wf6jsJ7>BWL#X^$~hY44AGkrbxD-k{n`R~#dU z+DO%8Gg;c`Z^5Lms;W7xkvx#$D7;W&D5Y>ls5VkHg_A-yy&xA>$YSM*V#qx15t7+yc5GL8pT=n3^Fq_}Zl|4)< z&0Xnka>y#a!rYba8<=25foPC1LdVxJp*zVH4obSD*;1GtMBhx(YI3&?W?S(MVWMBU zdlGeMO*2>f;c5|I-^|q(xH6i%60_kdpYJetB}yCXBk_XE(QnRtRcd9)6Ob&5+*k?< zWnL;NQF3yoA%#<*Cg1;xP4)Ng8a&oDbqDm+^gETz)sGcHkHFVGhgV{l8xdkPn4MXs%Q0PzH-fc z6*u#hBZ|y?#j8?E;bFFFa|TrSHuE(}6sUH{Vb~~ibcSBa6qzn7+#0mh4(R|yc+)1b|sBo-pw`^6;bi>IT+^hO+BkjReAny10-FFY0a6;B49;Zm-gfsEyd z{S5DocDfWZYa8$`%Z4S~1Ty#AwE6X7UQ>TU9EU&m+qC<$#63;@3vswHHqK9=*JX+u zG&NiU1zhw&?Z@YsN|VxVNxo^tF{a^M#6d6H29r<85iK9b$IC~$!;`@_a+ylmU+aZf zARndVYLusvH1-p;F~w~^a?34fR`<;(aX{x(0%-{D92yZk8s8-I_#&yVpB_;LOrKf(Xb zKjI(rlL*Njb4mG_Ln>2eicIrFPh+O&S^gY<9{rw|Fkd8Ri?;K6-<`EZ(d55r222=)kOD7Rr=@&}Z`ARZ}a<5@6zJYQn2eiYWL#IX>p z2N}&1_!vn1kI?J+7&DM#*-5Msk#mEoJdLN5^ibz&F^fGEGrE)HoGn_2Q1(5~Wp7}f zm*#KTa9+eNLY72f4zmQ`7iD}hpTf&AXI;T7c@?kbQ&~CsQIlzwSMJTIz<7sjS#F_< zRbk$D2Ij@T!^|&7J5$ZE(jW7`auygf$C&NDm)E1!n5WJr-;X)-cy^s;hZnKYn9p5| z8utl$+|w}|{t#b+xp75)EN5d-vlG$l8jBg^mHaQ5e@|k`G?$E-Vpa!?RnkQ^KM3w$G5usN6&PKD%3W9j@w%q4GT%&0aT2JIoUw zWnU{2kL_m%_%WjFY>SXH~d@v z9sizR;y>`q{6~I8ox#5<=kVnWKEIAH!yEi2|DE6B4Fb)j5JDrgLMQaXLRbnbVJ&Qg zt*{gJ!a+C+C(QPD5-ynOcf+i|hv=-X0ccBlv{jG&Aj1M6*McDHLLmQ!Kqd|op|t*E zqzDt?B0@xpC=o4Uu>K^gHt7<&ai}DhWij|mgUnL&H%=_b5?eYX>;-3;vV@1wd z@vK;fl{o83LcM^Q_!q?{tc%%#wKZGC%VHbbgB2jZ;Jeic{k5)W9XF$mc@FDJ)?u~E z0~puQqm>>hUco&6t6~RMB-r8`GM9~DVdAe?{qq{h;MbWWzRI_=SH&I{fpLqyVjsRx z-)1|ohG8drP3%XXYZKdxbv!y|Ee?o-ST%GAt8orv4bBnqF4~o&Se+x+5FBF*uv*4K ze1N{p2KF!Up*SJ_j#WD!V;#`_Sfz7{trGu0TmK`w!getOTZTSkH}v*aqSyBVdVK5A zCtM*uWlyp->~Z$IIE{6Qmh3QAlYEBNKIgEe1nWt}7m&cau^!0^J;sIXA%=M^^doby zB4<83D83RG#n<8+@vZodEkzG4l0A$SHK(we<}}tH{R1OMXW4o2J;phH(1+*O6iqL( zsI4sXiw=)g)QDhJ^;K1WRSi_saFZITzN_xT2dnSls;c@A_qPbI$SbI+sZ1zs;0j7jRi05FO>atA5h`twYFv?O*pVu2 zk%89Hjp3+XRB|H1Euz)vqm}4Y`$(HnO=L(jUq63;>rsutXk+v9YHVVgyYY*TRFfAu z*dk61H%c1HPz83`c=B4qwUh0(VMQaKV4zJF1@C#vP7RwnV=u9kUPGKT&wYgWkoeb z)5@ml$CuRPO)s*U&}6BwEGAGtfvnV5mA^5A^_di$b!KDf(C3*YC_F~-5D}uPeySRv zszHhxZc?Mvch!CP5cNGmRaO7t0Ty{`BJsgKELxa6qIy^b$mc|^TkDTwJOb+y;%I_QKN6CPyNRU83g%^Rosyf8v zjzkIE5mS5veN}T`lYgqhuqR@ml8}K)?1FoxLU!~7qrOV$$mnnQTet(rdf0Zs@lRFdLru3WQ zQR($n!}nF=^Ht;VRm1T&(Wl1e>u;4l%~+FHW-OSgde0V;=r zR89n_=?GFe8lZ9@!1Qi%r*b4fl^D@S}H3q3goSt4s5=5kYQkf>Dv`Uv+ zR#K6tNz1FXNLPZ=B$bwFBJm_mD^sEgA7V}M8?nK3nynfmy;vef(pbp|6?-*&uevHprNrcpy zh;uT3QLNP<=*2;OF*$c>@vit#Nun2PicQf{!WAcKV(G;ai6xE2tfHDKA7il|A3IVp z&X8}G#?qQ1`EF5MRa--9*>tj>R#qqNr{PnqQfi9IaQcmOP+3L+NPhVH4kkeal>`w~ z5=2l*5J4ru0F?vIh)bK*o@Iut^ zLe%g=)bK)vSQk_wZ+zrB4u_(;0%UJt)ePm`X=-iJG-Fv+Wur-_>Y|#mszUj}hu(N0 z`LeIht0}51FDf=FfzWFWaX|)ZpO4N|QIj>jHd#B$_bEljMh8x+L6Z|IQNH0MGW~u1 z{A@~RR+oa=GIX1w%EG*9rK-N7QLk{_FUGl~wyeCosG_PVf=-P_O;M0DEHx>CTQwcz zjEu~jDKY1$1jwFGC)KE=rYH|Qq27>HUS*XLNiI`GNJw;-Mjqu9`HoCTkY8*_|99NorysITGLgkxM zQv7|xsY!xLO$$_|MEUy#Sr%1P8)r@{GAay@2(mRLN^;stQbT;U$g8QTno(PAO)7E6 zinOwrC_2eSd-R5D%JNE3P>i)Tl@3j=DHMBpZ)7(m-On%BB2Ots_61edGn+W?NW3uP zJJA@R6m#EzkRTmZUOSUapf}Iz8Y-7LB{jyH9K0yu(E=r!N3g~SHO_~U>&}>qTzAG? zlyzq`<0Dr~(Ok%sqMAyK`Dii5ju|q)G|YX7-WeGGWdpYM`G#f`;h@>&-{(TJ zcf2&@DkcrsUnw7Xk$w@>L&`g2ea?{iHAOX2ZJ*n`rrCpo;k#GOgOUBoct1Mw4qLT}^MTbq&W9GZ`*DgIl{XH!;O;Zgx9;OTOODeOvYo+xnbr%e*Z! zQ<~bc`%jrQ=7;2CW4(sHSv_w*_V)PNo_YHr-MJ0zQ&+t;OUI?AuuSP_Fq3etX-o0` zcgTt6;l`b4qM~h`IltR>(Z1>b#BckdDmKm2aXZ)lckLT4v~H{ZClttbj%1OPMk-tfBeNOR&(sN#ZTu(X%$ntY`GHf@f{p2C#w5w^S<%_S}J$(5!l-nYJy zYU@j}SIkX%Q=1@7&E+ZPh3TE@gPEMgJS$z~&m@~+FT*pJ&x)(I(G+F_WU!3WwnQKXvwX~P-=5Ayjl9{=DRr8$8uSry{cGCy>Y`Umyn7y0K?^HGAMN5B@_Qt$x7Ec_d z)jVycvfC<*cIrUGrKUbXGe1e{n@MeE`NuCU(_y0hk9{?_ZOadpQ8QjN_wnz4Ym0_I zip4x1OzzqW!(`UXeW=MtbJe{3HMi;bo2mYH)CZL}v-Y{8{{B1MwNsu|;>=~RIsZG# zXLCR1>1|JrnY+0YeOvLEn>E%2v%ZxKzrDOR_uCOVOzur;NBo%FnEN5wcxcSw_T-ZE z(^wxim_lf-%J3T9n91zsu9UZigHl!GqSTq=4Vlp`vynFwTUL# zmUrY*S4=-bXjNX*kyBc%zRx~naYkCO|pKN^Fz9Ci{|#e6WelYit|6An$v2^JCj*6TBt|U zngSCaTHAEoq$A(KG9Kc!>~pLA*#@;fo9LA9CfZxZF3ssdUXj|4EHL-i7_Rgq!$#i9 z%RCMfO=Pb2r^%5SnCLV!YFBHDqosrP%v-wqZ-4J-c`@<6WqR5(Z|Uwo|K8{5oy8I3 zthn#^_|Tn&-{iV29+{U}lS}jZU^4qps{hV1+^QV?iSLO2Xj->HPAuxvtT)kCz9=*_ zwf*KaQQu}!$IBVwyTdZ6#ujQGE9eI8cY3wgp519U|0(yTw(XC?Z$Ez&UmXdfz3^`r zc6}Y$31iq8|C9m6y{141K+J3Y}zbRNbW+^ZE?PVSwjb-MN)gOY152iRweOJ>r zTU(yZ<7vyjEi+R*ZP}TdHEe798_L|xAL%<9&mV;$?b@TGt+vEGPRa+hm25cG)L#A> zp0!1zDK3-RmYep>?!-^Sr9Qiwh0~%zs$`| zw3^$S%37m;p=OZ|S;wbhgZl#mT zKgP-0|H2N!>p0`*AK00mjPo8ca1QlA>}#HcJ=Qrmi`pOimrJm>x*Ydl>|LIUv-9Q2 z)Wc~%@<=+#I-J*ICvqfCv|fnw!53l2aV+*5KZ4!VkK#_o3Daw^hx$pJJ&=KY#4lmr z^vk%*aPq)T?2F!oQ>m+PBJ}};`xZ_!z}ay)&0qmeGx$5g`xtw9AHZJT&v5?1Iqc+J zhF!cD5$bo?zq=CqcmIhKuCHR}?h~|g_i27Zc=5H^vEQ8^#K{#!{4MNiozCCKZq_;c z7LdJw+?C4zjy4Z3vwRZ3!I+EeHir z;OumqxPY~PopF0(?<@Xui9M@w*Q&e)awr5i82QwP@=1q$io-7XROE~u<&(f^^HbqQ z{-L51^64Jn9NZ2#hiWeNmEOzdK|de+NiDFebPZ1KcmlUQ_LDw`-J=z^?%0hTrcd;{RJl1*snMT;5u%ObChmi$L>um zhSZ@vx#QFfoaxA%a0WnU)(a=%doo{~1mMlwa0);#=FEHZeyl4_$j4a@*sUMQY_Kyw z3TOL8#?KW8$0UfK|dd77wJ%% z?`MuEd)RG^GQ9{cALI`*E0pes5W*7ds`o)De;DqU;S3io&ShWD^eFX@u)A^U#tMXh z(vOt>1-tCsu)}^8xbYbF*jvlp^+@BB{Asv(2K(#1P%EB=?K+%w#BTe&u-(V^f!h6iKTHnr1K|8Yeh}tw;S6|=Q%2rK?i}WanJZ2k zc?aPi!M};QJaq&)^*8=ExO|Vl2g={)@53+7Ac4toejN0F$UlV36Z|8%l&6vCuq*!) z@Z=Of1umWDr-7)?IHTkoKL<{p=jY)DKd68?>NZTiG3-&> ze}w)DzXF$6`BmtD;lIGmHI6f3Q2&30{yIlF#Qyyopz0>S3H|Tw>-gI3Wo8`!#G3_W0wEuh{3WV>a09uV>w{-`@hKl~@W()&=|it(c$O z`wtUaVao>M1OPjPU@vgC6;1+hz)8oB!VxA;!U-n$2LSXQ0%sxNp8#E0fBYA~3$a5s z;e;6M;%EKjK7Q5%d-gMcSpsKv;S3?Cz|MGif}sYIM9MM`-1a2T9AMi6=YsS^Tcf2mM@wyv7s)btK1CeXO2^}$4V`f6 zL?UE?Y=7>8T&qB+mAJKNg{l#TBI9b%BK1X!R11{y&Yk3)JDZK$k))nANxhyV^{maL zo(D<2o@P?7E2Q3=*uB4(WZ!=5?YDvCQ)J&e2n{Ft<1C${kZCx}2>0D2(K&yG1p2$;6y7T_r zAEhmT4~EPd0_lbm?;zc5NxC_cbh9Jr)(6rp8FfR-H4VwN9{3YPCd&FmJ`s`v?JzKh z=b^+)N!N)aoxN<6p;u&FCC*jrL6XjaB%L)$Ix90t*HgCD2veRN(w`)q14%kTl8%$4 zvqxLI5Vc53I!9H~Eyg(__Nu(Y`5}l=N;>?F8In$sZmuNVT+O7L9@1?MWQUY)oTQrp z((O6ur7Ytl%Lb4vvmsgLO0vw3q?j|L*h_GO-T`ns&J1ycEZYG+{#gONlxQ~1WSNv= z+)Rqu;$)vgpjpZ3q1~)e9S*aTqor@N+h#1 zI8g*=JK$82Pmvc=erZU4bv2V;9H)$Y4*p0frZJOZ)~%#iPctc|LGR_C9g=0XB)^--AQsek<4-?ndJ#-B%8XCZ1N`A)Qx15H_4`MB%54FHo1^IvN4lK z?j(;K&=-3Q5^%Lx4aq2FleL*_!bxtB^-+wmUksnhyW3So{bTgn>9r2`BIWKpz~dW_ zk30_Jk@sG+`>@Bh7WXQTj^9h&uQYSL)Z;d{`*HV!?yooA8{FmnH15T?k9Yj`SPHk( z&F)I~=J!Fvr!uXQ3sAQq?c1atrmNhKD^w!Z<9L()qNaV1f|H0tvsJSdEr&e8O`fPL|E%nW_9YQ5g=|^8@NcFn^sZ%V;*IuV z9-XxE99pQ2kpBUY_Fv-cJt@`8NrqQ)^g?lllI(FVBH6o?r0yz`x{r~hT}_hqagwoX zNV+~jXEZ%YJ;kR<9zG+#Y1mro!#z)uZ6o#4UL4lrfciQIsh9H>^=}Tz zbBWm7BnRH1x_<x2!iii$2F_b`GZm zjbj&t>~nlUeU2}wMt?_@80 zSE#oBlWOZv@`N4mor@_n4xh^Q(|7g(!h?iw5gsCZoA5BOGH;tq9D`|Y6*3OdO{0AOF}C`YeE}BTS7ZR zdqM|7M?xn;XTnZ|E`+XxZiMcHo`l^94TRkZdl2>{>_xbW@L9t3gf9?oCfr51n~?a- ziNl;Y%!#+0xW^R^+$5K`BnnEep!5n#qo6bjN|&It2oG}EnUH7}aksMIH=gieX^ysU zmmJeVcoH9`@hzM^P31ulY6!K2Izm061)(LO6`?hu4WTWe9ics;1EC|K6QMIhRgqsLSZm=z+e~EA_;md^E2wx%GPWUR}4#K|@?j(GTkmL{BP5Rdf-yqyW z_$J|A!vC$B_y23V&|A*y;Tu$9U&6ZymwCZEUJLh;`#)}3{#;$`c#EUd zLZzN9X4DF?E>xzx2)zk|2!jbj2r~#L5M~liB+Me5M3_yOLzqjLN0?7oKv+mvL|9B% zLRd;zMmU*p3Sl{61z{!ORKht8xu}u%64n#WBm4{DD#FJIR}(%?xQ4LfwdW|sb&T*% z`%wRFEl@}LM|t0ZT5HJMK%{XL|=pKuZ3LuCHPwS;6I zKS1uy>xa_kdY{Zcq;Sp=UL^dE%rBAtN5ZRw*9aB<-yk~F_ml5KBLNGZ^i2tQQe?_7|E!P<_iYoQJFuyN%r90L${4JK#%%33C z5NZi^gnB{?LQ6s`LTf@BLR&&RLVH37LPtU;LTAEGgf4`xgl>fHgt3y>Xn7?nwS6wF zL};td%T@5JXnp!)gsTZ3CtO4L1o?fEa_%X@rwN}STub;Y;X1)`T{MwuE+s_Jj_Ej)YEx&V-!^T?kzX-3Z+Y>6?&uL^dmB(bVUVl$)2HXDPIG zl)Ck#e}Ql_p;ESXlYSq$AwH|$w@R5iLbk`rmbi+qTp7zv!drw5(iTz^=IHrLByQj< zSL&7WM`_2mt~BXP=t)TZ3o(v%g>x~k0VNh$;7vVioaRqo2S%);v4=UUmuK7eYB*(a zOh^gfBH|^&!X7rgA*E2pnIO~_7bVE$0Zl46OpTf@#E-QdDx94`6a0OfxUm^&uDp3s6&`oX+Bq5KCI z!#t0?YzgIGzQ9{T`6n*4282$8%8n2Dzb{64z~sN)aLKVnhA~B4Xn`bV6P6J!BZTCH z`DsF=1NtixA)n-B`0r_Nnn`&@zAG}iwJavdi#(KR(;)w3zQz+yX*i|pi(E3w?*`aW zpGX*~*34GjPm?OlBr0Yc`Hp&J=HA?Hnyfu22h%We3x5={PL>C@y;=Cv)evP!9%{=K z4YSoehHw;NEMXjBJmF}<1VVF(@*wudwe9&wY7?Y<0AB{fkL1q=r~>1zk_RSkB{U?M+<-5zL7ARQAwMCJ({~4_ z?*&dV@;PKakC48B_zzM~GDha1%gdWVAMJ=Y8@E04vbm{%nURAL5$-|G)tLm!8PL; zMk0Wl2r&`?Jw_scFA-uS0(y)@05K8)#7G1XBN0H1L;!aXVk83korD;PfF2_eK=jIi z7>NMBLAZwyBM~s!OSm7!Hi;b|JV^K!Avg*1w+RmuVowiD(9;BdKzN++LqeQ71M`mw zv4RNtPYEG8pg%*odtK`BKL?4Bc|gcKpq3Ca4|+WzWFGX8c|gcKAY>j8G7kuu2ZYQ6 zLgoP>^MHa zjNG7Q*Nnp&7a8)igzE`kAlyv2n-DEHToRu#8zS8k5BVtyhx$DH0_mxA^DCrRxObD> z-;#)x84@ie<(L7Hdg2e}L8PAeAUsIlnb4E4y9^EEc*uE-TuYoJSHFrJ!aTwP!Xk;d zEbArO0kx>Rxo|s8ZSBEv$Uq=kFX9pGW#}X$@dpe}>t6+-hEPkWBh(XG5Lyyi5n2=4 z5ZV&j5!w?v5IPb%5jqogB6J~iC3GWnCyWJ&gJn$wZX}5&fHgr1T@wC|UI*@R_; z%Ltc~?J9El80l9NK2G>7nXe~&fp9Y+Z6SDG~orpD})Mv zZj#Ge5;5BY3em5Ydg1{3)lyHX#_W;Qdk}Ue^d#&qLqpqzdWp76;=?i=Tv&>S^+Oyr z`#gGw5;cVQhdJ~*LOr1cp_BpV(L0oAO=v@COK3-EPv}7CNa#f9OxTIgh0v7{|1n2s za>n^QdWRCT3CjqV5uPGEO^E+_!_5_m=p9Ndr9PEOR^%dm(@A>Frd|Q6B2uZltt+$_ z-~>>2mj2Jgbmw7$J`&cH=Hox5Z?kKBB-Z6@!&;RF{3oge|515Qe5bL{1Zz?>4`}vj zu4+4L$7=7#dupTY|avU-W+ZA^Jr96#WwYOZt=g-z<7rV~zwbsy_d*2&fd)^*koS+B8v(fW1k&#b?D=5*Mp!P(!r#(9?WeCJ)xS32F*DYMh6P9M8;b}4r`?Ap_H zh3k)Q$!<@&ZFYOp?UdW+ZnxZfx+l6XaNq8J!NblY#KY*Z$>U;YL+8xSFLb`)+0Qf8 zGs|A7nqsZIJgMzd;d$iU%17tsnH> zpznR{d=0)4z6rkBzNNnNeOLLO^KtC28IRJ z2EGvZNswnyTF|3GCxRV=^MZE;{}AF8GCpK&$i|R!gS!kKJ9x(64TDb&aTqdo$jTv~ z4fP*7Yv|!&F2e$cO&C@%Z1u2rhy4=TJG3x#b?C9shT(&T*9_k}{KpZ#BdSNdHPUNj z+Q~b9hMjxbT|rhr(YA-yi-__~i(@h`SmO#`K9PjH!sJjoBS@Am-OmKBIz1 zjTjX(YU-$+qrQvn5!)|zZ0v&AXJa?UZj0R?`%au=TzXtVTwUCvxLtAk;tt1)`0nw! z@wM?!#J?7QH2%xc)}wok4jDaSbmHjr(OIL*M;k{!KKiTCml7Nk?n)S%kd{!Kuqxqb z!f#_D$IKj4KW5vQ3uC?;^LwH$(Kc~(;ysD`5|1Zd8{2#A?6JGY?jL(@>^Eb7Od6P! zom8AOJ!wwTf}|x$k0!mFY?YjvJTdvHGQ-`gPjywCJ?Q z)Bcg(ExmVoN&1TPHR*fOFQ#7}H*8$QxN+lFj5|Kwa(u}6lJS+}Ysc>#e=EZ`qbQ>) zoWZ7rAW_e}x z%<7lrmo+!*{;VZgk7ljOdNylg*2`Htv);`5=OnjDJtxIYGERDM(vC@APx>X>FS{sv zTK10YvpKzU{Bz=R(sFWgCg;@UEX+Bc>zV79J1RFncX{p;xzFcr%iWu2ndg`{FmH6; zqP#cq-pf0kcL6`lw9FrxACaGupP8SVUz%T?KRthT{`~w!`AhRx|R)2_-x_FMSY4&i`Er= zSImp=D}JT;M#-R(DJ3tLoGtmSv`cAd>8R3-(%e!b?zyGQN}nvr<9k zmQhw(wxjIx$rh8{CI?N9o18Rx+2n&$ET_0m@tWc@C45Til#(fPraU@j)0DT$?v2t$Z{gq2A*Hvz*+*x_J@1L~ zjwMyAs$Q)+QT1unL*&$|ngAIlVY#>SvHc1Uozdet^4F)_QYt}Z(PD`GW*V^4Y3p381`|-xSUGq;g+d;C8)paRmNq z_XK;&cFt%|8&@0rEu~NA4o8}d{{@cVWAQ&jSN0t{$R5KN$Y0p=SnK>g{-$;UE1$8C zqZvU;njZs2F8LYp;loFaj!U?7?%N*y2Zu*Tg$@pmKt8Nzt8GU1zV+2tWo2VW4s>;m z!2io0XZ2V;)=ps&g@wi_{J?BA-OG7hT>_T8S<4&C@h;Malatt1@)Pb?9Si@kXc5PT zfIhxO8;&r~YvW@g+KX6!AvE;Dg-aKjC6PiHWfp=!2$6gk{&I;m&@IYGQ>C+3!)jH9 zD7{h{)(>%vQ?N z92x)17UB4T-52g-;-aUPOh`%?lbJE`#^qoAhK@=~OC3EbIsygrRrW%=(K@QphK45O zR##V-CM6}{r*{|VK7qylTj}17#hynIPiQkTD369oDtvsx@w>hay33b~i`y$U78?zQ zjq+(7Mu1s9w@ax;W^QZ0e(ozA3{w&kN2lnpWJ77$140fWegdBL9?uD9Jm6_wh*nG{sh3y6Frq{dN9T<_G z8o{i(4vx>CJ$v@#TrXFBYtD_vFWo&Ap^hIp;}TrMEffPO@%2H@w{G2X&WS>)MmvFf zo-RHvKCXGpimcaNv+;?@m=(sBTF%Od@Ue-&rN-DO^JU6h%Jb7kkBUuC9e3^9p9cg* zj2@d9gYGL^i(+c(?h;#oibm>9OPah znI_X!uh|mg4579cMPd(kNs3^{@%4C|Cncd|jB6ODozKuXg*A-h#-m|u(>$e)6@cb> z4&F)G2?+@W)yDFen3(Zt!C`E_WrWlI<;$1vYw!$b``LaA&wuS&v0}ydp0KKCvGeY; zIeX~XI~RYts(0}1*J)lP+d}tW?204YkF8s`E-^9DaPHueh=w$IE9G%4;u3)Yo#$J3 z4hZxeleKQs&w+(e5FrP#W#};5U{x6S^QLuKV|-cGym{K2HrCOIyH9^i{w;TLz1}o-Q_U0Ks#V#2P4vl zhWg9)2A4e|UYKEzk^SN9IL0WT8rBsD)dJqfo}-oSEtE9UndqyF9${>kCijeaq!wyjGYf)BSQ{aT`cqk9Y=_p{6AOr% z$Tsy-T80pdbc@wjjA86O5$x|1iA9osWjA=mJR~(7iIzzwRTCP@Lg$%x`{tAQ)fkOO zkA|ZRZBjXh_S4aUM7UYHv?nIFO_x<;zpo`_@t`cM;U50?@nP&`(b?505*@K=_(6RC z+tG$jLp!up%hWD7lM4>(dU$(IpD;5T0{VA$1J7q{vu4(m+9{D}w;#d%0Grt=A~54V z6DQm0P_z@X?DL8HKg$bayFGe#>oRKipx!-t`ug~Nc<7jgW7ppO?(W{T%ROQEAeqxD zQibz(*xzeK29*~bsF;UrRN9xWj9FPWHnf_n&WIUH`gq`}bG5wY{B=H{?|0wG+i3_pDk)M7+g3r(UPz zUp-5bd6y)g`mly5p1n@te+g*O^cWW6HDA+bc#p~Q3vPw>*l8Epp?QwFGe_79j)j4z z9$LIMG_-t5d3kw6S5`i6p4HFaUS^$~&}%yo-B8(3+K|(bF543A1L)k$i9k1T3)^Hp z@#`&H`t=*qb3kXkQU(_xzD_-@KY#b#F=GOHI$gg$XHH@0pNPNJ5Xqk)NGTsPL6La$ z`>HVZj^1zJ-G73X#=gRz&0|}X65Fj=eXfsc za#tTNKfHzH9Y3=eEl@?|_!`1T1390<^aPi} z*yFrUMHo9bk1Xo(y)uuw;}0nD8sLWS_Xx-K{Hx;sF!w%iQB>>y_?g++Wr1b+BO)Rq zA}S&xBBCPVvMh**Xo#qYWQJr$WQdAnw6l;}*UY?TX67X`GcWluGczM2GOpLWUNRyw zQ!+&&Bt=C;-2J}K%q)WWXIHtu*N@@f?#`U&Ip;j*InV!-mXh4zHZ#dccVeZs_nc@U zy|HW_5w~IQ9o{<(Vta^ckaNO%CN9u?M0A+k8WE#Af11%OehT@9u>F3StRb&=KtoWt zB^<`$4w<*C-@+&qPk9eND{ZIwcJI?YV$7If{W?5zH}<_ZU#GxcoBP;Is#t%F=0EAs)V;Z=*aO4_5LuGHGmzg$b|{zV1{v#ruBxhv5j5)! zFhRTlGF!>a$NaD7s~X?F!?-;ni}H@9zT+_i%20G1yF^V zU8avlLT@=)fuB->o0vG5a6K#JS4SF z?Q60vA)~@I zS)mSV`epaFZJ&L9@cY)*)>FF#3yRn#hHwSXJE@PYTD5A;Iwk#Tt0+I=hrSfh2E{p? zg~nV{6Ln)oj*5nRNU#*%VhV3-Yd#nRs|g#W-52 z(^`x30^rQJL*80T)+(y2r%bUJv38e-6(B_pHwQ4WF~PTI`7j6$600Gl0g~d!*JKkc z?v?0Hv79``9u)$Ptb^;-p@ZwyV$gRJ60Pnv_1zH{>grtWFn>mv=a3=3p6)L879--& zeC?~P|J?E4GMO!DyLR1b`23yWTe~ATcSeJn2dzLw*xz3OCcI7%XIvh5IGicJ?2L=G zc;w$25$6swVijfHJ{;8TXDyv>yM>3k10D}cx$4fNa~G?h)!2S?3tMaHxO@8431fl- zhlYoZ{iWhWH_t)Ap(BP28mNWE@^$i(9zx9n6ZyU6EKve_5}32k+drZqUn_=xWwXrU zrDsE_{6Q#vHXtfoU6Yb521Q-82FnRZSWim<1+tGa$v_7WtZcID6_oAc9nsrAl^WNR)O@ghm0qvE zB~>e0%KXwSo?*rqqCuJ0bZbX>PWve8ZjBzBe?D~R&;_69h@hn8 zP*+#ip_6CKS+ESNgWJEax=&IJc@dR?tA*y^Na1|<}y)W#2>y1sDUyC$7XIf^z`N##GX?kA$Y1Pd^ z5xSJ*DH9?>dMiwY0+v*&){xhjKJzr>$NvSoqUPD9IPa$PwKpyykw#;#m8<>-gKVa} zE*or7lC7keFJJUz2I3r)%NESg>2wogwJ|WSZ^cZ~5Klxz{x4?XE>R0zH7#QA=)5qW zIY&YWxL`AV>g4~$yqJR^x)$@#f%)zJ4te7L0RK+hq|1m)Sjn%T4?2X>Z6QnP9;aUX zeBSilH((_jI6qV_A3Gj<=a)!D`HUPOd+DV_gqCeLpW$(HwO?x zyRnOY!1Jia9F48_6W9ajwOW}l$mhy8r5>AK)0$G~#jRUkeBdB2-}+_!wc5r4n;Tbs zd)PX9^%VJwy50KD?IV8Ke(-HVI)U#*z_}Z>T?+mK#Eex>5$=E^(0r3{54E~#OiNgm zlPip*vczNi_VIE*zid<}=G&tZgg^i>8cT$$fQU$~w;q0o@fF&EMLI~K5)Ktr zt-ZY~X3X#sAS(r574=k^5fGn@1O}|Ho5Bz!${S>ZySE<8eVUvJO*Kv9^N>-`!RW+)kLGfJgkcT*fb ztkK-3^;D zj;wdc(K@3n@L)ebFQ@x}xvQPSGRThuG8sxke?K64)#911YPFg(I{MuQs)LCf`Q*T! z{riZC+`MtaveiK4(>uYkY=Jm;1}Ug@mi6%|faMM!o-@b#ehSu#&VViJD}~wKSEpO! zY60Qg-cf3rxL-$w_4Hatm=3pKJ{V!CJ9qnTW(bQg+tLxUyHS}>XD0jj%!%5Faq)4J zF4WZb3mC19i5@$8B#pm*kGx6aX00GWH5f9vXJD|Tg$kaWrSgdy@`Yl^=ggw}tb$wi z1&c<3`gIi?r_1AJ#8%NLi8_cI>*3-gD^`fy zI*{Jc+<-_zW=f;@I}&osPNo@#pPjh7h&n|5ab^U5!(&-4c`U>6oW`8cbe{%nWUMpv4o%$T8e~3U}Ay?t?t374M|87Dc{Bb?-MA+dDrZr>y$cVmd==BY?$f`$y}rt^Zp%{pSPK z(wLEvBj?PXJvI7{y>8#p9Xsds+-h3DXEs{akQci>_jLN4$Fee?ab#7|3A57YKk?|S znOeyA|GHb}?qG7Yzm+0!8x*WtNbEG&0KpZ}rVPX+mosyq7?cx^%JpzlOR|0c_UKFT z?kEiJ1guM@^J79_a{Hq~Pe!39vdzJvVHzY^<>hg4LEh@-=DfVeMt9*@{#93b zc*Mn7FQu4!$$vR?C^k0GOLgT+ZtkNaAHOsbnr~N*_Fp>Co;}+IWFq$D0D2&~f5A zZ`LH8CNd#@@}FlK`Uj5D#f~30W;D%6exJN$uN}nH)n#TTYJ=R}wFpApObTSPx^Ms< zGO@Xh83#^&2|eirSUz8X?ejfcZ;@BJvQPI5 zC42a8io56;VTN$JX_AH%_jYqphXwic?C#dDukWExziO2^_3Z8G>ZHzEzOV~dw6!5< zUDekj^wo~zmM=eaC_KCi`_rz#b^-B;v$6@8N^89f@f2;mT{W5;fuo{+n_FsEVklUQ zy9&dDz){{G%BU5GS6lEeFCKiTb&KXou^GO8zr_8${7 zaZ;C21FeOkU|FrU3y~HqY!5%s!p=m;xu=WikF#gbUb6Q}Oq`yYnyMQW;O?p!>(hlO z-z^8F%f|UX=!e%pmp_00`R6}f28O3CTC^xFeBgkA1BD;_>B7!fU}fy0**StkIZk`#htuwT14oVt3l0pRX>Oayny%co?qFHzP<*1xW~v%}Ql0zd%kA5@ANcR_ z(@jlhwWcw+B5mKTY7Lt^cW$q$cOy-Ym`13tv|Kn7Wcp;=n{RC1)RLX2Ltx2!qS=C7 z&~mlFPNp6ZR=MG=ZB-tVsTY5vYn#2gn{Uwg_^FABXDZL#3<-JjO;U%Hg`3S6n{OKA ze^wnm>iTajaxHPmR#%WXtr(ReUmD@=s)Tg|o@j^J0gnIu;%xn~BVT@f)-Fy%^t`>j zvE_zM&%wco{#Q3hwh^wdwDFhU4VdT4-`DIoevKTBi#saW;nUGWXI-$5mzS4!zX3rL zQE6bZ#%QDoq$Q+?6xq37K7djKpVX;GF4NLCNRccroXi2H#+$~rn+8tC8I7g_cHKJQ zgo-HKl0f6JRa(J8aQZWIw|;uVKQ#)i(qbB4 z+b;2U{B+CQrBlfaw6Cxs*RC6wW80!l`KAT590;y__6V6AB@P8y%jTqoh}KbRqhk|J z`oPp}App@(V;z*o0Y$cUfPbKd9CFF5v#clW5ii!kS`JwKW5x~-N{9;afv>Q;iz+!Y zCo3y!nq^?`5}0B!5~74ado0PP-H^{_8R&LZ%3@#we@_D}rlm!Q>9=$e#j+Zfl8V@; zM`@oty`2r+x(9`g)o8Wah`?Ci+S1a}^Mev|b#Nz$X^PvGPnVW%+cp=a`aq5*qmh#t z0a6+(ZW1soqwFSU^v9XvgMU$yb=*WNC# z{qMoUzxNt7YE)1^&uE-uc>p3AC}lB%ujzBtZ8E~IW`4}+?EU`W)o#K<*k*0*XFQ^yD{o5dlyfI9L;vfrOYmWp;$SnjFaoqEV=GqFU{O&c3MJLw zA?KNy)(vzg8->+4(1=$>RaI8jt!z1~df&;~U^R4M1m5^_k%oNBju#?OK0JwUlB$6g?wPYKF_7)N0V<6e}z(7C2agIJ{nODZNB91L!NUgi@27>C_BHaN zbwg;aDsKhiBCnDIa~>Q>lqR0QE2h3)a3NWTs8r|4@&zu8+dK+g%q+8N{-o?OE1}Un zUGdx7j?=7zCFsmn46M+zm4J0O-Pl6#OjA?AU3Fh;@tI5ePV-GH1TOANV$O->!z|Ji zwE#QkXN3h09vB)iQ5&v0bs{J*AvYGv&_*oQB7`Py$Im8Gz+|Lo5q7?k6k11ZHrx%h zU+7bL&qX4bUElq=lzwd|ep8R08Xu{No1KyQLS~w0HydkMjxcAh1Mhp*1hfvx`{-)c z&fqLyr(zugQL1N{yNm47Rk;>Mk^N+ka+2b>Uavpy5=(`pgUTxxWxIDLB?)514Wd@t z804-2At4QjO%uwqvRd+yR#YpLuawnYCLsq8hKTYL^^>b=Yim!}G%B_8)6>$@qu@>6 zN-tiPmPXQey~4n6t*9~CA(wn1y<{&PAqF1KWpVERjuyCJUr*nFNW^rMb$c|V=7SyE zx0jYe!J3^iCD6+fDU5xZDq1o}$5w3=xRX#rkePpm<{)j`2IqYqSFmwwS%bl1*;MQu z^#*duW4pJt30fqVk*o`Vl1}4l-;h7kB}o6pFWdFK*p6N>U-pQEMf|eO?3vTL1n+ju zr7H(rq=*B+R~Tjtzio<-35jrSaxG*h42kbvbN$66tnM1)t zYV4pZ8z1XQKCdTb(WXVFu_h@9U;R?q*(#skYfGzQyj};FB)vW%+j+*lFE69m zNc-QIJ3TGc#g?HiHo0R9=IIyrDNnS4Ak@1OF4^wQmv(P9|8xId}-4hEuJhTF9*^0#9 zTQuGY0kIC17wzfMqXJ)Ljvb4n>A!lUR>A;620>b>l;-#)+$VQ&a+;9-)I`XQhj49% zPX3`_K&m1~aUDY$BWh4BqHBs%Diq;q@enMjIhUqu(wfeDP=@{|;o))&YxwM!Z%-8pYb%xZ_Cc7@efg{a zTjI1&017MCVR#^5av?DGK5S0}DD>)Sz>{qX3K^v#$8480JxKrBgD5SBO0(2ES9MY* zJP2rqj->(bx5aembspfmxnAA7d9z`~RshE4822FXoz}j%dYzGLtv^%Gvg!+OS%!kR znl}K4d!EPeK=XeG6{KSzu0>NM;x`HSu_42H7@XaQg@it`V8Mb%Muz;cZQHi`5aZ!cMIi4c zL0g5z0$}0PoVhSQ+>1CnD@j}PpB4e9nQZ^u`%(3`Di>QhVXcC>_^&W6=h&e<%{A-* znhGQqf){9q`awhv_aOZ?Cs%erz(U|6(>S~TG%C#8duS|PL7WlF)Vo8SWV6Mzqs`Y7%_TOP=LP{O4~-V+A;+0FuNV% zm#o4Fx+|ecg>_Q?XF6WqF``G(2`!H3$;yJ%dcTOCX8;X+3C6Prf{A=CeBTyIbRWP1 zZCMe+v29yv>0E1LII^*d)zeGuk-wp~pjsxX(W1=_V+Kt1|Yf#9uiuNDoLsYHe z4a(M5g`21CwQGb{nSI1Gi_`z3#>rLEWRuX|tt!dZG%Skz_imj^7_!|laS4&+EQ0Qo zR$*=qPq9TMlbN2L;`x2spzc`Hl};9ryMhzuc?s>2rwxs`DJA<{(*w_; zG|1*dXIfB#l~Z>2@gF*T6wMSY{3CEdGz3a|y$`{2t<94YPy;B4XldDQFHK6eRIti^ zvfGUxzudR$<8La?Tr+Ttv8C!AttlM=W>4FHc;b?)pWm>cL4Mthk{yc&eD;yk{WYl7 z`fzC2(k=hNP!UFg+J4YeR_)@35PEr1#lxYal9D_h0;3{ea|1^Y4@iVEnl87dxlSSb#;$~1g&#jU7ad!skpvA6N?<# zqR3J6#ml{-<5S|{8xb|cw4OF+DscH3)OO^FU0yn3WG3*bA_a8WL zV2SXRo5Hj7@FjR&^@^LM&D3eX#1Ae0D`>Ld* zDsrEe%+YmufXf7n6h@`0{@T$)1LDR7dD+?7Ijfb4^A}R{(A|M~-jUAl2c6oAWTQq! z%#DP_VHGoKl(}}&ihJ~{TSp4HAA|%z;4Xe$eDF+rU8Xm?JrNb7`1bSCQnlLK{p88g z()@fl%M>jw-jmXXH}WJR8v70PvEjr?vR%0}qPPF49v&@V+c0IX5XVTcay@f1N<%)e zta&@iGA#rl%<&>%ey0OlAXLV5n3ZZj?Fg&pYOj8*O5Zm)3c zpgmdyvRz+v?W`|ag+&&6Nd`sY5ZQMf^PZWihBMjNV(ViO0d_aPPZB3Rs29S^<|q zil(L}#h{3(2-z1ZQoAgk8ZkB^LioWSi}1NE0nPodWv;Bkj9NSN>_x~PVznzbyhen` zekKJclB4#f9*F?F0Y~acG6(g>3LIk;ol#EF66)q`KO=2Dm}khzP_O?jb8MEqV;LQVGUiOKL^iN+9mk zkiRp1pU{w>?)->2#O;rXqZ^Fpq|HxZee1x<8Tj$3%<=;W-;cX>jfxp^4+({)hl+GM?yY^;UO~TkhCMyPV?G zSx&iyiqNht+SN0cFz$M31@)k)!P6zwozj>TjLJ#FsF(mJfIz2An z>e;E*6kCB)*gBvmR+SuA@vQK8X?(O-Kg*0@t!m+dJpNI3An{6FE1kE z?(x2{v5^eNaC!~HA;OneLWjI_@UIxnqX2ngK%r~mexWDSCQI51hFrK*`^$~GGmXk# zzWq%m#)h+Z>W*sV4GoRwuBzI8`}Hhr0^i?saz*vPe#8}LZZWbOjE$|mllP4ufBdob zGJ||er47rp@!cAKzeK#f9UAI>J>PJ?_V@EmoSnC1v+5;OZ4;cCGFFDu+Av$h-oe2` z-THfgaF8P|Yyq$*M;rJst`3Lx~vOA@TszO9r<}sTOq{D=USpNoIFGb}}aY zW${&IrG}i)lXPBBPXr7QzVXj$e2e4!CfeZU&eZfnC4Heny8FV+8U0Y%+F{JxC!WOd zY6~Bq6zkZ~&=8WYg)!x0Qo&{A<}X+{5fNoYqA`cmGYXi%X^37ad7XIzb5n_!b%ny1 z7mY;c55%Z-@`g*Q@AdLs#sA#7b7wOBn#xomKUqlXW!9cT)pnva*fTf(tl?Zpv%R0Y zjfkN;ge%%m;m7@K0VH#!_tnE#$za!F+H0achd0`e#sD{*nVuE!oRK} zDa@bHOpB-JmV`&?8Gg1`R0(OhxoHWA-V|1=AT!`W@lmn@2l{D9DW@z8$wTi}@CNA= zJ-y&Q7w1@kk;DqBGOwT@`g(#8cGu~QM)DHVW44C;S5NPi4p)>H5h_VYDCkk( z_WapDt~56_rluAZMI)VjJw|gaB*H@Z^2fD3ajv313M9D>i6k${QUgaX(?Gj?3K4r4 zQsvvKXtsdW}=ARbO?RWgKZ~^oJ@N5Wph8p zXH%tT#aDjPE9@+U-Ea(oK)F1qX#NZm%IlFjp<%ql{XZO^+b528ZfRM*d~TwKMsLy- zhri37>im%N?PeLN5plL&M=RWk6DMj>{_jdH{t@}sdh!zY+`@$mpNm5A9O0-6*@+Xs z57VKiqL+YYh9MMVJtI!b?3pvX{h2doe{X7K z)IB^sVZ}U%z;LZ;f+>^T*E%)^rF!;=&L1p})$Hh|ol&O8Ogef`C%Py%kJo5mYqNj< z-ouAZp1;;^UfW=w8Zbcq?Y{l{mn^|qBzh%GoaN=^|2bLD+qzF$ngS((&mQxj@Uc6wkJnnZ?i`(tAJ-8y1Yhp{O!Rlo|k4Gacti~Y|)e-V)yNe5jP zKkALUgvkYqN3umAO6$eEkIkv8|-J$ucQg11Nt~} z1u>M%3Rf|zX|1tM4q%UbSp4CxUFNvsg@7mX^K*5j9`$`E(`)l96oR;aFmTDrtfk~H%1 z67nxXahc3a@$Y~Ekx1e^!{jWALp98`*mm~9t4Oa;z&h6=+cYOfL(Zt@XFiiTr5Dmg zdQHK9^YIq&+>IE+az0vCA;Sq_f^7-sns)^&l zUH%Uf3V{b^-D2_%mzN%-s)$5_M7rEVtweMbU&t6u2APa&cvp+fk8+hfqY zM!KSRanJa)G+q|@%j1B?dG~NpVdLCK-sj@f^`91LP0Mhwcn-61BaKIWN>=z^^-QWb z?C0%lz59fW*Nz?R1y>%=U2bggc_s=$%(UGN$~@h##l^|V4sVKE$qd=M4M@^wOvZLe zjP~@d`2)9eLK@wPb8+b@4%DMthR*N7KK`ox3vf4pNz{16O?fs76jVw-H(;qkx}`sQ zWo%4LSXkI_UpL;iZqG|A)~#E&amzMw#p-FR;(S*_|HS;u!p8W!7~Ea_$ApcJo12r9 zGdFH@m`B~$|F^67%X1!KDfHZuj|i=a@h=daeMwC-BE62B`KEn0l`MS^cQJL zn#WUSijD;Q7)z({o^$sYTB7NMMcko779U_G+lr~;tvsWwA5sqOMae}vOT#te0P?y_ zTgVB>Acw<{+W-!}2CS8BAPq2AX5qk=wt zT(qEwPbW%Gzl59J`g(Y{%5PpZI(zi#-rLoA5aj{kX;bQ-mUhGmbmUl7|+3Eyx7;8)(;S>?GRO3c>3IkJ zOaF0qK;`t4wQV?1DUyoYthRBZ))Yc7c4K1%GCV|D(gP#bXiU)%zWb8&iKzKBQ9E{Q z_{5mVw6wH2u?exTLmX83_$b>?vA!+)_a7`REroF_B4PwN`}G^EUqp=# zY4zMoEAggSF0+}WAzwbEPBI-W@rr8=wWq6(AERlf1~7xwOihir%-74y%hTsQwI~YG z%37rTzm1K6+Oep?P!Bs1O~{$c<7J^DKi^>3+BPGKyo3D&hqsF3DbBY!)4lVnX8^m? zsj60GN#CkZ$61~#ElAwdPM~L2*&*Em4$W9ni7NuKd?BMEq4X8rrPd}`9QXn##suLs z4LoTODSk1YRyHe}8kR2$CI6xabI~iql*{c>t9P%^BDMop&yrTJC{4pn+qO2UU(a5c zH)|Zquy~nJafO$iI}*H5sxhK^I;4L?Nl6K{a2|gv3Y!|evYE9m1uC|+-rm06om2Jo zR+&WTf`%gM9c&B)um-ZqWW)yLO64WvMToltBn@R4)leX3NxF%ks<3QEe-+pJIrlA4Sk|1cIRyX5HAgw_krm>cIt^ha`ioC*; z&hXunqojN&YU|P9-23jcG~_?`xLwe=rDvk>3~z5FawW`3ZA-Ar#6Y@!lN6y(AA$h4 z%d-8cY)k92uYBw2BOh^mG zakXNK?8uQL@@ayF0ApFm>s>jHF|l&Ct*x=KtquQ}7>@^vcM{n3Hq|eW${xBL^@|n<-DkMn<)mc6~=$iGmhG zjL}ebYRLwuBzmH!x6vPI$ApdAR!R3-;VTAq9;{gN-AqsrS{82a*m@^zr=}ebEIx{d zNVk-CGi}gzP>?{T9rx72Xb4}gO4^QIHf3r+Abb_7^hwU_rDMmA{qE!)pN=;G2AYl? z_OrCqsbl;Dd>snZNWRL?TRcDDdhHqPlQm-dX|#P8R|u0hC4!kTZ9Mk<74WUTN8>bc zZIER=snvmts`4Hm=x&2TiKYRebMx>9Kx_eBLan(s7?JKf_QN4~=&U(QUx4VgP!r|p zMe$D>%QXFp%&?z`Q+N_}zRZ{r$+$AE;Lon+ek6t8u^*ybz6$-|ZH$aUsgN^R;P5Uv za{Yz|$4rikiwkqHYrA-?;x_KVWc%YdM~4BnRfj&`x_0f_502M+55PNm5@+TqNRkwJ ziqOZ|smWsp2l~ki1(Qe*GlLPvH8*FPe{1cT({y0P9zRY;R?<^3yjU`hAyXfnyW|B( z1`D)d0p3Q4*}_tCNFJ@Y^2c#d>`$!6lw_)?l04qPq@{HR>rzug{sK(k4B|S`O6!D< zH5<^!-eB7;Ae5HG+jreZ3zqi-X1JFDlK2@lv1z*{h7rLSZ}|OqEZIlDPak zji#&&=i7_gafixyLQH6&&2L!#C1fi%F{tvz)zs8c$gM7{9yKQt#Is-3QZMrjj7>>N znLGw*r^rr8!}*wN#m3}=%gQvG?{t#L|1_;Wam>J7Z#Jsz3&qh>(E043XSsId#ahaX z**u8RRP*shu8gz_rKdKUIixr)`O>)1T|EXv{v)Zuc^>=e;A4Uk#7TgdSp%`SjFjT@ zi^5ffr2Z1_S$}{3Y^qjVVd9001?6AF27mhFty>GALdEwn$c;BidrID9;~vQj$!;gN zGViKd9Z>!dIni%g%X>GFrKSmsO+C0Ploi z1Jx1k<{}2_`XY($!83^gM_@21ZCXKD+l%qq3-y2;mvWA^xPun9-9R?EET21ncFK(D zFyC(2u;Gn$ub&GD*J9D(Lx909tG7#-qS0tZ1`Zt&8pPWM_3Gtai|Ua@#VbTQNk&_W zKzvygr7r{7m01-F|1R2h3?1zZhZDXoU%cuaqnjB|3J4T9T&ExyHN#x}hdDseB`Z3j z?`1?eIwvHIbEHv@vCmIH_WuFQ1JzvVMNI&^n8gG_8qeGP;p!hA(f0FaB{gjZb=|R} zrrsC59sWGe46wU_QKG?bFWOBGBzuZ=5q?(>nZp}xSMA=t`>=mZY)poRY!@kjRtIz( z&<%KA|7p+ew-DA`?K*>ME=L^f9J{%D4+(<+IyP<&h{|Z} z?l&lC7))K4PJE~Z8(3gU=iaGmF){}GYiBCH`FPj9FMqt@HdzXL=CV^XUQ#5b9FTcb zmJwwjDtoiW6-s1+x69sebTKP%yyK{L*}D?FT?g6Qg(ohB1Ob1I;?qhz%`{+eitoIN zg@`=OKvQk)f(27Ffo^VE*i$wUUT#^y*F#0*%>ZsnhkuQJ;kG~pmpjBVf=BVzN0&() zY=^n;Zh)v4;t&|Buq-)6(_@YSv;>ephT<2F(TSv`pb^cV?pH7|>a$FTBPg>L? zlxPs51Z^7{EDc2w_NOK21(h3V&42>C`Hu}(DuV|1adW$dldA;D172SzrzpWbRZd=0 zXT>-}xy<^1| z)sdRU^5ROAFucvH6DxfxQj^&S3Hg_Bxw+onf3XXJVlZiiw^@hG&q*-ay&1Q|r1Ggy zUCnY+Sy@CxnH8q8JIfmq9#-1CZu5t&FwbpA4~t$SC=Ys!3soZ^OG4*hlZ!jf!2HfL z(5~@GShf4gsLl@MGL2^r9nz387)bFOVLjv!4=~%HI?fRf+GSX*Gdc|&q4;%%(r)V% zXLe!dPupiGi_S=6V{`qfx|>kJ#09wn;>4TmxW+~SiXzRn^-^PF;{-@E^m+@*`hFNN z-lcZ~F|ZMv{RejJJo^62tAJmaX$rS~_v!R>o=bn?&(eQHnx>h;c$-y*e^%`JXUTpE z=jQ=e=IXlsD}$}N@34@O<5S>Gj)WhLaw2c3j@2X~_ro#9RS5TC8>@UCT2@}P%2gD;)-)yK!l&;z+mr`|3tE>1`&E-roh zM8betC^0B`91Uq;uQUbkvCohM=olswk#hzue5Xc+Ezla+98VFHFD(CAd|ni;UkN0l(eMQjFg^;CA*4Dt)OSeYFi#x7D6wA$%FwFV|Qu zfOX;`d(G?qA9SPwhoZKJ$~QzR?lB_t^y<~CPlnQh?lr>oiaNZ%lMN(khmeevNlU!E z;?TRdK$+CnCDQSK;I`QYjb{O@;KnQGPW|V=J1b6|`n9g{>J2PxYU>_X506zSz1Vje zaRA>ASjK0Wo;1Cn*j&8lv{K0$qQ$a;@U&%{7Asb7d8c^)*M~*PaVf=9JG!UArRA4{ zhfk_hD&O$%!0`B%s~!>0>8NP{*B+Py{r#h%T9?APA{bt5pB=@1SrQ);L@k~wlghiV zigTy-%vQF5aakJ$NP?|X_15u=kUe1nZRj8Jw6ssl76uQ&q)uc&wc7n@Ajj2_CvMm>l&{et>;Lm>zz9&RsseTkm~KZsw9`=>4z4I{J6SdaTp4H(E{Nj7vMo4l|{O0Sd2lQrUTW zcm(*(rH`!#PyB=`ETZJ8r;;bne1}T>Y4k#Aik7CUMgwF~Y2@P21{7*qIgnm&IL(XH zasU1dW}W5BPnezdaek3_OD)FLnV6qNYJZkE%B)jZ#vs0(Q(La4=+F~6BO4<-+4CnwH*GBp8b0<5sJaj9=?wZC?%@g`#+(5>TcwHW*H_Nd#Sv>-XXBR zlY!$DtjTC#zjzrWOWEqPgUE^3%+=)o_FY@QEXq~|iJWRt%C@Exzj9;K;`E>tX4QxPIIL`KF)&BA zQPYuhrh?pXwDqe0k{Ds6=vcicj8wF(I5ru8&4vteHgLw4i=38@`GN)X5ZG$+4;2#J zrbmQa!ifJF@>mo$USZzY#js28Ha1GqSabf$O~eDjovT>8vYf_p2{H$pL_Ul%O~=+f z&GbBbggin`s3KRM26#5jvXKIHCXLqeqWY^ESQS#}s~r2*()a+au?oz9CkQMRr>(K5EZy z!)o~&uOoPSc8G3-E0%!k2;C5IrbN2x91mr-h*R#QxN_y{`P*f&&9Vi3u3U*E+sHPX z<)PePq_KVAdvDWEv8t&wcDD9-Wy|+__d*)WFfEgPb+Q#+ez*W27~!{Ux?RK3NX%~r zZ?m`f$F}3gYR^jRb0sif{rfo@*k88<(t}mSWwyK9a|wRb3<8tVyddSz@58=Y=?DE) zodZ+P_+bBWWBmeq_5i!KVOfu1LADNr=3Mjj!P962l*pPt0_h)N+S+138v8($;=8ZN zrUf^E>*VTHYMa2JG;HCwlP7;;#nGa+$@Ux$YuKic8F)X|DbmwpfWc_Bf=$;RcE~)7 zx$`ByJ!|fMpoCRw4@i+XN!JIqk~O^OU7t%wJWrXRA*I%lop5@urKBhJG?VtUQ?`Ir zgswCv5LgFQjJh5#tj3NJ1!ms*DzH)v`%=F2VdxG)cqu7eOM5g@jLjx7AaiTQW5MnD z;4*Rng&bE6SIXLl72`xq9zvEA`IT9IdYWQ3XOv?B>o{u8cxX}|Nu072|E;o{Gv0&R zRXxVfSr!eWWSMkr7q+%io|ogPqQjRAic5!!&|bU~;9f+Cb3^6}?52OXzKl`-@;(Kjsviq=k%xs3KKU(~X{^ z#;3wpILTKP$qZopWEmMG0~{M#85ci7abiD>h7zGJ<32rk@+w<^la;>M_i>Sk+AdTD z5Z4af<+$J=cLpo?0D0FgJ%l-mAf?t((b@1fE5f#JHX75@qZof&9NDGF&dyFAiPQp= zq39kMo1%mXLCd@Yz%g`h#C;W zl))7DlPVhTc9Pql8=o7`?3%&LW*BGe;zjHhQ|vffB@68Kkz($-Q1#ER(yA4>WUrr5 zhfp7Rk<7`--=AW#2eY<^yl?k(=$IfMA0Ia*@qyZOv$ZWCDatHLbI%WDE6jC0*;kL( zqSz=^q?g!DNeOOlZZ;|44=xn=8ePGxn>~wDta^s_YE!R&w>5NTG+g^40qi9o+vjL> zqY+f$>)}=l#Rh30rcBwUI`>!|1X=N}^=u5`pmuOQgZH1h0V>jE9D6g$JUQ(Cnk5)# z+jxXxvPzDaKsz)F(jW`rDpyLSm}M&~xlro5+AY!z#t`QP?WI}=&SMgaHw(T#I^ZJa zYboZMAQywWdh6yN6iAH=)FfuYrAMWvQ_w~~q*fgeSJo5_hpGaM<^`>&uPB#mn7w=V z3UAP|sz;d@z~S^fZ|^+7U&T1pyIrV0-DFTTovyyny;dz#`44c$d|^88cj~H%sSm$u`5nFVP#7jGY^=voL&e%pb>oS2izv%8Fy`3d5v&ID%t+A86h+PwLn zm2JM05OI|{$+zvdeemRL+V@*qcP({PYr7Xb=z8Y>0l5*9Od`BG+UeEdPOqvuy}H=x zReh&d2RgkvXnsYhKyPpF>V~gwH*0QqC_!qnMV>WZM4O(MC~&SR-Ts}bq8H%*bK;7-!wEMBBt@w1>#ugn%p@iX5}pE-4U)!6A( z=jS4%LF@J!tW*!~(X{CfQKpS51@e;)sOJ7QPYuGw?!=y#yKpKH#@gM)+RXG-zz zS8)9Ba#M4?j}vR?d%D6c2-?=TzUm#jzQcIHtID!u*Di#oCL|>EB`3`>*^z+52KJN} zsN_8ZwaJv8QpN`kD6kzAIFgdmFeEcTu(TT(Q&Qb*aBcayB*xF4O-vjcyT1f}v#<8Y zMoDp{h`Aw*$%L(U)+{m$#)Eg?VO9t{N=xV1-FWIalm_DNr<<1C+KN-RdeK|v&l*E_`exU-E_h#CLtRAXanp|Yi+=H$=EGp0%j7JaNaYjOrM>Aps zjGjItG@d1UcqTYl3;WGZlov`xwC~_v$Om#Y8XepN#yv76Awcaq=3BQB0#O-OwkMhFlKtLZD*}@d}?aXHJ+GfmM)rjlut8 zpl$J-(#6%(%*(ppXuM?Tcd7A4kJh#pldFpohy=MdgIb$igA)Dy?Ha!WQr|V&`HLLw z|0_CoLI0>lKl`EC8OF z7MBCeXM>qxOc*mxxI&n|On2rZ=5ygyZxkS#NW(svapD3fPTf*4TB-!ufc|YG#jO8wI(m0oPY$=%6Uf4!xAFHk8vXrGoDjPx z;sXs#IFW$3<8T3TSkRUkq9ne+Q_tD;81qORg2@Gw4MWQYZRELaXKKz|X(M2i`9JKPkrK@z+XKAEmdvxH?REh{kJE6{=V{~f@s9J z%|<;sdOhRv@}{qjAHU)2izou)g%dyjaBAO*NE|epr6_#AwCq9~1Q|qRNqpmPz$pPB zir@=)8ObyyFf<*3fm<>i;QduDlS5TU4}4U-bIF2Wx1J+n0&R?(K^c-T!MW<_e@~nc z%v;F5qDXOWGFA5>BL?^e1|&RBYX)tV3Godq_W#GZRiGE>X%R2c-!MA1^$nNXNZaLx`XyO9sHN{io_ZfHgv=$VKj2(hRezJn zdiVz%{qnmj#5O28Se%vNcoLGHvYFI=Q~u3aV>8u~^xU$AnG2CEsi(CIoGXK)gKWu_ z@4h@5;NJtgWqpXfRjI@o=(o&wB5&KWIearjb+A66rcKPY`wgbqCYi?U0ucy$< z8M64?d#(LnS_|zFl6Xz`AV{+em8~#KNx&oll0p#E4@&dz3}4H(xN zlp3Es9`!)6=^2;^qQQfTi+lF&*|vVY)-(#&4CVG4o!?-#-;l-$yo2a9WGnWzFrHbv zR_s0Hk;tc!r4OL#i*Oz>Iq(t|U`5j4!B$wMV@(mvx*A@-ZQJ@4g)79T?C=zX3wi@F z@2Ru!UOe!v5iu5UzldI#n0`zUP>wJNh_eafXGenE)(CjeBq4uOzWiuI!`ij_Xm~9b z;-}+#TJb#*u0LQzV;l_lImK|~luSibso)DtDv&wC+cz4WDvW{RZ>iq{@QggXp}ZkH z79C}?VzY$6JoJvtmcN{#B+dE}GY9-%VZ#DXGZlN{f7ne@xMQyVuE#W{R6X(Mm7w^eZlpe>OzLOH zC+;i{iMTWIX58rvyEh%$tmced_aWiEn;QPTtEBSmAGIf^_%)$?!L#r}&xGo z`h+Jg%+FmwPeGUa?f(3`+G_?T+{d7}cK-P1yZta%dPl|8!*3&)cKvqz+FX8FA zz_9(-zaDvn{tQBywU5cijP#dx^qP_uA0Pkt!kkQ0CY_!X3hSa$>s$&K$CoCLNCJbS zjmNe9&~E?mz(;!2pm1zctJ@ll#zyns0{c^kj{U2)>59TJ$iExx_%z?Ln7rFG{fsWfl?lH)7`NzRvr<&p?$m=8uA1gS=)(^ zbO?z^#7HNbmfC$(&5n$nk)#bBnT-`&W!eYX(0}cy{XgiEe^xrol zki+;X5hd5XPRmM5x8ZbeKfkF{>ABcQn^TS-Kg7k(GkB=GL4MWf89IKv7OvG3y?xnJOOG8q;Sipl?(ZK?khFhx&YZI%H7)=KcA9ZRTEn!`OjgSOpdL`ZWlu2Bz_p4ovhB(@aEU zBy$B7Wcc$@Fg#GIJ7DMQLiUG-?mrqAcT}Ram4K-Z);==Eb#C^&QSLs9AGU3bhOCig ziZFFEsZ9yo)^&!rVC7q4T4wjk|CN7p-d&fFGA9qxsNxBhEiWf05%hoMG8YSJWF3A) z0zZhzh1E>V8cNu)rsqhxfmJ!QjTh|BsUoqPrkXPBicSVji6202?T@|SuC@`R3lO0m zS{>t+6@_ukqCc$LMz{}k3{qr8x@raXEDEwyS#7E}irU!O5T}Vu8S@IGAvfV?K0%Q1 zit88rci3im@;F!Jg@2XlkovM3u@rmA2kbL6;U6_;88=rNVzS2Aie9Iw_FXgncIeQd z^KHt|;BO2WcQ=!6{o@siWRjVtcAcx(SzTSeYsU`;+m@!r$`ikzsjW26;8ctO^TQ93P_SN8=7-jOTk_xQ zy?aMug&yG+n~ol;<(arRo&}ZCJ;)G^ew0HWECsLIgnGK?jvf2;hl)m%Tw_|yxH)J{ zPs+ZUJM$BfB40+^t|=+WQ!}zAq3UpLx5TU`CnwLCF?U)VcI|aAh3`PL1HHC_C#)j{ z%(S^sOhE&pdMf^pVSxr>ic_9EdGhC)Q&pe6IUd{WbU5&n1V=uc2L=U0Os95l(#Iex zEDl$!@H>ksE(S9xkgYs)Xr)B4G!vcg-SlT;^C9r^_f9ll`lCswGhVXy7(Na*p%EVT zmyCc5wG*k0Ld=vN<|Ny)q@<*&Go~Aly&nVM9YH$+^u=GQzAP;(Eh{VX@+uO2N7Uw| zcR;FDz0471c`ufKe!PI0Pid@*uAV&#>DL084*EZ_(Ip4 zmw&C?wV`P1jvYIY-kzA0G}2hPGY0z><@M79mq{|SV>i#e>FD_8%RG5m9P@nIzgWTI z@fjRS-;>jzU0fXeG0}Ont^z%=dmRlIfhFjIa`NO6!#y0kb?Zh?5~9}*n?=);l4lJY zh6%sGzi318V3@yvUST71=o4Tv1tiBnDWS_&;G6BSqf|bmfu^Q>`+}G8_4a4wKQ}Zs zP!Vm8{no==e0~K4kxFNB1(gnQm+TrqmnQ#%!8gOai3-KhLw?Fy8$q}++17{69CL1A9JZw|sZ#K2* z>9w0ZJT{9|0$q#9LJl1Z|C`nnZ(2q6Dwnk1AVRylEJ?kltQ$c>#R?eER_GyW%Ik^S zHj*l~4GoVp~^{b|SG6@&jY9Wf9tkNs1{qW%uWU7hdicid(n_=Z}-D!x1 zx1qnw$l(|fr*(Lm%Y1CkQ+Ya~2MxaoNqw^_Voda`1xuGbotivxR2Vg~y+!^me?D_g z+LIk`a}aOKduDcW+_*93`&|@$P&@wpzI`WcJv}{Ll?KK28%pK*&$dcE(*ZYHdTVvJ+{}#m&n;bk<@|51HQT;|)%fVnoo6GF-#9mV=+L3#rcdwn)f%ZavviJ( zW426lQkZ`s$3x*3aKJ4aRvfm@SVxs@KbV5V(OClwoWsZTf=Qy+XkFN|Y02>!&u77G zLH)Ay+Sqq#)^q9c5RYPqXhZ!1{6e+!a^X+TotHg%tX8WXJ2^XFi?Etia;6HP9x*4U818ui&r#5ta7(js=|M4`vMYZ~jm-MMws+ePp1D*MxY zfVYc#$n>;~$LFLZMmX1lD2`Z>w<$< ztyA3V*tx`PW8vhxol2-1E6ua~v7&|*Lp*rmiZAg53e^eDc{!7E44 z-VR1$RC{@P{PEE%8{b?{op?E}+skW9KRMjuhcW`g2YP;ojqC?c$kh;!$v!q6QMUT- ztb#MPBLDfOIxrzc>&Yo=-P6z1;sQr%}NC$akDgQ zF(}8;U6?gz%oe@6Nq=RETvFK9R{5I7w19ohHv^US*$hEyXJ7YQfJkX(L7QHNdq6_# z*r&{&Jay}p>~+7V#BqI`kUm~_guPndCn;@FR{)7;CI61?i^2? z7qgZP8I#ymSH}^zfHTR|po%cfRI~b;BF>#HZCNX?~!=ca;OSbsr7E(-zEJ*v@2^W}rvWYQh7g9pRI>7A)4PDpt(ZsXa~ z(h%w1ez^B9IyYPkOl_^klr1aSzV-xOiST{z44+!<=gUs4-Clwx@SN1LAMR$akSkhp z66(fDHp2_#u?I}Af|oaNo}OF-Wzd{yA>pJaKBssqZ(O}@806SX)p)5*)yIEyg4 zmA{9xQ`474MMY)JE-vn#?(UiM=TA^6tKKVmBNEe{&wa{zPlEl&fPLt1Ty~tN>KFR{ zT+UE*&ePVQ^z!Y8gwH;Z3V6l!KYu&r?C)XdX7AQRbrTuz2X^n@0WMbF>J^zYUJ}FC zfk$qk|NHs%gJwM;#IyhPPpObt(O7rccP3&Kr-$tRiTL?Bd3HlQv6JT@KXHtHK_58J z-YO--dooH@>o+_8g%U?S!^1}gPkMIw3%S{QN>Bdb;1iz{gDp>peE^(QgzvF@dFg?D z`}QOEeA)|*OvQY}UYp%68Ky-x#YcWs_znpSj1nZ1ugQLg z>};Q;sflyWmVR1(`~snFiCu^iFSLI=Jaki0N#*a}qb49yV+A%@DO!s1K&LZjs?UF0 zvV9X`G&a6l@@;K(v>EG7$!&_Qf64R8U z-cWk1yc#D%x5(9YE`5Ay^zGB!Cydibc=?1-2gLpGw###;!u=lNjeJ%3gC{+;JO+cf z2RhCFBkoNAqbRcf@u})L0)#;B03qg1h8(RWM&fB@BjBxW-`;$ zUGLSaSMRD!E%{Bi|zpVeY-GK-6e>~f;yvopd9GoE&OTtca6H?Adm(Us}Ig1;_r{_SUw-5q7 zW_S;0LBYP~rR7UUVYv3f*?%7{>kZ_1dEzz=P9^$7jSxc8-pYiiBMy-c^bJDEL6i=+(JxO={cDy)uSQ zoC^bJ?!=*)Nzq|pVbMvM_l!Xjz?gf|(vk-E&(0n?WJpp(_q+e<=;O zP?toVuPMNxZBo+Gys%m2M&qO`#N4q-XCTqjCPK%&qdE?iO*;Qh`khO2v zy`su+Ld6*}yS+EK1ch}G+MsunH}}TBA@E$zlSd=o&$or_zcSS@b?TMO<7%Xy?EfmM ztjthWjqO%yYpO=mclLSVj_Pd0P7R92&8^is@J$TtQz-g3s%?AD?60lX+JkP@tSdt{ zZbrp5ysPD9MQ`zp9*xF&0V*tOJ`ed4JnGv~*@h>fL{3(#g-=zw2H-baJznkCrfQ~S zb7dP|V95)!4S(ko{?-e(;--(PEtJtK%AN|yopB#K-vi6`%uA}6I|~n8sWxl1(KYPp z9_jq7xfKJM7w+qUdm5_sQ_Hk_z!jpFCFZsmpW;rqYXzTGoJi#jo-rJ?tEi;nkF_h` zj;aU{W-%tL#Jo%ojD%G;ayaDf+nTs~8>ZB9dg0gK+9l)^A$+!>ji_f2t35|;Z6I*; zcorl&zqQ))@qa3B*!Y!YXH#Vf@;wvLIt+5j5aw^ZcInckzkYu)HghoMUkpTSfXm~A zG`i4S3+d4bXQ3(WV*1RP^B|H3CKU`xN+=$eiHaXVNR7NZC1>oI97CI~eTGk6ym;}f z($GM&_B73c?8b+d>$MBu1F2pj(an&H^(dHLuO*!@S6agMhV7+c75Ku>_xB^a5@MCa zG3RDsy&8W?`l$hVKTs8XQbQeSmEy+v62q<4h*O`WU)Hhk2JHu4*D$FXgJXagHMGYR zGnfpfI)emXydBml{SCo}E{3}dQHD4}cSA1&Ml?A!A~5DQU#E?IZr$dqqM|lGfBx{{ z6+?#1&&ipenOV`N&w|9niXJ^Gx^-I+9bFL~KEE^AmK$J^8J`Bugs~x+C1ZgrnfU|5 zc4=GvxDl~LuEx(m8I^#>!p2X8j}{ z@{&Q@2*ce5tin2vp%j;v78D$W$!x)=xY$_7QMIS^+p%GV699sk{uo5o1CU} z_khtGy`!Ui6Cy?o>mQI9H7TY`+n^iX4(MqkZ}|z_6{pTY7MZ}KBF({3v1!RkVH}JW zmmFmFI4mJxP*YDnY;z#0hY^Zmx-^efQN6U$FmW-~kUHf&pXA1uGi5xUT z`N{pr>GNr@!EEp`K&!FzJW;NVU8EZZZMfzNCl=|*nm5+C!bnIii;$QGhOv#H@wJ!67b=zTN+rt) zLM;sT%A4~XS_FTSEEKv?)fdif#Z+3?IyOYi!4^9-<3jHcJCMqvS43(APXHV6fMUg% zsc_O?Y_K()Unb3?T*DlJ1~b*&`ZC|v(prPBp_2jD8S^;o+|X>em5z@1TKroYX>FF9 zuK>D6g$M_VvI=&&;Y-J6&9$=Zf|{Ymy`W|gI3^OflTJxVOG^*-;ekSRrUyG}PNR3P z{N6+656S93GdnG^OL|aJME9Nd<|S)l1WJ;>#cC-7`I+Q=e|}?26+E=<;+2 z_@axjY;4ezlY)W*p|Rtjxl>{xVS$PRzf4UnO2xmlqLh@Pv|a-S^y)cqpw$Tpa|nL3 z-6s$1nUI~G&~xC;Ee>et@-hulo$M>%;7X=DK>yj@6uIt52J=eC$#HX$189|V%!fdI zlB|%cV7$PHbYV0}zWAuUU_h}X<;!fvJeMy{kmfa9;d`tlQmCm;?pD8s>?j4(PPQJR ze`s|bv}h$-^)-Y-3K)`oBUE}?;#9Im#Y7~f5oKee!0777sEBnuvi$I@6?2RmU!M2Q z!-_XMQ~u(V@lO`x)7jCh@((Ogl1z2u>tps!{(CX!i*LU96|*E3r735^POyTD7g@mx<>og5@BC%I^7;GkE9VbfIUewhaue33 zfF-QP2#gsNk>nZTzoC+zXc4NgE2}($w!tP1OR_#WI6c`C)dKjoP`72LOFFiQfo1Ui z{a0^p@Q4C>uS5z;2VgajW=&kWuyRxIA=x4ZxdnXQh{)^`niSo$x12Om&PcweJgY^_ zl*o?W!viv+le>33yCX1fU~U;Z;StSdlu|7be~$yj{-o-mO)cVMR;?A!q_W{awli@G zv_%V~dAc&cW!O+nHw#+oJw+vY3UiT>SzAxY=Shi%FQCmgbk5!ee2`Vgjk%?0xHoQD))XN(p%6ckCO?BKW_>}U=8*eI~d=PArWV1Qs z0Bf^n4{LKE#B4?n5ved90Yv%~1J4xR7o*wl5pDo4Qrflzo9~rnYjMjMh(23rZUmF3 zq|J_&kzhIM3{h%Y!bF`RFj74e1Lb`*YH%~GGN3|oQEC8n$Un6cuj?EnswPFbdbf}; zx>suV3|~op+}@@qb&ph+!us<>39`afDFHgzq&kze9?xrP#nv~y8St;bT2PeQ(X4s< zb_Q!E@s*}_w7CHy=?w|3AT_{m)pU(+qmm78-`W7p4iq;0`_4^j_PbrH$_l9g*pcl{ zo&i~7&2i*jmCDKHXp&)Ov03eUqi9R?xjC+6qS@nGq2VelcEts*sj>C-rdsGN+UNFI4AZc1XP;XicE-h(ZLVX2*`e~47vfb@ zFgtWUBRNU@PUC0?p8Hi|N1V;_Fq8a)VXilar-=_Rw_HDs;rDKOTDQmTo~D^;n`=(P z%(Ez(@LSXKHXGa?zG#7LaMu>NU0>K=np@zTnp$^bee1~TloGbLpp=lYcGH?&wiY!t zr(50j&nuxuFVHH;gT-x!uQa#0#oh4lCJ1h=Z5^aOwnoTg_ZF@lME;2z3)hj!oUB_r zX`{+oTrePqx=Gfqlfn_nldbxl#b7jH$wiuuk-jp)`_RlrBg{s!{57)?Y7(X@SvK6r z-+S)b?$KmDo@IU+1t~x6*Nw+Fugp}c*q0Jps{9%@_cZf=;UXLT&b!+qS3UQVa$Xpa zPcZY#a%c~qW6I~hBd#YqG7|o22Lo0en?;*ihtG$I6Y8u@JV~4M z0G_%f)f#fKH&w~;LaQ*-jH=e*q;M0BTsuQ=k{EC#;-Vlo0?hucquc7X;lWZ?r5w~6 z%n&~A5ZSe|a%2{}+&b*Ek(Cfh)a<_2kc-3qYYjd^1QxUc6EPQzJ1`f7-GSNQo9r8f z_jR&VF3SzuOm)^`zPG-V@3qodyRceJnc{9pmt@?l-!H+MtJ9vj4D;XaG3kmdYckxp zqnWMH)CAk9@`AEPYp2Oju)~2F>I??Mb>JYW7}Uw+xB@p^XGKCzW4*+mye{B(1E$a$ zvTgv;eU0aF-wA<7TgYXNTXAa-6?{4FZi>|zq{a)l|1B*_1S*mCiNMPrkFXqy4duy1 zZ#^0WOpt@d%-R#Mb79rv-VK%0#eHg&gYHsRFtg|f;cIu7u7X6bGO4$=UP1d%TRWh1 zWYH@T?os=n0jMKkKlmB~(O$DS58+$dOu59%Zd3|@gI1#NLYvTfn6TKBePh{W}tSWw$CqjMf44>Ted(4r0~BRs4e}!p6GO3LsN+ zT|m8K21DZ&enwzT(j9&(IjV4bQqdqHJaB~A^qNy)g~3~94y=jm(njX~of2ON-L^Df z!lRi(lIG?~HyXUsCl~dQ3o)K(P1+{-inP4SR|vPeZ1$kaYx|qGt}$P&OCYMBU@QU7 z;9Z)3hzBAli0R5t&$nhte15|X%)NYm*qU$c$gf&EikYWy@k~3_3T~2xtf>X=tqDA( z#I7wn!KOI3D`ui}0=SqoSpW$@^sGNB=6fAD=7iu}=vjR}Mg*!elAp27N!$&KPuy)f z%3}>94M+m!!#PK@nAI@tPMAnOVfEZzjuY;%Zm#QSuLf*KbTvGt5*}zlB7N_l%3wRv zlT*2ml89&6du1$Ps?|rsTS!FHHQetldWGN`1frmEJ}={w;Ax(lWIczwPnk06ry9@S z>Sp}%RyS>A>hPSfD&V)^IT{#g4&>+--2a=YEfyEO>#w6%>);F53EH$ruY{YUKf%8y z?@iX8vr(z})1+@Pyf*JmZ-5=sd^azx4V&X;hXUaMt{{Bdk-TuCtcE?Gt5MflueIew z;U=s=H|1&3UI^iY`4a`+PZd)N(Y!bYDkd}INI5s(Kc=&9SMRjgz`%y%)K630&E4Zh z8_flhlvA2E?k?lm`s+k58=YwOBTC^W`iW*$T1#2(WPsU5O&6k|sSm0plZz-}2Ut2h6%c=3PU)GR(*l<+$~k ztml3`UX2B+vqk0HVDp8Fjab`LrnLem3KbmELuBsJ z!un#SvciGYx(~U%|1WjrgDG#(F=d>l9v=}C%xP#wybh4pag1!@zBgG?p zlCUtoTOr$Y49GH%5Z-9B$5l3@z_JX@ZgA3|<8i|iUVsyWQj(1yEXXX)zti2NSBS{fZ` z7v`vvgSQCJ0!4`AR?bgNDvQ{Lrj;UE)Jp6#cKNDj94IQ%I<43iw$d_a#de5nbT=4+ zYvdU~+M!YnO$9=TbkgQN`suV;PGv0;8^cHARMoin}+kbg=G8#R46PRsK~>N6D!y<+dpbV z+_12LL;I&@92l<@vi-x?B*yjREIp~`(t%2$sZO~Z)*+#5<_qISZ|#>eQn}3L59-@% ze*d90y?dtWy0OWQF`;Z=OCx2hS2pLcIo8!4hbWZI$x*fpZh2_bRl)`6H|1SpmC%mX z7E6S01W;n`h`q12qxFK6q+H2meHv!DBl*)mhWmiv!_%x)&Qv#wxWxj^TUFQ4X%07y zw>unoK4!1=qCV`NG0|flDhsp%AJu#9OtZ)q4Y9GHJCs zF`}7$7wsBwjIOkZ8u!zKT7|8{%`qy|5`u_TVICVNfs@nJbuBGMiL|Q(9v71!QI;L$ zxdmH9yp>Yif`Uz9Qec1&=d(8dZF^@qPI-0dvGTMr#Zk((ya#im9yz~u{pF2*vQOpl z1rHu5ow%|;>lQnA_Rt|K-Uyp~=#h?F4(4x~GHh!9=(cl$bL6tVa}P`@|HHD5Gj|ms zL#uE`U%%pZ-Sd}Eo49pWU(9ZSpWw3$P)}HO5FP0vn$$WhtH5vzZWmKEY;wEk+VCYl z>~G^i3WMJc%B(_V$i&;lTII|Qw~IEYtV9Yf2tY+sMAryg+K=FnsdamO;2F-jTOqgG z8&|1Zx7)R5Bhs`cd0QhgUG?7UZnqCw5dXG^gz$#!pqbeYvPW`3@9BF= zC%wIvdG~&H+3O#7_OX<-Z5y%Yi#erxAM2s~H0g~+zlQm?mpil@_0;{7HjOjou|G!) zIlN@>s+@-N($afoblbF}f9miErJdKLS5|!VJxC}rtyV)CV+9Ke5^GoyzC7A`)i~dJ z)ihF;>yLu!0WeVM9?DqOGtbgUH z7IsvC5fRb4Ss6Nc7vs8y-~;DYqWwI zC6u)wma}LF2rMu}%3+o9BXu`6$i*9W=jQI-P%Lk_5w`Nz=cU~ZiynLXq14oe-hND4 z*)VDMud9F&jpx@w64UydCW&3N_#x!&!%nTL;?zykwzet1fud7{sojn!u~UW6b;8^( zpJ9MAJcrbUmf(`K#tu(g#M>32*fCWCY<7mEr6Rb40Xt35O^v?MJ}g`8%@r7_n~1nM z1JPn^Zh&BHu-kyfSrTibBC6)9b-ti7)2V4Sdgi8K+sIJNj7D8}|1Mj87GC}!r?&OT zSWB%_JdCm*;?kUUO^Nm6<|AL!%q!1vg6uNtf_it#^0C%9K{w0UmGPmLHba=X!;G6BxPdD;!?YSRiegyQe+S5F1gjO$ zpK>|h>@Y|tN7xM#7+3I)=siWJ2n+kzlm{9hBE`FVB!h@SowGy4Ehz{K_`vRI;g;{5 zM&f>#Txf>^XhFdx57ZENs1YSpYIhowR&uBlR`D(Js{_aU<|1t524S6U6f25RElw>; z>tW<1mwQK?aCYYDKz9M5*6_Ny!YK$O@Yb}6g;rR_%D0FBx#D<1?poKou z{3z%BLT)775!;TSrkz7Qoso01b7+Bke1#^WSdNPCx7djf^eBVGxER{Q47-TZNRk;3 zvR*G{m)PZED=olZFvY5QGVv(Nu4P1McjD$K>?=R1loet(*SsR}rj!*a8_T00J2Y-Ii%o%tTsGa54CdH$Gde)NIkYMjLy#$cE*sUi z_Bj(*wQr|m9AUZI$cn)UVD*Y$8GD1hRi^l{`Z6U$$>bOKDeF>RW6iL}@-5am-~#+- zMD7c!8^AhN2gR3NQX)&)KDM`1iRNWerZt*hY&dD%K_e;d7bA)M7-0|UPw`-k;`XK| z)uD6!JdGPN2zA>_Z-V)$V^(h8iaJn^?H~k5cx$6XJf+)LdQ-egq~_T*qz#KhL+B4| zDC)mN`99Q&q_@C6uDq}5abYnnc6hiMHAr9Cp8gBQa$?i-npJerbqiz*bq4(An#R`N z-e&3~ys33pmd_Usp!AWWT$*q|a;XiN4vp`2Blwl;O+cF`);U0AhBAy%ENDv^QK^g^ z80;Y|lZG_BCB4^hT*?VECCHKW=L?9h#Jz&A^o%Im6MQm>hZmZjQ#q^o)5KN83)+4b zTLY{{3-xtwYD06$Y>ns`ki||IkUeB;fM-=&4>dij?l7=D3heV6zn4xL3lWQk^&osM z+NYYu(=BO~C{FbN;ObIz8lG>knNo?;($i8>v6IyzHnRq?Q%m<2j9Gc_q_1A<8D@I& z*yg8fvKq;pW+Pt&`;f? zyIp+wXbj5VZxd(Z&&=oMk9UB$jyJ`49>AvcRV`1?0QCJJ^uM$^p47Y+zh(29%ARsNg-YRF*7L2ND}sc zW9-=3${@Qw-tOp;I5+qCzc7Nj4|ChRnNGd4D_QdVe>sB6wfg8g8$*(9=AXf(5r%sV z<6X?XW?D;(@NM_FzukB?FOcH@-}JwE0hODDcR&oRVhj0ct`QkkBMg&?8ZL9v|2KG? zKjTIV+ZSY6b(?fCedg$u*%xfNBRQta=8Mq2-Rv^vY}){COAYo>Ex>NK*(>AEWG2@C zb_av1j_DsEhiUg|f{44T{)C!)3Pp3ftjz2;s#>jg)EA^(-jUEH9u-peB6PgoAF^he z)UD)AYXxU*DyCD@VX;$$q$p4Yrf46nc``?TK!vh&;jxw zNV>q+(LD&=qPy*Bc($_YHW3$2_@p~PTh~!de$O4-Wk&e*TW=F@@_QgI?nFCjm_u6l zVi)i*@JllxaYxn``=D7oA^0|XWjfF-WPL|sfq05+Ow-~foj{=^W-lkRb!7bgEt{%>%~@Rd>=8DXsZlNoB@u_RRIx%2RhRfoyu1YD;#3 z+#1u=TUQma?uaLOPZt63jN9z9@vzI%TJ@Liq}MuU?ts1D&oGMm?8@4|BVOgl-B|a^ ziQDb+&yFVj9SItBge?K!X3AMBAnlAu7L`1p*p**kRB9|0lteO^A8nHbi^r&0oxu^2 zB8C+l30o{nN{&F#OO1Mo1P_XONj{v`U`&IzeLDO8&$m5&W_qFdjbl{@#=dua(Yt+f z?ylL_&}GG+dUgq5MrC)eB@f@bZNrf>_sm`LU<@na=FBISJo<<5qQIp`XY4r{+0N2A zZu@bzK*|n#g7Ft$Vup=RD*rwbiqi!j{igOC<=>Ty<0n<+$PZ8HyL^fz-s|qkg%2&1 z4?I=!@W4>B_xh9fl~;vMTb8wHdEC9-l+{(W89Wjj)YbKyL}=y?*bj*nVTZn>EK&?@ zGW124MKX&@h@c=|yz%tRS)V+$>El^*KDBO_&8%w4ONC?hRWCg-uJAzEHYPo{hp{J~ zQj}f073))B*hG7z;ZR!Dw`(_o;1*Q`6?cfp{GVYQ6C^fEK7-?${QyaL3ysQfl} zR&>ouEkb!!#QnknP>m~b_i(BZGurJ5mKw@)3(y9Uyh7UZqsC-mN_X|C?LDh{Y!#eu2yig#8P^ze;f*|K{;W3#kJjg{5;XGOb$wl6#}V((w~J^$;9!he7A=`~6E&)LuaY0OVyQ&X^B4BN((Z#oKGP8lfI zYq+r#JKIInLCZO#{RbikLs&gc5*Jd(4VCtxj?YdHlrkQc8UtxyF z*D1dp3;o;8#{c=B#-$75*KF0KEU01ZCgf0r$h*-bk-e620dX0`(Q_W4s?x|KXu%f* znRX9#InUDnS*BSE(35IyJ?sTEK`iTjk}=9?-E5M74wu@u({b zsS&3Z@L@wW3W*)o-*`Yt9Wk-Lx<0xUIAv=wqcnIM9%Du=VOXeP=psP$S&R6(3I=&Z z-o|rk+=mXK*0FbOPQ!UZ2= zG^*!`%z0&|2h>!p>Io468956iuWt`BtL*0ivKm@x5wer?R--5_3>J?^$Rx)DY&|^E zD(E5-B2JlT9tmlMC~sb0Z;%JfT!uJ2qN(!*#WFO$aMJ27bt;>Rz(s>6Ohk~vBWU7D zrL*9SyO0$)5VOoo`|Xg>N~s9xJ`z&zEoP)4zoRR|jyd7}8KZn-^5a5N1LGqi zGW^2XUXR_;DM`L}s-Z7>T{3Tr>I zc-L8KzaS+jblM5KE^wgZ`@?n7bkW zFbcUjgTO_Bh!O>Z8L=`4*13~z@Ri!;rHt11!`w|R!gz20|9t&l^#wde`yRTB{3unc z-%%eTD=S)$J!$?M7w!yiWIJPmaNr$G6=oq1C`gHzoCSgbhrdXHqE2J7uXLza$#Qwx z+QRs}MSpyx;PKhRV;k1wnR({?mmWyFr*g+Q{^noG6UHrluGD(eRM&9GdIXfff1+)o z3H(D}k}hr`u*>YMOBli<&G!hY@ec$^?5GN(z0F!}M)$x5sYal*-X%o6jH4%*s4s`^ zkyN27IkqZ{bvWnT6kb(Tf0+)YLVQl#W$Z)LpsIUyvbZjuW<|oArKic4#jk3GJ2GnO z&FXz>%}L>Tx~Vy-x&pTX=>En=weF&@HQyHd?lmmA+3;83D>>AT6g$?CRYjZ%=u~G? zmW8*o!`1_#G0;z1{~}DuZo|E59ZA*vQei{4P`%Va!pd@6M{-=VwWHp$D^sbz=)PV@ zQmpR)x?>$l6B#yNiyI|}I+EC%MM6+4?>nrg=(RFkR+B{6T^;aYF*ebn@c?@GHxt{J)ec&ZG^PUySQ_sQJWmczzFr*^gZ0{g@^UrfjAa{#Xn z^!8k%75Bl{!hJjl&&L}6Z4fat7HcAWHz*u6Q)zcBvd8`Hs`?7f*YNa{9gtHYPsQW& z1V+UFS`|pD0eMd5K~w><;Vpq9)&UR$y8@fY7yN;!T!gxO)T!IuKwp#%$z{Cz( zq8(WyBBlycR;&E%Mv+iI_2Q>&sY}TA{Nw>VsH1@I`72TTsFZo7D8i_eCwrF4tKR?>0#8}`Aa)p z;?`UidO!|ym?FSEFuywJt6jE;iEu5b#HP9?;K#G-E>_I{_SxqyK3MVXt~00q`0?Yv zFZxs1X2$pJXNC=%8VxTVP#SAx*5S|JGRxu%8!jr>iN^&5q3is`T8DFtxfz@pVDraa z!6Gid`J-omIy~a@AhFivBC6{Q(zdGO8mgKv=NYcLg3v>YQ+5`6sI@#YblAbWZw9?U z3!jM~?o{_Ui0G|nG$^`^m6%zI_+m^8)zH&tk z=cuhS7Ho<}4H2+?*A*fTk=j;a(p!tw#)}!KjSF0V+600q3_c+;;~0Z!hL;{lb`hri z&OMf{;-aIDj8F4ML+{07{-lM8w zq-DT+T0UJnM1VI8NQ2yOa{5a#+BP}muJ*8 z37=^E89TMQidw^swH zDw8!B16`DZez^2_lUv^xw*mEcTkepvwRk(7(e#uS#ZaX^nL6adc%1Wh(OPv5-nzP; z?rJrG7H5+#g=uX^llh{qfcu%v@~FIkOD#h=r=A51poGJwDS?imN^;mkpX^X0(u0&M z$=nmf_jF#JSr=rT6~qR+LV<4C-FaokPP*loXH@i_6Bor9Qrz*7XZ`n)n`M~uuCR0F zlNKhdx;|?;1X{%zK9W`-YJk(X@lHNjxG_=q8!q$u=;1rDEkiBkMorJkJ?f)}Qz(so>_1U>Axz@Hxhzv|dH@hW(2*$>6|PT3-tE zU6FdLg^PL(+B&drueQcMiW1plWJvd(mJazH{4-P6JdtDlS&NPEIYI&;R6;YK zuyksTmC$@Y=iH_ikagGcN;F>~Vzh_~`fDerHg9~T8l!m6sdX(l;RbPTSfp^#|90e& zI!CUY@L!|{vYcSphaubm61k*Tq@&>X?jn~fPF>Cu1=tcGs6-44g&mRCB@*AXLajXL z^=n-VyL?#t#LFp9J@NE`d!`><@W?w4w<+|0w|3QmjM}Q*uMWEyx_wp0vdX1%`i;nn z?>1^$ZsCgIqt=&nd~i9hC`lfc8Gg4sZ_E=f(kSKQES#falu!pw%QkH-CaD;_Wv#$Q z_~t*g4kaBU^3jukku4jwl}NEzb=2cqftQ*IcB6HO5v`D~O5+t^$OdL^@>N@l=Wou* ztF6J$^|`-m#PpXY*?_XmINNLnNhv4=Iezg;lggO6HWI-}%bo?LQ`AmPCU9_NhT>BvM$PmRJL@&vK!E89hKZ<*x~vF6K1T2DT-ziA;jMWsS#)^CxHp(4o$UTe!(~X&i%H!naGkD z=#tGHc&2STb<=YZ&~5Mt)2Pu|jb(|P`KDNwJFt8|1+wP5>aG^ZI^hW@#umFdiEs4v zAQ8oK2aWxwa-b_vB7{}u36O1Mn^vBLckd1okAgFQuPazu_{uXaHJ%mT``uKFwz==_ z@0NpIxa!HSFmd6V*T;Awg22kSQ^c)e)N199b^F{=!#Kd9L6utCtOsr=-vWKVbq zPBZSrj8XqD%=53CSTtrOItnv~73qS@eH?idF; z(CE#|5iL^b<|jCD%Ol&iOev3C>@;rQy60&}N@1_CXa3gk0)NvPDCx~>K)N~urFG+J zeYRP0hfD5v;J4<*D?QHyovu58KUJ_C)QkgN#;81uzQYVrYTSexzHgBy(w)*iTQLsO zFG4p`MavlU!xoWpM{mhO?kR<%VqBx6g`+%97hNk;EK1eNu&vzFhvl%#*Ihvq={GIv zh)}5>IwA<`03!C|Dk}$3MK>0KRO(_Y^9!z!ahbZq&a^1Yq+iVHzT{*zPgupz)xKo+ zQlF=|*nAx43QKo8jIVgW6Betm&`djEvBEPKXR72X+u*Mwt|x5)bG0*YS=0Y2KP6I7SvI7?2wLy zuplW*FfPq$>AtCF?8RmpbCUxA7lz`EiS{T%I;iv`>2QxG9ZWXqa8~7vrW%^@lD9Ql zTF`KvUksDpD~fE$Dbi-Qbk>0*nkraQii>qh({t46dUNxqbzW#zLsLI4C!whgE#_%! zM9eH;KwQwQh9>*E()zXCv$TKsYulrs09M$H=S9xnMWTS=BF9>ldfnUu*W;K>SHP?I zUA->7VR!#u`APhWbH*S~f5LS&<`9apQ!|#E?~x#XcoxgH7CP;fU`ClXNHl~t-!#z> zN?C${j`)MA527Sm{DH;trGSp86A$HgA(|I1&i$c^2(%>ast#x>_TXT9FMg#@6z2&D zVqOy_=JxONRCbs-Bfs}>pRob+CJdh$+|Dwp!(Fqpa|_#gvB;doJtLEncxdUs?0fw> z1hnZsc65)VPD4khq;~2Tt{kcM8!%wR@TAT;?K`yX(7x^6v5~1dUgLpRJ46T!h>8F& zN@ffo6x2u)ftMGq-LPQM+I0)%f-z&p=8xf_bxW5$yKBj^U1R5spK$*J<0s5T-yxZe zPn)n44w8%7Mr_}hXR@8eMJH_7&N!luMa4v@{X}33ap#D2Qm)mT9d~gA*plk%CE5XC zvJzl_2AFI#hP=OU(R(2#2^bK>N`V2QO27i&r3PStBvE8$0{%K_Uc(hroqD>5_*K-F zCHX4W4Co&A2}#L`bf;csn(h~^89SP@DN`D)!Rw&*66J<-rnas%FWf4$5*QTq<(?YQG(~Sp5QyYBXGF-T|H%j z^e^Fx7f$4&F%U1lQ>e&G)J?v}S|Wu~`P$0LoAW>(>mm9_6oiH~pFocwyM_KjPe=vf z2y>i%9>9-VYv%JxnODxY*6`!=t+}!_mtUo0=Jl6)+`Kq)o{96DVlfB7dWdTx+)9Ud zsQdlF0g+LuNjgJlJEE{lfzFGwi1XXbyie`f>aK+$558Qn_~_KKLvx1@2xw!<9uc(r z@CVyB9UE9Q@V8gS?^<}zqT)x^&i^({UOzVa-UWk3Z=W-7NvctjWX8Pq)YR_WSRGk= zBxy?RENu?Y~9l7kMAO5v^!t?Wcs#uy(H6FY=TIXHk4)HxhmEWj~fK!3-JCv6;>kb14 zb=DDV-*v`lxnTu}fTrt@YCVUf!7~ro+K3lz0CW5fE~c6a_?^zx5wf)Ee;Eyn5=2U`93@jyEkHe@G*?1Q_3**@L2eo2R`3dR<`f+ z2V~oavRBvU=dXSBjr)$$-=p`v=AXIs=Vy2ReQTzFaWOvsedn`3l-axQuRWg{xK*k$z)(&zBNFNLtk>HG$8_7B;1lOr%$G zpvib^wJg=jTk7gCq(^B;udCy3mespyCg}Hrai`Ni{Z`ywN;9&t)chmvHj$(o!p0dBUxu$)G}O!VOCrJFHh_>5Oc>diAOXvGgkO1kr|c zn|87#AUT|EDKwi{tI<@O>{72e9BpYm;iT9cPPT0P84#xeBCTLRHb9@W0JH8W^J)?5 zNZA8eZMG%=YAjo1AnU8YQJtj$jeqru#q~J0NctMTnAuDqC^V1dEnK%C;}`y{7&+26 zG!`vTNXlwUz`Zp_{ltiPiLq0*5QlY zk4CZp@h>7ZB9_Nm%toaD8j_QYe!)T5tgv@`{eeh1l?9)uzBuCE6bZX?LM_$5zVey! zQ@5ftHtg@8vCt<>rcKJluz8V~lPR1~qQR>!3i#d!V|K+^WLwc+B^VNd}t^T}L|2zNC z!0X8Zv-}LlRy^il5EV7trt4h z?o*Gf<$eu|%2=OVc=9 z1+XKu*^OC4R51901~80lRguUe@LjhKR6m~e!`6SZcC)_Tv-D_b>G2J9pWf|nA;K$Z zbEhl+R{pWC(NHVTzf|}1$w^Dr-}A&^B^rD{aD2o#@4!dZsx1awNO^xZBYe5}@YZ=4 zTjF$RaL`knpiHZ~EuB(bW87igO6v5_k|1y-468+(URag2_r7@2lV3mV z+r=libWp!t2cPTjox1&{r_;wx^v=4l`iZaRKl1&Pr_O$P;>5YLVau5J=VRaBpLp-8 zZgD+3`?t?;6A+dVmGbTO`yPDkU%PkzbGiK6nNR=quTMYuRr3+fBZANg_6_#`qM;k>pIlPzrm zXZ0U^UtkBgZh}Xp65!h8$MJL46~;dB#Z_G_m5BI;D?d)$sf8)&e!aD?!5`FX@{N4g z@x?MVSa)5tnAzCTm;-nnA)UyNBQfph#)$#ToGf?o5AkcmH$OLIK;tV|n@7v|sM8QXdUJMJ1uI;)Op#`ahrzqEYppN*I~Jk00>)LY9d&$O<=}JZ zNn2HOY92XEW7|GlV;}1cr#-#s%{ehkAQjbUhs~E~f1{AcAp7UF8ROUrd4&Ix&H!={ zAg2Zi0cPhB^7#H)p>2FS2Yd6^Wb5@d0YM?{eY%z8w~Hz0GvlAE$9w3=mB)TP|MGw# z>|m!#=KaA3%x6(2maiP{v`D%3!3WCq%1(;R z4bm~6*att@y!qq<(e2wuKX7vM<_{i>)qF}?{{RgJ3%bF=lGcE3oyn&ZE@d2CUPv~P zMg~cc4rHaYl$WOMJ{R8Bi_1oH;J&8{U+62dgI#)VT-9yj2%k%r;^o-t$GgXIwF(1# zKVz-R4NbmVa-cD@Az$W8IcpV)0oFXc1~zq>r9-?WKiVhZn8P@h5>ZZvxj zuA07j8$x8Vv90gqd2`A-dKrliF$P%wf^`;2kRX7Yd@~oYUgPV&_XrFO?-rWy2#uhs z=vTDbGsyU0HLeE^h@(Ll)-6o!Co9+fb5F)>%2s zl_XuJNgc2Bnj9Z9EOFwPo~6N8^4moh^c5mb8P2`hk`1RNZ1a2mD0tzs`rwxcNHa1p)6>=!6jj^dtvNh&t>w2P}Ak^)s-54Ze_P$ix;h z5$A1!TOlJwbzD@}P;FEDO2eviOf*6ih-ad}XT_hN*{N0Gru#t*OoR2Z5KAiQ5H?Wrk$;q$TjEf)tDB)zfVoe#aOAMv!d zr`}t5tY?1G-=BTv&lL|}3|mmYhPCKd9eSgBxqrnc zNw1h9EGf)HJreE@E@L?q0nd}0R)j^36|GVSY_Wf-elZL(ZX&<=FKCe>P2Za~pcg?j zdqfmF3R(+8D5_cu^y8AW{hg?ZE!A!R>S4=yS^msY`PJ6l-#%DzanC1z{P^SJ=g)+# z{AGvYL&|E`FUx7y`hG>hu66H=mzY`kfi|rRk5t+LKB02mwMF#TfGAG8^VK3Q_WdL z1~O`zNZY!M=uBk`^lVaj-MTzB($(9mjit+pu;&+skLu2A zkpezoO4``x7p!^1tF7_w$Zjylq>Ecub|2nl;zL4y z2zjFIE)fwEAx|wdv26%zOsYu|_REQj!(QEuQtJm+)t(3_Z1eCtk1RMk{hk9)Ke09S z{1x|Yv_wljF~6j9iBNXxuk+Gf3Rc8`ccDI6y{DF)h#}2M87#pE6X~r+D;M; zdHtwahk5!f<|!i!)no?ey7XUyRR86I%r0pDOOUJt3IF8>sit8m?<~sUbhiG0Pd|v( z7a21!>n&^=a=5qQik4Xo%LZ{y@PwMjb0-~FJ<1YmSfrHahksC{EBB@x`jY1ZEgnB(S81WmiRN{Y@?|R6t@QSvJg~En{om)%o~Vf*#uy&t?miT$(wG96wZ}?uwJ$W_E7kZmK(JgA#ty0LtB3@ z15B)jfLr(I+}pIK37R-XL@Dz`!WslQXB6)jo>@{F84aYIxW()j#6H6o;s2G0DEeH7 zo~l-ni2aMzt2>ElqTkKdZtFz93jjg2^$(kf3oPXK2n|UT#S3e;w=No^8?q6qLx{Q_ zjyi>O$+y<11L_sosE1ZCLigLg=Lske0E+P6iMk?ckjkj>2o{JVgxB!})-}eCklxd+ zPfI2Fyb4i0*}5g%x<$YVz$mR2?O+ucaPePLB%JU-?|KNh4~gID{^{cWruERT0XK&1 zummpDv9|w-Tx`{i!3R+s)#A%nFMP8qI<&29>JatWE_v7IksVBOo37C{Z$5%kN(wg> z=gtk|eoA4?;sLeCxpbi+_C)P~#W8HZ^_Q@@xf3=Oa%ByAOIYP%rKFqSPN&rovdl$n zQd=LPW#Zt#OqQ|XVBy}YPd|NiZ{dNBtPhU(&fD{l$vAA&r;jZ^TRUR#+@%@v9F~7+ z8#BLu|NZYP_1i8fhv&%h-3OWB#mD~n=IoiT|9yGQ`R5-96O_~ER4nkpDj@%=(O<~| z#pwFaphz6%0i0O+MR`U4#k4{El3}>!{0sX;{Ljl(a4VOiHBLLLV`AgMyF{YkWz z4;mDSXLhjR+An_=zYG=}c?|22|1|7xdPcYWwUfZzSpH2JFIYpC@bv%A+2JISLSZWi zi`~JS0&~zg&lKKwIOIZXrk?%TGq3%h@%Ht_wx|Iote} z=VF|F;NY}gWuv{v&7AOD`QztD(`Qpm>)!S4oI!bSu>ZpR8Nz8Y3<2L*Oa-su$ zjM}m!c?XEESfpZ0r(so7@}FNl=I*7{T^@UFy0u6O?>2MZ%(0yoRre}By4uor*j)q4 zdiz`*Q`*a?L-zWmIZvEi8@ldzxh(g%|DicWovV(n9W--6Z)P;L&90t}@q-4SN_MK9 zCdTi3<3UI#3Ar(_gEgPXNlGy9)6{EgztN(SefUBX%2*_t6UyJCwf3k)8V*mH7xc+P;fZF2f!lPdqrnlMD}*8Og?QEHQKv56QEKL&0^PF*s?iZrcV5UGQ)GR*SX>bSJAa#ChslK;^0S>c-1YwR;) z-O!PesYAt{@d#qW{1rM3^OXZrtSnG00&JX&Wp3J>8I>x`$2deJZT@o}Jm-b4_n~=b zw-+<-*6BktCQqAatt4C_i^=i^u30LsSi|ZIT>BEY5QC~w19}(!SQFPGW{{K|7^#O4 zVBBi_^L3j(+gG2zIeXoLNek{-xTEOJf8`YAy#CCVlSk@;M+`8v2^cW^ftSnX|MewH z_UQ1;>B$|gR@~QV_S= zHFF4S&8G&|q*>I=EQ(D+Vv|*kwOdF`#MVQu8V_HUktr;K>l9=)Xi_g;m$Xe%PXuAm zVVk_8=}EIPXr08`%ssjG=}%_P`Q*vR-cMu|TKSGW`J?tws(RYUpvS*jte?wOS!$iG z*Y0@q!uoZWs`Pn?wE(hniL!ACMy*t#Mp6hS8cux@w(Yz5(dGNgdifw|w&SOi{GP{_ zJod_X#yX97a&gGe5nje2BNRl>Wh-I}%4IH+sc#kKu*Y?(rHZ(#K- z^pgb|qc0t+Erb2gOiT{VRBjx4{?-PWnLhuN85w)@i&bAL3PvNT@tRoW=-f1)NYX^8 zo4)lq#rKOjrLKP1Cf2}gePS(z5mw_cwV3dhub?kon(=>tj}(PHLzRO2<>e*4As66Rj#Q1S#yd`Sageq1tc33tlXhOJ7_NEG)Rl>bKDp+arzJG(LA;D9=*5MOI~% zK2^kw=3Diw=&91Isz~;w^}Wz}x$>qGNhwje=CWxIxCY*vs^$C;O%-V*!kHu?pjFsU zq0^|q!>Sxe#*qwPKK8Qw+DAQJeRkl=8RfHM-Rm*9w|Inv*2Jrxc|ZM zW#2z}nf0BKH&OZZ#KL#yO|2_oVaoU8>+XB#-39M6?+JM`l~dn7_5G4%k3F^w+$3bB zSP#VLQjKbV*AlZHOM{8cgeL71xY^)Fa+38$lGs!Sy*U%BEn>fxG8{MfRotjbYoufUJn~_s`ab2k($eRqi1#pD{0P z@e7})-eIl9Ul74iiMNZUI*Qvx_?l8? zjDGjI?)P;|;!`_YxfEvolJ92h$+;hd4S8&0=)y&kH0i~PLm!0%4CceL!bowN#gPqkPfO5O%dh>BuL^2M@9&+1UBk59~c9OIFT`C!vo6 z@Fdv*m}U0683exmH;{@cmj`0{|d3>*!qZ9)_t{^R0=ZIHgCOW@JNx2UP_ftc7*SJ=M%f@dGGpqv*w~+~n8p z7Zm+&T{DWBj)QvED=JBgh;(d@8tDRx`f-e?JPM4?psA(pK!b=w~okuiy$v%K6d?T*=g~KrZrw z(l%T{bXDAw-}p1H68E56;tJh!Qd}9JU%@?Zh%4FJm356j@T&sWK)oG2l_&1W(XLeD zo}=QPL3BlMu6PQvpa(E>#ksVPP;>u?b16S3&iV=$M3e-|iXAFge($}@3)J_*MT-i@KKdv(;tHFkUQ@Q>Z~miW z*}THa%0i`f?4zWkcqp4s`~?WUmeh3UsL1{M3wWp$KfPpn!(v=Elwl3;py?>>zw_60 z6#0I^E>skIIxTWRVZTXfOeS1vMs`W>rpycB(L8h{TaZ|kIBsFaq_ol1!|$ybl`ap9 zRkmd2lceKbyb4%^i+&0fB~+oKic*CbE1R~ZYhK9gL6c|BE$Nirxf`nrQGB>j*>5bb zE6%OrUNa|z3>nJ!{7I$r+qcVRr&>eT+c`FlqliF-@ucYdl(N*jA4AzcJqj{?^&@CM0J7FM#Zi(qC^?lUAm z*%|hT+G`@VRjwX3dCJ(aPd+q%%h>UJYU!|QdHnrzii_vYE6SfbH9v3aRL%!2ESUIs zSW57ckrh={6(g7WB}J^BSg>$V{HYV85ms{POfZ0zUt$o-u=3mf$IDB@MM_QQeE+Drc z0E|@*NW}}JJPQTujb^BV>+8V7As8Es7$xM?VJ9!HRyO8fUFIl7(}L5Ht(%H~C0M%B z92EkWJNHNmkLeeF^lDBv^x8$kQH1DL7~=RatT`Qpmd0Pnn|^7`VMvTI#PZ=#W9s!2 zxDJ@okANAAI}P+4E5~2_&d=d5rxhVK*hE}JnYMsTr}13Hz95uWLL3YEmAD7VJA=`- zxW^yia~!)8MfU(;)sIx)gbTl89jt_&;tVRWv_X*?TP_cRT@dfIEP!CFn6nhW!Bq(` z*-=YG`1Dz^v7N)i6ITr#S$%g*gxtM%=8I#CpYNO0qi3(^nNvG;V#UR5jr`z%%rQgm z?lCzzt2DAte9xXSGy3E_o}87ImX%U{?>#lW;)doWCf+wxS+I4hz)M*qtpn~VUh1l! z2r~i+XjsBAS=~aR=I&^ir=kbA4+W5eZcNtt^KfmiRI2?>N( z=i*|1NJFc8X>$M4sPwyg_Kcm84z&8Gr}s~-9(m95gt**$6BDNoVQYXEX~|d?!cU3y zSG)cEA4Mi6MusOO@YsaNsGdC$VlyzdhaHkqq%6vA)g&O%P)I#KuWw0udP(2Z@o8z} z*`bsPefv&GQQvVhI3s~}i;-UPk4uk9H(_XFf=$7exbFw=RvJ2^Rsu72RvPLC*GUOU zm*jkz`6<8P&!wbGL_3lq`|*4ivKNrXmae{%r3eHBA}mPE#4E}_Lh`2^w)aoH7(d=m>F2KfhqU zZNb4wlMXJ(s@PLhyl0-WZzUVT%HMc{m9rr$Q&uW}R1P25ryNvHRe_epXemT($v+NN z5Rlv;;+hnk=i9R8?I|wWQ;{VaRQ9D+u?!~fJAg%$bZY4>W!oEXDBF~`fCg|H%s((+ zq5Ma1DeTC&xY(5Rbi`^=tU|1~Atqw61e4E3OK^PrxJQF28fhR4VIyzcPzHO|$v3Uq%B6T#_Wde4ofPEA-k_>=N)N-ao6;T2`ujC{7IW8<0T8pgYMI6~CcU z-_bUGpRImZH8j2t7anmPbnRtmU+7wdM^V34@E24uCK#?lO2J}4Ib7P95rv3R`qjkT z${ZFFWz6u-&bfLtyk86#G}Z1VJdp_kNvOB1;%V1{#qKk^tTp$Z-fW(&*%aF+YRxcD zDwRG$mLKqmVK5?@NxVyu%6?Xu1?MMG*=qYZj~!wM6O|Q8O``1+T3LryM(VAk$1#@9 zB=P=H79KyB#R|LWALb>prEGEH|5w|yfHifcnRD(l|0QO9xJ*0PT4IIY@NUB}tlPQUJqU%#$j z%eanX4uj)1jBTk#zSgeE}Gx-x>GlGK05%lIdj3(k(Hp_7J1a1wj zzYW0b5C``v?I1d#Ok5AT1X!nl4gh+%lP#{7a1ij5&_k5Q-++(M6VQo@`#AJC$G5`u zE^!}1PliEH=pguy^uo5h-m6UGT~Lfcua ziC{*cL0$w`7sSUCImv;03Yk0~G)w2JV0otcfo~KHY-NFHtCNT7!EA&!Yc;ZX8VWjQ zg&y?Jk|@hddSbN5_%bFvm0xSi}Z}gL9_1ucQ|A zD<*&j`B?bizMY)E#Ml{c#U;;bBzgCM9#nGl3Dc4?U}?|5$9SSxNjTKCZ^ zHi}$y94ue3vk<;r&fAiEWF3n=_tjU5?61GRe?mGxXpna-=%{(&BfTN!lih2M7TYr$ zOG|eb*>W0f4Q)tIxdZ0}9JQecA9$vLDHFg01GMCs0^z*=$dG~_NI8xM?zm7b2#yQe z?OQ&N$({C3tG6C2pu)$$#rTcnbDJ$EN9?%Y*?qrt#L1Js%`3qWz2=M7o9rG$aWuR# z;v~q`78vCnKs*89^MU*umyW_AGpFQPAmuHQ>l4%hd5UpR2pmbK=IPee4Rmwc^6&E!VfIa!0>#qOPX14 z^?o~gez|@5W<%+MAL@>jJ3idtSMzqk)34;0Zj3H49^SU*S3v1ohx5Bu)stsXzjyTkh;X%uvT)seFM1& z(3^~}^pEmoaEj7Hz6{_8LDoWwf~kNzcnSsA*iA@Cff+OG?_|Wx+B)YS@2y#V@AS3} zF&Xc;S_G%7O>klPGbs#g2Wt8^4koc+RV zI>o$jrnY?1vwo`s7p|(a3IC=EnMkt7k5{K{tfAkPZO9HEH9;d}1qG=RmM?S%zVcR1 z5IqNg@EwwcJAAGNyDRr^8p@(&p+RiDFxeeP?d~2D1&J4a?@69j2+JY1Kn#TzM1b#R zjO;n+?jbqQWJ)kfGU`y=Bi@4X`3Rv}tAHY;mzb*zgG%0Fu0ly83J)YE%*srXj}|(E z4O0WA#765(JJRNFHiQ{Q*heHrTMoL93b$Or!Y#HjFhV=*DtzUfw7Tf236NnY zE78GaMr7KCbl!qCoTHk!oo^y>|rcLhEvhUwf*c-pL9SYL zeEHJj4gR7?eeCv!r%+53(xvg!3^1YQXcA~&VFi;Q;(a+BB5#12=`jFi-OEWU<|8a= zgUNf2bs{J^kjMZwB}O~#39U_&^QN;#vPm_4FqO%46g~RR}G*aFHm}od& zyY0*ABYgohzslCUyw<8Qa=~@i2= z1Oo(?5$GIXI#G+FRW*>a5-^2;7R&+ICLUx~uUiH`;v9s+y_;4`zYck%pg%;>zy5eF zt#9Y#)yxU$eL|; z_lYareI$B_mR&ZqoGCd@q;U@Djx#+>I(RQRTZ!n*2=vl zQJ2v#_u-#1x#9sv0>MA@bmveFz7d)yAB?8!0NOIp&zO^imXSlGyNv0m49MxO-F}x= z2^P1G-gKRF??qk%`tiy769N<X=XjCkcEC!X;BtwA3&%3UB>+nM zKwo*=qou^QfjWC|{Uv8Dbe9E|S^f}lSm=(V(}Pd`osxpYTcd=5Jh>$2&|9dsGBXEh zKSVB*%O>Vv4y27f=*!!{8!KeKh%<+uj9G~Bfp+tK)xCQZ2M3M8Kw~?`8oTcaz2p=L zw39&got=Hh`Vntb_Gi*63@NtmIKE>#x=5Pz7()f=$@$jK-UXf#^UP&c zMNw1rqVHl=WNC7ev$|Lx8#5L!Qj?Zd6?+veTU-(u5#=)y%V>XGdYpOg_|)a;@TDv% zd3h#l_r4W7M+^6W%5IT}Y^hqaH6bp1{)+ikP3h@PRq!PoFQ3WTU^H&X z$t{kED5hKaDVN{6<#^}0lDEqCG?z4Hl)kla;oD`In@dVIXBO7(+Ex2Zz13R(jC90n zgt$N!D&~tC_=bA|%0S)*)6e6w!ElathebGRVYK`g8?&-D{-V6>5S|Z})f`>C_-M_N zqt(?%LoGtD)m7(RinZ<4N0$uM0=%}dUhw5X4->HdqQnyb+#;aKs6?Zu036q78rJ5s zsn{{9k&X)0>1>()%|4roYVa~F3@=%4c0rx^)tC%hXlRHc`}q|QL;W)IwD2A0Bp~W< zVIEcT6^wf(gKw3cnQ^hf#D!RAV71U9euJ~W8abN;&}1jT8Qh{8=mWO`g7%1;2Gc*e zLxKfalZ|e&K$y}zgL_x0dzS%izCkd;TNJG*XM+wrUduB~Uhcg|VvkX)-Q7-0=eaK` zLhjdrM2&GD;71NJx(G8I`UrmwS*A|QG{AF0{40=!m8wnP;K~2j zQ)Aux#VSf)yt!xQ3yy+G%Fu}^|Gv}reL}PJ`cd^?wx7Rv^`mp2eXgO6cRIWNvY8UK zVb1jQO-Y+y^|kpP*#30)pAYT-&&K?}e){LtCFznaj6+@hZs&PeHOXH!md^Buc+!OtL!Q-@2Y%y{nPljE<1ajYXELx!TySh z{oVy=(tEJBe1BC%R|Ooa_Lti|kMW1;sV5eJ-Uiz;;^t#*nJ=sSziH3^Q+pMEd;*64 z_lR(H`~;5vZmrUAZ4_Udn&mquu*K5 zkL8*x@EQR=B7x8yUb-sz*oPuo#1o9ItF4COXa?@bktUW zEe-tKwX$L*PpobAi=4vI_DWo9j!NiRisO3>X30~fO!qzw0or^!K`4=+& zy$^``__HI&bg!v=d2EDc>%6>%*%_;!O?_1tk&|1MP-vEq$UFG`&W`>g3#xzBw{_7c z*}C%IH`*81@9@$YCSV?L{2cZIo|PQ71j^GQp|Sd(jCGdp2U#8&pz+@;o) zww-t0E3G^8!-iM?^6TnEV|wx|8dy=daA8GpDR84yRtXY2dMH{kH?btG>myL)hE)}o z-67c<^lB)wghK04RRppMy2I+xwyp#dC>P^#ErtgPzrZTscxij-x9P?>flM`o6&ao@ zswglnGi9~L>5}|&f|I5?6D#M>&X^JtWDcPUg-8p>j69Rn6PQ0`)*CtVUW`kQd&+$$ zu0*Q?nu2|N-qWOKnVPfbv?WHH{qtY1S$uR_aG)+CC_}HaL~24JL)YbH6qzT?2xb~> zqUO7^WmS{kw{}L(RcqDufJu7mIP&!vlYZ(7i)C5VEC>>!U}?G~TnGB~$M`A8kR}OZ zz-?f=A;Ag!r>FBJ(Mw8qd3dEAo+$*$xc)?K?TLDoT)3a9IDJ18-d4;`3FqY+kUnkj zHm<+nB*15Tl=9GzQbORMbS@-#6osDRVJf-7YE)DB%SLs6;ELwtXI`=8?On0rSasdI zrHY2`1-pt~{J`mWHk&tLd@y=fc1nT)okrM)@#>GtF_t9rr&n=wRd9XBo@&R|$? z$=En;hB0z%l1`I2cWg+=NqByp_xpfd0pBm17$d(6?GFPljU?>x$1yN`9oMU%clme; z(}#`YLO9x2Y3WaJf#!=di>5~v*m4^-X$*0}o2cza8N#0G>KWbpR&v1L4Laq6-c4$P_YV9?fO0bjI9g*d6L*cj+t&~^B2QAE%bX~6wDg9e=?`^L3Ognd+c%krt1kD3jO}c%PY@l zLwo>kkdtv7p0IO84~*FXAJ2W@Nzs;sJ~k)p5M0l*@VkOA4?kLBfHm)h=k@esVg*Df zX$Qb+_3*SSp#&naAj>xhl1k4J_D@C!cF&Py7vf4$C>`yExMG0Q3?6A_Aza^s1J${2 z!io99!F?Jf@Mf9JXENX>0%rmE_20oN3>+18;2Fw0fJSWWC|Zew*8y4EIIfSE08Et%JDs4c!2aMn1^ou{s6bRHBx{5lq#jDA zdrBd~h3_bVrAUE)gtEW#GUzcLnFKZHPK>%qgM;)Eo_)U$YTi_)b5BNT51u}0;~j+^ zx7y**J*pw?%+e=qNUTghd>Dj&gkMKb@!a(iWqR$INv?V2o$=06k5JxK1kncNUZ829 ziyz@pK`jj;6)b+_Njd0OmfDlewX)-%Sp`w`wZZZh0r8f8eb7TvNxs&hlYE&8Pf41yO@Q<+6`1NQG*oiT(!nm4*(W z2`N6zg7hQEI@lebV#T}AM}a8gji3RKb7Yt^TU5sCWax1t(}jgX%0IBy$V5>@8}s@P z*{}PXoBPS!4^NZKd(F-F*y%HuNHy`>vxoJu-Ip%0-3&5E5I<5axY#vz&E5TY?bG}X M1-! literal 0 HcmV?d00001 diff --git a/apps/frontend/static/images/avatar.png b/apps/frontend/static/images/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..09892098aa943af5fe95e7dd434a07fe03e7ccd1 GIT binary patch literal 2011 zcmV<12PF83P)D!y%g4vZ*x1<2%*?Q`u)x5;+S=OH)zz}HvdGBD zw6wIs!oteR%FWHqrlzLJ$;r^r(7(UGt*x!9s;a}o!>FjJ#>U3Iy}i%R&$YF+x3{;h zuCCtR-nO>3udlDMv9Z$9(zv*|ySuyA*4EqG+ZlWNivR!y*-1n}RCr$O+*gj>NDPM2 zpNco<96RTn9rwSi8F&oCZFlz#Qq{xgJ4+x4LZrx&UjP6A00000000000000000000 z0000000000h)13Iv~gIg9X6)(&Ma2$kko6>k^LchuGJIe1?9VT%ih&$-<6da;(GtY zRygU`V`YKSGPJdX%aOK)>O)}bd#Gv~*uA$6-S28s*y-CA`a9YL;;WKvrF0c*ZKyul z)*h=`7wH^W2Elqsjqd}?Xz*Ptp%PkVVMQxo5?Bs`i9Upe<)onxU}?Eo>Ro7Cj@nv* ze0H1D=vvOYdIRQ`ySe^?YRPg}s_G9&F3o|+E;KJf72K|Qva4ddGB4O%8ki>o6?HI`i*6#+w#5|Y?<y|LwN+|wJ-DOs*c9lZ(lz;YDS^$r~880p|x z??Sy~IVsik0el`?F2Y>=KFtcZ1R zaGELyc&axa?H!NJ`jhg5v~%-y@6vvAx%c&^lgbGsy=v!Q)n20Y000000000000000 z0000001(G=0_9$3(l{U9f4Ydqf&CDSql-`X!}G?Z(<>_@q?M!EmqlnRgo`h=qe`mH zp^F!L#dKMP=0-j66q)X!m2_UKU< z?=gcl`PrjD-Z_UQ!Oo*l-Y$R@ac4on7M&O-j9L_~HG+QG8x@#l(9NwQst~Om6uTU# z#^D0B?jkA@-9e`x4^-yi2NWuOs^gbXXX{ZRZwp;qmr8Y=vc4+;?DuSiV=6Wdp+vB! zYWn~xgnKHt51~MOqTGjPD@PoaW3S(TV1qAdpLGPU;{}n+CNZ&z1&l zAk)yBhI<>K$Szko#25&!NK0f~qZEphQ8{ z3aIf)#a^Mvnrf|~$}^REhB95M)P*|TX3iCPq&kn#$aR|+5*6uFl|Ix;s7M0Ewp3#a z)kYpw@J3LsOsPOxLcOd<@p>5y2p=fiLkJUGpQ800tayqjR`dizEyTFw96Gb|` z!YX_?aV~Q~zz%VD}8+~-=U|TkA>Od+oZ}G}nbJRjS zi+DxREJj@nPrRDb5Owl07n$q)ia(q8)f$NXg*KhY?6EB-U9U5WqIxO65S z|C%@HT<{BxaK1b6FYI^o5WlAp7RhA6Kem`8MTkGt$mX+;7JpOgV>ZtqfL8IK*!Dm6 zzQ>>P{QXCNTMXj=+Nnb$0D%|)f;hy>mx(1hlm3f?BA8dZ4V=5bdfd6DDtbSk-BC3w tPksUb00000000000000000000fEW70#Mag`cL)Fg002ovPDHLkV1gNB=am2e literal 0 HcmV?d00001 diff --git a/apps/frontend/static/robots.txt b/apps/frontend/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/apps/frontend/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/apps/frontend/svelte.config.js b/apps/frontend/svelte.config.js new file mode 100644 index 0000000..2afacd0 --- /dev/null +++ b/apps/frontend/svelte.config.js @@ -0,0 +1,20 @@ +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import adapter from "@sveltejs/adapter-node"; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter({ out: "build" }), + experimental: { + remoteFunctions: true, + tracing: { server: true }, + instrumentation: { server: true }, + }, + }, + compilerOptions: { + experimental: { async: true }, + }, +}; + +export default config; diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json new file mode 100644 index 0000000..2c2ed3c --- /dev/null +++ b/apps/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts new file mode 100644 index 0000000..1f01308 --- /dev/null +++ b/apps/frontend/vite.config.ts @@ -0,0 +1,46 @@ +import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vitest/config"; +import tailwindcss from "@tailwindcss/vite"; +import Icons from "unplugin-icons/vite"; +import { resolve } from "path"; + +export default defineConfig({ + plugins: [sveltekit(), tailwindcss(), Icons({ compiler: "svelte" })], + + ssr: { + external: [ + "argon2", + "node-gyp-build", + "sharp", + "@img/sharp-linux-x64", + "@img/sharp-linuxmusl-x64", + "@img/sharp-wasm32", + ], + }, + + resolve: { + alias: { + "@core": resolve(__dirname, "../../packages/logic/core"), + "@domains": resolve(__dirname, "../../packages/logic/domains"), + "@/core": resolve(__dirname, "../../packages/logic/core"), + "@/domains": resolve(__dirname, "../../packages/logic/domains"), + }, + }, + + test: { + expect: { requireAssertions: true }, + + projects: [ + { + extends: "./vite.config.ts", + + test: { + name: "server", + environment: "node", + include: ["src/**/*.{test,spec}.{js,ts}"], + exclude: ["src/**/*.svelte.{test,spec}.{js,ts}"], + }, + }, + ], + }, +}); diff --git a/apps/main/src/lib/core/constants.ts b/apps/main/src/lib/core/constants.ts index 1638b68..ae52a54 100644 --- a/apps/main/src/lib/core/constants.ts +++ b/apps/main/src/lib/core/constants.ts @@ -1,4 +1,5 @@ import LayoutDashboard from "@lucide/svelte/icons/layout-dashboard"; +import { PUBLIC_WS_SCRCPY_SVC_URL } from "$env/static/public"; import AppWindow from "@lucide/svelte/icons/app-window"; import { BellRingIcon, Link } from "@lucide/svelte"; import UserCircle from "~icons/lucide/user-circle"; @@ -46,7 +47,7 @@ export const secondaryNavTree = [ }, ] as AppSidebarItem[]; -export const WS_SCRCPY_URL = "https://iotam-ws-scrcpy.snapyra.com"; +export const WS_SCRCPY_URL = PUBLIC_WS_SCRCPY_SVC_URL; export const COMPANY_NAME = "SaaS Template"; export const WEBSITE_URL = "https://company.com"; diff --git a/dockerfiles/front.Dockerfile b/dockerfiles/frontend.Dockerfile similarity index 67% rename from dockerfiles/front.Dockerfile rename to dockerfiles/frontend.Dockerfile index a59d75f..fea09d2 100644 --- a/dockerfiles/front.Dockerfile +++ b/dockerfiles/frontend.Dockerfile @@ -6,13 +6,13 @@ WORKDIR /app COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./ -COPY apps/front/package.json ./apps/front/package.json +COPY apps/frontend/package.json ./apps/frontend/package.json COPY packages ./packages RUN pnpm install -COPY apps/front ./apps/front +COPY apps/frontend ./apps/frontend RUN pnpm install @@ -24,4 +24,4 @@ EXPOSE 3000 RUN chmod +x scripts/prod.start.sh -CMD ["/bin/sh", "scripts/prod.start.sh", "apps/front"] +CMD ["/bin/sh", "scripts/prod.start.sh", "apps/frontend"] diff --git a/memory.log.md b/memory.log.md index 155f72a..8c01d2d 100644 --- a/memory.log.md +++ b/memory.log.md @@ -148,3 +148,15 @@ Update rule: - Split `apps/front` into a thin entrypoint, shared `src/core/utils.ts`, and a dedicated `src/domains/links/{router,service}.ts` flow - Moved link resolve/prepare logic and orchestrator calling into the links domain service so `src/index.ts` only wires middleware and routes - Verified the cleaned `apps/front` app compiles cleanly with `tsc --noEmit` + +### 21 — Frontend Remote Function Migration + +- Migrated the legacy front server-side link flow into `apps/frontend` as SvelteKit remote functions in `src/lib/domains/link/link.remote.ts` +- Added frontend request flow IDs in `src/hooks.server.ts` so remote functions build `FlowExecCtx` from `event.locals` consistently +- Preserved the two server operations as a query/command split: link resolve remains read-only and session prepare remains the mutating flow that calls orchestrator + +### 22 — Frontend Link Domain Refactor + +- Reduced `apps/frontend/src/lib/domains/link/link.remote.ts` to thin remote-function entrypoints only +- Split link-domain server responsibilities into dedicated files: `link.data.ts`, `link.utils.ts`, `orchestrator.service.ts`, and `link.service.ts` +- Moved orchestrator calling, link validation shaping, and reusable error creation out of the remote layer for a cleaner domain boundary diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e20738c..ab16f77 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,7 +27,167 @@ importers: specifier: ^5.9.3 version: 5.9.3 - apps/front: + apps/frontend: + dependencies: + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/auto-instrumentations-node': + specifier: ^0.70.1 + version: 0.70.1(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)) + '@opentelemetry/exporter-logs-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@pkg/db': + specifier: workspace:* + version: link:../../packages/db + '@pkg/keystore': + specifier: workspace:* + version: link:../../packages/keystore + '@pkg/logger': + specifier: workspace:* + version: link:../../packages/logger + '@pkg/logic': + specifier: workspace:* + version: link:../../packages/logic + '@pkg/result': + specifier: workspace:* + version: link:../../packages/result + '@pkg/settings': + specifier: workspace:* + version: link:../../packages/settings + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + import-in-the-middle: + specifier: ^3.0.0 + version: 3.0.0 + nanoid: + specifier: ^5.1.6 + version: 5.1.6 + neverthrow: + specifier: ^8.2.0 + version: 8.2.0 + valibot: + specifier: ^1.2.0 + version: 1.2.0(typescript@5.9.3) + devDependencies: + '@iconify/json': + specifier: ^2.2.434 + version: 2.2.444 + '@internationalized/date': + specifier: ^3.10.0 + version: 3.11.0 + '@lucide/svelte': + specifier: ^0.561.0 + version: 0.561.0(svelte@5.53.6) + '@sveltejs/adapter-node': + specifier: ^5.5.4 + version: 5.5.4(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) + '@sveltejs/kit': + specifier: ^2.53.4 + version: 2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': + specifier: ^6.2.4 + version: 6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tailwindcss/forms': + specifier: ^0.5.10 + version: 0.5.11(tailwindcss@4.2.1) + '@tailwindcss/typography': + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@4.2.1) + '@tailwindcss/vite': + specifier: ^4.1.18 + version: 4.2.1(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/table-core': + specifier: ^8.21.3 + version: 8.21.3 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 + bits-ui: + specifier: ^2.14.4 + version: 2.16.2(@internationalized/date@3.11.0)(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6) + clsx: + specifier: ^2.1.1 + version: 2.1.1 + embla-carousel-svelte: + specifier: ^8.6.0 + version: 8.6.0(svelte@5.53.6) + formsnap: + specifier: ^2.0.1 + version: 2.0.1(svelte@5.53.6)(sveltekit-superforms@2.30.0(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)) + layerchart: + specifier: 2.0.0-next.43 + version: 2.0.0-next.43(svelte@5.53.6) + mode-watcher: + specifier: ^1.1.0 + version: 1.1.0(svelte@5.53.6) + paneforge: + specifier: ^1.0.2 + version: 1.0.2(svelte@5.53.6) + prettier: + specifier: ^3.7.4 + version: 3.8.1 + prettier-plugin-svelte: + specifier: ^3.4.0 + version: 3.5.0(prettier@3.8.1)(svelte@5.53.6) + prettier-plugin-tailwindcss: + specifier: ^0.7.2 + version: 0.7.2(prettier-plugin-sort-imports@1.8.11(typescript@5.9.3))(prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.53.6))(prettier@3.8.1) + svelte: + specifier: ^5.53.6 + version: 5.53.6 + svelte-check: + specifier: ^4.4.4 + version: 4.4.4(picomatch@4.0.3)(svelte@5.53.6)(typescript@5.9.3) + svelte-sonner: + specifier: ^1.0.7 + version: 1.0.7(svelte@5.53.6) + sveltekit-superforms: + specifier: ^2.30.0 + version: 2.30.0(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3) + tailwind-merge: + specifier: ^3.4.0 + version: 3.5.0 + tailwind-variants: + specifier: ^3.2.2 + version: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.1) + tailwindcss: + specifier: ^4.1.18 + version: 4.2.1 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + unplugin-icons: + specifier: ^23.0.1 + version: 23.0.1(svelte@5.53.6) + vaul-svelte: + specifier: ^1.0.0-next.7 + version: 1.0.0-next.7(svelte@5.53.6) + vite: + specifier: ^7.2.6 + version: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: + specifier: ^4.0.15 + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + + apps/frontlegacy: dependencies: '@hono/node-server': specifier: ^1.19.9