@comitor/ui 2.1.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -24
- package/dist/chart/chart.d.ts +6 -1
- package/dist/chart/chart.js +3 -1
- package/dist/chart/chart.js.map +1 -1
- package/dist/chart/comitor-chart.d.ts +4 -1
- package/dist/chart/comitor-chart.js +3 -1
- package/dist/chart/comitor-chart.js.map +1 -1
- package/dist/components/composite/avatar-group.d.ts +9 -6
- package/dist/components/composite/avatar-group.js +6 -2
- package/dist/components/composite/avatar-group.js.map +1 -1
- package/dist/components/composite/combobox.d.ts +11 -11
- package/dist/components/composite/combobox.js +25 -11
- package/dist/components/composite/combobox.js.map +1 -1
- package/dist/components/composite/command-palette-constants.d.ts +15 -5
- package/dist/components/composite/command-palette-constants.js +3 -2
- package/dist/components/composite/command-palette-constants.js.map +1 -1
- package/dist/components/composite/command-palette.d.ts +2 -2
- package/dist/components/composite/command-palette.js +8 -5
- package/dist/components/composite/command-palette.js.map +1 -1
- package/dist/components/composite/confirm-dialog.d.ts +8 -8
- package/dist/components/composite/confirm-dialog.js +8 -3
- package/dist/components/composite/confirm-dialog.js.map +1 -1
- package/dist/components/composite/copy-button.d.ts +7 -3
- package/dist/components/composite/copy-button.js +7 -3
- package/dist/components/composite/copy-button.js.map +1 -1
- package/dist/components/composite/data-table-constants.d.ts +7 -3
- package/dist/components/composite/data-table-constants.js +2 -9
- package/dist/components/composite/data-table-constants.js.map +1 -1
- package/dist/components/composite/data-table.js +3 -3
- package/dist/components/composite/data-table.js.map +1 -1
- package/dist/components/composite/date-picker-constants.d.ts +32 -9
- package/dist/components/composite/date-picker-constants.js +16 -11
- package/dist/components/composite/date-picker-constants.js.map +1 -1
- package/dist/components/composite/date-picker.d.ts +19 -10
- package/dist/components/composite/date-picker.js +33 -9
- package/dist/components/composite/date-picker.js.map +1 -1
- package/dist/components/composite/filter-chips.d.ts +4 -3
- package/dist/components/composite/filter-chips.js +6 -2
- package/dist/components/composite/filter-chips.js.map +1 -1
- package/dist/components/composite/inline-edit.d.ts +14 -3
- package/dist/components/composite/inline-edit.js +8 -4
- package/dist/components/composite/inline-edit.js.map +1 -1
- package/dist/components/composite/relative-time.d.ts +4 -4
- package/dist/components/composite/relative-time.js +4 -2
- package/dist/components/composite/relative-time.js.map +1 -1
- package/dist/components/composite/route-tabs-nav.d.ts +9 -0
- package/dist/components/composite/route-tabs-nav.js +11 -0
- package/dist/components/composite/route-tabs-nav.js.map +1 -0
- package/dist/components/composite/route-tabs.d.ts +4 -1
- package/dist/components/composite/route-tabs.js +3 -2
- package/dist/components/composite/route-tabs.js.map +1 -1
- package/dist/components/composite/search-filter-bar-constants.d.ts +18 -8
- package/dist/components/composite/search-filter-bar-constants.js +2 -14
- package/dist/components/composite/search-filter-bar-constants.js.map +1 -1
- package/dist/components/composite/search-filter-bar.d.ts +5 -2
- package/dist/components/composite/search-filter-bar.js +2 -2
- package/dist/components/composite/search-filter-bar.js.map +1 -1
- package/dist/components/composite/stat-card.d.ts +1 -1
- package/dist/components/composite/table-pagination-constants.d.ts +16 -11
- package/dist/components/composite/table-pagination-constants.js +2 -12
- package/dist/components/composite/table-pagination-constants.js.map +1 -1
- package/dist/components/composite/table-pagination.d.ts +6 -3
- package/dist/components/composite/table-pagination.js +3 -5
- package/dist/components/composite/table-pagination.js.map +1 -1
- package/dist/components/composite/toast.d.ts +5 -2
- package/dist/components/composite/toast.js +3 -1
- package/dist/components/composite/toast.js.map +1 -1
- package/dist/components/ui/action-bar-constants.d.ts +8 -4
- package/dist/components/ui/action-bar-constants.js +2 -5
- package/dist/components/ui/action-bar-constants.js.map +1 -1
- package/dist/components/ui/action-bar.d.ts +5 -4
- package/dist/components/ui/action-bar.js +47 -3
- package/dist/components/ui/action-bar.js.map +1 -1
- package/dist/components/ui/alert.d.ts +1 -1
- package/dist/components/ui/badge-overflow.d.ts +2 -2
- package/dist/components/ui/badge-overflow.js +4 -2
- package/dist/components/ui/badge-overflow.js.map +1 -1
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/banner.d.ts +6 -5
- package/dist/components/ui/banner.js +3 -1
- package/dist/components/ui/banner.js.map +1 -1
- package/dist/components/ui/breadcrumb.d.ts +11 -4
- package/dist/components/ui/breadcrumb.js +7 -2
- package/dist/components/ui/breadcrumb.js.map +1 -1
- package/dist/components/ui/button.d.ts +2 -2
- package/dist/components/ui/calendar.d.ts +12 -5
- package/dist/components/ui/calendar.js +38 -3
- package/dist/components/ui/calendar.js.map +1 -1
- package/dist/components/ui/carousel.d.ts +4 -3
- package/dist/components/ui/carousel.js +11 -4
- package/dist/components/ui/carousel.js.map +1 -1
- package/dist/components/ui/circular-progress.js +10 -2
- package/dist/components/ui/circular-progress.js.map +1 -1
- package/dist/components/ui/command.d.ts +5 -4
- package/dist/components/ui/command.js +6 -2
- package/dist/components/ui/command.js.map +1 -1
- package/dist/components/ui/cropper-constants.d.ts +8 -3
- package/dist/components/ui/cropper-constants.js +2 -6
- package/dist/components/ui/cropper-constants.js.map +1 -1
- package/dist/components/ui/cropper.d.ts +2 -1
- package/dist/components/ui/cropper.js +2 -2
- package/dist/components/ui/cropper.js.map +1 -1
- package/dist/components/ui/dialog.d.ts +2 -2
- package/dist/components/ui/dialog.js +4 -1
- package/dist/components/ui/dialog.js.map +1 -1
- package/dist/components/ui/item.d.ts +2 -2
- package/dist/components/ui/kanban-constants.d.ts +7 -1
- package/dist/components/ui/kanban-constants.js +2 -17
- package/dist/components/ui/kanban-constants.js.map +1 -1
- package/dist/components/ui/kanban.d.ts +2 -2
- package/dist/components/ui/kanban.js +11 -5
- package/dist/components/ui/kanban.js.map +1 -1
- package/dist/components/ui/key-value-constants.d.ts +7 -3
- package/dist/components/ui/key-value-constants.js +2 -7
- package/dist/components/ui/key-value-constants.js.map +1 -1
- package/dist/components/ui/key-value.d.ts +5 -5
- package/dist/components/ui/key-value.js +72 -78
- package/dist/components/ui/key-value.js.map +1 -1
- package/dist/components/ui/mask-input-constants.d.ts +2 -3
- package/dist/components/ui/mask-input-constants.js +36 -77
- package/dist/components/ui/mask-input-constants.js.map +1 -1
- package/dist/components/ui/mask-input.d.ts +7 -4
- package/dist/components/ui/mask-input.js +22 -22
- package/dist/components/ui/mask-input.js.map +1 -1
- package/dist/components/ui/mention.d.ts +3 -2
- package/dist/components/ui/mention.js +4 -6
- package/dist/components/ui/mention.js.map +1 -1
- package/dist/components/ui/pagination.d.ts +13 -9
- package/dist/components/ui/pagination.js +15 -6
- package/dist/components/ui/pagination.js.map +1 -1
- package/dist/components/ui/phone-input-constants.d.ts +17 -6
- package/dist/components/ui/phone-input-constants.js +2 -5
- package/dist/components/ui/phone-input-constants.js.map +1 -1
- package/dist/components/ui/phone-input.d.ts +18 -11
- package/dist/components/ui/phone-input.js +14 -12
- package/dist/components/ui/phone-input.js.map +1 -1
- package/dist/components/ui/progress.d.ts +2 -2
- package/dist/components/ui/rating.d.ts +2 -1
- package/dist/components/ui/rating.js +8 -4
- package/dist/components/ui/rating.js.map +1 -1
- package/dist/components/ui/scroll-spy.js +3 -1
- package/dist/components/ui/scroll-spy.js.map +1 -1
- package/dist/components/ui/sheet.d.ts +2 -2
- package/dist/components/ui/sheet.js +10 -1
- package/dist/components/ui/sheet.js.map +1 -1
- package/dist/components/ui/slider.d.ts +1 -1
- package/dist/components/ui/slider.js +15 -1
- package/dist/components/ui/slider.js.map +1 -1
- package/dist/components/ui/sonner.js +2 -0
- package/dist/components/ui/sonner.js.map +1 -1
- package/dist/components/ui/spinner.js +4 -1
- package/dist/components/ui/spinner.js.map +1 -1
- package/dist/components/ui/stepper-constants.d.ts +7 -2
- package/dist/components/ui/stepper-constants.js +2 -5
- package/dist/components/ui/stepper-constants.js.map +1 -1
- package/dist/components/ui/stepper.d.ts +2 -1
- package/dist/components/ui/stepper.js +2 -2
- package/dist/components/ui/stepper.js.map +1 -1
- package/dist/components/ui/tags-input.d.ts +3 -2
- package/dist/components/ui/tags-input.js +4 -2
- package/dist/components/ui/tags-input.js.map +1 -1
- package/dist/components/ui/time-picker-constants.d.ts +12 -9
- package/dist/components/ui/time-picker-constants.js +2 -10
- package/dist/components/ui/time-picker-constants.js.map +1 -1
- package/dist/components/ui/time-picker.d.ts +11 -6
- package/dist/components/ui/time-picker.js +5 -7
- package/dist/components/ui/time-picker.js.map +1 -1
- package/dist/components/ui/toggle-group.js +12 -1
- package/dist/components/ui/toggle-group.js.map +1 -1
- package/dist/components/ui/toggle.d.ts +1 -1
- package/dist/components/ui/tour-constants.d.ts +7 -2
- package/dist/components/ui/tour-constants.js +2 -8
- package/dist/components/ui/tour-constants.js.map +1 -1
- package/dist/components/ui/tour.d.ts +3 -2
- package/dist/components/ui/tour.js +5 -4
- package/dist/components/ui/tour.js.map +1 -1
- package/dist/data-grid/data-grid-cell-variants.js +4 -4
- package/dist/data-grid/data-grid-cell-variants.js.map +1 -1
- package/dist/data-grid/data-grid-column-header.js +1 -1
- package/dist/data-grid/data-grid-column-header.js.map +1 -1
- package/dist/data-grid/data-grid-filter-menu.d.ts +5 -4
- package/dist/data-grid/data-grid-filter-menu.js +13 -8
- package/dist/data-grid/data-grid-filter-menu.js.map +1 -1
- package/dist/data-grid/data-grid-keyboard-shortcuts.d.ts +4 -4
- package/dist/data-grid/data-grid-keyboard-shortcuts.js.map +1 -1
- package/dist/data-grid/data-grid-locale.d.ts +58 -6
- package/dist/data-grid/data-grid-locale.js +41 -9
- package/dist/data-grid/data-grid-locale.js.map +1 -1
- package/dist/data-grid/data-grid-row-height-menu.d.ts +4 -3
- package/dist/data-grid/data-grid-row-height-menu.js.map +1 -1
- package/dist/data-grid/data-grid-sort-menu.d.ts +4 -4
- package/dist/data-grid/data-grid-sort-menu.js +1 -1
- package/dist/data-grid/data-grid-sort-menu.js.map +1 -1
- package/dist/data-grid/data-grid-view-menu.d.ts +4 -4
- package/dist/data-grid/data-grid-view-menu.js.map +1 -1
- package/dist/data-grid/data-grid.d.ts +4 -3
- package/dist/data-grid/data-grid.js +2 -3
- package/dist/data-grid/data-grid.js.map +1 -1
- package/dist/data-grid/index.d.ts +1 -0
- package/dist/data-grid/index.js.map +1 -1
- package/dist/data-grid/messages-en.d.ts +23 -4
- package/dist/data-grid/messages-en.js +20 -0
- package/dist/data-grid/messages-en.js.map +1 -1
- package/dist/data-grid/messages-vi.d.ts +3 -1
- package/dist/data-grid/messages-vi.js +14 -0
- package/dist/data-grid/messages-vi.js.map +1 -1
- package/dist/data-grid/messages.d.ts +29 -3
- package/dist/data-grid/messages.js.map +1 -1
- package/dist/data-grid/sortable.js +16 -19
- package/dist/data-grid/sortable.js.map +1 -1
- package/dist/data-grid/types.d.ts +1 -1
- package/dist/data-grid/use-data-grid-undo-redo.d.ts +5 -4
- package/dist/data-grid/use-data-grid-undo-redo.js +2 -2
- package/dist/data-grid/use-data-grid-undo-redo.js.map +1 -1
- package/dist/data-grid/use-data-grid.d.ts +23 -8
- package/dist/data-grid/use-data-grid.js +8 -6
- package/dist/data-grid/use-data-grid.js.map +1 -1
- package/dist/data-grid/utils.d.ts +24 -4
- package/dist/data-grid/utils.js +15 -5
- package/dist/data-grid/utils.js.map +1 -1
- package/dist/i18n/locale-provider.d.ts +127 -0
- package/dist/i18n/locale-provider.js +47 -0
- package/dist/i18n/locale-provider.js.map +1 -0
- package/dist/i18n/locale.d.ts +27 -0
- package/dist/i18n/locale.js +10 -0
- package/dist/i18n/locale.js.map +1 -0
- package/dist/i18n/messages.d.ts +116 -0
- package/dist/i18n/messages.js +149 -0
- package/dist/i18n/messages.js.map +1 -0
- package/dist/i18n/namespaces/action-bar.d.ts +7 -0
- package/dist/i18n/namespaces/action-bar.js +15 -0
- package/dist/i18n/namespaces/action-bar.js.map +1 -0
- package/dist/i18n/namespaces/avatar-group.d.ts +14 -0
- package/dist/i18n/namespaces/avatar-group.js +13 -0
- package/dist/i18n/namespaces/avatar-group.js.map +1 -0
- package/dist/i18n/namespaces/badge-overflow.d.ts +12 -0
- package/dist/i18n/namespaces/badge-overflow.js +11 -0
- package/dist/i18n/namespaces/badge-overflow.js.map +1 -0
- package/dist/i18n/namespaces/banner.d.ts +9 -0
- package/dist/i18n/namespaces/banner.js +7 -0
- package/dist/i18n/namespaces/banner.js.map +1 -0
- package/dist/i18n/namespaces/breadcrumb.d.ts +11 -0
- package/dist/i18n/namespaces/breadcrumb.js +13 -0
- package/dist/i18n/namespaces/breadcrumb.js.map +1 -0
- package/dist/i18n/namespaces/calendar.d.ts +40 -0
- package/dist/i18n/namespaces/calendar.js +25 -0
- package/dist/i18n/namespaces/calendar.js.map +1 -0
- package/dist/i18n/namespaces/carousel.d.ts +15 -0
- package/dist/i18n/namespaces/carousel.js +17 -0
- package/dist/i18n/namespaces/carousel.js.map +1 -0
- package/dist/i18n/namespaces/combobox.d.ts +21 -0
- package/dist/i18n/namespaces/combobox.js +23 -0
- package/dist/i18n/namespaces/combobox.js.map +1 -0
- package/dist/i18n/namespaces/command-palette.d.ts +20 -0
- package/dist/i18n/namespaces/command-palette.js +19 -0
- package/dist/i18n/namespaces/command-palette.js.map +1 -0
- package/dist/i18n/namespaces/command.d.ts +11 -0
- package/dist/i18n/namespaces/command.js +13 -0
- package/dist/i18n/namespaces/command.js.map +1 -0
- package/dist/i18n/namespaces/confirm-dialog.d.ts +13 -0
- package/dist/i18n/namespaces/confirm-dialog.js +15 -0
- package/dist/i18n/namespaces/confirm-dialog.js.map +1 -0
- package/dist/i18n/namespaces/contrast.d.ts +7 -0
- package/dist/i18n/namespaces/contrast.js +13 -0
- package/dist/i18n/namespaces/contrast.js.map +1 -0
- package/dist/i18n/namespaces/copy-button.d.ts +11 -0
- package/dist/i18n/namespaces/copy-button.js +13 -0
- package/dist/i18n/namespaces/copy-button.js.map +1 -0
- package/dist/i18n/namespaces/cropper.d.ts +7 -0
- package/dist/i18n/namespaces/cropper.js +17 -0
- package/dist/i18n/namespaces/cropper.js.map +1 -0
- package/dist/i18n/namespaces/data-table.d.ts +7 -0
- package/dist/i18n/namespaces/data-table.js +23 -0
- package/dist/i18n/namespaces/data-table.js.map +1 -0
- package/dist/i18n/namespaces/date-picker.d.ts +42 -0
- package/dist/i18n/namespaces/date-picker.js +35 -0
- package/dist/i18n/namespaces/date-picker.js.map +1 -0
- package/dist/i18n/namespaces/dialog.d.ts +9 -0
- package/dist/i18n/namespaces/dialog.js +7 -0
- package/dist/i18n/namespaces/dialog.js.map +1 -0
- package/dist/i18n/namespaces/display-axis.d.ts +25 -0
- package/dist/i18n/namespaces/display-axis.js +21 -0
- package/dist/i18n/namespaces/display-axis.js.map +1 -0
- package/dist/i18n/namespaces/file-upload.d.ts +11 -0
- package/dist/i18n/namespaces/file-upload.js +19 -0
- package/dist/i18n/namespaces/file-upload.js.map +1 -0
- package/dist/i18n/namespaces/filter-chips.d.ts +11 -0
- package/dist/i18n/namespaces/filter-chips.js +13 -0
- package/dist/i18n/namespaces/filter-chips.js.map +1 -0
- package/dist/i18n/namespaces/image-upload-field.d.ts +12 -0
- package/dist/i18n/namespaces/image-upload-field.js +23 -0
- package/dist/i18n/namespaces/image-upload-field.js.map +1 -0
- package/dist/i18n/namespaces/inline-edit.d.ts +15 -0
- package/dist/i18n/namespaces/inline-edit.js +17 -0
- package/dist/i18n/namespaces/inline-edit.js.map +1 -0
- package/dist/i18n/namespaces/kanban.d.ts +11 -0
- package/dist/i18n/namespaces/kanban.js +39 -0
- package/dist/i18n/namespaces/kanban.js.map +1 -0
- package/dist/i18n/namespaces/key-value.d.ts +10 -0
- package/dist/i18n/namespaces/key-value.js +19 -0
- package/dist/i18n/namespaces/key-value.js.map +1 -0
- package/dist/i18n/namespaces/mention.d.ts +9 -0
- package/dist/i18n/namespaces/mention.js +7 -0
- package/dist/i18n/namespaces/mention.js.map +1 -0
- package/dist/i18n/namespaces/pagination.d.ts +19 -0
- package/dist/i18n/namespaces/pagination.js +21 -0
- package/dist/i18n/namespaces/pagination.js.map +1 -0
- package/dist/i18n/namespaces/phone-input.d.ts +10 -0
- package/dist/i18n/namespaces/phone-input.js +17 -0
- package/dist/i18n/namespaces/phone-input.js.map +1 -0
- package/dist/i18n/namespaces/rating.d.ts +12 -0
- package/dist/i18n/namespaces/rating.js +12 -0
- package/dist/i18n/namespaces/rating.js.map +1 -0
- package/dist/i18n/namespaces/route-tabs.d.ts +9 -0
- package/dist/i18n/namespaces/route-tabs.js +7 -0
- package/dist/i18n/namespaces/route-tabs.js.map +1 -0
- package/dist/i18n/namespaces/scroll-spy.d.ts +9 -0
- package/dist/i18n/namespaces/scroll-spy.js +7 -0
- package/dist/i18n/namespaces/scroll-spy.js.map +1 -0
- package/dist/i18n/namespaces/search-filter-bar.d.ts +15 -0
- package/dist/i18n/namespaces/search-filter-bar.js +32 -0
- package/dist/i18n/namespaces/search-filter-bar.js.map +1 -0
- package/dist/i18n/namespaces/sheet.d.ts +9 -0
- package/dist/i18n/namespaces/sheet.js +7 -0
- package/dist/i18n/namespaces/sheet.js.map +1 -0
- package/dist/i18n/namespaces/shell.d.ts +21 -0
- package/dist/i18n/namespaces/shell.js +75 -0
- package/dist/i18n/namespaces/shell.js.map +1 -0
- package/dist/i18n/namespaces/spinner.d.ts +9 -0
- package/dist/i18n/namespaces/spinner.js +7 -0
- package/dist/i18n/namespaces/spinner.js.map +1 -0
- package/dist/i18n/namespaces/stepper.d.ts +7 -0
- package/dist/i18n/namespaces/stepper.js +15 -0
- package/dist/i18n/namespaces/stepper.js.map +1 -0
- package/dist/i18n/namespaces/table-pagination.d.ts +7 -0
- package/dist/i18n/namespaces/table-pagination.js +29 -0
- package/dist/i18n/namespaces/table-pagination.js.map +1 -0
- package/dist/i18n/namespaces/tags-input.d.ts +12 -0
- package/dist/i18n/namespaces/tags-input.js +11 -0
- package/dist/i18n/namespaces/tags-input.js.map +1 -0
- package/dist/i18n/namespaces/time-picker.d.ts +15 -0
- package/dist/i18n/namespaces/time-picker.js +25 -0
- package/dist/i18n/namespaces/time-picker.js.map +1 -0
- package/dist/i18n/namespaces/toast.d.ts +16 -0
- package/dist/i18n/namespaces/toast.js +7 -0
- package/dist/i18n/namespaces/toast.js.map +1 -0
- package/dist/i18n/namespaces/tour.d.ts +7 -0
- package/dist/i18n/namespaces/tour.js +21 -0
- package/dist/i18n/namespaces/tour.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +24 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/toast-language.d.ts +8 -0
- package/dist/lib/toast-language.js +19 -0
- package/dist/lib/toast-language.js.map +1 -0
- package/dist/shell/app-header.js.map +1 -1
- package/dist/shell/app-shell.d.ts +20 -2
- package/dist/shell/app-shell.js +4 -1
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/contrast-constants.d.ts +9 -1
- package/dist/shell/contrast-constants.js +2 -4
- package/dist/shell/contrast-constants.js.map +1 -1
- package/dist/shell/contrast-toggle.d.ts +4 -1
- package/dist/shell/contrast-toggle.js +2 -2
- package/dist/shell/contrast-toggle.js.map +1 -1
- package/dist/shell/display-axis-constants.d.ts +2 -1
- package/dist/shell/display-axis-constants.js.map +1 -1
- package/dist/shell/display-axis-controls.d.ts +15 -6
- package/dist/shell/display-axis-controls.js +11 -9
- package/dist/shell/display-axis-controls.js.map +1 -1
- package/dist/shell/display-axis-provider.d.ts +1 -1
- package/dist/shell/mobile-menu.js.map +1 -1
- package/dist/shell/notifications-button.d.ts +1 -1
- package/dist/shell/notifications-button.js +3 -2
- package/dist/shell/notifications-button.js.map +1 -1
- package/dist/shell/shell-context.d.ts +4 -1
- package/dist/shell/shell-context.js +2 -4
- package/dist/shell/shell-context.js.map +1 -1
- package/dist/shell/sidebar.d.ts +1 -1
- package/dist/shell/sidebar.js.map +1 -1
- package/dist/shell/theme-toggle.d.ts +6 -3
- package/dist/shell/theme-toggle.js +2 -2
- package/dist/shell/theme-toggle.js.map +1 -1
- package/dist/shell/types.d.ts +14 -8
- package/dist/shell/types.js +2 -35
- package/dist/shell/types.js.map +1 -1
- package/dist/shell/user-menu.d.ts +6 -6
- package/dist/shell/user-menu.js.map +1 -1
- package/dist/shell/utils.d.ts +1 -1
- package/dist/shell/utils.js.map +1 -1
- package/dist/shell/workspace-switcher.js +1 -1
- package/dist/shell/workspace-switcher.js.map +1 -1
- package/dist/uploader/file-upload-constants.d.ts +13 -0
- package/dist/uploader/file-upload-constants.js +4 -16
- package/dist/uploader/file-upload-constants.js.map +1 -1
- package/dist/uploader/file-upload.d.ts +4 -1
- package/dist/uploader/file-upload.js +2 -2
- package/dist/uploader/file-upload.js.map +1 -1
- package/dist/uploader/image-upload-field.d.ts +5 -1
- package/dist/uploader/image-upload-field.js +2 -2
- package/dist/uploader/image-upload-field.js.map +1 -1
- package/dist/uploader/use-file-upload.d.ts +4 -0
- package/dist/uploader/use-file-upload.js +6 -2
- package/dist/uploader/use-file-upload.js.map +1 -1
- package/package.json +4 -3
package/dist/shell/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/types.ts"],"sourcesContent":["/**\n * Hợp đồng dữ liệu của Tier 3 (shell).\n *\n * Nguyên tắc: package KHÔNG chứa dữ liệu app. Toàn bộ workspace / danh sách app /\n * menu / user đều là *type* — app truyền vào qua props. File này là **module thuần**\n * (chỉ type + hằng nhãn, không hook, không directive) nên dùng được ở cả server lẫn client.\n *\n * Hai trục điều hướng của Comitor — một user thuộc NHIỀU workspace, và mỗi workspace\n * lại bật NHIỀU sản phẩm. Hai chiều đó vuông góc nhau (đổi workspace không đổi app đang\n * dùng, và ngược lại) nên không gộp được vào một menu phẳng:\n * trục 1 — WORKSPACE: đổi ngữ cảnh dữ liệu (Acme Corp ▸ Beta Ltd)\n * trục 2 — APP: đổi sản phẩm trong cùng workspace (Tasks ▸ Chat ▸ CRM ▸ HR)\n */\n\nimport type { LucideIcon } from \"lucide-react\";\nimport type { AnchorHTMLAttributes, ComponentType } from \"react\";\n\n/**\n * Icon dùng trong shell: `LucideIcon` là trường hợp phổ biến, nhưng app có thể\n * truyền component SVG riêng miễn nhận `className` (để shell điều khiển kích thước).\n */\nexport type ShellIcon = LucideIcon | ComponentType<{ className?: string }>;\n\n/** Workspace (trục 1) — một tổ chức/không gian làm việc mà user thuộc về. */\nexport interface Workspace {\n id: string;\n name: string;\n /** Định danh trên URL/subdomain — shell chỉ hiển thị, không tự điều hướng. */\n slug: string;\n logoUrl?: string;\n /** \"Free\" | \"Pro\" | \"Business\"… — chỉ để hiển thị nhãn gói, shell không suy luận quyền từ đây. */\n plan?: string;\n /** Vai trò của user trong workspace này (chuỗi hiển thị sẵn). */\n role?: string;\n memberCount?: number;\n}\n\n/** Một sản phẩm trong hệ sinh thái (trục 2) — Tasks, Chat, CRM, HR… */\nexport interface AppDescriptor {\n /** \"tasks\" | \"chat\" | \"crm\" | \"hr\" | … */\n id: string;\n /** Tên hiển thị (mặc định tiếng Việt). */\n name: string;\n description?: string;\n icon: ShellIcon;\n href: string;\n /**\n * Tone màu định danh — MỘT tên trong `AVATAR_TONE_NAMES` của `@comitor/ui`.\n *\n * Bỏ trống thì màu TỰ SUY từ `id` bằng bảng tone dùng chung với `LetterAvatar`, nên một sản\n * phẩm mới có màu ngay mà không ai phải cấu hình. Tên lạ cũng rơi về màu suy ra — không ném.\n *\n * ⚠ Hạt giống là `id` (khoá app), KHÔNG phải `name`: tên đổi theo ngôn ngữ giao diện, và đo\n * được 4/6 app đổi màu khi chuyển vi ⇄ en nếu băm theo tên. Xem `IconAvatar` prop `seed`.\n */\n tone?: string;\n /**\n * Màu nhấn riêng của app — bất kỳ giá trị màu CSS hợp lệ (nên là `var(--…)`).\n * Shell gán vào biến `--app-accent` bằng inline style, KHÔNG sinh class động.\n *\n * ⚠ `--app-accent` CHỈ để TÔ NỀN. Khai `accent` thì phải khai kèm `accentInk`\n * (và `accentForeground` nếu nền đặc của bạn không mang được mực) — xem\n * `docs/hop-dong-mau.md` §5.\n */\n /** @deprecated Từ 1.10.0 `AppLauncher` KHÔNG đọc prop này nữa — dùng `tone`. Xem ghi chú trên. */\n accent?: string;\n /** Màu chữ/icon nằm TRÊN nền `--app-accent` ĐẶC; mặc định giữ `--app-accent-foreground` của theme. */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentForeground?: string;\n /**\n * Chính màu app khi nó đi lên NỀN TRANG (chữ, icon rời, viền, vạch active, dấu chọn)\n * → biến `--app-accent-ink`. Mặc định giữ bậc ink của theme.\n *\n * Vì sao phải khai riêng chứ không dẫn xuất từ `accent`: bậc nền và bậc mực có ràng buộc\n * NGƯỢC NHAU — nền càng đúng brand thì cùng màu đó làm chữ càng chìm. Gold mặc định là ví\n * dụ: `--app-accent` #E8B824 làm chữ trên nền trắng chỉ **1,86:1**, trên chính tint 15%\n * của nó **1,69:1**; `--app-accent-ink` #7A5800 đạt **6,51:1** trên `--background`,\n * **6,24:1** trên `--sidebar`, **5,96:1** trên `--sidebar-accent`. Bỏ trống trường này mà\n * vẫn đổi `accent` thì mọi chỗ dùng `text-app-accent-ink` giữ nguyên gold — sai màu app.\n */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentInk?: string;\n /** `false` = workspace chưa bật/chưa mua → hiện khoá và gọi `onUpsell` thay vì điều hướng. */\n entitled?: boolean;\n badge?: number | string;\n}\n\n/** Variant badge của mục menu — map thẳng sang variant của primitive `Badge`. */\nexport type NavBadgeVariant = \"default\" | \"destructive\" | \"warning\" | \"success\";\n\n/** Một mục điều hướng trong sidebar. */\nexport interface NavItem {\n id: string;\n label: string;\n href: string;\n icon?: ShellIcon;\n badge?: number | string;\n badgeVariant?: NavBadgeVariant;\n /** Mục con — render trong `Collapsible`; cha có children thì click cha chỉ mở/đóng. */\n children?: NavItem[];\n /**\n * Ép trạng thái active thay vì để shell tự suy từ `pathname`\n * (dùng khi route thật khác href, vd trang chi tiết `/tasks/123`).\n */\n isActive?: boolean;\n /** Mở link ở tab mới (link ngoài: tài liệu, trạng thái hệ thống…). */\n external?: boolean;\n disabled?: boolean;\n}\n\n/** Nhóm mục menu (Linear-style: nhãn nhỏ + thu gọn được). */\nexport interface NavGroup {\n id: string;\n /** Bỏ trống = nhóm không nhãn (dùng cho nhóm đầu tiên). */\n label?: string;\n items: NavItem[];\n /** Cho phép thu gọn cả nhóm; mặc định true khi có `label`. */\n collapsible?: boolean;\n /** Trạng thái ban đầu khi `collapsible` — mặc định mở. */\n defaultCollapsed?: boolean;\n}\n\n/**\n * Menu app truyền vào: mảng phẳng `NavItem[]` (app đơn giản) hoặc `NavGroup[]`.\n * Shell chuẩn hoá về `NavGroup[]` bằng `toNavGroups()` trong `utils.ts`.\n */\nexport type ShellNav = NavItem[] | NavGroup[];\n\n/** Người dùng đang đăng nhập. */\nexport interface ShellUser {\n id: string;\n name: string;\n email?: string;\n avatarUrl?: string;\n /** Vai trò trong workspace hiện tại — chuỗi hiển thị sẵn, shell không map mã. */\n role?: string;\n}\n\n/** Mục bổ sung do app chèn vào `UserMenu`. */\nexport interface UserMenuItem {\n id: string;\n label: string;\n icon?: ShellIcon;\n /** Có `href` thì render link; không thì render nút gọi `onSelect`. */\n href?: string;\n onSelect?: () => void;\n /** Nhãn phím tắt hiển thị bên phải, vd \"Ctrl+P\" (chỉ hiển thị, không tự gắn phím). */\n shortcut?: string;\n variant?: \"default\" | \"destructive\";\n}\n\n/**\n * Props của component link do app cung cấp (`LinkComponent`).\n * Mặc định shell dùng `next/link`; app không-Next (Vite, Storybook) truyền\n * component router của mình miễn khớp hình dạng props này.\n */\nexport interface ShellLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"href\"> {\n /** Chỉ nhận chuỗi — shell không dùng dạng `UrlObject` của Next để adapter khác còn dùng lại được. */\n href: string;\n}\n\n/** Component link thay thế `next/link`. */\nexport type ShellLinkComponent = ComponentType<ShellLinkProps>;\n\n/**\n * Toàn bộ chuỗi hiển thị của shell — app tiếng Anh (hoặc ngôn ngữ khác) override\n * từng khoá qua prop `labels`. Mặc định là tiếng Việt.\n */\nexport interface ShellLabels {\n /* Sidebar & header */\n collapseSidebar: string;\n /**\n * Nhãn NGẮN hiện trên chính nút thu gọn (khi thanh bên đang mở rộng).\n *\n * Tách khỏi `collapseSidebar` vì hai chuỗi có hai việc khác nhau: chuỗi dài là TÊN TRUY CẬP\n * (`aria-label` + tooltip) — nó phải nói rõ thu gọn cái gì, vì người dùng trình đọc màn hình\n * nghe nó tách khỏi ngữ cảnh; chuỗi ngắn là chữ NHÌN THẤY trong một nút rộng 240px nằm ngay\n * dưới thanh bên, nơi ngữ cảnh đã hiển nhiên và chữ dài chỉ làm nút chật.\n *\n * ⚠ WCAG 2.5.3 (Label in Name): chuỗi ngắn PHẢI là một phần của chuỗi dài, nếu không lệnh thoại\n * \"bấm Thu gọn\" sẽ không khớp tên truy cập. \"Thu gọn\" ⊂ \"Thu gọn thanh bên\" ✓ — giữ quan hệ đó\n * khi dịch sang ngôn ngữ khác.\n */\n collapseSidebarShort: string;\n expandSidebar: string;\n openMenu: string;\n closeMenu: string;\n search: string;\n searchPlaceholder: string;\n notifications: string;\n navigationLabel: string;\n /* Workspace switcher */\n workspace: string;\n workspaceSwitcherLabel: string;\n workspaceSearchPlaceholder: string;\n workspaceEmpty: string;\n createWorkspace: string;\n workspaceSettings: string;\n inviteMembers: string;\n /**\n * Đơn vị đếm thành viên của một workspace. Là HÀM chứ không phải chuỗi vì nó CHÈN SỐ, và\n * trật tự từ đổi theo ngôn ngữ: tiếng Việt đặt số trước (\"12 thành viên\"), tiếng Anh cũng vậy\n * (\"12 members\"), nhưng nhiều ngôn ngữ thì không — nối `${count} ${đơn_vị}` ở phía component\n * là khoá cứng trật tự tiếng Việt vào mọi bản dịch. Cùng khuôn với `range`/`pageStatus` của\n * `TablePagination` (README §5, ba quy tắc của cơ chế `labels`).\n */\n members: (count: number) => string;\n /* App launcher */\n apps: string;\n appLauncherLabel: string;\n myApps: string;\n discoverApps: string;\n locked: string;\n lockedHint: string;\n /* User menu */\n account: string;\n profile: string;\n settings: string;\n appearance: string;\n help: string;\n logout: string;\n /* Theme */\n themeLight: string;\n themeDark: string;\n themeSystem: string;\n toggleTheme: string;\n}\n\n/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */\nexport const DEFAULT_SHELL_LABELS: ShellLabels = {\n collapseSidebar: \"Thu gọn thanh bên\",\n collapseSidebarShort: \"Thu gọn\",\n expandSidebar: \"Mở rộng thanh bên\",\n openMenu: \"Mở menu\",\n closeMenu: \"Đóng menu\",\n search: \"Tìm kiếm\",\n searchPlaceholder: \"Tìm kiếm…\",\n notifications: \"Thông báo\",\n navigationLabel: \"Điều hướng chính\",\n workspace: \"Không gian làm việc\",\n workspaceSwitcherLabel: \"Đổi không gian làm việc\",\n workspaceSearchPlaceholder: \"Tìm không gian làm việc…\",\n workspaceEmpty: \"Không tìm thấy không gian làm việc\",\n createWorkspace: \"Tạo không gian làm việc\",\n workspaceSettings: \"Cài đặt không gian làm việc\",\n inviteMembers: \"Mời thành viên\",\n members: (count) => `${count} thành viên`,\n apps: \"Ứng dụng\",\n appLauncherLabel: \"Mở danh sách ứng dụng\",\n myApps: \"Ứng dụng của bạn\",\n discoverApps: \"Khám phá thêm\",\n locked: \"Chưa mở khoá\",\n lockedHint: \"Ứng dụng chưa có trong gói hiện tại\",\n account: \"Tài khoản\",\n profile: \"Hồ sơ\",\n settings: \"Cài đặt\",\n appearance: \"Giao diện\",\n help: \"Trợ giúp\",\n logout: \"Đăng xuất\",\n themeLight: \"Sáng\",\n themeDark: \"Tối\",\n themeSystem: \"Hệ thống\",\n toggleTheme: \"Đổi giao diện\"\n};\n"],"mappings":"AAqOO,MAAM,uBAAoC;AAAA,EAC/C,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,wBAAwB;AAAA,EACxB,4BAA4B;AAAA,EAC5B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,SAAS,CAAC,UAAU,GAAG,KAAK;AAAA,EAC5B,MAAM;AAAA,EACN,kBAAkB;AAAA,EAClB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AACf;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/types.ts"],"sourcesContent":["/**\n * Hợp đồng dữ liệu của Tier 3 (shell).\n *\n * Nguyên tắc: package KHÔNG chứa dữ liệu app. Toàn bộ workspace / danh sách app /\n * menu / user đều là *type* — app truyền vào qua props. File này là **module thuần**\n * (chỉ type + hằng nhãn, không hook, không directive) nên dùng được ở cả server lẫn client.\n *\n * Hai trục điều hướng của Comitor — một user thuộc NHIỀU workspace, và mỗi workspace\n * lại bật NHIỀU sản phẩm. Hai chiều đó vuông góc nhau (đổi workspace không đổi app đang\n * dùng, và ngược lại) nên không gộp được vào một menu phẳng:\n * trục 1 — WORKSPACE: đổi ngữ cảnh dữ liệu (Acme Corp ▸ Beta Ltd)\n * trục 2 — APP: đổi sản phẩm trong cùng workspace (Tasks ▸ Chat ▸ CRM ▸ HR)\n */\n\nimport type { LucideIcon } from \"lucide-react\";\nimport type { AnchorHTMLAttributes, ComponentType } from \"react\";\nimport { shellEn } from \"../i18n/namespaces/shell.js\";\n\n/**\n * Icon dùng trong shell: `LucideIcon` là trường hợp phổ biến, nhưng app có thể\n * truyền component SVG riêng miễn nhận `className` (để shell điều khiển kích thước).\n */\nexport type ShellIcon = LucideIcon | ComponentType<{ className?: string }>;\n\n/** Workspace (trục 1) — một tổ chức/không gian làm việc mà user thuộc về. */\nexport interface Workspace {\n id: string;\n name: string;\n /** Định danh trên URL/subdomain — shell chỉ hiển thị, không tự điều hướng. */\n slug: string;\n logoUrl?: string;\n /** \"Free\" | \"Pro\" | \"Business\"… — chỉ để hiển thị nhãn gói, shell không suy luận quyền từ đây. */\n plan?: string;\n /** Vai trò của user trong workspace này (chuỗi hiển thị sẵn). */\n role?: string;\n memberCount?: number;\n}\n\n/** Một sản phẩm trong hệ sinh thái (trục 2) — Tasks, Chat, CRM, HR… */\nexport interface AppDescriptor {\n /** \"tasks\" | \"chat\" | \"crm\" | \"hr\" | … */\n id: string;\n /** Tên hiển thị — app truyền theo ngôn ngữ giao diện đang dùng. */\n name: string;\n description?: string;\n icon: ShellIcon;\n href: string;\n /**\n * Tone màu định danh — MỘT tên trong `AVATAR_TONE_NAMES` của `@comitor/ui`.\n *\n * Bỏ trống thì màu TỰ SUY từ `id` bằng bảng tone dùng chung với `LetterAvatar`, nên một sản\n * phẩm mới có màu ngay mà không ai phải cấu hình. Tên lạ cũng rơi về màu suy ra — không ném.\n *\n * ⚠ Hạt giống là `id` (khoá app), KHÔNG phải `name`: tên đổi theo ngôn ngữ giao diện, và đo\n * được 4/6 app đổi màu khi chuyển vi ⇄ en nếu băm theo tên. Xem `IconAvatar` prop `seed`.\n */\n tone?: string;\n /**\n * Màu nhấn riêng của app — bất kỳ giá trị màu CSS hợp lệ (nên là `var(--…)`).\n * Shell gán vào biến `--app-accent` bằng inline style, KHÔNG sinh class động.\n *\n * ⚠ `--app-accent` CHỈ để TÔ NỀN. Khai `accent` thì phải khai kèm `accentInk`\n * (và `accentForeground` nếu nền đặc của bạn không mang được mực) — xem\n * `docs/hop-dong-mau.md` §5.\n */\n /** @deprecated Từ 1.10.0 `AppLauncher` KHÔNG đọc prop này nữa — dùng `tone`. Xem ghi chú trên. */\n accent?: string;\n /** Màu chữ/icon nằm TRÊN nền `--app-accent` ĐẶC; mặc định giữ `--app-accent-foreground` của theme. */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentForeground?: string;\n /**\n * Chính màu app khi nó đi lên NỀN TRANG (chữ, icon rời, viền, vạch active, dấu chọn)\n * → biến `--app-accent-ink`. Mặc định giữ bậc ink của theme.\n *\n * Vì sao phải khai riêng chứ không dẫn xuất từ `accent`: bậc nền và bậc mực có ràng buộc\n * NGƯỢC NHAU — nền càng đúng brand thì cùng màu đó làm chữ càng chìm. Gold mặc định là ví\n * dụ: `--app-accent` #E8B824 làm chữ trên nền trắng chỉ **1,86:1**, trên chính tint 15%\n * của nó **1,69:1**; `--app-accent-ink` #7A5800 đạt **6,51:1** trên `--background`,\n * **6,24:1** trên `--sidebar`, **5,96:1** trên `--sidebar-accent`. Bỏ trống trường này mà\n * vẫn đổi `accent` thì mọi chỗ dùng `text-app-accent-ink` giữ nguyên gold — sai màu app.\n */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentInk?: string;\n /** `false` = workspace chưa bật/chưa mua → hiện khoá và gọi `onUpsell` thay vì điều hướng. */\n entitled?: boolean;\n badge?: number | string;\n}\n\n/** Variant badge của mục menu — map thẳng sang variant của primitive `Badge`. */\nexport type NavBadgeVariant = \"default\" | \"destructive\" | \"warning\" | \"success\";\n\n/** Một mục điều hướng trong sidebar. */\nexport interface NavItem {\n id: string;\n label: string;\n href: string;\n icon?: ShellIcon;\n badge?: number | string;\n badgeVariant?: NavBadgeVariant;\n /** Mục con — render trong `Collapsible`; cha có children thì click cha chỉ mở/đóng. */\n children?: NavItem[];\n /**\n * Ép trạng thái active thay vì để shell tự suy từ `pathname`\n * (dùng khi route thật khác href, vd trang chi tiết `/tasks/123`).\n */\n isActive?: boolean;\n /** Mở link ở tab mới (link ngoài: tài liệu, trạng thái hệ thống…). */\n external?: boolean;\n disabled?: boolean;\n}\n\n/** Nhóm mục menu (Linear-style: nhãn nhỏ + thu gọn được). */\nexport interface NavGroup {\n id: string;\n /** Bỏ trống = nhóm không nhãn (dùng cho nhóm đầu tiên). */\n label?: string;\n items: NavItem[];\n /** Cho phép thu gọn cả nhóm; mặc định true khi có `label`. */\n collapsible?: boolean;\n /** Trạng thái ban đầu khi `collapsible` — mặc định mở. */\n defaultCollapsed?: boolean;\n}\n\n/**\n * Menu app truyền vào: mảng phẳng `NavItem[]` (app đơn giản) hoặc `NavGroup[]`.\n * Shell chuẩn hoá về `NavGroup[]` bằng `toNavGroups()` trong `utils.ts`.\n */\nexport type ShellNav = NavItem[] | NavGroup[];\n\n/** Người dùng đang đăng nhập. */\nexport interface ShellUser {\n id: string;\n name: string;\n email?: string;\n avatarUrl?: string;\n /** Vai trò trong workspace hiện tại — chuỗi hiển thị sẵn, shell không map mã. */\n role?: string;\n}\n\n/** Mục bổ sung do app chèn vào `UserMenu`. */\nexport interface UserMenuItem {\n id: string;\n label: string;\n icon?: ShellIcon;\n /** Có `href` thì render link; không thì render nút gọi `onSelect`. */\n href?: string;\n onSelect?: () => void;\n /** Nhãn phím tắt hiển thị bên phải, vd \"Ctrl+P\" (chỉ hiển thị, không tự gắn phím). */\n shortcut?: string;\n variant?: \"default\" | \"destructive\";\n}\n\n/**\n * Props của component link do app cung cấp (`LinkComponent`).\n * Mặc định shell dùng `next/link`; app không-Next (Vite, Storybook) truyền\n * component router của mình miễn khớp hình dạng props này.\n */\nexport interface ShellLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"href\"> {\n /** Chỉ nhận chuỗi — shell không dùng dạng `UrlObject` của Next để adapter khác còn dùng lại được. */\n href: string;\n}\n\n/** Component link thay thế `next/link`. */\nexport type ShellLinkComponent = ComponentType<ShellLinkProps>;\n\n/**\n * Toàn bộ chuỗi hiển thị của shell. Theo `ComitorLocaleProvider` (namespace `shell` — mặc định\n * tiếng Anh, bảng tiếng Việt dựng sẵn); override từng khoá qua prop `labels` của\n * `ShellProvider`/`AppShell`.\n */\nexport interface ShellLabels {\n /* Sidebar & header */\n collapseSidebar: string;\n /**\n * Nhãn NGẮN hiện trên chính nút thu gọn (khi thanh bên đang mở rộng).\n *\n * Tách khỏi `collapseSidebar` vì hai chuỗi có hai việc khác nhau: chuỗi dài là TÊN TRUY CẬP\n * (`aria-label` + tooltip) — nó phải nói rõ thu gọn cái gì, vì người dùng trình đọc màn hình\n * nghe nó tách khỏi ngữ cảnh; chuỗi ngắn là chữ NHÌN THẤY trong một nút rộng 240px nằm ngay\n * dưới thanh bên, nơi ngữ cảnh đã hiển nhiên và chữ dài chỉ làm nút chật.\n *\n * ⚠ WCAG 2.5.3 (Label in Name): chuỗi ngắn PHẢI là một phần của chuỗi dài, nếu không lệnh thoại\n * \"bấm Collapse\" sẽ không khớp tên truy cập. \"Collapse\" ⊂ \"Collapse sidebar\" ✓, \"Thu gọn\" ⊂\n * \"Thu gọn thanh bên\" ✓ — giữ quan hệ đó khi dịch sang ngôn ngữ khác.\n */\n collapseSidebarShort: string;\n expandSidebar: string;\n openMenu: string;\n closeMenu: string;\n search: string;\n searchPlaceholder: string;\n notifications: string;\n navigationLabel: string;\n /* Workspace switcher */\n workspace: string;\n workspaceSwitcherLabel: string;\n workspaceSearchPlaceholder: string;\n workspaceEmpty: string;\n createWorkspace: string;\n workspaceSettings: string;\n inviteMembers: string;\n /**\n * Đơn vị đếm thành viên của một workspace. Là HÀM chứ không phải chuỗi vì nó CHÈN SỐ, và\n * trật tự từ đổi theo ngôn ngữ: tiếng Việt đặt số trước (\"12 thành viên\"), tiếng Anh cũng vậy\n * (\"12 members\"), nhưng nhiều ngôn ngữ thì không — nối `${count} ${đơn_vị}` ở phía component\n * là khoá cứng trật tự của một ngôn ngữ vào mọi bản dịch. Hàm còn chọn được dạng số ít/nhiều\n * (\"1 member\" ⇄ \"2 members\"), thứ tiếng Việt không có mà tiếng Anh thì bắt buộc. Cùng khuôn\n * với `range`/`pageStatus` của `TablePagination` (README §5, ba quy tắc của cơ chế `labels`).\n */\n members: (count: number) => string;\n /* App launcher */\n apps: string;\n appLauncherLabel: string;\n myApps: string;\n discoverApps: string;\n locked: string;\n lockedHint: string;\n /* User menu */\n account: string;\n profile: string;\n settings: string;\n appearance: string;\n help: string;\n logout: string;\n /* Theme */\n themeLight: string;\n themeDark: string;\n themeSystem: string;\n toggleTheme: string;\n}\n\n/**\n * Nhãn TIẾNG ANH mặc định (từ `3.0.0`; trước đó là tiếng Việt). Bảng tiếng Việt:\n * `comitorMessagesVi.shell`. Shell không đọc thẳng hằng này — `ShellProvider` đọc bảng của\n * `ComitorLocaleProvider` rồi trộn `labels` lên trên; hằng ở đây để app trải ra khi tự dựng nhãn.\n */\nexport const DEFAULT_SHELL_LABELS: ShellLabels = { ...shellEn };\n"],"mappings":"AAgBA,SAAS,eAAe;AA4NjB,MAAM,uBAAoC,EAAE,GAAG,QAAQ;","names":[]}
|
|
@@ -17,16 +17,16 @@ interface UserMenuProps {
|
|
|
17
17
|
className?: string;
|
|
18
18
|
/** Hiện tên + vai trò cạnh avatar; màn hình hẹp nên để `false`. */
|
|
19
19
|
showName?: boolean;
|
|
20
|
-
/** Link
|
|
20
|
+
/** Link hồ sơ (nhãn `profile`) — bỏ trống thì không render mục này (tránh mục dẫn đi đâu không rõ). */
|
|
21
21
|
profileHref?: string;
|
|
22
|
-
/** Link
|
|
22
|
+
/** Link cài đặt (nhãn `settings`). */
|
|
23
23
|
settingsHref?: string;
|
|
24
|
-
/** Link
|
|
24
|
+
/** Link trợ giúp (nhãn `help`). */
|
|
25
25
|
helpHref?: string;
|
|
26
|
-
/** Mục bổ sung của app — chèn ngay trên
|
|
26
|
+
/** Mục bổ sung của app — chèn ngay trên mục đăng xuất. */
|
|
27
27
|
items?: UserMenuItem[];
|
|
28
28
|
/**
|
|
29
|
-
* Submenu
|
|
29
|
+
* Submenu đổi giao diện (nhãn `appearance`) trong menu người dùng.
|
|
30
30
|
*
|
|
31
31
|
* `"mobile"` (mặc định) — CHỈ hiện dưới `md`. Từ `md` trở lên `AppHeader` đã có
|
|
32
32
|
* `ThemeToggle` riêng, để cả hai là hai lối vào cho cùng một thứ.
|
|
@@ -45,7 +45,7 @@ interface UserMenuProps {
|
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* Menu người dùng ở góc phải header: avatar + tên/email, các mục do app khai báo,
|
|
48
|
-
* submenu đổi giao diện, và
|
|
48
|
+
* submenu đổi giao diện, và mục đăng xuất (tô màu destructive).
|
|
49
49
|
*
|
|
50
50
|
* Mục dựng sẵn chỉ hiện khi app cấp `href` tương ứng — package không tự bịa route.
|
|
51
51
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/user-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDown, CircleUser, HelpCircle, LogOut, Monitor, Moon, Palette, Settings, Sun } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { getAvatarToneClasses } from \"../components/composite/letter-avatar.js\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../components/ui/avatar.js\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger\n} from \"../components/ui/dropdown-menu.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ShellNavLink } from \"./shell-link.js\";\nimport type { ShellUser, UserMenuItem } from \"./types.js\";\nimport { getVietnameseInitials } from \"./utils.js\";\n\nexport interface UserAvatarProps {\n user: ShellUser;\n className?: string;\n}\n\n/**\n * Avatar người dùng: ảnh nếu có, không thì chữ cái kiểu VN (HỌ + TÊN) tô màu theo tên.\n *\n * Tô màu theo `user.name` chứ KHÔNG theo email: `LetterAvatar`/`AvatarGroup` ở Tier 2 seed\n * bằng tên, nên lấy khoá khác ở đây là cùng một người ra hai màu trên cùng một trang.\n */\nexport function UserAvatar({ user, className }: UserAvatarProps) {\n return (\n <Avatar className={cn(\"size-8\", className)}>\n {user.avatarUrl ? <AvatarImage src={user.avatarUrl} alt=\"\" /> : null}\n <AvatarFallback className={cn(\"text-xs font-semibold\", getAvatarToneClasses(user.name))}>\n {getVietnameseInitials(user.name)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\nexport interface UserMenuProps {\n className?: string;\n /** Hiện tên + vai trò cạnh avatar; màn hình hẹp nên để `false`. */\n showName?: boolean;\n /** Link \"Hồ sơ\" — bỏ trống thì không render mục này (tránh mục dẫn đi đâu không rõ). */\n profileHref?: string;\n /** Link \"Cài đặt\". */\n settingsHref?: string;\n /** Link \"Trợ giúp\". */\n helpHref?: string;\n /** Mục bổ sung của app — chèn ngay trên \"Đăng xuất\". */\n items?: UserMenuItem[];\n /**\n * Submenu \"Giao diện\" trong menu người dùng.\n *\n * `\"mobile\"` (mặc định) — CHỈ hiện dưới `md`. Từ `md` trở lên `AppHeader` đã có\n * `ThemeToggle` riêng, để cả hai là hai lối vào cho cùng một thứ.\n * `true` — luôn hiện. Dùng khi app tắt `showThemeToggle` của header, nếu không màn\n * hình rộng sẽ KHÔNG còn chỗ nào đổi giao diện.\n * `false` — không bao giờ hiện.\n *\n * ⚠ Nhánh `\"mobile\"` ẩn bằng CSS (`md:hidden`), không phải bằng điều kiện render: nút gạt theo\n * bề rộng cần `window`, mà đọc `window` lúc render đầu là lệch hydration. `display: none` loại\n * phần tử khỏi CẢ cây trợ năng nên trình đọc màn hình ở desktop cũng không thấy mục thừa.\n */\n showThemeSubmenu?: boolean | \"mobile\";\n /** Ghi đè handler đăng xuất của context. */\n onLogout?: () => void;\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * Menu người dùng ở góc phải header: avatar + tên/email, các mục do app khai báo,\n * submenu đổi giao diện, và \"Đăng xuất\" (tô màu destructive).\n *\n * Mục dựng sẵn chỉ hiện khi app cấp `href` tương ứng — package không tự bịa route.\n */\nexport function UserMenu({\n className,\n showName = false,\n profileHref,\n settingsHref,\n helpHref,\n items = [],\n showThemeSubmenu = \"mobile\",\n onLogout,\n align = \"end\"\n}: UserMenuProps) {\n const shell = useShell();\n const { theme, setTheme } = useTheme();\n const { user, labels } = shell;\n\n if (!user) return null;\n\n const handleLogout = onLogout ?? shell.onLogout;\n\n const renderItem = (item: UserMenuItem) => {\n const Icon = item.icon;\n const content = (\n <>\n {Icon ? <Icon className=\"size-4\" /> : null}\n <span className=\"truncate\">{item.label}</span>\n {item.shortcut ? <DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut> : null}\n </>\n );\n\n if (item.href) {\n return (\n <DropdownMenuItem key={item.id} variant={item.variant} asChild>\n <ShellNavLink href={item.href} onClick={() => item.onSelect?.()}>\n {content}\n </ShellNavLink>\n </DropdownMenuItem>\n );\n }\n\n return (\n <DropdownMenuItem key={item.id} variant={item.variant} onClick={() => item.onSelect?.()}>\n {content}\n </DropdownMenuItem>\n );\n };\n\n const builtInItems: (UserMenuItem | null)[] = [\n profileHref ? { id: \"profile\", label: labels.profile, icon: CircleUser, href: profileHref } : null,\n settingsHref ? { id: \"settings\", label: labels.settings, icon: Settings, href: settingsHref } : null,\n helpHref ? { id: \"help\", label: labels.help, icon: HelpCircle, href: helpHref } : null\n ];\n const visibleBuiltIns = builtInItems.filter((item): item is UserMenuItem => item !== null);\n\n const hasEntries = visibleBuiltIns.length > 0 || items.length > 0 || showThemeSubmenu;\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n aria-label={labels.account}\n className={cn(\n \"flex items-center gap-2 rounded-md p-1 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-muted\",\n className\n )}\n >\n <UserAvatar user={user} />\n {showName ? (\n <>\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-medium leading-tight text-foreground\">{user.name}</span>\n {user.role ? (\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">{user.role}</span>\n ) : null}\n </span>\n <ChevronDown className=\"size-3.5 shrink-0 text-muted-foreground\" />\n </>\n ) : null}\n </button>\n </DropdownMenuTrigger>\n\n <DropdownMenuContent align={align} className=\"w-60\">\n <DropdownMenuLabel className=\"flex items-center gap-2 font-normal\">\n <UserAvatar user={user} />\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-medium\">{user.name}</span>\n {user.email ? <span className=\"truncate text-xs text-muted-foreground\">{user.email}</span> : null}\n {user.role ? <span className=\"truncate text-[11px] text-muted-foreground\">{user.role}</span> : null}\n </span>\n </DropdownMenuLabel>\n\n {hasEntries ? <DropdownMenuSeparator /> : null}\n\n {visibleBuiltIns.map(renderItem)}\n {items.map(renderItem)}\n\n {showThemeSubmenu !== false ? (\n <DropdownMenuSub>\n <DropdownMenuSubTrigger className={cn(showThemeSubmenu === \"mobile\" && \"md:hidden\")}>\n <Palette className=\"size-4\" />\n <span>{labels.appearance}</span>\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent>\n <DropdownMenuItem onClick={() => setTheme(\"light\")} className={cn(theme === \"light\" && \"bg-accent\")}>\n <Sun className=\"size-4\" />\n {labels.themeLight}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"dark\")} className={cn(theme === \"dark\" && \"bg-accent\")}>\n <Moon className=\"size-4\" />\n {labels.themeDark}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"system\")} className={cn(theme === \"system\" && \"bg-accent\")}>\n <Monitor className=\"size-4\" />\n {labels.themeSystem}\n </DropdownMenuItem>\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n ) : null}\n\n {handleLogout ? (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem variant=\"destructive\" onClick={handleLogout}>\n <LogOut className=\"size-4\" />\n <span>{labels.logout}</span>\n </DropdownMenuItem>\n </>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/user-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDown, CircleUser, HelpCircle, LogOut, Monitor, Moon, Palette, Settings, Sun } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { getAvatarToneClasses } from \"../components/composite/letter-avatar.js\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../components/ui/avatar.js\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger\n} from \"../components/ui/dropdown-menu.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ShellNavLink } from \"./shell-link.js\";\nimport type { ShellUser, UserMenuItem } from \"./types.js\";\nimport { getVietnameseInitials } from \"./utils.js\";\n\nexport interface UserAvatarProps {\n user: ShellUser;\n className?: string;\n}\n\n/**\n * Avatar người dùng: ảnh nếu có, không thì chữ cái kiểu VN (HỌ + TÊN) tô màu theo tên.\n *\n * Tô màu theo `user.name` chứ KHÔNG theo email: `LetterAvatar`/`AvatarGroup` ở Tier 2 seed\n * bằng tên, nên lấy khoá khác ở đây là cùng một người ra hai màu trên cùng một trang.\n */\nexport function UserAvatar({ user, className }: UserAvatarProps) {\n return (\n <Avatar className={cn(\"size-8\", className)}>\n {user.avatarUrl ? <AvatarImage src={user.avatarUrl} alt=\"\" /> : null}\n <AvatarFallback className={cn(\"text-xs font-semibold\", getAvatarToneClasses(user.name))}>\n {getVietnameseInitials(user.name)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\nexport interface UserMenuProps {\n className?: string;\n /** Hiện tên + vai trò cạnh avatar; màn hình hẹp nên để `false`. */\n showName?: boolean;\n /** Link hồ sơ (nhãn `profile`) — bỏ trống thì không render mục này (tránh mục dẫn đi đâu không rõ). */\n profileHref?: string;\n /** Link cài đặt (nhãn `settings`). */\n settingsHref?: string;\n /** Link trợ giúp (nhãn `help`). */\n helpHref?: string;\n /** Mục bổ sung của app — chèn ngay trên mục đăng xuất. */\n items?: UserMenuItem[];\n /**\n * Submenu đổi giao diện (nhãn `appearance`) trong menu người dùng.\n *\n * `\"mobile\"` (mặc định) — CHỈ hiện dưới `md`. Từ `md` trở lên `AppHeader` đã có\n * `ThemeToggle` riêng, để cả hai là hai lối vào cho cùng một thứ.\n * `true` — luôn hiện. Dùng khi app tắt `showThemeToggle` của header, nếu không màn\n * hình rộng sẽ KHÔNG còn chỗ nào đổi giao diện.\n * `false` — không bao giờ hiện.\n *\n * ⚠ Nhánh `\"mobile\"` ẩn bằng CSS (`md:hidden`), không phải bằng điều kiện render: nút gạt theo\n * bề rộng cần `window`, mà đọc `window` lúc render đầu là lệch hydration. `display: none` loại\n * phần tử khỏi CẢ cây trợ năng nên trình đọc màn hình ở desktop cũng không thấy mục thừa.\n */\n showThemeSubmenu?: boolean | \"mobile\";\n /** Ghi đè handler đăng xuất của context. */\n onLogout?: () => void;\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * Menu người dùng ở góc phải header: avatar + tên/email, các mục do app khai báo,\n * submenu đổi giao diện, và mục đăng xuất (tô màu destructive).\n *\n * Mục dựng sẵn chỉ hiện khi app cấp `href` tương ứng — package không tự bịa route.\n */\nexport function UserMenu({\n className,\n showName = false,\n profileHref,\n settingsHref,\n helpHref,\n items = [],\n showThemeSubmenu = \"mobile\",\n onLogout,\n align = \"end\"\n}: UserMenuProps) {\n const shell = useShell();\n const { theme, setTheme } = useTheme();\n const { user, labels } = shell;\n\n if (!user) return null;\n\n const handleLogout = onLogout ?? shell.onLogout;\n\n const renderItem = (item: UserMenuItem) => {\n const Icon = item.icon;\n const content = (\n <>\n {Icon ? <Icon className=\"size-4\" /> : null}\n <span className=\"truncate\">{item.label}</span>\n {item.shortcut ? <DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut> : null}\n </>\n );\n\n if (item.href) {\n return (\n <DropdownMenuItem key={item.id} variant={item.variant} asChild>\n <ShellNavLink href={item.href} onClick={() => item.onSelect?.()}>\n {content}\n </ShellNavLink>\n </DropdownMenuItem>\n );\n }\n\n return (\n <DropdownMenuItem key={item.id} variant={item.variant} onClick={() => item.onSelect?.()}>\n {content}\n </DropdownMenuItem>\n );\n };\n\n const builtInItems: (UserMenuItem | null)[] = [\n profileHref ? { id: \"profile\", label: labels.profile, icon: CircleUser, href: profileHref } : null,\n settingsHref ? { id: \"settings\", label: labels.settings, icon: Settings, href: settingsHref } : null,\n helpHref ? { id: \"help\", label: labels.help, icon: HelpCircle, href: helpHref } : null\n ];\n const visibleBuiltIns = builtInItems.filter((item): item is UserMenuItem => item !== null);\n\n const hasEntries = visibleBuiltIns.length > 0 || items.length > 0 || showThemeSubmenu;\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n aria-label={labels.account}\n className={cn(\n \"flex items-center gap-2 rounded-md p-1 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-muted\",\n className\n )}\n >\n <UserAvatar user={user} />\n {showName ? (\n <>\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-medium leading-tight text-foreground\">{user.name}</span>\n {user.role ? (\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">{user.role}</span>\n ) : null}\n </span>\n <ChevronDown className=\"size-3.5 shrink-0 text-muted-foreground\" />\n </>\n ) : null}\n </button>\n </DropdownMenuTrigger>\n\n <DropdownMenuContent align={align} className=\"w-60\">\n <DropdownMenuLabel className=\"flex items-center gap-2 font-normal\">\n <UserAvatar user={user} />\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-medium\">{user.name}</span>\n {user.email ? <span className=\"truncate text-xs text-muted-foreground\">{user.email}</span> : null}\n {user.role ? <span className=\"truncate text-[11px] text-muted-foreground\">{user.role}</span> : null}\n </span>\n </DropdownMenuLabel>\n\n {hasEntries ? <DropdownMenuSeparator /> : null}\n\n {visibleBuiltIns.map(renderItem)}\n {items.map(renderItem)}\n\n {showThemeSubmenu !== false ? (\n <DropdownMenuSub>\n <DropdownMenuSubTrigger className={cn(showThemeSubmenu === \"mobile\" && \"md:hidden\")}>\n <Palette className=\"size-4\" />\n <span>{labels.appearance}</span>\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent>\n <DropdownMenuItem onClick={() => setTheme(\"light\")} className={cn(theme === \"light\" && \"bg-accent\")}>\n <Sun className=\"size-4\" />\n {labels.themeLight}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"dark\")} className={cn(theme === \"dark\" && \"bg-accent\")}>\n <Moon className=\"size-4\" />\n {labels.themeDark}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"system\")} className={cn(theme === \"system\" && \"bg-accent\")}>\n <Monitor className=\"size-4\" />\n {labels.themeSystem}\n </DropdownMenuItem>\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n ) : null}\n\n {handleLogout ? (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem variant=\"destructive\" onClick={handleLogout}>\n <LogOut className=\"size-4\" />\n <span>{labels.logout}</span>\n </DropdownMenuItem>\n </>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/shell/utils.d.ts
CHANGED
|
@@ -60,7 +60,7 @@ declare function isNavItemActive(item: NavItem, pathname: string, siblings?: Nav
|
|
|
60
60
|
declare function isNavBranchActive(item: NavItem, pathname: string, siblings?: NavItem[], searchParams?: NavSearchParams | null): boolean;
|
|
61
61
|
/** App đã được cấp quyền (`entitled` khác `false`) — thứ tự giữ nguyên như app truyền vào. */
|
|
62
62
|
declare function getEntitledApps(apps: AppDescriptor[]): AppDescriptor[];
|
|
63
|
-
/** App chưa mở khoá — hiển thị ở nhóm
|
|
63
|
+
/** App chưa mở khoá — hiển thị ở nhóm `discoverApps` của AppLauncher. */
|
|
64
64
|
declare function getLockedApps(apps: AppDescriptor[]): AppDescriptor[];
|
|
65
65
|
/**
|
|
66
66
|
* Suy ra app hiện tại từ `pathname` — khớp prefix DÀI NHẤT trước để
|
package/dist/shell/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/utils.ts"],"sourcesContent":["/**\n * Hàm thuần dùng chung cho tầng shell — KHÔNG hook, KHÔNG directive\n * (dùng được ở cả server lẫn client).\n */\n\nimport type { CSSProperties } from \"react\";\nimport type { AppDescriptor, NavGroup, NavItem, ShellNav } from \"./types.js\";\n\n/**\n * Tiện ích chuỗi tiếng Việt — **cùng một binding** với entry chính (`@comitor/ui`).\n *\n * Re-export chứ không viết lại. Hai lần repo này đã trả giá cho việc khai lại:\n * `getVietnameseInitials` từng có hai thân hàm (`toUpperCase()` ở đây,\n * `toLocaleUpperCase(\"vi-VN\")` ở Tier 2) nên cùng một tên cho ra hai kết quả tuỳ chỗ import;\n * `normalizeVi` thì có hai khai báo giống nhau chữ-cho-chữ, chưa sai kết quả nhưng là hai chỗ\n * để sửa lệch nhau lần sau. Tên vẫn xuất ở cả hai entry (không phá API của tầng shell), nhưng\n * chỉ còn MỘT thân hàm — xem docs/quyet-dinh-tich-hop.md §28 (\"một tên, một cửa\").\n */\nexport { getVietnameseInitials, normalizeVi } from \"../components/composite/text-utils.js\";\n\n/* ────────────────────────────────────────────────────────────────────────────\n Điều hướng\n ──────────────────────────────────────────────────────────────────────────── */\n\n/** Phân biệt `NavGroup` với `NavItem` khi app truyền mảng phẳng. */\nfunction isNavGroup(value: NavItem | NavGroup): value is NavGroup {\n return Array.isArray((value as NavGroup).items);\n}\n\n/**\n * Chuẩn hoá menu app truyền vào về `NavGroup[]`.\n * Mảng phẳng `NavItem[]` → một nhóm không nhãn (id \"default\").\n */\nexport function toNavGroups(nav: ShellNav | undefined): NavGroup[] {\n if (!nav || nav.length === 0) return [];\n const first = nav[0];\n if (first && isNavGroup(first)) return nav as NavGroup[];\n return [{ id: \"default\", items: nav as NavItem[] }];\n}\n\n/** Gộp mọi item của mọi nhóm thành một mảng phẳng (kể cả mục con). */\nexport function flattenNavItems(groups: NavGroup[]): NavItem[] {\n const out: NavItem[] = [];\n const walk = (items: NavItem[]) => {\n for (const item of items) {\n out.push(item);\n if (item.children?.length) walk(item.children);\n }\n };\n for (const group of groups) walk(group.items);\n return out;\n}\n\n/**\n * Query string hiện tại, theo mọi hình dạng app thường có sẵn trong tay:\n * `useSearchParams()` của Next (một `URLSearchParams`), chuỗi `\"?tab=x\"` / `\"tab=x\"`,\n * hoặc object `searchParams` mà Server Component nhận được.\n */\nexport type NavSearchParams = string | URLSearchParams | Record<string, string | string[] | undefined>;\n\n/** Tách `href` thành đường dẫn + query. Hash bị bỏ: `pathname` không bao giờ chứa nó. */\nfunction splitHref(href: string): { path: string; query: string } {\n const hashAt = href.indexOf(\"#\");\n const noHash = hashAt === -1 ? href : href.slice(0, hashAt);\n const queryAt = noHash.indexOf(\"?\");\n if (queryAt === -1) return { path: noHash, query: \"\" };\n return { path: noHash.slice(0, queryAt), query: noHash.slice(queryAt + 1) };\n}\n\n/** Quy mọi hình dạng `NavSearchParams` về `URLSearchParams`. `null`/`undefined` → `null`. */\nexport function toSearchParams(input: NavSearchParams | null | undefined): URLSearchParams | null {\n if (input === null || input === undefined) return null;\n if (typeof input === \"string\") return new URLSearchParams(input.startsWith(\"?\") ? input.slice(1) : input);\n // Duck-typing thay vì `instanceof`: `ReadonlyURLSearchParams` của Next kế thừa\n // `URLSearchParams`, nhưng `instanceof` còn hỏng khi hai realm khác nhau (test, iframe).\n if (typeof (input as URLSearchParams).getAll === \"function\") return input as URLSearchParams;\n const out = new URLSearchParams();\n for (const [key, value] of Object.entries(input as Record<string, string | string[] | undefined>)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) for (const one of value) out.append(key, one);\n else out.append(key, value);\n }\n return out;\n}\n\n/** Mọi cặp key=value trong `query` đều có mặt ở `current` (khớp TẬP CON, không đòi bằng hệt). */\nfunction queryIsSubsetOf(query: string, current: URLSearchParams): boolean {\n for (const [key, value] of new URLSearchParams(query)) {\n if (!current.getAll(key).includes(value)) return false;\n }\n return true;\n}\n\n/** Đường dẫn hiệu dụng của một href. `\"?tab=x\"` (chỉ có query) = chính trang đang mở. */\nfunction navItemPath(href: string, pathname: string): string {\n const { path } = splitHref(href);\n return path === \"\" ? pathname : path;\n}\n\n/**\n * Active matching \"thông minh\": chỉ dùng `startsWith` khi KHÔNG có mục nào khác\n * khớp CHÍNH XÁC `pathname`. Nếu không, `/tasks` vẫn sáng khi đang đứng ở\n * `/tasks/inbox` (có mục riêng cho nó).\n *\n * ─ QUERY TRONG `href` ─\n * `usePathname()` của Next **không bao giờ** chứa `?query`, nên so thẳng `pathname === href`\n * làm mục kiểu `/cai-dat?tab=thanh-vien` vĩnh viễn không sáng. Query bị tách khỏi `href`\n * trước khi so đường dẫn; phần query chỉ được xét khi app truyền `searchParams`.\n *\n * Không truyền `searchParams` → shell không biết đang ở tab nào, và khi đó **chỉ mục KHÔNG mang\n * query mới sáng** (`/cai-dat` sáng, `/cai-dat?tab=…` thì không).\n *\n * ⚠ Bản trước cho MỌI mục cùng đường dẫn cùng sáng, với lý lẽ \"đúng mức thông tin đang có, và tốt\n * hơn không mục nào sáng\". Lý lẽ đó dựng một lựa chọn nhị phân giả: còn khả năng thứ ba, và nó tốt\n * hơn cả hai. Bốn hàng cùng sáng không phải là \"chưa đủ thông tin\" — nó là một câu KHẲNG ĐỊNH SAI\n * với người dùng (\"bạn đang ở cả bốn chỗ\"), trong khi tô đúng một hàng canonical chỉ là chưa đủ\n * chi tiết. Ở `comitor-starter` lỗi này làm sáng cùng lúc \"Thành viên\", \"Chung\", \"Thông báo\" và\n * \"Nâng cao\".\n *\n * Ngoại lệ giữ lại: nếu KHÔNG có mục nào cùng đường dẫn mà không mang query (tức mọi lối vào trang\n * đó đều là một tab), thì các mục có query lại cùng sáng — vì lúc đó \"không hàng nào sáng\" mới là\n * kết quả tệ nhất.\n *\n * Muốn đúng từng tab thì truyền `searchParams` — xem prop cùng tên của `ShellProvider`.\n *\n * @param siblings Danh sách mục để đối chiếu khớp chính xác — nên truyền toàn bộ\n * mục của app hiện tại (`flattenNavItems`), không chỉ mục cùng cấp.\n * @param searchParams Query hiện tại (TUỲ CHỌN). Có nó thì mục có query phải khớp TẬP CON\n * (`?tab=a` sáng ở `?tab=a&q=x`), và mục KHÔNG query nhường cho mục cùng\n * đường dẫn có query khớp — tab mặc định không sáng cùng lúc với tab đang mở.\n */\nexport function isNavItemActive(\n item: NavItem,\n pathname: string,\n siblings: NavItem[] = [],\n searchParams?: NavSearchParams | null\n): boolean {\n if (item.isActive !== undefined) return item.isActive;\n if (!pathname || !item.href) return false;\n\n const { query } = splitHref(item.href);\n const itemPath = navItemPath(item.href, pathname);\n const current = toSearchParams(searchParams);\n\n if (pathname === itemPath) {\n if (!current) {\n // Query chưa biết → mục canonical (không query) thắng; xem chú thích ở đầu hàm.\n if (query === \"\") return true;\n return !siblings.some(\n (other) =>\n other.href !== undefined && splitHref(other.href).query === \"\" && splitHref(other.href).path === pathname\n );\n }\n if (query !== \"\") return queryIsSubsetOf(query, current);\n return !siblings.some((other) => {\n if (other.href === item.href) return false;\n const otherQuery = splitHref(other.href).query;\n return (\n otherQuery !== \"\" && navItemPath(other.href, pathname) === pathname && queryIsSubsetOf(otherQuery, current)\n );\n });\n }\n\n // So khớp chính xác của mục khác cũng phải bỏ query, nếu không `/cai-dat?tab=x` sẽ không\n // bao giờ được tính là \"đã có mục khớp đúng\" và mục cha `/cai-dat` sáng nhầm theo startsWith.\n const hasExactMatchElsewhere = siblings.some(\n (other) => other.href !== item.href && splitHref(other.href).path === pathname\n );\n if (hasExactMatchElsewhere) return false;\n return itemPath !== \"/\" && pathname.startsWith(`${itemPath}/`);\n}\n\n/** Mục cha được coi là active khi chính nó hoặc bất kỳ mục con nào active. */\nexport function isNavBranchActive(\n item: NavItem,\n pathname: string,\n siblings: NavItem[] = [],\n searchParams?: NavSearchParams | null\n): boolean {\n if (isNavItemActive(item, pathname, siblings, searchParams)) return true;\n return (item.children ?? []).some((child) => isNavBranchActive(child, pathname, siblings, searchParams));\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n App (trục 2)\n ──────────────────────────────────────────────────────────────────────────── */\n\n/** App đã được cấp quyền (`entitled` khác `false`) — thứ tự giữ nguyên như app truyền vào. */\nexport function getEntitledApps(apps: AppDescriptor[]): AppDescriptor[] {\n return apps.filter((app) => app.entitled !== false);\n}\n\n/** App chưa mở khoá — hiển thị ở nhóm \"Khám phá thêm\" của AppLauncher. */\nexport function getLockedApps(apps: AppDescriptor[]): AppDescriptor[] {\n return apps.filter((app) => app.entitled === false);\n}\n\n/**\n * Suy ra app hiện tại từ `pathname` — khớp prefix DÀI NHẤT trước để\n * \"/crm/deals\" không rơi nhầm vào app có href \"/\".\n *\n * Query trong `href` bị tách ra trước khi so, cùng lý do với `isNavItemActive`: `pathname`\n * không mang query, nên app khai `href: \"/crm?view=board\"` mà so nguyên chuỗi thì không bao\n * giờ được nhận ra. Sắp xếp cũng theo độ dài ĐƯỜNG DẪN (query dài không được đẩy app lên trước).\n */\nexport function resolveCurrentApp(apps: AppDescriptor[], pathname: string): AppDescriptor | null {\n if (apps.length === 0) return null;\n const matched = apps\n .map((app) => ({ app, path: splitHref(app.href).path }))\n .sort((a, b) => b.path.length - a.path.length)\n .find(({ path }) => pathname === path || (path !== \"/\" && path !== \"\" && pathname.startsWith(`${path}/`)))?.app;\n return matched ?? apps[0] ?? null;\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Biến CSS theo app\n\n (Màu avatar KHÔNG còn cửa riêng ở tầng shell: `getAvatarToneClass` — alias một dòng của\n `getAvatarToneClasses`, lệch tên tham số `seed` ⇄ `name` và phải kèm chú thích dặn \"seed\n phải là tên hiển thị\" — đã bị xoá ở 0.2.0. `UserMenu`/`WorkspaceSwitcher` nhập thẳng\n `getAvatarToneClasses` từ `composite/letter-avatar.js`; app dùng `@comitor/ui`.)\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Inline style gán bộ biến accent cho một app cụ thể.\n * Trả về `undefined` khi app không khai gì → giữ nguyên token mặc định của theme.\n *\n * Cả BA vai trò đều phải đi qua đây, không chỉ `--app-accent`: theo `docs/hop-dong-mau.md` §5\n * thì `--app-accent` chỉ để TÔ NỀN, `--app-accent-foreground` là chữ trên nền đặc đó, còn\n * `--app-accent-ink` là chính màu app khi đi lên nền trang. Thiếu cửa cho `--app-accent-ink`\n * thì app đổi accent sang màu khác vẫn để lại bậc mực gold mặc định (#7A5800 ở light /\n * #F5D060 ở dark) ở mọi chỗ dùng `text-app-accent-ink` — sai màu mà không sai chuẩn.\n */\nexport function appAccentStyle(\n app: Pick<AppDescriptor, \"accent\" | \"accentForeground\" | \"accentInk\"> | null | undefined\n) {\n if (!app?.accent && !app?.accentForeground && !app?.accentInk) return undefined;\n const style: Record<string, string> = {};\n if (app.accent) style[\"--app-accent\"] = app.accent;\n if (app.accentForeground) style[\"--app-accent-foreground\"] = app.accentForeground;\n if (app.accentInk) style[\"--app-accent-ink\"] = app.accentInk;\n return style as CSSProperties;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/utils.ts"],"sourcesContent":["/**\n * Hàm thuần dùng chung cho tầng shell — KHÔNG hook, KHÔNG directive\n * (dùng được ở cả server lẫn client).\n */\n\nimport type { CSSProperties } from \"react\";\nimport type { AppDescriptor, NavGroup, NavItem, ShellNav } from \"./types.js\";\n\n/**\n * Tiện ích chuỗi tiếng Việt — **cùng một binding** với entry chính (`@comitor/ui`).\n *\n * Re-export chứ không viết lại. Hai lần repo này đã trả giá cho việc khai lại:\n * `getVietnameseInitials` từng có hai thân hàm (`toUpperCase()` ở đây,\n * `toLocaleUpperCase(\"vi-VN\")` ở Tier 2) nên cùng một tên cho ra hai kết quả tuỳ chỗ import;\n * `normalizeVi` thì có hai khai báo giống nhau chữ-cho-chữ, chưa sai kết quả nhưng là hai chỗ\n * để sửa lệch nhau lần sau. Tên vẫn xuất ở cả hai entry (không phá API của tầng shell), nhưng\n * chỉ còn MỘT thân hàm — xem docs/quyet-dinh-tich-hop.md §28 (\"một tên, một cửa\").\n */\nexport { getVietnameseInitials, normalizeVi } from \"../components/composite/text-utils.js\";\n\n/* ────────────────────────────────────────────────────────────────────────────\n Điều hướng\n ──────────────────────────────────────────────────────────────────────────── */\n\n/** Phân biệt `NavGroup` với `NavItem` khi app truyền mảng phẳng. */\nfunction isNavGroup(value: NavItem | NavGroup): value is NavGroup {\n return Array.isArray((value as NavGroup).items);\n}\n\n/**\n * Chuẩn hoá menu app truyền vào về `NavGroup[]`.\n * Mảng phẳng `NavItem[]` → một nhóm không nhãn (id \"default\").\n */\nexport function toNavGroups(nav: ShellNav | undefined): NavGroup[] {\n if (!nav || nav.length === 0) return [];\n const first = nav[0];\n if (first && isNavGroup(first)) return nav as NavGroup[];\n return [{ id: \"default\", items: nav as NavItem[] }];\n}\n\n/** Gộp mọi item của mọi nhóm thành một mảng phẳng (kể cả mục con). */\nexport function flattenNavItems(groups: NavGroup[]): NavItem[] {\n const out: NavItem[] = [];\n const walk = (items: NavItem[]) => {\n for (const item of items) {\n out.push(item);\n if (item.children?.length) walk(item.children);\n }\n };\n for (const group of groups) walk(group.items);\n return out;\n}\n\n/**\n * Query string hiện tại, theo mọi hình dạng app thường có sẵn trong tay:\n * `useSearchParams()` của Next (một `URLSearchParams`), chuỗi `\"?tab=x\"` / `\"tab=x\"`,\n * hoặc object `searchParams` mà Server Component nhận được.\n */\nexport type NavSearchParams = string | URLSearchParams | Record<string, string | string[] | undefined>;\n\n/** Tách `href` thành đường dẫn + query. Hash bị bỏ: `pathname` không bao giờ chứa nó. */\nfunction splitHref(href: string): { path: string; query: string } {\n const hashAt = href.indexOf(\"#\");\n const noHash = hashAt === -1 ? href : href.slice(0, hashAt);\n const queryAt = noHash.indexOf(\"?\");\n if (queryAt === -1) return { path: noHash, query: \"\" };\n return { path: noHash.slice(0, queryAt), query: noHash.slice(queryAt + 1) };\n}\n\n/** Quy mọi hình dạng `NavSearchParams` về `URLSearchParams`. `null`/`undefined` → `null`. */\nexport function toSearchParams(input: NavSearchParams | null | undefined): URLSearchParams | null {\n if (input === null || input === undefined) return null;\n if (typeof input === \"string\") return new URLSearchParams(input.startsWith(\"?\") ? input.slice(1) : input);\n // Duck-typing thay vì `instanceof`: `ReadonlyURLSearchParams` của Next kế thừa\n // `URLSearchParams`, nhưng `instanceof` còn hỏng khi hai realm khác nhau (test, iframe).\n if (typeof (input as URLSearchParams).getAll === \"function\") return input as URLSearchParams;\n const out = new URLSearchParams();\n for (const [key, value] of Object.entries(input as Record<string, string | string[] | undefined>)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) for (const one of value) out.append(key, one);\n else out.append(key, value);\n }\n return out;\n}\n\n/** Mọi cặp key=value trong `query` đều có mặt ở `current` (khớp TẬP CON, không đòi bằng hệt). */\nfunction queryIsSubsetOf(query: string, current: URLSearchParams): boolean {\n for (const [key, value] of new URLSearchParams(query)) {\n if (!current.getAll(key).includes(value)) return false;\n }\n return true;\n}\n\n/** Đường dẫn hiệu dụng của một href. `\"?tab=x\"` (chỉ có query) = chính trang đang mở. */\nfunction navItemPath(href: string, pathname: string): string {\n const { path } = splitHref(href);\n return path === \"\" ? pathname : path;\n}\n\n/**\n * Active matching \"thông minh\": chỉ dùng `startsWith` khi KHÔNG có mục nào khác\n * khớp CHÍNH XÁC `pathname`. Nếu không, `/tasks` vẫn sáng khi đang đứng ở\n * `/tasks/inbox` (có mục riêng cho nó).\n *\n * ─ QUERY TRONG `href` ─\n * `usePathname()` của Next **không bao giờ** chứa `?query`, nên so thẳng `pathname === href`\n * làm mục kiểu `/cai-dat?tab=thanh-vien` vĩnh viễn không sáng. Query bị tách khỏi `href`\n * trước khi so đường dẫn; phần query chỉ được xét khi app truyền `searchParams`.\n *\n * Không truyền `searchParams` → shell không biết đang ở tab nào, và khi đó **chỉ mục KHÔNG mang\n * query mới sáng** (`/cai-dat` sáng, `/cai-dat?tab=…` thì không).\n *\n * ⚠ Bản trước cho MỌI mục cùng đường dẫn cùng sáng, với lý lẽ \"đúng mức thông tin đang có, và tốt\n * hơn không mục nào sáng\". Lý lẽ đó dựng một lựa chọn nhị phân giả: còn khả năng thứ ba, và nó tốt\n * hơn cả hai. Bốn hàng cùng sáng không phải là \"chưa đủ thông tin\" — nó là một câu KHẲNG ĐỊNH SAI\n * với người dùng (\"bạn đang ở cả bốn chỗ\"), trong khi tô đúng một hàng canonical chỉ là chưa đủ\n * chi tiết. Ở `comitor-starter` lỗi này làm sáng cùng lúc \"Thành viên\", \"Chung\", \"Thông báo\" và\n * \"Nâng cao\".\n *\n * Ngoại lệ giữ lại: nếu KHÔNG có mục nào cùng đường dẫn mà không mang query (tức mọi lối vào trang\n * đó đều là một tab), thì các mục có query lại cùng sáng — vì lúc đó \"không hàng nào sáng\" mới là\n * kết quả tệ nhất.\n *\n * Muốn đúng từng tab thì truyền `searchParams` — xem prop cùng tên của `ShellProvider`.\n *\n * @param siblings Danh sách mục để đối chiếu khớp chính xác — nên truyền toàn bộ\n * mục của app hiện tại (`flattenNavItems`), không chỉ mục cùng cấp.\n * @param searchParams Query hiện tại (TUỲ CHỌN). Có nó thì mục có query phải khớp TẬP CON\n * (`?tab=a` sáng ở `?tab=a&q=x`), và mục KHÔNG query nhường cho mục cùng\n * đường dẫn có query khớp — tab mặc định không sáng cùng lúc với tab đang mở.\n */\nexport function isNavItemActive(\n item: NavItem,\n pathname: string,\n siblings: NavItem[] = [],\n searchParams?: NavSearchParams | null\n): boolean {\n if (item.isActive !== undefined) return item.isActive;\n if (!pathname || !item.href) return false;\n\n const { query } = splitHref(item.href);\n const itemPath = navItemPath(item.href, pathname);\n const current = toSearchParams(searchParams);\n\n if (pathname === itemPath) {\n if (!current) {\n // Query chưa biết → mục canonical (không query) thắng; xem chú thích ở đầu hàm.\n if (query === \"\") return true;\n return !siblings.some(\n (other) =>\n other.href !== undefined && splitHref(other.href).query === \"\" && splitHref(other.href).path === pathname\n );\n }\n if (query !== \"\") return queryIsSubsetOf(query, current);\n return !siblings.some((other) => {\n if (other.href === item.href) return false;\n const otherQuery = splitHref(other.href).query;\n return (\n otherQuery !== \"\" && navItemPath(other.href, pathname) === pathname && queryIsSubsetOf(otherQuery, current)\n );\n });\n }\n\n // So khớp chính xác của mục khác cũng phải bỏ query, nếu không `/cai-dat?tab=x` sẽ không\n // bao giờ được tính là \"đã có mục khớp đúng\" và mục cha `/cai-dat` sáng nhầm theo startsWith.\n const hasExactMatchElsewhere = siblings.some(\n (other) => other.href !== item.href && splitHref(other.href).path === pathname\n );\n if (hasExactMatchElsewhere) return false;\n return itemPath !== \"/\" && pathname.startsWith(`${itemPath}/`);\n}\n\n/** Mục cha được coi là active khi chính nó hoặc bất kỳ mục con nào active. */\nexport function isNavBranchActive(\n item: NavItem,\n pathname: string,\n siblings: NavItem[] = [],\n searchParams?: NavSearchParams | null\n): boolean {\n if (isNavItemActive(item, pathname, siblings, searchParams)) return true;\n return (item.children ?? []).some((child) => isNavBranchActive(child, pathname, siblings, searchParams));\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n App (trục 2)\n ──────────────────────────────────────────────────────────────────────────── */\n\n/** App đã được cấp quyền (`entitled` khác `false`) — thứ tự giữ nguyên như app truyền vào. */\nexport function getEntitledApps(apps: AppDescriptor[]): AppDescriptor[] {\n return apps.filter((app) => app.entitled !== false);\n}\n\n/** App chưa mở khoá — hiển thị ở nhóm `discoverApps` của AppLauncher. */\nexport function getLockedApps(apps: AppDescriptor[]): AppDescriptor[] {\n return apps.filter((app) => app.entitled === false);\n}\n\n/**\n * Suy ra app hiện tại từ `pathname` — khớp prefix DÀI NHẤT trước để\n * \"/crm/deals\" không rơi nhầm vào app có href \"/\".\n *\n * Query trong `href` bị tách ra trước khi so, cùng lý do với `isNavItemActive`: `pathname`\n * không mang query, nên app khai `href: \"/crm?view=board\"` mà so nguyên chuỗi thì không bao\n * giờ được nhận ra. Sắp xếp cũng theo độ dài ĐƯỜNG DẪN (query dài không được đẩy app lên trước).\n */\nexport function resolveCurrentApp(apps: AppDescriptor[], pathname: string): AppDescriptor | null {\n if (apps.length === 0) return null;\n const matched = apps\n .map((app) => ({ app, path: splitHref(app.href).path }))\n .sort((a, b) => b.path.length - a.path.length)\n .find(({ path }) => pathname === path || (path !== \"/\" && path !== \"\" && pathname.startsWith(`${path}/`)))?.app;\n return matched ?? apps[0] ?? null;\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Biến CSS theo app\n\n (Màu avatar KHÔNG còn cửa riêng ở tầng shell: `getAvatarToneClass` — alias một dòng của\n `getAvatarToneClasses`, lệch tên tham số `seed` ⇄ `name` và phải kèm chú thích dặn \"seed\n phải là tên hiển thị\" — đã bị xoá ở 0.2.0. `UserMenu`/`WorkspaceSwitcher` nhập thẳng\n `getAvatarToneClasses` từ `composite/letter-avatar.js`; app dùng `@comitor/ui`.)\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Inline style gán bộ biến accent cho một app cụ thể.\n * Trả về `undefined` khi app không khai gì → giữ nguyên token mặc định của theme.\n *\n * Cả BA vai trò đều phải đi qua đây, không chỉ `--app-accent`: theo `docs/hop-dong-mau.md` §5\n * thì `--app-accent` chỉ để TÔ NỀN, `--app-accent-foreground` là chữ trên nền đặc đó, còn\n * `--app-accent-ink` là chính màu app khi đi lên nền trang. Thiếu cửa cho `--app-accent-ink`\n * thì app đổi accent sang màu khác vẫn để lại bậc mực gold mặc định (#7A5800 ở light /\n * #F5D060 ở dark) ở mọi chỗ dùng `text-app-accent-ink` — sai màu mà không sai chuẩn.\n */\nexport function appAccentStyle(\n app: Pick<AppDescriptor, \"accent\" | \"accentForeground\" | \"accentInk\"> | null | undefined\n) {\n if (!app?.accent && !app?.accentForeground && !app?.accentInk) return undefined;\n const style: Record<string, string> = {};\n if (app.accent) style[\"--app-accent\"] = app.accent;\n if (app.accentForeground) style[\"--app-accent-foreground\"] = app.accentForeground;\n if (app.accentInk) style[\"--app-accent-ink\"] = app.accentInk;\n return style as CSSProperties;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -228,7 +228,7 @@ function ActionRow({
|
|
|
228
228
|
className: cn(
|
|
229
229
|
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm",
|
|
230
230
|
"transition-colors duration-150 ease-out",
|
|
231
|
-
// Bậc ink chứ không `text-destructive`: dòng
|
|
231
|
+
// Bậc ink chứ không `text-destructive`: dòng đăng xuất là CHỮ trên nền `--popover`,
|
|
232
232
|
// mà `--destructive` (token NỀN) chỉ được 3,53:1 trên `--popover` ở dark ✗. Ink đạt cả
|
|
233
233
|
// hai: 7,15:1 (light) · 6,42:1 (dark). Icon ở `opacity-70` là thành phần phi-văn-bản
|
|
234
234
|
// (ngưỡng 3:1): 3,69:1 / 3,73:1 lúc nghỉ, và 3,37:1 / 3,62:1 trên nền hover
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/workspace-switcher.tsx"],"sourcesContent":["\"use client\";\n\nimport { Building2, Check, ChevronsUpDown, LogOut, Plus, Search, Settings, UserPlus } from \"lucide-react\";\nimport { useMemo, useRef, useState } from \"react\";\nimport { getAvatarToneClasses } from \"../components/composite/letter-avatar.js\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../components/ui/avatar.js\";\nimport { Badge } from \"../components/ui/badge.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../components/ui/popover.js\";\nimport { Separator } from \"../components/ui/separator.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport type { Workspace } from \"./types.js\";\nimport { getVietnameseInitials, normalizeVi } from \"./utils.js\";\n\n/** Từ số workspace này trở lên thì hiện ô tìm kiếm trong danh sách. */\nconst SEARCH_THRESHOLD = 8;\n\n/**\n * Ảnh đại diện workspace: logo nếu có, không thì ô chữ cái tô màu deterministic.\n *\n * Seed là `workspace.name` chứ KHÔNG phải slug: app hiển thị workspace bằng\n * `<LetterAvatar name={workspace.name} square />` ở chỗ khác, seed lệch là màu lệch.\n */\nfunction WorkspaceAvatar({ workspace, className }: { workspace: Workspace; className?: string }) {\n return (\n <Avatar className={cn(\"size-6 rounded-md\", className)}>\n {workspace.logoUrl ? <AvatarImage src={workspace.logoUrl} alt=\"\" className=\"rounded-md\" /> : null}\n <AvatarFallback className={cn(\"rounded-md text-[10px] font-semibold\", getAvatarToneClasses(workspace.name))}>\n {getVietnameseInitials(workspace.name)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\nexport interface WorkspaceSwitcherProps {\n className?: string;\n /** Sidebar thu gọn → chỉ hiện avatar + tooltip. */\n collapsed?: boolean;\n /** Ẩn khu \"Hành động\" (tạo workspace, cài đặt, mời thành viên, đăng xuất). */\n showActions?: boolean;\n /** Căn panel — mặc định \"start\" (mở xuôi theo mép trái sidebar). */\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * **Trục 1 — đổi không gian làm việc.**\n *\n * Chỉ gọi `onWorkspaceChange(id)`; điều hướng/đổi subdomain là việc của app\n * (đổi workspace kéo theo đổi toàn bộ dữ liệu + quyền nên shell không tự quyết).\n * Có ≤1 workspace thì phần danh sách tự ẩn, chỉ còn nhãn workspace hiện tại.\n *\n * Dùng `Popover` chứ không `DropdownMenu`: menu của Radix có typeahead, gõ vào\n * ô tìm kiếm bên trong sẽ bị nó cướp phím.\n */\nexport function WorkspaceSwitcher({\n className,\n collapsed = false,\n showActions = true,\n align = \"start\"\n}: WorkspaceSwitcherProps) {\n const {\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n onLogout,\n labels\n } = useShell();\n\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const searchRef = useRef<HTMLInputElement>(null);\n\n const showList = workspaces.length > 1;\n const showSearch = workspaces.length >= SEARCH_THRESHOLD;\n\n const filtered = useMemo(() => {\n if (!query.trim()) return workspaces;\n const needle = normalizeVi(query);\n return workspaces.filter(\n (workspace) => normalizeVi(workspace.name).includes(needle) || normalizeVi(workspace.slug).includes(needle)\n );\n }, [workspaces, query]);\n\n const hasActions = showActions && Boolean(onCreateWorkspace || onWorkspaceSettings || onInviteMembers || onLogout);\n\n if (!currentWorkspace) return null;\n\n const handleSelect = (workspace: Workspace) => {\n setOpen(false);\n setQuery(\"\");\n if (workspace.id !== currentWorkspace.id) onWorkspaceChange?.(workspace.id);\n };\n\n /** Đóng panel rồi mới chạy hành động — tránh panel còn mở đè lên dialog app mở ra sau đó. */\n const closeThen = (action: () => void) => () => {\n setOpen(false);\n action();\n };\n\n const trigger = collapsed ? (\n <button\n type=\"button\"\n aria-label={labels.workspaceSwitcherLabel}\n className={cn(\n \"flex size-9 items-center justify-center rounded-md\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent\",\n className\n )}\n >\n <WorkspaceAvatar workspace={currentWorkspace} />\n </button>\n ) : (\n <button\n type=\"button\"\n aria-label={labels.workspaceSwitcherLabel}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent\",\n className\n )}\n >\n {/*\n `size-8` (32px) thay vì `size-6` (24px): avatar cao BẰNG khối chữ hai dòng bên cạnh\n (17,5px + 13,75px ≈ 31px sau khi cả hai dòng dùng `leading-tight`), nên hàng này đọc ra\n một khối chứ không phải một ô vuông nhỏ lửng lơ cạnh hai dòng chữ.\n ⚠ Chỉ đổi ở trigger dạng ĐẦY ĐỦ. Trigger lúc thanh bên thu gọn vẫn `size-6` trong một nút\n 36px — ở đó avatar là toàn bộ nội dung nên nó cần đệm quanh, không cần lấp đầy.\n */}\n <WorkspaceAvatar workspace={currentWorkspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n {/* `leading-tight` ở CẢ hai dòng: mặc định `text-sm` có line-height 20px và dòng gói\n 11px thừa hưởng line-height của body (~1,5) — cộng lại là một khoảng hở rộng hơn cả\n thân chữ, khiến tên gói trông như không thuộc về tên workspace. */}\n <span className=\"truncate text-sm font-semibold leading-tight text-sidebar-foreground\">\n {currentWorkspace.name}\n </span>\n {currentWorkspace.plan ? (\n // `text-muted-foreground` chứ không `text-sidebar-foreground/55`: đây là CHỮ (11px) nên\n // ngưỡng là AA 4,5:1, mà /55 trên `--sidebar` #FAFAF9 chỉ được 4,13:1 ✗ (dark 5,78:1 —\n // mỗi theme hỏng một nửa). `--muted-foreground` là bậc chữ phụ đã đo cho đúng vai trò:\n // 5,01:1 (light) · 5,35:1 (dark) trên `--sidebar` ✓.\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">{currentWorkspace.plan}</span>\n ) : null}\n </span>\n {/* Mũi tên MANG THÔNG TIN (nút này mở ra một panel chọn) nên chịu ngưỡng 3:1 của WCAG\n 1.4.11, mà `text-sidebar-foreground/45` chỉ được 3,02:1 trên `--sidebar` ở light và\n tụt xuống 3,01:1 khi hover đổi nền sang `--sidebar-accent` — sát/dưới vạch, và ở\n layout \"header-first\" nút này còn nằm trên nền `--background` khác nữa.\n `--muted-foreground` là bậc đã đo và dư biên ở mọi nền nó có thể đứng: 5,01:1 trên\n `--sidebar` · 4,78:1 trên `--sidebar-accent` · 5,23:1 trên `--background` (light);\n 5,35 / 4,86 / 5,70:1 (dark) ✓. (Dark của `/45` là 4,26 / 4,14:1 — chỉ light hỏng,\n nhưng bậc mờ theo alpha luôn lệch nhau như vậy giữa hai theme.) */}\n <ChevronsUpDown className=\"size-3.5 shrink-0 text-muted-foreground\" />\n </button>\n );\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n {collapsed ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n </TooltipTrigger>\n <TooltipContent side=\"right\">{currentWorkspace.name}</TooltipContent>\n </Tooltip>\n ) : (\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n )}\n\n <PopoverContent\n align={align}\n sideOffset={6}\n /*\n * `w-80` (320px), không phải `w-72` (288px). Ở 288px dòng phụ của hàng ĐANG CHỌN bị cắt —\n * đo được thiếu đúng 4px cho \"Quản trị viên · 6 thành viên\". Hàng đang chọn là hàng chật\n * nhất vì nó mang THÊM huy hiệu gói và dấu check, hai thứ mà các hàng khác không có.\n * Không nới vừa đủ 4px: tên vai trò và tên workspace là dữ liệu của app, dài ngắn tuỳ nơi\n * dùng — chọn mức có dư thay vì mức vừa khít với đúng bộ dữ liệu mẫu này.\n */\n className=\"w-80 p-0\"\n onOpenAutoFocus={(event) => {\n // Mặc định Radix focus vào khung panel; có ô tìm thì đưa con trỏ vào đó luôn.\n if (!searchRef.current) return;\n event.preventDefault();\n searchRef.current.focus();\n }}\n >\n {showList ? (\n <>\n {showSearch ? (\n <div className=\"relative border-b border-border p-2\">\n <Search className=\"pointer-events-none absolute left-4 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n ref={searchRef}\n type=\"search\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n placeholder={labels.workspaceSearchPlaceholder}\n aria-label={labels.workspaceSearchPlaceholder}\n className={cn(\n \"h-8 w-full rounded-md border border-input bg-background pl-7 pr-2 text-sm\",\n // Focus dùng `--primary-ink`, KHÔNG `--ring` cũng không `--gold-300`. Cả hai\n // bậc kia ở light đều là gold #E8B824 — chỉ 1,73:1 trên nền trang, tức lúc\n // focus ô tìm MẤT ranh giới (`--ring` chỉ hợp cho vòng focus `ring-*`, thứ có\n // luật riêng, không phải cho viền control). `--primary-ink` giữ đúng sắc gold\n // mà đạt 6,09–6,51:1 (light) · 10,80–12,67:1 (dark) — hợp đồng màu §3.\n \"placeholder:text-muted-foreground focus:border-primary-ink focus:outline-none focus:ring-2 focus:ring-ring/50\"\n )}\n />\n </div>\n ) : null}\n\n {/*\n CỐ Ý không dùng role=\"listbox\"/\"option\": bộ ARIA đó hứa với trình đọc màn hình\n một widget điều khiển bằng ↑/↓/Home/End + roving tabindex + aria-activedescendant,\n mà hành vi thật ở đây chỉ là Tab qua từng nút trong Popover (Popover được chọn\n thay DropdownMenu vì menu của Radix cướp phím gõ của ô tìm kiếm — xem chú thích\n component). Khai đúng cái mình làm còn hơn khai sang trọng rồi lừa người dùng:\n danh sách nút thường + `aria-current` cho mục đang chọn.\n */}\n {/*\n `flex flex-col gap-1`: các hàng là khối `rounded-md` có nền khi hover/đang chọn —\n dính sát nhau thì hai nền bo góc chạm nhau và đọc thành một mảng liền, không ra ba\n mục riêng. 4px khớp đúng `p-1` của khung, nên đệm trong và khoảng cách giữa bằng nhau.\n */}\n <div\n className=\"flex max-h-64 flex-col gap-1 overflow-y-auto scrollbar-thin p-1\"\n role=\"group\"\n aria-label={labels.workspace}\n >\n {filtered.length === 0 ? (\n <p className=\"px-2 py-6 text-center text-sm text-muted-foreground\">{labels.workspaceEmpty}</p>\n ) : (\n filtered.map((workspace) => {\n const isCurrent = workspace.id === currentWorkspace.id;\n return (\n <button\n key={workspace.id}\n type=\"button\"\n aria-current={isCurrent ? \"true\" : undefined}\n onClick={() => handleSelect(workspace)}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-accent\",\n isCurrent && \"bg-accent/60\"\n )}\n >\n {/* Cùng cách xử lý với trigger ở trên: avatar cao bằng khối chữ hai dòng,\n và cả hai dòng `leading-tight` để dòng phụ dính vào tên chứ không trôi\n lửng lơ. Trigger và danh sách là cùng một thứ ở hai chỗ — để hai kiểu\n khác nhau là người dùng thấy giao diện \"nhảy\" khi mở popover. */}\n <WorkspaceAvatar workspace={workspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate text-sm font-medium leading-tight text-foreground\">\n {workspace.name}\n </span>\n {workspace.role || workspace.memberCount !== undefined ? (\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">\n {[\n workspace.role,\n workspace.memberCount === undefined ? undefined : labels.members(workspace.memberCount)\n ]\n .filter(Boolean)\n .join(\" · \")}\n </span>\n ) : null}\n </span>\n {workspace.plan ? (\n <Badge variant=\"secondary\" size=\"sm\" className=\"shrink-0\">\n {workspace.plan}\n </Badge>\n ) : null}\n {/* Ô check LUÔN chiếm chỗ, chỉ ẩn nội dung khi không phải mục đang chọn —\n `invisible` chứ không phải render có điều kiện. Render có điều kiện làm\n badge gói thuê bao của DÒNG ĐANG CHỌN bị đẩy lệch 24px so với các dòng\n khác, nên cột badge không thẳng hàng.\n Cùng thủ pháp với `OptionRow` của `Combobox` trong gói.\n\n Và vì sao KHÔNG bỏ hẳn dấu check: mục đang chọn khi đó chỉ còn `bg-accent/60`\n để nhận biết — ~1,03:1 với nền popover, tức không dấu hiệu nào đạt ngưỡng, và\n trạng thái lại đi bằng MỖI màu (WCAG 1.4.1). `aria-current` chỉ cứu trình đọc\n màn hình, không cứu người nhìn.\n\n KHÔNG dùng `text-app-accent` cho dấu check: accent mặc định (gold #E8B824)\n chỉ đạt 1,86:1 trên nền popover trắng — dưới ngưỡng 3:1 của WCAG 1.4.11 cho\n thành phần phi-văn-bản. `text-popover-foreground` đạt 18,9:1 (light)\n và 16,6:1 (dark). */}\n <Check\n aria-hidden=\"true\"\n className={cn(\"size-4 shrink-0 text-popover-foreground\", !isCurrent && \"invisible\")}\n />\n </button>\n );\n })\n )}\n </div>\n </>\n ) : (\n <div className=\"flex items-center gap-2 p-3\">\n <WorkspaceAvatar workspace={currentWorkspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-semibold text-foreground\">{currentWorkspace.name}</span>\n <span className=\"truncate text-[11px] text-muted-foreground\">\n {currentWorkspace.plan ?? currentWorkspace.slug}\n </span>\n </span>\n </div>\n )}\n\n {hasActions ? (\n <>\n <Separator />\n {/* `gap-1` — cùng 4px với danh sách workspace ở trên và với khoảng cách mục của họ\n menu (`DropdownMenuItem`…). Thiếu nó thì bốn hàng hành động dính liền thành một\n khối, và vùng `hover:bg-accent` của chúng cũng liền một mảng. */}\n <div className=\"flex flex-col gap-1 p-1\">\n {onCreateWorkspace ? (\n <ActionRow icon={Plus} label={labels.createWorkspace} onClick={closeThen(onCreateWorkspace)} />\n ) : null}\n {onWorkspaceSettings ? (\n <ActionRow\n icon={Settings}\n label={labels.workspaceSettings}\n onClick={closeThen(() => onWorkspaceSettings(currentWorkspace.id))}\n />\n ) : null}\n {onInviteMembers ? (\n <ActionRow\n icon={UserPlus}\n label={labels.inviteMembers}\n onClick={closeThen(() => onInviteMembers(currentWorkspace.id))}\n />\n ) : null}\n {onLogout ? (\n <ActionRow icon={LogOut} label={labels.logout} destructive onClick={closeThen(onLogout)} />\n ) : null}\n </div>\n </>\n ) : null}\n </PopoverContent>\n </Popover>\n );\n}\n\n/** Một dòng hành động trong panel (không phải mục chọn workspace). */\nfunction ActionRow({\n icon: Icon = Building2,\n label,\n onClick,\n destructive = false\n}: {\n icon?: typeof Building2;\n label: string;\n onClick: () => void;\n destructive?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm\",\n \"transition-colors duration-150 ease-out\",\n // Bậc ink chứ không `text-destructive`: dòng \"Đăng xuất\" là CHỮ trên nền `--popover`,\n // mà `--destructive` (token NỀN) chỉ được 3,53:1 trên `--popover` ở dark ✗. Ink đạt cả\n // hai: 7,15:1 (light) · 6,42:1 (dark). Icon ở `opacity-70` là thành phần phi-văn-bản\n // (ngưỡng 3:1): 3,69:1 / 3,73:1 lúc nghỉ, và 3,37:1 / 3,62:1 trên nền hover\n // `bg-destructive/10` — đó mới là tổ hợp chặt nhất, vẫn ✓. Chữ lúc hover: 6,18:1 / 5,94:1.\n destructive ? \"text-destructive-ink hover:bg-destructive/10\" : \"text-foreground hover:bg-accent\"\n )}\n >\n <Icon className=\"size-4 shrink-0 opacity-70\" />\n <span className=\"truncate\">{label}</span>\n </button>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/workspace-switcher.tsx"],"sourcesContent":["\"use client\";\n\nimport { Building2, Check, ChevronsUpDown, LogOut, Plus, Search, Settings, UserPlus } from \"lucide-react\";\nimport { useMemo, useRef, useState } from \"react\";\nimport { getAvatarToneClasses } from \"../components/composite/letter-avatar.js\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../components/ui/avatar.js\";\nimport { Badge } from \"../components/ui/badge.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../components/ui/popover.js\";\nimport { Separator } from \"../components/ui/separator.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport type { Workspace } from \"./types.js\";\nimport { getVietnameseInitials, normalizeVi } from \"./utils.js\";\n\n/** Từ số workspace này trở lên thì hiện ô tìm kiếm trong danh sách. */\nconst SEARCH_THRESHOLD = 8;\n\n/**\n * Ảnh đại diện workspace: logo nếu có, không thì ô chữ cái tô màu deterministic.\n *\n * Seed là `workspace.name` chứ KHÔNG phải slug: app hiển thị workspace bằng\n * `<LetterAvatar name={workspace.name} square />` ở chỗ khác, seed lệch là màu lệch.\n */\nfunction WorkspaceAvatar({ workspace, className }: { workspace: Workspace; className?: string }) {\n return (\n <Avatar className={cn(\"size-6 rounded-md\", className)}>\n {workspace.logoUrl ? <AvatarImage src={workspace.logoUrl} alt=\"\" className=\"rounded-md\" /> : null}\n <AvatarFallback className={cn(\"rounded-md text-[10px] font-semibold\", getAvatarToneClasses(workspace.name))}>\n {getVietnameseInitials(workspace.name)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\nexport interface WorkspaceSwitcherProps {\n className?: string;\n /** Sidebar thu gọn → chỉ hiện avatar + tooltip. */\n collapsed?: boolean;\n /** Ẩn khu \"Hành động\" (tạo workspace, cài đặt, mời thành viên, đăng xuất). */\n showActions?: boolean;\n /** Căn panel — mặc định \"start\" (mở xuôi theo mép trái sidebar). */\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * **Trục 1 — đổi không gian làm việc.**\n *\n * Chỉ gọi `onWorkspaceChange(id)`; điều hướng/đổi subdomain là việc của app\n * (đổi workspace kéo theo đổi toàn bộ dữ liệu + quyền nên shell không tự quyết).\n * Có ≤1 workspace thì phần danh sách tự ẩn, chỉ còn nhãn workspace hiện tại.\n *\n * Dùng `Popover` chứ không `DropdownMenu`: menu của Radix có typeahead, gõ vào\n * ô tìm kiếm bên trong sẽ bị nó cướp phím.\n */\nexport function WorkspaceSwitcher({\n className,\n collapsed = false,\n showActions = true,\n align = \"start\"\n}: WorkspaceSwitcherProps) {\n const {\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n onLogout,\n labels\n } = useShell();\n\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const searchRef = useRef<HTMLInputElement>(null);\n\n const showList = workspaces.length > 1;\n const showSearch = workspaces.length >= SEARCH_THRESHOLD;\n\n const filtered = useMemo(() => {\n if (!query.trim()) return workspaces;\n const needle = normalizeVi(query);\n return workspaces.filter(\n (workspace) => normalizeVi(workspace.name).includes(needle) || normalizeVi(workspace.slug).includes(needle)\n );\n }, [workspaces, query]);\n\n const hasActions = showActions && Boolean(onCreateWorkspace || onWorkspaceSettings || onInviteMembers || onLogout);\n\n if (!currentWorkspace) return null;\n\n const handleSelect = (workspace: Workspace) => {\n setOpen(false);\n setQuery(\"\");\n if (workspace.id !== currentWorkspace.id) onWorkspaceChange?.(workspace.id);\n };\n\n /** Đóng panel rồi mới chạy hành động — tránh panel còn mở đè lên dialog app mở ra sau đó. */\n const closeThen = (action: () => void) => () => {\n setOpen(false);\n action();\n };\n\n const trigger = collapsed ? (\n <button\n type=\"button\"\n aria-label={labels.workspaceSwitcherLabel}\n className={cn(\n \"flex size-9 items-center justify-center rounded-md\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent\",\n className\n )}\n >\n <WorkspaceAvatar workspace={currentWorkspace} />\n </button>\n ) : (\n <button\n type=\"button\"\n aria-label={labels.workspaceSwitcherLabel}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent\",\n className\n )}\n >\n {/*\n `size-8` (32px) thay vì `size-6` (24px): avatar cao BẰNG khối chữ hai dòng bên cạnh\n (17,5px + 13,75px ≈ 31px sau khi cả hai dòng dùng `leading-tight`), nên hàng này đọc ra\n một khối chứ không phải một ô vuông nhỏ lửng lơ cạnh hai dòng chữ.\n ⚠ Chỉ đổi ở trigger dạng ĐẦY ĐỦ. Trigger lúc thanh bên thu gọn vẫn `size-6` trong một nút\n 36px — ở đó avatar là toàn bộ nội dung nên nó cần đệm quanh, không cần lấp đầy.\n */}\n <WorkspaceAvatar workspace={currentWorkspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n {/* `leading-tight` ở CẢ hai dòng: mặc định `text-sm` có line-height 20px và dòng gói\n 11px thừa hưởng line-height của body (~1,5) — cộng lại là một khoảng hở rộng hơn cả\n thân chữ, khiến tên gói trông như không thuộc về tên workspace. */}\n <span className=\"truncate text-sm font-semibold leading-tight text-sidebar-foreground\">\n {currentWorkspace.name}\n </span>\n {currentWorkspace.plan ? (\n // `text-muted-foreground` chứ không `text-sidebar-foreground/55`: đây là CHỮ (11px) nên\n // ngưỡng là AA 4,5:1, mà /55 trên `--sidebar` #FAFAF9 chỉ được 4,13:1 ✗ (dark 5,78:1 —\n // mỗi theme hỏng một nửa). `--muted-foreground` là bậc chữ phụ đã đo cho đúng vai trò:\n // 5,01:1 (light) · 5,35:1 (dark) trên `--sidebar` ✓.\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">{currentWorkspace.plan}</span>\n ) : null}\n </span>\n {/* Mũi tên MANG THÔNG TIN (nút này mở ra một panel chọn) nên chịu ngưỡng 3:1 của WCAG\n 1.4.11, mà `text-sidebar-foreground/45` chỉ được 3,02:1 trên `--sidebar` ở light và\n tụt xuống 3,01:1 khi hover đổi nền sang `--sidebar-accent` — sát/dưới vạch, và ở\n layout \"header-first\" nút này còn nằm trên nền `--background` khác nữa.\n `--muted-foreground` là bậc đã đo và dư biên ở mọi nền nó có thể đứng: 5,01:1 trên\n `--sidebar` · 4,78:1 trên `--sidebar-accent` · 5,23:1 trên `--background` (light);\n 5,35 / 4,86 / 5,70:1 (dark) ✓. (Dark của `/45` là 4,26 / 4,14:1 — chỉ light hỏng,\n nhưng bậc mờ theo alpha luôn lệch nhau như vậy giữa hai theme.) */}\n <ChevronsUpDown className=\"size-3.5 shrink-0 text-muted-foreground\" />\n </button>\n );\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n {collapsed ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n </TooltipTrigger>\n <TooltipContent side=\"right\">{currentWorkspace.name}</TooltipContent>\n </Tooltip>\n ) : (\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n )}\n\n <PopoverContent\n align={align}\n sideOffset={6}\n /*\n * `w-80` (320px), không phải `w-72` (288px). Ở 288px dòng phụ của hàng ĐANG CHỌN bị cắt —\n * đo được thiếu đúng 4px cho \"Quản trị viên · 6 thành viên\". Hàng đang chọn là hàng chật\n * nhất vì nó mang THÊM huy hiệu gói và dấu check, hai thứ mà các hàng khác không có.\n * Không nới vừa đủ 4px: tên vai trò và tên workspace là dữ liệu của app, dài ngắn tuỳ nơi\n * dùng — chọn mức có dư thay vì mức vừa khít với đúng bộ dữ liệu mẫu này.\n */\n className=\"w-80 p-0\"\n onOpenAutoFocus={(event) => {\n // Mặc định Radix focus vào khung panel; có ô tìm thì đưa con trỏ vào đó luôn.\n if (!searchRef.current) return;\n event.preventDefault();\n searchRef.current.focus();\n }}\n >\n {showList ? (\n <>\n {showSearch ? (\n <div className=\"relative border-b border-border p-2\">\n <Search className=\"pointer-events-none absolute left-4 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n ref={searchRef}\n type=\"search\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n placeholder={labels.workspaceSearchPlaceholder}\n aria-label={labels.workspaceSearchPlaceholder}\n className={cn(\n \"h-8 w-full rounded-md border border-input bg-background pl-7 pr-2 text-sm\",\n // Focus dùng `--primary-ink`, KHÔNG `--ring` cũng không `--gold-300`. Cả hai\n // bậc kia ở light đều là gold #E8B824 — chỉ 1,73:1 trên nền trang, tức lúc\n // focus ô tìm MẤT ranh giới (`--ring` chỉ hợp cho vòng focus `ring-*`, thứ có\n // luật riêng, không phải cho viền control). `--primary-ink` giữ đúng sắc gold\n // mà đạt 6,09–6,51:1 (light) · 10,80–12,67:1 (dark) — hợp đồng màu §3.\n \"placeholder:text-muted-foreground focus:border-primary-ink focus:outline-none focus:ring-2 focus:ring-ring/50\"\n )}\n />\n </div>\n ) : null}\n\n {/*\n CỐ Ý không dùng role=\"listbox\"/\"option\": bộ ARIA đó hứa với trình đọc màn hình\n một widget điều khiển bằng ↑/↓/Home/End + roving tabindex + aria-activedescendant,\n mà hành vi thật ở đây chỉ là Tab qua từng nút trong Popover (Popover được chọn\n thay DropdownMenu vì menu của Radix cướp phím gõ của ô tìm kiếm — xem chú thích\n component). Khai đúng cái mình làm còn hơn khai sang trọng rồi lừa người dùng:\n danh sách nút thường + `aria-current` cho mục đang chọn.\n */}\n {/*\n `flex flex-col gap-1`: các hàng là khối `rounded-md` có nền khi hover/đang chọn —\n dính sát nhau thì hai nền bo góc chạm nhau và đọc thành một mảng liền, không ra ba\n mục riêng. 4px khớp đúng `p-1` của khung, nên đệm trong và khoảng cách giữa bằng nhau.\n */}\n <div\n className=\"flex max-h-64 flex-col gap-1 overflow-y-auto scrollbar-thin p-1\"\n role=\"group\"\n aria-label={labels.workspace}\n >\n {filtered.length === 0 ? (\n <p className=\"px-2 py-6 text-center text-sm text-muted-foreground\">{labels.workspaceEmpty}</p>\n ) : (\n filtered.map((workspace) => {\n const isCurrent = workspace.id === currentWorkspace.id;\n return (\n <button\n key={workspace.id}\n type=\"button\"\n aria-current={isCurrent ? \"true\" : undefined}\n onClick={() => handleSelect(workspace)}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-accent\",\n isCurrent && \"bg-accent/60\"\n )}\n >\n {/* Cùng cách xử lý với trigger ở trên: avatar cao bằng khối chữ hai dòng,\n và cả hai dòng `leading-tight` để dòng phụ dính vào tên chứ không trôi\n lửng lơ. Trigger và danh sách là cùng một thứ ở hai chỗ — để hai kiểu\n khác nhau là người dùng thấy giao diện \"nhảy\" khi mở popover. */}\n <WorkspaceAvatar workspace={workspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate text-sm font-medium leading-tight text-foreground\">\n {workspace.name}\n </span>\n {workspace.role || workspace.memberCount !== undefined ? (\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">\n {[\n workspace.role,\n workspace.memberCount === undefined ? undefined : labels.members(workspace.memberCount)\n ]\n .filter(Boolean)\n .join(\" · \")}\n </span>\n ) : null}\n </span>\n {workspace.plan ? (\n <Badge variant=\"secondary\" size=\"sm\" className=\"shrink-0\">\n {workspace.plan}\n </Badge>\n ) : null}\n {/* Ô check LUÔN chiếm chỗ, chỉ ẩn nội dung khi không phải mục đang chọn —\n `invisible` chứ không phải render có điều kiện. Render có điều kiện làm\n badge gói thuê bao của DÒNG ĐANG CHỌN bị đẩy lệch 24px so với các dòng\n khác, nên cột badge không thẳng hàng.\n Cùng thủ pháp với `OptionRow` của `Combobox` trong gói.\n\n Và vì sao KHÔNG bỏ hẳn dấu check: mục đang chọn khi đó chỉ còn `bg-accent/60`\n để nhận biết — ~1,03:1 với nền popover, tức không dấu hiệu nào đạt ngưỡng, và\n trạng thái lại đi bằng MỖI màu (WCAG 1.4.1). `aria-current` chỉ cứu trình đọc\n màn hình, không cứu người nhìn.\n\n KHÔNG dùng `text-app-accent` cho dấu check: accent mặc định (gold #E8B824)\n chỉ đạt 1,86:1 trên nền popover trắng — dưới ngưỡng 3:1 của WCAG 1.4.11 cho\n thành phần phi-văn-bản. `text-popover-foreground` đạt 18,9:1 (light)\n và 16,6:1 (dark). */}\n <Check\n aria-hidden=\"true\"\n className={cn(\"size-4 shrink-0 text-popover-foreground\", !isCurrent && \"invisible\")}\n />\n </button>\n );\n })\n )}\n </div>\n </>\n ) : (\n <div className=\"flex items-center gap-2 p-3\">\n <WorkspaceAvatar workspace={currentWorkspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-semibold text-foreground\">{currentWorkspace.name}</span>\n <span className=\"truncate text-[11px] text-muted-foreground\">\n {currentWorkspace.plan ?? currentWorkspace.slug}\n </span>\n </span>\n </div>\n )}\n\n {hasActions ? (\n <>\n <Separator />\n {/* `gap-1` — cùng 4px với danh sách workspace ở trên và với khoảng cách mục của họ\n menu (`DropdownMenuItem`…). Thiếu nó thì bốn hàng hành động dính liền thành một\n khối, và vùng `hover:bg-accent` của chúng cũng liền một mảng. */}\n <div className=\"flex flex-col gap-1 p-1\">\n {onCreateWorkspace ? (\n <ActionRow icon={Plus} label={labels.createWorkspace} onClick={closeThen(onCreateWorkspace)} />\n ) : null}\n {onWorkspaceSettings ? (\n <ActionRow\n icon={Settings}\n label={labels.workspaceSettings}\n onClick={closeThen(() => onWorkspaceSettings(currentWorkspace.id))}\n />\n ) : null}\n {onInviteMembers ? (\n <ActionRow\n icon={UserPlus}\n label={labels.inviteMembers}\n onClick={closeThen(() => onInviteMembers(currentWorkspace.id))}\n />\n ) : null}\n {onLogout ? (\n <ActionRow icon={LogOut} label={labels.logout} destructive onClick={closeThen(onLogout)} />\n ) : null}\n </div>\n </>\n ) : null}\n </PopoverContent>\n </Popover>\n );\n}\n\n/** Một dòng hành động trong panel (không phải mục chọn workspace). */\nfunction ActionRow({\n icon: Icon = Building2,\n label,\n onClick,\n destructive = false\n}: {\n icon?: typeof Building2;\n label: string;\n onClick: () => void;\n destructive?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm\",\n \"transition-colors duration-150 ease-out\",\n // Bậc ink chứ không `text-destructive`: dòng đăng xuất là CHỮ trên nền `--popover`,\n // mà `--destructive` (token NỀN) chỉ được 3,53:1 trên `--popover` ở dark ✗. Ink đạt cả\n // hai: 7,15:1 (light) · 6,42:1 (dark). Icon ở `opacity-70` là thành phần phi-văn-bản\n // (ngưỡng 3:1): 3,69:1 / 3,73:1 lúc nghỉ, và 3,37:1 / 3,62:1 trên nền hover\n // `bg-destructive/10` — đó mới là tổ hợp chặt nhất, vẫn ✓. Chữ lúc hover: 6,18:1 / 5,94:1.\n destructive ? \"text-destructive-ink hover:bg-destructive/10\" : \"text-foreground hover:bg-accent\"\n )}\n >\n <Icon className=\"size-4 shrink-0 opacity-70\" />\n <span className=\"truncate\">{label}</span>\n </button>\n );\n}\n"],"mappings":";AA0BI,SAsKM,UArKiB,KADvB;AAxBJ,SAAS,WAAW,OAAO,gBAAgB,QAAQ,MAAM,QAAQ,UAAU,gBAAgB;AAC3F,SAAS,SAAS,QAAQ,gBAAgB;AAC1C,SAAS,4BAA4B;AACrC,SAAS,QAAQ,gBAAgB,mBAAmB;AACpD,SAAS,aAAa;AACtB,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,iBAAiB;AAC1B,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,UAAU;AACnB,SAAS,gBAAgB;AAEzB,SAAS,uBAAuB,mBAAmB;AAGnD,MAAM,mBAAmB;AAQzB,SAAS,gBAAgB,EAAE,WAAW,UAAU,GAAiD;AAC/F,SACE,qBAAC,UAAO,WAAW,GAAG,qBAAqB,SAAS,GACjD;AAAA,cAAU,UAAU,oBAAC,eAAY,KAAK,UAAU,SAAS,KAAI,IAAG,WAAU,cAAa,IAAK;AAAA,IAC7F,oBAAC,kBAAe,WAAW,GAAG,wCAAwC,qBAAqB,UAAU,IAAI,CAAC,GACvG,gCAAsB,UAAU,IAAI,GACvC;AAAA,KACF;AAEJ;AAsBO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,QAAQ;AACV,GAA2B;AACzB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,SAAS;AAEb,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,YAAY,OAAyB,IAAI;AAE/C,QAAM,WAAW,WAAW,SAAS;AACrC,QAAM,aAAa,WAAW,UAAU;AAExC,QAAM,WAAW,QAAQ,MAAM;AAC7B,QAAI,CAAC,MAAM,KAAK,EAAG,QAAO;AAC1B,UAAM,SAAS,YAAY,KAAK;AAChC,WAAO,WAAW;AAAA,MAChB,CAAC,cAAc,YAAY,UAAU,IAAI,EAAE,SAAS,MAAM,KAAK,YAAY,UAAU,IAAI,EAAE,SAAS,MAAM;AAAA,IAC5G;AAAA,EACF,GAAG,CAAC,YAAY,KAAK,CAAC;AAEtB,QAAM,aAAa,eAAe,QAAQ,qBAAqB,uBAAuB,mBAAmB,QAAQ;AAEjH,MAAI,CAAC,iBAAkB,QAAO;AAE9B,QAAM,eAAe,CAAC,cAAyB;AAC7C,YAAQ,KAAK;AACb,aAAS,EAAE;AACX,QAAI,UAAU,OAAO,iBAAiB,GAAI,qBAAoB,UAAU,EAAE;AAAA,EAC5E;AAGA,QAAM,YAAY,CAAC,WAAuB,MAAM;AAC9C,YAAQ,KAAK;AACb,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,YACd;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY,OAAO;AAAA,MACnB,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MAEA,8BAAC,mBAAgB,WAAW,kBAAkB;AAAA;AAAA,EAChD,IAEA;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY,OAAO;AAAA,MACnB,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MASA;AAAA,4BAAC,mBAAgB,WAAW,kBAAkB,WAAU,UAAS;AAAA,QACjE,qBAAC,UAAK,WAAU,gCAId;AAAA,8BAAC,UAAK,WAAU,wEACb,2BAAiB,MACpB;AAAA,UACC,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA,YAKhB,oBAAC,UAAK,WAAU,4DAA4D,2BAAiB,MAAK;AAAA,cAChG;AAAA,WACN;AAAA,QASA,oBAAC,kBAAe,WAAU,2CAA0C;AAAA;AAAA;AAAA,EACtE;AAGF,SACE,qBAAC,WAAQ,MAAY,cAAc,SAChC;AAAA,gBACC,qBAAC,WACC;AAAA,0BAAC,kBAAe,SAAO,MACrB,8BAAC,kBAAe,SAAO,MAAE,mBAAQ,GACnC;AAAA,MACA,oBAAC,kBAAe,MAAK,SAAS,2BAAiB,MAAK;AAAA,OACtD,IAEA,oBAAC,kBAAe,SAAO,MAAE,mBAAQ;AAAA,IAGnC;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,YAAY;AAAA,QAQZ,WAAU;AAAA,QACV,iBAAiB,CAAC,UAAU;AAE1B,cAAI,CAAC,UAAU,QAAS;AACxB,gBAAM,eAAe;AACrB,oBAAU,QAAQ,MAAM;AAAA,QAC1B;AAAA,QAEC;AAAA,qBACC,iCACG;AAAA,yBACC,qBAAC,SAAI,WAAU,uCACb;AAAA,kCAAC,UAAO,WAAU,+FAA8F;AAAA,cAChH;AAAA,gBAAC;AAAA;AAAA,kBACC,KAAK;AAAA,kBACL,MAAK;AAAA,kBACL,OAAO;AAAA,kBACP,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK;AAAA,kBAChD,aAAa,OAAO;AAAA,kBACpB,cAAY,OAAO;AAAA,kBACnB,WAAW;AAAA,oBACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAMA;AAAA,kBACF;AAAA;AAAA,cACF;AAAA,eACF,IACE;AAAA,YAeJ;AAAA,cAAC;AAAA;AAAA,gBACC,WAAU;AAAA,gBACV,MAAK;AAAA,gBACL,cAAY,OAAO;AAAA,gBAElB,mBAAS,WAAW,IACnB,oBAAC,OAAE,WAAU,uDAAuD,iBAAO,gBAAe,IAE1F,SAAS,IAAI,CAAC,cAAc;AAC1B,wBAAM,YAAY,UAAU,OAAO,iBAAiB;AACpD,yBACE;AAAA,oBAAC;AAAA;AAAA,sBAEC,MAAK;AAAA,sBACL,gBAAc,YAAY,SAAS;AAAA,sBACnC,SAAS,MAAM,aAAa,SAAS;AAAA,sBACrC,WAAW;AAAA,wBACT;AAAA,wBACA;AAAA,wBACA,aAAa;AAAA,sBACf;AAAA,sBAMA;AAAA,4CAAC,mBAAgB,WAAsB,WAAU,UAAS;AAAA,wBAC1D,qBAAC,UAAK,WAAU,gCACd;AAAA,8CAAC,UAAK,WAAU,8DACb,oBAAU,MACb;AAAA,0BACC,UAAU,QAAQ,UAAU,gBAAgB,SAC3C,oBAAC,UAAK,WAAU,4DACb;AAAA,4BACC,UAAU;AAAA,4BACV,UAAU,gBAAgB,SAAY,SAAY,OAAO,QAAQ,UAAU,WAAW;AAAA,0BACxF,EACG,OAAO,OAAO,EACd,KAAK,QAAK,GACf,IACE;AAAA,2BACN;AAAA,wBACC,UAAU,OACT,oBAAC,SAAM,SAAQ,aAAY,MAAK,MAAK,WAAU,YAC5C,oBAAU,MACb,IACE;AAAA,wBAgBJ;AAAA,0BAAC;AAAA;AAAA,4BACC,eAAY;AAAA,4BACZ,WAAW,GAAG,2CAA2C,CAAC,aAAa,WAAW;AAAA;AAAA,wBACpF;AAAA;AAAA;AAAA,oBArDK,UAAU;AAAA,kBAsDjB;AAAA,gBAEJ,CAAC;AAAA;AAAA,YAEL;AAAA,aACF,IAEA,qBAAC,SAAI,WAAU,+BACb;AAAA,gCAAC,mBAAgB,WAAW,kBAAkB,WAAU,UAAS;AAAA,YACjE,qBAAC,UAAK,WAAU,yBACd;AAAA,kCAAC,UAAK,WAAU,kDAAkD,2BAAiB,MAAK;AAAA,cACxF,oBAAC,UAAK,WAAU,8CACb,2BAAiB,QAAQ,iBAAiB,MAC7C;AAAA,eACF;AAAA,aACF;AAAA,UAGD,aACC,iCACE;AAAA,gCAAC,aAAU;AAAA,YAIX,qBAAC,SAAI,WAAU,2BACZ;AAAA,kCACC,oBAAC,aAAU,MAAM,MAAM,OAAO,OAAO,iBAAiB,SAAS,UAAU,iBAAiB,GAAG,IAC3F;AAAA,cACH,sBACC;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAM;AAAA,kBACN,OAAO,OAAO;AAAA,kBACd,SAAS,UAAU,MAAM,oBAAoB,iBAAiB,EAAE,CAAC;AAAA;AAAA,cACnE,IACE;AAAA,cACH,kBACC;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAM;AAAA,kBACN,OAAO,OAAO;AAAA,kBACd,SAAS,UAAU,MAAM,gBAAgB,iBAAiB,EAAE,CAAC;AAAA;AAAA,cAC/D,IACE;AAAA,cACH,WACC,oBAAC,aAAU,MAAM,QAAQ,OAAO,OAAO,QAAQ,aAAW,MAAC,SAAS,UAAU,QAAQ,GAAG,IACvF;AAAA,eACN;AAAA,aACF,IACE;AAAA;AAAA;AAAA,IACN;AAAA,KACF;AAEJ;AAGA,SAAS,UAAU;AAAA,EACjB,MAAM,OAAO;AAAA,EACb;AAAA,EACA;AAAA,EACA,cAAc;AAChB,GAKG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAMA,cAAc,iDAAiD;AAAA,MACjE;AAAA,MAEA;AAAA,4BAAC,QAAK,WAAU,8BAA6B;AAAA,QAC7C,oBAAC,UAAK,WAAU,YAAY,iBAAM;AAAA;AAAA;AAAA,EACpC;AAEJ;","names":[]}
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* ⚠ `verify:directives` chặn việc khai hằng dữ liệu thuần trong một file `"use client"`: làm vậy
|
|
5
5
|
* thì mọi chỗ import hằng đó (kể cả Server Component chỉ cần đọc một chuỗi) bị kéo sang phía
|
|
6
6
|
* client. Đó là lý do file này tách ra, cùng khuôn với `data-table-constants.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Chữ theo `ComitorLocaleProvider` (namespace `fileUpload` / `imageUploadField`, mặc định tiếng
|
|
9
|
+
* Anh, bảng tiếng Việt dựng sẵn); override từng khoá qua prop `labels`.
|
|
7
10
|
*/
|
|
8
11
|
/**
|
|
9
12
|
* Hai chuỗi mà phần MÁY MÓC dùng — `useFileUpload` chỉ nhận đúng chúng.
|
|
@@ -30,6 +33,11 @@ interface FileUploadLabels extends FileUploadLogicLabels {
|
|
|
30
33
|
/** Nhãn nút và `aria-label` của thanh tiến độ khi đang tải. */
|
|
31
34
|
uploading: string;
|
|
32
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Nhãn TIẾNG ANH mặc định của `FileUpload` (từ `3.0.0`; trước đó là tiếng Việt). Bảng tiếng Việt:
|
|
38
|
+
* `comitorMessagesVi.fileUpload`. Component (và `useFileUpload`) không đọc thẳng hằng này — chúng
|
|
39
|
+
* đọc bảng của `ComitorLocaleProvider`; hằng ở đây để app trải ra khi tự dựng nhãn.
|
|
40
|
+
*/
|
|
33
41
|
declare const DEFAULT_FILE_UPLOAD_LABELS: FileUploadLabels;
|
|
34
42
|
/**
|
|
35
43
|
* Nhãn của `ImageUploadField` — ô ảnh đại diện / logo.
|
|
@@ -49,6 +57,11 @@ interface ImageUploadFieldLabels extends FileUploadLogicLabels {
|
|
|
49
57
|
/** Chữ hiện đè lên khối khi đang rê tệp qua. */
|
|
50
58
|
dropPrompt: string;
|
|
51
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Nhãn TIẾNG ANH mặc định của `ImageUploadField` (từ `3.0.0`; trước đó là tiếng Việt). Bảng tiếng
|
|
62
|
+
* Việt: `comitorMessagesVi.imageUploadField`. Component không đọc thẳng hằng này — nó đọc bảng của
|
|
63
|
+
* `ComitorLocaleProvider`; hằng ở đây để app trải ra khi tự dựng nhãn.
|
|
64
|
+
*/
|
|
52
65
|
declare const DEFAULT_IMAGE_UPLOAD_FIELD_LABELS: ImageUploadFieldLabels;
|
|
53
66
|
|
|
54
67
|
export { DEFAULT_FILE_UPLOAD_LABELS, DEFAULT_IMAGE_UPLOAD_FIELD_LABELS, type FileUploadLabels, type FileUploadLogicLabels, type ImageUploadFieldLabels };
|
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
uploadFailed: "Kh\xF4ng t\u1EA3i l\xEAn \u0111\u01B0\u1EE3c. Th\u1EED l\u1EA1i.",
|
|
6
|
-
missingDependency: "Ch\u1EE9c n\u0103ng t\u1EA3i t\u1EC7p ch\u01B0a s\u1EB5n s\xE0ng: thi\u1EBFu g\xF3i @uppy/core v\xE0 @uppy/xhr-upload."
|
|
7
|
-
};
|
|
8
|
-
const DEFAULT_IMAGE_UPLOAD_FIELD_LABELS = {
|
|
9
|
-
choose: "T\u1EA3i l\xEAn",
|
|
10
|
-
uploading: "\u0110ang t\u1EA3i l\xEAn\u2026",
|
|
11
|
-
remove: "G\u1EE1",
|
|
12
|
-
hint: "K\xE9o \u1EA3nh v\xE0o \u0111\xE2y ho\u1EB7c b\u1EA5m T\u1EA3i l\xEAn.",
|
|
13
|
-
dropPrompt: "Th\u1EA3 \u1EA3nh v\xE0o \u0111\xE2y",
|
|
14
|
-
uploadFailed: "Kh\xF4ng t\u1EA3i l\xEAn \u0111\u01B0\u1EE3c. Th\u1EED l\u1EA1i.",
|
|
15
|
-
missingDependency: "Ch\u1EE9c n\u0103ng t\u1EA3i t\u1EC7p ch\u01B0a s\u1EB5n s\xE0ng: thi\u1EBFu g\xF3i @uppy/core v\xE0 @uppy/xhr-upload."
|
|
16
|
-
};
|
|
1
|
+
import { fileUploadEn } from "../i18n/namespaces/file-upload.js";
|
|
2
|
+
import { imageUploadFieldEn } from "../i18n/namespaces/image-upload-field.js";
|
|
3
|
+
const DEFAULT_FILE_UPLOAD_LABELS = { ...fileUploadEn };
|
|
4
|
+
const DEFAULT_IMAGE_UPLOAD_FIELD_LABELS = { ...imageUploadFieldEn };
|
|
17
5
|
export {
|
|
18
6
|
DEFAULT_FILE_UPLOAD_LABELS,
|
|
19
7
|
DEFAULT_IMAGE_UPLOAD_FIELD_LABELS
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/uploader/file-upload-constants.ts"],"sourcesContent":["/**\n * Nhãn của `FileUpload` — file RIÊNG, không `\"use client\"`.\n *\n * ⚠ `verify:directives` chặn việc khai hằng dữ liệu thuần trong một file `\"use client\"`: làm vậy\n * thì mọi chỗ import hằng đó (kể cả Server Component chỉ cần đọc một chuỗi) bị kéo sang phía\n * client. Đó là lý do file này tách ra, cùng khuôn với `data-table-constants.ts`.\n */\n\n/**\n * Hai chuỗi mà phần MÁY MÓC dùng — `useFileUpload` chỉ nhận đúng chúng.\n *\n * Tách ra vì ba chuỗi còn lại của `FileUploadLabels` thuần bố cục: để chúng lọt vào chữ ký của hook\n * là mời người viết truyền một prop không bao giờ chạy, và kiểu thì không báo gì.\n */\nexport interface FileUploadLogicLabels {\n /** Thông điệp dự phòng khi endpoint không trả `error.code` đọc được. */\n uploadFailed: string;\n /**\n * Khi `@uppy/core` / `@uppy/xhr-upload` chưa được cài.\n *\n * Chúng là peer TUỲ CHỌN nên `import()` sẽ hỏng ở đúng lúc người dùng chọn tệp. Không có câu này\n * thì thứ họ thấy là một `ChunkLoadError`, và người sửa đi tìm nguyên nhân ở webpack.\n */\n missingDependency: string;\n}\n\nexport interface FileUploadLabels extends FileUploadLogicLabels {\n /** Chữ trong vùng kéo thả. */\n dropHint: string;\n /** Nhãn nút mở hộp chọn tệp. */\n choose: string;\n /** Nhãn nút và `aria-label` của thanh tiến độ khi đang tải. */\n uploading: string;\n}\n\
|
|
1
|
+
{"version":3,"sources":["../../src/uploader/file-upload-constants.ts"],"sourcesContent":["/**\n * Nhãn của `FileUpload` — file RIÊNG, không `\"use client\"`.\n *\n * ⚠ `verify:directives` chặn việc khai hằng dữ liệu thuần trong một file `\"use client\"`: làm vậy\n * thì mọi chỗ import hằng đó (kể cả Server Component chỉ cần đọc một chuỗi) bị kéo sang phía\n * client. Đó là lý do file này tách ra, cùng khuôn với `data-table-constants.ts`.\n *\n * Chữ theo `ComitorLocaleProvider` (namespace `fileUpload` / `imageUploadField`, mặc định tiếng\n * Anh, bảng tiếng Việt dựng sẵn); override từng khoá qua prop `labels`.\n */\n\nimport { fileUploadEn } from \"../i18n/namespaces/file-upload.js\";\nimport { imageUploadFieldEn } from \"../i18n/namespaces/image-upload-field.js\";\n\n/**\n * Hai chuỗi mà phần MÁY MÓC dùng — `useFileUpload` chỉ nhận đúng chúng.\n *\n * Tách ra vì ba chuỗi còn lại của `FileUploadLabels` thuần bố cục: để chúng lọt vào chữ ký của hook\n * là mời người viết truyền một prop không bao giờ chạy, và kiểu thì không báo gì.\n */\nexport interface FileUploadLogicLabels {\n /** Thông điệp dự phòng khi endpoint không trả `error.code` đọc được. */\n uploadFailed: string;\n /**\n * Khi `@uppy/core` / `@uppy/xhr-upload` chưa được cài.\n *\n * Chúng là peer TUỲ CHỌN nên `import()` sẽ hỏng ở đúng lúc người dùng chọn tệp. Không có câu này\n * thì thứ họ thấy là một `ChunkLoadError`, và người sửa đi tìm nguyên nhân ở webpack.\n */\n missingDependency: string;\n}\n\nexport interface FileUploadLabels extends FileUploadLogicLabels {\n /** Chữ trong vùng kéo thả. */\n dropHint: string;\n /** Nhãn nút mở hộp chọn tệp. */\n choose: string;\n /** Nhãn nút và `aria-label` của thanh tiến độ khi đang tải. */\n uploading: string;\n}\n\n/**\n * Nhãn TIẾNG ANH mặc định của `FileUpload` (từ `3.0.0`; trước đó là tiếng Việt). Bảng tiếng Việt:\n * `comitorMessagesVi.fileUpload`. Component (và `useFileUpload`) không đọc thẳng hằng này — chúng\n * đọc bảng của `ComitorLocaleProvider`; hằng ở đây để app trải ra khi tự dựng nhãn.\n */\nexport const DEFAULT_FILE_UPLOAD_LABELS: FileUploadLabels = { ...fileUploadEn };\n\n/**\n * Nhãn của `ImageUploadField` — ô ảnh đại diện / logo.\n *\n * ⚠ `hint` là chỗ DUY NHẤT người dùng học được rằng kéo-thả chạy được, vì bố cục này KHÔNG có khung\n * đứt nét thường trực để tự nói ra điều đó. Đừng rút gọn nó thành mỗi giới hạn dung lượng.\n */\nexport interface ImageUploadFieldLabels extends FileUploadLogicLabels {\n /** Nhãn nút mở hộp chọn tệp. */\n choose: string;\n /** Nhãn nút lúc đang tải. */\n uploading: string;\n /** Nhãn nút gỡ ảnh. */\n remove: string;\n /** Dòng chú thích dưới hai nút — nói cả giới hạn LẪN việc kéo-thả được. */\n hint: string;\n /** Chữ hiện đè lên khối khi đang rê tệp qua. */\n dropPrompt: string;\n}\n\n/**\n * Nhãn TIẾNG ANH mặc định của `ImageUploadField` (từ `3.0.0`; trước đó là tiếng Việt). Bảng tiếng\n * Việt: `comitorMessagesVi.imageUploadField`. Component không đọc thẳng hằng này — nó đọc bảng của\n * `ComitorLocaleProvider`; hằng ở đây để app trải ra khi tự dựng nhãn.\n */\nexport const DEFAULT_IMAGE_UPLOAD_FIELD_LABELS: ImageUploadFieldLabels = { ...imageUploadFieldEn };\n"],"mappings":"AAWA,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AAkC5B,MAAM,6BAA+C,EAAE,GAAG,aAAa;AA0BvE,MAAM,oCAA4D,EAAE,GAAG,mBAAmB;","names":[]}
|
|
@@ -55,7 +55,10 @@ interface FileUploadProps<T = unknown> {
|
|
|
55
55
|
status?: number;
|
|
56
56
|
message: string;
|
|
57
57
|
}) => void;
|
|
58
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Nhãn hiển thị. Không truyền thì theo namespace `fileUpload` của `ComitorLocaleProvider`
|
|
60
|
+
* (không có provider → tiếng Anh); truyền `Partial<>` để đổi từng chuỗi.
|
|
61
|
+
*/
|
|
59
62
|
labels?: Partial<FileUploadLabels>;
|
|
60
63
|
disabled?: boolean;
|
|
61
64
|
className?: string;
|
|
@@ -3,8 +3,8 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useId } from "react";
|
|
4
4
|
import { Button } from "../components/ui/button.js";
|
|
5
5
|
import { Progress } from "../components/ui/progress.js";
|
|
6
|
+
import { useComitorLabels } from "../i18n/locale-provider.js";
|
|
6
7
|
import { cn } from "../lib/cn.js";
|
|
7
|
-
import { DEFAULT_FILE_UPLOAD_LABELS } from "./file-upload-constants.js";
|
|
8
8
|
import { useFileUpload } from "./use-file-upload.js";
|
|
9
9
|
function FileUpload({
|
|
10
10
|
endpoint,
|
|
@@ -18,7 +18,7 @@ function FileUpload({
|
|
|
18
18
|
disabled = false,
|
|
19
19
|
className
|
|
20
20
|
}) {
|
|
21
|
-
const text =
|
|
21
|
+
const text = useComitorLabels("fileUpload", labels);
|
|
22
22
|
const inputId = useId();
|
|
23
23
|
const upload = useFileUpload({
|
|
24
24
|
endpoint,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/uploader/file-upload.tsx"],"sourcesContent":["\"use client\";\n\nimport { useId } from \"react\";\nimport { Button } from \"../components/ui/button.js\";\nimport { Progress } from \"../components/ui/progress.js\";\nimport { cn } from \"../lib/cn.js\";\nimport {
|
|
1
|
+
{"version":3,"sources":["../../src/uploader/file-upload.tsx"],"sourcesContent":["\"use client\";\n\nimport { useId } from \"react\";\nimport { Button } from \"../components/ui/button.js\";\nimport { Progress } from \"../components/ui/progress.js\";\nimport { useComitorLabels } from \"../i18n/locale-provider.js\";\nimport { cn } from \"../lib/cn.js\";\nimport type { FileUploadLabels } from \"./file-upload-constants.js\";\nimport { type FileUploadResult, useFileUpload } from \"./use-file-upload.js\";\n\n/**\n * Ô tải tệp dùng chung cho mọi app Comitor — **Uppy chạy HEADLESS, giao diện là của gói này**.\n *\n * ── VÌ SAO KHÔNG DÙNG `@uppy/dashboard` HAY `@uppy/react` ──────────────────────────────────\n * Cả hai đều kéo theo **Preact** và **một bộ CSS riêng**, tức app đã có React lại ship thêm một\n * runtime giao diện thứ hai, và bảng màu của Uppy không đi qua token của Comitor nên nó KHÔNG qua\n * được cổng tương phản (`pnpm lint:a11y`).\n *\n * Đo được trên một cây đã cài Uppy 5: `@uppy/dashboard` 1.236 KB, `@uppy/react` 432 KB, `preact`\n * 1.852 KB trên đĩa. Trong khi `@uppy/core` khai `sideEffects: [\"*.css\"]` và chỉ có `UIPlugin.js`\n * import `preact` — nên **chỉ nạp `Uppy` + `XHRUpload` thì bundler cắt được cả nhánh Preact**.\n *\n * Cái ta giữ lại của Uppy là phần đáng giá và khó viết đúng: hàng đợi, tiến độ theo byte, thử lại,\n * huỷ, và bộ luật hạn chế (kiểu tệp / kích thước / số lượng) áp NGAY trên trình duyệt.\n *\n * ── VÌ SAO `XHRUpload` CHỨ KHÔNG PHẢI `AwsS3` (presigned PUT) ──────────────────────────────\n * Một app Comitor khác (babrik) tải thẳng lên S3 bằng URL đã ký. Cách đó đúng cho một bucket CÔNG\n * KHAI, nơi URL trả về chính là URL hiển thị. Comitor thì dùng **bucket RIÊNG TƯ**: database lưu\n * KHOÁ object chứ không lưu URL, và URL hiển thị được sinh lại có chữ ký ở mỗi lần đọc.\n *\n * Hệ quả: máy chủ vẫn phải là nơi đặt tên khoá, kiểm byte thật, và ghi khoá vào database. Đẩy\n * việc tải thẳng lên S3 sẽ lấy mất của máy chủ khả năng nhìn thấy nội dung tệp — với một IdP phục\n * vụ ảnh do người dùng tải lên thì đó là đánh đổi không đáng.\n *\n * `XHRUpload` POST một `multipart/form-data` tới chính endpoint của app, nên **không route nào phải\n * đổi** và không có bề mặt bảo mật mới nào mở ra.\n *\n * ── CSP ─────────────────────────────────────────────────────────────────────────────────────\n * Uppy KHÔNG dùng `eval` hay `new Function` (đã grep `@uppy/core`, `@uppy/dashboard`, `@uppy/react`:\n * 0 file). Nên nó chạy được dưới `script-src 'self' 'nonce-…'` mà không cần nới gì.\n */\n\nexport interface FileUploadProps<T = unknown> {\n /** Đường dẫn endpoint nhận multipart, ví dụ `/api/account/avatar`. */\n endpoint: string;\n /** Tên trường trong `FormData`. Mặc định `\"file\"` — khớp khuôn route của Comitor. */\n fieldName?: string;\n /** Giới hạn kích thước mỗi tệp, byte. */\n maxFileSize: number;\n /** MIME được nhận, ví dụ `[\"image/jpeg\", \"image/png\", \"image/webp\"]`. */\n allowedFileTypes: string[];\n /** Số tệp tối đa mỗi lượt. Mặc định 1. */\n maxNumberOfFiles?: number;\n /** Chạy khi MỘT tệp tải xong. Nhận nguyên thân JSON của endpoint. */\n onUploaded: (result: FileUploadResult<T>) => void;\n /**\n * Chạy khi tải hỏng. `code` là `error.code` từ thân phản hồi nếu endpoint trả theo khuôn\n * `{ error: { code } }` của Comitor — nhờ đó chỗ gọi tra được bảng dịch của chính nó.\n */\n onError?: (error: { code?: string; status?: number; message: string }) => void;\n /**\n * Nhãn hiển thị. Không truyền thì theo namespace `fileUpload` của `ComitorLocaleProvider`\n * (không có provider → tiếng Anh); truyền `Partial<>` để đổi từng chuỗi.\n */\n labels?: Partial<FileUploadLabels>;\n disabled?: boolean;\n className?: string;\n}\n\nexport function FileUpload<T = unknown>({\n endpoint,\n fieldName,\n maxFileSize,\n allowedFileTypes,\n maxNumberOfFiles = 1,\n onUploaded,\n onError,\n labels,\n disabled = false,\n className\n}: FileUploadProps<T>) {\n const text = useComitorLabels(\"fileUpload\", labels);\n const inputId = useId();\n\n /*\n * Toàn bộ máy móc nằm ở `useFileUpload` — cùng một bộ với `ImageUploadField`. Hai component này\n * khác nhau ĐÚNG ở phần hình dạng, và đó là lý do bốn cạm bẫy của `@uppy/xhr-upload` chỉ được\n * xử lý ở MỘT chỗ.\n */\n const upload = useFileUpload<T>({\n endpoint,\n ...(fieldName ? { fieldName } : {}),\n maxFileSize,\n allowedFileTypes,\n maxNumberOfFiles,\n onUploaded,\n ...(onError ? { onError } : {}),\n labels: { uploadFailed: text.uploadFailed, missingDependency: text.missingDependency },\n disabled\n });\n\n return (\n <div className={cn(\"flex flex-col gap-2\", className)}>\n <input {...upload.inputProps} id={inputId} className=\"sr-only\" />\n\n {/*\n Vùng kéo thả. `<label>` bọc lấy nút nên bấm vào đâu trong vùng cũng mở hộp chọn tệp, và\n bàn phím vẫn tới được qua chính `<input>` (nó `sr-only`, không phải `display:none`).\n */}\n <div\n {...upload.dropProps}\n /*\n * ⚠ Trạng thái \"đang kéo tệp qua\" được vẽ bằng VIỀN, không bằng nền mờ.\n *\n * Bản đầu dùng `bg-primary/5` và cổng `pnpm lint:a11y` bác ngay: **1,03:1** ở nền sáng,\n * 1,09:1 ở nền tối — mắt không phân biệt được, tức dấu hiệu kéo-thả là một dấu hiệu GIẢ.\n *\n * ⚠ Và phải là `border-primary-INK`, không phải `border-primary`. Cổng bác cả bản thứ hai:\n * `--primary` là bậc TÔ NỀN của tone, vai trò nét/chữ phải đi qua `--primary-ink` (1,40:1\n * so với 3:1 cần thiết). Cùng luật ba bậc mà CLAUDE.md của Account ghi cho `iconColor`.\n *\n * Giữ `border-2` ở CẢ hai trạng thái để bố cục không nhảy 1px khi kéo tệp qua.\n */\n className={cn(\n \"flex flex-col items-center gap-2 rounded-lg border-2 border-dashed p-4 text-center transition-colors\",\n upload.dragging ? \"border-primary-ink\" : \"border-border\"\n )}\n >\n {/*\n * ⚠ `opacity-60` của trạng thái vô hiệu nằm ở NỘI DUNG, không ở khung.\n *\n * Không phải chuyện thẩm mỹ: cổng `lint:a11y` nhân opacity vào mọi lớp cùng khối, nên để nó\n * ở khung thì `border-primary-ink` rớt còn 2,64:1. Hai trạng thái ấy không bao giờ xảy ra\n * cùng lúc (kéo-thả đã bị chặn khi `disabled || busy`), nhưng cổng đọc TĨNH nên nó ghép —\n * và ở đây cổng đúng về hướng: làm mờ cả khung đứt nét thì người dùng mất luôn dấu hiệu\n * \"chỗ này thả tệp được\".\n */}\n <div className={cn(\"flex flex-col items-center gap-2\", (disabled || upload.busy) && \"opacity-60\")}>\n <p className=\"text-muted-foreground text-sm\">{text.dropHint}</p>\n <Button asChild variant=\"outline\" size=\"sm\" disabled={disabled || upload.busy} loading={upload.busy}>\n <label htmlFor={inputId} className={disabled || upload.busy ? \"pointer-events-none\" : \"cursor-pointer\"}>\n {upload.busy ? text.uploading : text.choose}\n </label>\n </Button>\n </div>\n </div>\n\n {/* Thanh tiến độ chỉ hiện khi đang tải — một thanh 0% đứng yên trông như hỏng. */}\n {upload.busy ? <Progress value={upload.percent} aria-label={text.uploading} /> : null}\n </div>\n );\n}\n"],"mappings":";AAuGM,cAkCE,YAlCF;AArGN,SAAS,aAAa;AACtB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,wBAAwB;AACjC,SAAS,UAAU;AAEnB,SAAgC,qBAAqB;AA6D9C,SAAS,WAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,mBAAmB;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AACF,GAAuB;AACrB,QAAM,OAAO,iBAAiB,cAAc,MAAM;AAClD,QAAM,UAAU,MAAM;AAOtB,QAAM,SAAS,cAAiB;AAAA,IAC9B;AAAA,IACA,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IACjC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,QAAQ,EAAE,cAAc,KAAK,cAAc,mBAAmB,KAAK,kBAAkB;AAAA,IACrF;AAAA,EACF,CAAC;AAED,SACE,qBAAC,SAAI,WAAW,GAAG,uBAAuB,SAAS,GACjD;AAAA,wBAAC,WAAO,GAAG,OAAO,YAAY,IAAI,SAAS,WAAU,WAAU;AAAA,IAM/D;AAAA,MAAC;AAAA;AAAA,QACE,GAAG,OAAO;AAAA,QAaX,WAAW;AAAA,UACT;AAAA,UACA,OAAO,WAAW,uBAAuB;AAAA,QAC3C;AAAA,QAWA,+BAAC,SAAI,WAAW,GAAG,qCAAqC,YAAY,OAAO,SAAS,YAAY,GAC9F;AAAA,8BAAC,OAAE,WAAU,iCAAiC,eAAK,UAAS;AAAA,UAC5D,oBAAC,UAAO,SAAO,MAAC,SAAQ,WAAU,MAAK,MAAK,UAAU,YAAY,OAAO,MAAM,SAAS,OAAO,MAC7F,8BAAC,WAAM,SAAS,SAAS,WAAW,YAAY,OAAO,OAAO,wBAAwB,kBACnF,iBAAO,OAAO,KAAK,YAAY,KAAK,QACvC,GACF;AAAA,WACF;AAAA;AAAA,IACF;AAAA,IAGC,OAAO,OAAO,oBAAC,YAAS,OAAO,OAAO,SAAS,cAAY,KAAK,WAAW,IAAK;AAAA,KACnF;AAEJ;","names":[]}
|
|
@@ -36,7 +36,7 @@ interface ImageUploadFieldProps<T = unknown> {
|
|
|
36
36
|
* cả node còn cho một cái nữa: ảnh bìa 16:9 cắm vừa cùng component này mà không thêm prop nào.
|
|
37
37
|
*/
|
|
38
38
|
preview: ReactNode;
|
|
39
|
-
/** Có ảnh thật hay không — quyết định nút
|
|
39
|
+
/** Có ảnh thật hay không — quyết định nút gỡ ảnh (`labels.remove`) có bấm được không. */
|
|
40
40
|
hasImage?: boolean;
|
|
41
41
|
/**
|
|
42
42
|
* `false` = chỉ hiện ảnh, không nút, không vùng thả.
|
|
@@ -54,6 +54,10 @@ interface ImageUploadFieldProps<T = unknown> {
|
|
|
54
54
|
status?: number;
|
|
55
55
|
message: string;
|
|
56
56
|
}) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Nhãn hiển thị. Không truyền thì theo namespace `imageUploadField` của `ComitorLocaleProvider`
|
|
59
|
+
* (không có provider → tiếng Anh); truyền `Partial<>` để đổi từng chuỗi.
|
|
60
|
+
*/
|
|
57
61
|
labels?: Partial<ImageUploadFieldLabels>;
|
|
58
62
|
className?: string;
|
|
59
63
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Button } from "../components/ui/button.js";
|
|
4
4
|
import { Progress } from "../components/ui/progress.js";
|
|
5
|
+
import { useComitorLabels } from "../i18n/locale-provider.js";
|
|
5
6
|
import { cn } from "../lib/cn.js";
|
|
6
|
-
import { DEFAULT_IMAGE_UPLOAD_FIELD_LABELS } from "./file-upload-constants.js";
|
|
7
7
|
import { useFileUpload } from "./use-file-upload.js";
|
|
8
8
|
function ImageUploadField({
|
|
9
9
|
endpoint,
|
|
@@ -20,7 +20,7 @@ function ImageUploadField({
|
|
|
20
20
|
labels,
|
|
21
21
|
className
|
|
22
22
|
}) {
|
|
23
|
-
const text =
|
|
23
|
+
const text = useComitorLabels("imageUploadField", labels);
|
|
24
24
|
const upload = useFileUpload({
|
|
25
25
|
endpoint,
|
|
26
26
|
...fieldName ? { fieldName } : {},
|