@vielzeug/refine 26.10.0 → 26.10.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 +1 -1
- package/dist/accordion-item.cjs.map +1 -1
- package/dist/accordion-item.js.map +1 -1
- package/dist/accordion.cjs.map +1 -1
- package/dist/accordion.js.map +1 -1
- package/dist/async.cjs.map +1 -1
- package/dist/async.js.map +1 -1
- package/dist/badge.cjs.map +1 -1
- package/dist/badge.js.map +1 -1
- package/dist/box.cjs.map +1 -1
- package/dist/box.js.map +1 -1
- package/dist/button.cjs +3 -3
- package/dist/button.cjs.map +1 -1
- package/dist/button.js +3 -3
- package/dist/button.js.map +1 -1
- package/dist/calendar.cjs.map +1 -1
- package/dist/calendar.js.map +1 -1
- package/dist/carousel.cjs.map +1 -1
- package/dist/carousel.js.map +1 -1
- package/dist/chat-message.cjs.map +1 -1
- package/dist/chat-message.js.map +1 -1
- package/dist/chat-panel.cjs.map +1 -1
- package/dist/chat-panel.js.map +1 -1
- package/dist/checkbox-group.cjs.map +1 -1
- package/dist/checkbox-group.js.map +1 -1
- package/dist/chip.cjs.map +1 -1
- package/dist/chip.js.map +1 -1
- package/dist/code-window.cjs.map +1 -1
- package/dist/code-window.js.map +1 -1
- package/dist/combobox.cjs.map +1 -1
- package/dist/combobox.js.map +1 -1
- package/dist/command-palette.cjs.map +1 -1
- package/dist/command-palette.js.map +1 -1
- package/dist/content/card/card.css.cjs.map +1 -1
- package/dist/content/card/card.css.js.map +1 -1
- package/dist/content/carousel/carousel-slide.css.cjs.map +1 -1
- package/dist/content/carousel/carousel-slide.css.js.map +1 -1
- package/dist/content/carousel/carousel.css.cjs.map +1 -1
- package/dist/content/carousel/carousel.css.js.map +1 -1
- package/dist/content/carousel/carousel.d.ts +5 -5
- package/dist/content/chat-message/chat-message.css.cjs +1 -1
- package/dist/content/chat-message/chat-message.css.cjs.map +1 -1
- package/dist/content/chat-message/chat-message.css.js +1 -1
- package/dist/content/chat-message/chat-message.css.js.map +1 -1
- package/dist/content/chat-message/chat-message.d.ts +4 -4
- package/dist/content/chat-message/chat-message.d.ts.map +1 -1
- package/dist/content/chat-panel/chat-panel.d.ts +1 -1
- package/dist/content/code-window/code-window.css.cjs.map +1 -1
- package/dist/content/code-window/code-window.css.js.map +1 -1
- package/dist/content/code-window/code-window.d.ts +4 -4
- package/dist/content/index.d.ts +4 -0
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/list/_list-item-reveal.cjs.map +1 -1
- package/dist/content/list/_list-item-reveal.d.ts +4 -4
- package/dist/content/list/_list-item-reveal.d.ts.map +1 -1
- package/dist/content/list/_list-item-reveal.js.map +1 -1
- package/dist/content/list/list-item.css.cjs.map +1 -1
- package/dist/content/list/list-item.css.js.map +1 -1
- package/dist/content/list/list-item.d.ts +8 -8
- package/dist/content/list/list-item.d.ts.map +1 -1
- package/dist/content/list/list.d.ts +8 -8
- package/dist/content/list/list.d.ts.map +1 -1
- package/dist/content/qr-code/qr-code.d.ts +1 -1
- package/dist/content/rank-list/rank-item.css.cjs +2 -0
- package/dist/content/rank-list/rank-item.css.cjs.map +1 -0
- package/dist/content/rank-list/rank-item.css.js +2 -0
- package/dist/content/rank-list/rank-item.css.js.map +1 -0
- package/dist/content/rank-list/rank-item.d.ts +68 -0
- package/dist/content/rank-list/rank-item.d.ts.map +1 -0
- package/dist/content/rank-list/rank-list.css.cjs +2 -0
- package/dist/content/rank-list/rank-list.css.cjs.map +1 -0
- package/dist/content/rank-list/rank-list.css.js +2 -0
- package/dist/content/rank-list/rank-list.css.js.map +1 -0
- package/dist/content/rank-list/rank-list.d.ts +44 -0
- package/dist/content/rank-list/rank-list.d.ts.map +1 -0
- package/dist/content/separator/separator.d.ts +1 -1
- package/dist/content/separator/separator.d.ts.map +1 -1
- package/dist/content/speech-player/speech-player.css.cjs.map +1 -1
- package/dist/content/speech-player/speech-player.css.js.map +1 -1
- package/dist/content/speech-player/speech-player.d.ts +12 -12
- package/dist/content/speech-player/speech-player.d.ts.map +1 -1
- package/dist/content/stepper/step.css.cjs.map +1 -1
- package/dist/content/stepper/step.css.js.map +1 -1
- package/dist/content/stepper/step.d.ts +3 -3
- package/dist/content/stepper/step.d.ts.map +1 -1
- package/dist/content/stepper/stepper.css.cjs.map +1 -1
- package/dist/content/stepper/stepper.css.js.map +1 -1
- package/dist/content/stepper/stepper.d.ts +3 -3
- package/dist/content/stepper/stepper.d.ts.map +1 -1
- package/dist/content/table/table.css.cjs.map +1 -1
- package/dist/content/table/table.css.js.map +1 -1
- package/dist/content/table/table.d.ts +3 -3
- package/dist/content/table/table.d.ts.map +1 -1
- package/dist/content/text/text.css.cjs.map +1 -1
- package/dist/content/text/text.css.js.map +1 -1
- package/dist/content/text/text.d.ts +2 -2
- package/dist/content/text/text.d.ts.map +1 -1
- package/dist/cookie-banner.cjs.map +1 -1
- package/dist/cookie-banner.js.map +1 -1
- package/dist/copy-command.cjs +1 -1
- package/dist/copy-command.cjs.map +1 -1
- package/dist/copy-command.js +1 -1
- package/dist/copy-command.js.map +1 -1
- package/dist/core/announcer.cjs.map +1 -1
- package/dist/core/announcer.d.ts +2 -2
- package/dist/core/announcer.js.map +1 -1
- package/dist/core/aria-reflection.cjs.map +1 -1
- package/dist/core/aria-reflection.d.ts +3 -3
- package/dist/core/aria-reflection.js.map +1 -1
- package/dist/core/auto-resize.cjs.map +1 -1
- package/dist/core/auto-resize.d.ts +1 -1
- package/dist/core/auto-resize.js.map +1 -1
- package/dist/core/checkable.cjs.map +1 -1
- package/dist/core/checkable.d.ts +2 -2
- package/dist/core/checkable.d.ts.map +1 -1
- package/dist/core/checkable.js.map +1 -1
- package/dist/core/choice-field.cjs.map +1 -1
- package/dist/core/choice-field.d.ts +3 -3
- package/dist/core/choice-field.d.ts.map +1 -1
- package/dist/core/choice-field.js.map +1 -1
- package/dist/core/composer.cjs.map +1 -1
- package/dist/core/composer.d.ts +4 -4
- package/dist/core/composer.d.ts.map +1 -1
- package/dist/core/composer.js.map +1 -1
- package/dist/core/date-picker.cjs.map +1 -1
- package/dist/core/date-picker.d.ts +5 -5
- package/dist/core/date-picker.d.ts.map +1 -1
- package/dist/core/date-picker.js.map +1 -1
- package/dist/core/direction.cjs.map +1 -1
- package/dist/core/direction.d.ts +1 -1
- package/dist/core/direction.js.map +1 -1
- package/dist/core/field-base.cjs.map +1 -1
- package/dist/core/field-base.d.ts +9 -9
- package/dist/core/field-base.d.ts.map +1 -1
- package/dist/core/field-base.js.map +1 -1
- package/dist/core/focus.cjs.map +1 -1
- package/dist/core/focus.d.ts +1 -1
- package/dist/core/focus.js.map +1 -1
- package/dist/core/icon-registry.cjs.map +1 -1
- package/dist/core/icon-registry.d.ts +1 -1
- package/dist/core/icon-registry.js.map +1 -1
- package/dist/core/icons.cjs.map +1 -1
- package/dist/core/icons.js.map +1 -1
- package/dist/core/keyboard.cjs.map +1 -1
- package/dist/core/keyboard.d.ts +1 -1
- package/dist/core/keyboard.js.map +1 -1
- package/dist/core/numbers.cjs.map +1 -1
- package/dist/core/numbers.d.ts +1 -1
- package/dist/core/numbers.js.map +1 -1
- package/dist/core/overlay.cjs.map +1 -1
- package/dist/core/overlay.d.ts +6 -6
- package/dist/core/overlay.js.map +1 -1
- package/dist/core/parse.cjs.map +1 -1
- package/dist/core/parse.d.ts +1 -1
- package/dist/core/parse.js.map +1 -1
- package/dist/core/positioner.cjs.map +1 -1
- package/dist/core/positioner.d.ts +3 -3
- package/dist/core/positioner.js.map +1 -1
- package/dist/core/ref-callback.cjs.map +1 -1
- package/dist/core/ref-callback.d.ts +2 -2
- package/dist/core/ref-callback.d.ts.map +1 -1
- package/dist/core/ref-callback.js.map +1 -1
- package/dist/core/text-field.cjs.map +1 -1
- package/dist/core/text-field.d.ts +6 -6
- package/dist/core/text-field.d.ts.map +1 -1
- package/dist/core/text-field.js.map +1 -1
- package/dist/counter.cjs +1 -0
- package/dist/counter.cjs.map +1 -1
- package/dist/counter.js +1 -0
- package/dist/counter.js.map +1 -1
- package/dist/custom-elements.json +315 -87
- package/dist/datagrid.cjs.map +1 -1
- package/dist/datagrid.js.map +1 -1
- package/dist/date-picker.cjs +1 -1
- package/dist/date-picker.cjs.map +1 -1
- package/dist/date-picker.js +1 -1
- package/dist/date-picker.js.map +1 -1
- package/dist/dialog.cjs.map +1 -1
- package/dist/dialog.js.map +1 -1
- package/dist/disclosure/accordion-item/accordion-item.css.cjs +1 -1
- package/dist/disclosure/accordion-item/accordion-item.css.cjs.map +1 -1
- package/dist/disclosure/accordion-item/accordion-item.css.js +1 -1
- package/dist/disclosure/accordion-item/accordion-item.css.js.map +1 -1
- package/dist/disclosure/tab-item/tab-item.d.ts +1 -1
- package/dist/disclosure/tab-item/tab-item.d.ts.map +1 -1
- package/dist/drawer.cjs.map +1 -1
- package/dist/drawer.js.map +1 -1
- package/dist/feedback/alert/alert.css.cjs.map +1 -1
- package/dist/feedback/alert/alert.css.js.map +1 -1
- package/dist/feedback/badge/badge.css.cjs.map +1 -1
- package/dist/feedback/badge/badge.css.js.map +1 -1
- package/dist/feedback/badge/badge.d.ts +1 -1
- package/dist/feedback/badge/badge.d.ts.map +1 -1
- package/dist/feedback/chip/chip.css.cjs.map +1 -1
- package/dist/feedback/chip/chip.css.js.map +1 -1
- package/dist/feedback/chip/chip.d.ts +3 -3
- package/dist/feedback/chip/chip.d.ts.map +1 -1
- package/dist/feedback/cookie-banner/cookie-banner.css.cjs.map +1 -1
- package/dist/feedback/cookie-banner/cookie-banner.css.js.map +1 -1
- package/dist/feedback/cookie-banner/cookie-banner.d.ts +8 -8
- package/dist/feedback/cookie-banner/cookie-banner.d.ts.map +1 -1
- package/dist/feedback/progress/progress.css.cjs.map +1 -1
- package/dist/feedback/progress/progress.css.js.map +1 -1
- package/dist/feedback/progress/progress.d.ts +1 -1
- package/dist/feedback/progress/progress.d.ts.map +1 -1
- package/dist/feedback/skeleton/skeleton.css.cjs.map +1 -1
- package/dist/feedback/skeleton/skeleton.css.js.map +1 -1
- package/dist/feedback/skeleton/skeleton.d.ts +1 -1
- package/dist/feedback/skeleton/skeleton.d.ts.map +1 -1
- package/dist/feedback/toast/toast.css.cjs +1 -1
- package/dist/feedback/toast/toast.css.cjs.map +1 -1
- package/dist/feedback/toast/toast.css.js +1 -1
- package/dist/feedback/toast/toast.css.js.map +1 -1
- package/dist/feedback/toast/toast.d.ts +1 -1
- package/dist/feedback/toast/toast.d.ts.map +1 -1
- package/dist/feedback/typing-indicator/typing-indicator.d.ts +2 -2
- package/dist/file-input.cjs +1 -1
- package/dist/file-input.cjs.map +1 -1
- package/dist/file-input.js +1 -1
- package/dist/file-input.js.map +1 -1
- package/dist/frameworks/elements.d.ts +1 -1
- package/dist/grid-item.cjs.map +1 -1
- package/dist/grid-item.js.map +1 -1
- package/dist/grid.cjs.map +1 -1
- package/dist/grid.js.map +1 -1
- package/dist/icon.cjs.map +1 -1
- package/dist/icon.js.map +1 -1
- package/dist/input.cjs.map +1 -1
- package/dist/input.js.map +1 -1
- package/dist/inputs/button/button.css.cjs.map +1 -1
- package/dist/inputs/button/button.css.js.map +1 -1
- package/dist/inputs/button/button.d.ts +3 -3
- package/dist/inputs/button/button.d.ts.map +1 -1
- package/dist/inputs/calendar/calendar.css.cjs.map +1 -1
- package/dist/inputs/calendar/calendar.css.js.map +1 -1
- package/dist/inputs/calendar/calendar.d.ts +2 -2
- package/dist/inputs/checkbox-group/checkbox-group.d.ts +2 -2
- package/dist/inputs/checkbox-group/checkbox-group.d.ts.map +1 -1
- package/dist/inputs/combobox/combobox.css.cjs.map +1 -1
- package/dist/inputs/combobox/combobox.css.js.map +1 -1
- package/dist/inputs/counter/counter.css.cjs +1 -1
- package/dist/inputs/counter/counter.css.cjs.map +1 -1
- package/dist/inputs/counter/counter.css.js +1 -1
- package/dist/inputs/counter/counter.css.js.map +1 -1
- package/dist/inputs/counter/counter.d.ts +2 -1
- package/dist/inputs/counter/counter.d.ts.map +1 -1
- package/dist/inputs/datagrid/datagrid.css.cjs.map +1 -1
- package/dist/inputs/datagrid/datagrid.css.js.map +1 -1
- package/dist/inputs/datagrid/datagrid.d.ts +5 -5
- package/dist/inputs/date-picker/date-picker.css.cjs.map +1 -1
- package/dist/inputs/date-picker/date-picker.css.js.map +1 -1
- package/dist/inputs/date-picker/date-picker.d.ts +1 -1
- package/dist/inputs/file-input/file-input-upload.cjs.map +1 -1
- package/dist/inputs/file-input/file-input-upload.d.ts +6 -6
- package/dist/inputs/file-input/file-input-upload.js.map +1 -1
- package/dist/inputs/file-input/file-input.css.cjs.map +1 -1
- package/dist/inputs/file-input/file-input.css.js.map +1 -1
- package/dist/inputs/file-input/file-input.d.ts +6 -6
- package/dist/inputs/input/input.css.cjs +1 -1
- package/dist/inputs/input/input.css.cjs.map +1 -1
- package/dist/inputs/input/input.css.js +1 -1
- package/dist/inputs/input/input.css.js.map +1 -1
- package/dist/inputs/input/input.d.ts +6 -6
- package/dist/inputs/input/input.d.ts.map +1 -1
- package/dist/inputs/message-composer/message-composer.css.cjs.map +1 -1
- package/dist/inputs/message-composer/message-composer.css.js.map +1 -1
- package/dist/inputs/message-composer/message-composer.d.ts +7 -7
- package/dist/inputs/message-composer/message-composer.d.ts.map +1 -1
- package/dist/inputs/number-input/number-input.css.cjs.map +1 -1
- package/dist/inputs/number-input/number-input.css.js.map +1 -1
- package/dist/inputs/number-input/number-input.d.ts +2 -2
- package/dist/inputs/otp-input/otp-input.css.cjs.map +1 -1
- package/dist/inputs/otp-input/otp-input.css.js.map +1 -1
- package/dist/inputs/qr-scanner/qr-scanner.d.ts +1 -1
- package/dist/inputs/qr-scanner/qr-scanner.d.ts.map +1 -1
- package/dist/inputs/rating/rating.d.ts +1 -1
- package/dist/inputs/rating/rating.d.ts.map +1 -1
- package/dist/inputs/select/select.css.cjs +1 -1
- package/dist/inputs/select/select.css.cjs.map +1 -1
- package/dist/inputs/select/select.css.js +1 -1
- package/dist/inputs/select/select.css.js.map +1 -1
- package/dist/inputs/select/select.d.ts +1 -1
- package/dist/inputs/select/select.d.ts.map +1 -1
- package/dist/inputs/shared/field-binding.cjs.map +1 -1
- package/dist/inputs/shared/field-binding.d.ts +2 -2
- package/dist/inputs/shared/field-binding.d.ts.map +1 -1
- package/dist/inputs/shared/field-binding.js.map +1 -1
- package/dist/inputs/shared/native-field-event.cjs.map +1 -1
- package/dist/inputs/shared/native-field-event.d.ts +1 -1
- package/dist/inputs/shared/native-field-event.js.map +1 -1
- package/dist/inputs/shared/templates.cjs.map +1 -1
- package/dist/inputs/shared/templates.d.ts +3 -3
- package/dist/inputs/shared/templates.js.map +1 -1
- package/dist/inputs/slider/slider.css.cjs.map +1 -1
- package/dist/inputs/slider/slider.css.js.map +1 -1
- package/dist/inputs/slider/slider.d.ts +1 -1
- package/dist/inputs/slider/slider.d.ts.map +1 -1
- package/dist/inputs/textarea/textarea.css.cjs.map +1 -1
- package/dist/inputs/textarea/textarea.css.js.map +1 -1
- package/dist/inputs/textarea/textarea.d.ts +1 -1
- package/dist/inputs/time-picker/time-picker.css.cjs.map +1 -1
- package/dist/inputs/time-picker/time-picker.css.js.map +1 -1
- package/dist/inputs/time-picker/time-picker.d.ts +1 -1
- package/dist/layout/box/box.css.cjs.map +1 -1
- package/dist/layout/box/box.css.js.map +1 -1
- package/dist/layout/box/box.d.ts +1 -1
- package/dist/layout/grid/grid.d.ts +1 -1
- package/dist/layout/grid-item/grid-item.d.ts +3 -3
- package/dist/layout/grid-item/grid-item.d.ts.map +1 -1
- package/dist/layout/navbar/navbar.css.cjs +1 -1
- package/dist/layout/navbar/navbar.css.cjs.map +1 -1
- package/dist/layout/navbar/navbar.css.js +1 -1
- package/dist/layout/navbar/navbar.css.js.map +1 -1
- package/dist/layout/navbar/navbar.d.ts +3 -3
- package/dist/layout/sidebar/sidebar-item.css.cjs.map +1 -1
- package/dist/layout/sidebar/sidebar-item.css.js.map +1 -1
- package/dist/layout/sidebar/sidebar.d.ts +5 -5
- package/dist/layout/sidebar/sidebar.d.ts.map +1 -1
- package/dist/list-item.cjs.map +1 -1
- package/dist/list-item.js.map +1 -1
- package/dist/list.cjs.map +1 -1
- package/dist/list.js.map +1 -1
- package/dist/menu.cjs +6 -5
- package/dist/menu.cjs.map +1 -1
- package/dist/menu.js +6 -5
- package/dist/menu.js.map +1 -1
- package/dist/message-composer.cjs.map +1 -1
- package/dist/message-composer.js.map +1 -1
- package/dist/navbar.cjs.map +1 -1
- package/dist/navbar.js.map +1 -1
- package/dist/number-input.cjs.map +1 -1
- package/dist/number-input.js.map +1 -1
- package/dist/otp-input.cjs.map +1 -1
- package/dist/otp-input.js.map +1 -1
- package/dist/overlay/command-palette/command-palette-items.cjs.map +1 -1
- package/dist/overlay/command-palette/command-palette-items.d.ts +1 -1
- package/dist/overlay/command-palette/command-palette-items.js.map +1 -1
- package/dist/overlay/command-palette/command-palette.css.cjs.map +1 -1
- package/dist/overlay/command-palette/command-palette.css.js.map +1 -1
- package/dist/overlay/command-palette/command-palette.d.ts +3 -3
- package/dist/overlay/command-palette/command-palette.types.d.ts +6 -6
- package/dist/overlay/command-palette/command-palette.types.d.ts.map +1 -1
- package/dist/overlay/dialog/dialog.css.cjs +1 -1
- package/dist/overlay/dialog/dialog.css.cjs.map +1 -1
- package/dist/overlay/dialog/dialog.css.js +1 -1
- package/dist/overlay/dialog/dialog.css.js.map +1 -1
- package/dist/overlay/dialog/dialog.d.ts +2 -2
- package/dist/overlay/dialog/dialog.d.ts.map +1 -1
- package/dist/overlay/drawer/drawer.css.cjs.map +1 -1
- package/dist/overlay/drawer/drawer.css.js.map +1 -1
- package/dist/overlay/drawer/drawer.d.ts +1 -1
- package/dist/overlay/drawer/drawer.d.ts.map +1 -1
- package/dist/overlay/menu/menu-item.css.cjs.map +1 -1
- package/dist/overlay/menu/menu-item.css.js.map +1 -1
- package/dist/overlay/menu/menu.d.ts.map +1 -1
- package/dist/overlay/shared/use-dialog.cjs.map +1 -1
- package/dist/overlay/shared/use-dialog.d.ts +6 -6
- package/dist/overlay/shared/use-dialog.d.ts.map +1 -1
- package/dist/overlay/shared/use-dialog.js.map +1 -1
- package/dist/overlay/shared/use-floating-trigger.cjs.map +1 -1
- package/dist/overlay/shared/use-floating-trigger.d.ts +1 -1
- package/dist/overlay/shared/use-floating-trigger.d.ts.map +1 -1
- package/dist/overlay/shared/use-floating-trigger.js.map +1 -1
- package/dist/overlay/tooltip/tooltip.css.cjs.map +1 -1
- package/dist/overlay/tooltip/tooltip.css.js.map +1 -1
- package/dist/progress.cjs.map +1 -1
- package/dist/progress.js.map +1 -1
- package/dist/qr-code.cjs.map +1 -1
- package/dist/qr-code.js.map +1 -1
- package/dist/qr-scanner.cjs.map +1 -1
- package/dist/qr-scanner.js.map +1 -1
- package/dist/radio-group.cjs.map +1 -1
- package/dist/radio-group.js.map +1 -1
- package/dist/radio.cjs.map +1 -1
- package/dist/radio.js.map +1 -1
- package/dist/rank-item.cjs +21 -0
- package/dist/rank-item.cjs.map +1 -0
- package/dist/rank-item.js +21 -0
- package/dist/rank-item.js.map +1 -0
- package/dist/rank-list.cjs +4 -0
- package/dist/rank-list.cjs.map +1 -0
- package/dist/rank-list.js +4 -0
- package/dist/rank-list.js.map +1 -0
- package/dist/rating.cjs.map +1 -1
- package/dist/rating.js.map +1 -1
- package/dist/refine.cjs +103 -80
- package/dist/refine.cjs.map +1 -1
- package/dist/refine.iife.js +103 -80
- package/dist/refine.iife.js.map +1 -1
- package/dist/refine.js +104 -81
- package/dist/refine.js.map +1 -1
- package/dist/select.cjs.map +1 -1
- package/dist/select.js.map +1 -1
- package/dist/separator.cjs.map +1 -1
- package/dist/separator.js.map +1 -1
- package/dist/shared/index.d.ts +3 -3
- package/dist/shared/size-presets.cjs.map +1 -1
- package/dist/shared/size-presets.d.ts +4 -4
- package/dist/shared/size-presets.js.map +1 -1
- package/dist/shared/types.d.ts +1 -1
- package/dist/sidebar.cjs.map +1 -1
- package/dist/sidebar.js.map +1 -1
- package/dist/skeleton.cjs.map +1 -1
- package/dist/skeleton.js.map +1 -1
- package/dist/slider.cjs.map +1 -1
- package/dist/slider.js.map +1 -1
- package/dist/speech-player.cjs.map +1 -1
- package/dist/speech-player.js.map +1 -1
- package/dist/step.cjs.map +1 -1
- package/dist/step.js.map +1 -1
- package/dist/stepper.cjs.map +1 -1
- package/dist/stepper.js.map +1 -1
- package/dist/styles/fouc.css +2 -2
- package/dist/styles/mixins/accessibility.css.cjs.map +1 -1
- package/dist/styles/mixins/accessibility.css.d.ts +4 -4
- package/dist/styles/mixins/accessibility.css.js.map +1 -1
- package/dist/styles/mixins/animation.css.cjs +2 -2
- package/dist/styles/mixins/animation.css.cjs.map +1 -1
- package/dist/styles/mixins/animation.css.d.ts +4 -4
- package/dist/styles/mixins/animation.css.js +2 -2
- package/dist/styles/mixins/animation.css.js.map +1 -1
- package/dist/styles/mixins/field-variants.css.cjs +2 -2
- package/dist/styles/mixins/field-variants.css.cjs.map +1 -1
- package/dist/styles/mixins/field-variants.css.d.ts +6 -6
- package/dist/styles/mixins/field-variants.css.d.ts.map +1 -1
- package/dist/styles/mixins/field-variants.css.js +2 -2
- package/dist/styles/mixins/field-variants.css.js.map +1 -1
- package/dist/styles/mixins/shape.css.cjs.map +1 -1
- package/dist/styles/mixins/shape.css.d.ts +9 -9
- package/dist/styles/mixins/shape.css.d.ts.map +1 -1
- package/dist/styles/mixins/shape.css.js.map +1 -1
- package/dist/styles/mixins/variants.css.cjs.map +1 -1
- package/dist/styles/mixins/variants.css.d.ts +1 -1
- package/dist/styles/mixins/variants.css.js.map +1 -1
- package/dist/styles/theme.css +36 -28
- package/dist/tab-item.cjs.map +1 -1
- package/dist/tab-item.js.map +1 -1
- package/dist/tab-panel.cjs.map +1 -1
- package/dist/tab-panel.js.map +1 -1
- package/dist/table.cjs.map +1 -1
- package/dist/table.js.map +1 -1
- package/dist/text.cjs.map +1 -1
- package/dist/text.js.map +1 -1
- package/dist/textarea.cjs.map +1 -1
- package/dist/textarea.js.map +1 -1
- package/dist/theme.cjs.map +1 -1
- package/dist/theme.d.ts +2 -2
- package/dist/theme.js.map +1 -1
- package/dist/time-picker.cjs +1 -1
- package/dist/time-picker.cjs.map +1 -1
- package/dist/time-picker.js +1 -1
- package/dist/time-picker.js.map +1 -1
- package/dist/toast.cjs.map +1 -1
- package/dist/toast.js.map +1 -1
- package/dist/types/elements.d.ts +6 -2
- package/dist/typing-indicator.cjs.map +1 -1
- package/dist/typing-indicator.js.map +1 -1
- package/dist/utils/css.cjs.map +1 -1
- package/dist/utils/css.js.map +1 -1
- package/dist/utils/link.cjs.map +1 -1
- package/dist/utils/link.d.ts +2 -2
- package/dist/utils/link.js.map +1 -1
- package/package.json +31 -14
package/dist/carousel.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"carousel.js","names":[],"sources":["../src/content/carousel/carousel.ts"],"sourcesContent":["import { createPanGesture } from '@vielzeug/gesture';\nimport { bind, define, getHost, html, onCleanup, onMounted, prop, useEmit } from '@vielzeug/ore';\nimport { computed, signal, watch } from '@vielzeug/ripple';\nimport { announce, createListControl, lifecycleSignal } from '../../core';\nimport type { ThemeColor } from '../../types';\nimport componentStyles from './carousel.css?inline';\nimport './carousel-slide';\n\nexport { ICON_TAG } from '../../content/icon/icon';\nexport { PROGRESS_TAG } from '../../feedback/progress/progress';\nexport { CAROUSEL_SLIDE_TAG } from './carousel-slide';\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type CarouselOrientation = 'horizontal' | 'vertical';\nexport type CarouselVariant = 'default' | 'fade' | 'filmstrip' | 'gallery';\n\nexport type OreCarouselEvents = {\n /** Fired when the active slide changes. */\n change: { index: number };\n};\n\nexport type OreCarouselProps = {\n /**\n * Whether to advance slides automatically. Defaults to `false`.\n * Opt in explicitly — autoplay on by default is a WCAG 2.1 SC 2.2.2 violation for many use cases.\n */\n autoplay?: boolean;\n /** Interval in milliseconds between automatic slide advances. Defaults to `5000`. */\n 'autoplay-interval'?: number;\n /** Theme color passed to the prev/next navigation buttons. */\n color?: ThemeColor;\n /** Accessible label for the carousel region. */\n label?: string;\n /** Whether the carousel loops from the last slide back to the first. Defaults to `true`. */\n loop?: boolean;\n /** Carousel orientation. Defaults to `'horizontal'`. */\n orientation?: CarouselOrientation;\n /** Show next/prev navigation buttons. Defaults to `true`. */\n 'show-controls'?: boolean;\n /** Show dot/indicator navigation. Defaults to `true`. */\n 'show-indicators'?: boolean;\n /** Index of the currently active slide (zero-based). Defaults to `0`. */\n 'slide-index'?: number;\n /**\n * Layout variant.\n * - `'default'` — slides translate in/out (default)\n * - `'fade'` — slides crossfade; no movement\n * - `'filmstrip'` — all slides visible side-by-side; active expands\n * - `'gallery'` — active slide fills the majority; adjacent slides show as thumbnails\n */\n variant?: CarouselVariant;\n};\n\n/**\n * An accessible, keyboard-navigable carousel / slideshow with optional\n * autoplay, swipe support, and indicator dots.\n *\n * Place `<ore-carousel-slide>` elements as direct children.\n *\n * @element ore-carousel\n * @element ore-carousel-slide - Child element for individual slides\n *\n * @attr {string} color - Theme color for navigation buttons: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'\n * @attr {boolean} autoplay - Advance slides automatically (default false)\n * @attr {number} autoplay-interval - Milliseconds between automatic advances (default 5000)\n * @attr {string} label - Accessible label for the carousel region\n * @attr {boolean} loop - Loop from last slide to first (default true)\n * @attr {string} orientation - 'horizontal' (default) | 'vertical'\n * @attr {string} variant - 'default' | 'fade' | 'filmstrip' | 'gallery'\n * @attr {boolean} show-controls - Show prev/next buttons (default true)\n * @attr {boolean} show-indicators - Show indicator dots (default true)\n * @attr {number} slide-index - Active slide index (zero-based, default 0)\n *\n * @fires change - Fires when the active slide changes. detail: { index: number }\n *\n * @slot - Place `<ore-carousel-slide>` elements here\n *\n * @cssprop --carousel-bg - Slide area background\n * @cssprop --carousel-radius - Border radius of the carousel\n * @cssprop --carousel-dot-bg - Inactive indicator color (default: var(--color-contrast-300))\n * @cssprop --carousel-dot-active-bg - Active indicator / fill color (default: var(--color-contrast-700))\n * @cssprop --carousel-transition-duration - Slide transition duration (default 0.35s; 0s under prefers-reduced-motion).\n * @cssprop --carousel-min-height - Minimum height when no explicit height is set (default 240px)\n * @cssprop --carousel-filmstrip-inactive - Width (horizontal) or height (vertical) of inactive slides in filmstrip mode\n * @cssprop --carousel-filmstrip-gap - Gap between slides in filmstrip mode\n * @cssprop --carousel-gallery-thumbnail - Width (horizontal) or height (vertical) of thumbnail slides in gallery mode (default var(--size-24))\n * @cssprop --carousel-gallery-gap - Gap between slides in gallery mode (default var(--size-2))\n *\n * @part track - The scrolling slide track element\n * @part controls - The prev/next button container\n * @part indicators - The indicator dots container\n * @part prev-btn - The previous-slide button\n * @part next-btn - The next-slide button\n *\n * @example\n * ```html\n * <ore-carousel label=\"Product highlights\" loop>\n * <ore-carousel-slide>Slide 1</ore-carousel-slide>\n * <ore-carousel-slide>Slide 2</ore-carousel-slide>\n * <ore-carousel-slide>Slide 3</ore-carousel-slide>\n * </ore-carousel>\n * ```\n */\nexport const CAROUSEL_TAG = 'ore-carousel' as const;\n\n// ── Slide-state sync helpers ──────────────────────────────────────────────────\n// Each function handles exactly one variant's attribute bookkeeping.\n// Called from a dispatch table in syncActiveState.\n\nconst syncDefaultSlides = (slides: HTMLElement[], current: number): void => {\n const count = slides.length;\n\n slides.forEach((slide, i) => {\n const active = i === current;\n\n slide.toggleAttribute('data-active', active);\n slide.setAttribute('aria-hidden', String(!active));\n slide.setAttribute('aria-label', slide.getAttribute('aria-label') ?? `Slide ${i + 1} of ${count}`);\n slide.removeAttribute('data-gallery-visible');\n slide.toggleAttribute('data-before', i < current);\n slide.toggleAttribute('data-after', i > current);\n });\n};\n\nconst syncFilmstripSlides = (slides: HTMLElement[], current: number): void => {\n const count = slides.length;\n\n slides.forEach((slide, i) => {\n const active = i === current;\n\n slide.toggleAttribute('data-active', active);\n slide.setAttribute('aria-hidden', String(!active));\n slide.setAttribute('aria-label', slide.getAttribute('aria-label') ?? `Slide ${i + 1} of ${count}`);\n slide.removeAttribute('data-gallery-visible');\n slide.removeAttribute('data-before');\n slide.removeAttribute('data-after');\n });\n};\n\nconst syncGallerySlides = (slides: HTMLElement[], current: number): void => {\n const count = slides.length;\n\n slides.forEach((slide, i) => {\n const active = i === current;\n const isPrev = current > 0 && i === current - 1;\n const isNext = current < count - 1 && i === current + 1;\n\n slide.toggleAttribute('data-active', active);\n slide.setAttribute('aria-hidden', String(!active));\n slide.setAttribute('aria-label', slide.getAttribute('aria-label') ?? `Slide ${i + 1} of ${count}`);\n slide.removeAttribute('data-before');\n slide.removeAttribute('data-after');\n slide.toggleAttribute('data-gallery-visible', active || isPrev || isNext);\n });\n};\n\n// ── ore-carousel ──────────────────────────────────────────────────────────────\n\ndefine<OreCarouselProps>(CAROUSEL_TAG, {\n props: {\n autoplay: prop.bool(false),\n 'autoplay-interval': prop.number(5000),\n color: prop.string<ThemeColor>(),\n label: prop.string('Carousel'),\n loop: prop.bool(true),\n orientation: prop.string<CarouselOrientation>('horizontal'),\n 'show-controls': prop.bool(true),\n 'show-indicators': prop.bool(true),\n 'slide-index': prop.number(0),\n variant: prop.string<CarouselVariant>('default'),\n },\n\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreCarouselEvents>();\n\n // ── State ────────────────────────────────────────────────────────────────\n\n const activeIndex = signal<number>(props['slide-index'].value ?? 0);\n let autoplayTimer: ReturnType<typeof setInterval> | null = null;\n\n // Slide cache — populated immediately, refreshed on slotchange.\n let slides: HTMLElement[] = Array.from(el.querySelectorAll<HTMLElement>(':scope > ore-carousel-slide'));\n const slideCount = signal(slides.length);\n\n const refreshSlides = (): void => {\n slides = Array.from(el.querySelectorAll<HTMLElement>(':scope > ore-carousel-slide'));\n slideCount.value = slides.length;\n };\n\n const isHorizontal = computed(() => props.orientation.value !== 'vertical');\n const looping = computed(() => props.loop.value !== false);\n const showControls = computed(() => props['show-controls'].value !== false);\n const showIndicators = computed(() => props['show-indicators'].value !== false);\n\n const canGoPrev = computed(() => looping.value || activeIndex.value > 0);\n const canGoNext = computed(() => looping.value || activeIndex.value < slideCount.value - 1);\n\n // ── Navigation ───────────────────────────────────────────────────────────\n\n const goTo = (index: number, announce_: boolean = true): void => {\n const count = slideCount.value;\n\n if (count === 0) return;\n\n const next = looping.value ? ((index % count) + count) % count : Math.max(0, Math.min(index, count - 1));\n\n if (next === activeIndex.value) return;\n\n activeIndex.value = next;\n el.setAttribute('slide-index', String(next));\n emit('change', { index: next });\n\n if (announce_) {\n announce(slides[next]?.getAttribute('aria-label') ?? `Slide ${next + 1} of ${count}`, {\n document: el.ownerDocument,\n });\n }\n };\n\n const prev = (): void => goTo(activeIndex.value - 1);\n const next = (): void => goTo(activeIndex.value + 1);\n\n // ── Sync prop → state ────────────────────────────────────────────────────\n\n watch(props['slide-index'], (v) => {\n if (typeof v === 'number' && v !== activeIndex.value) goTo(v, false);\n });\n\n // ── Sync variant + orientation → slides ──────────────────────────────────\n\n const syncSlideVariants = (): void => {\n const variant = props.variant.value ?? 'default';\n const orientation = props.orientation.value ?? 'horizontal';\n\n slides.forEach((slide) => {\n slide.setAttribute('data-variant', variant);\n slide.setAttribute('data-orientation', orientation);\n });\n };\n\n watch(\n computed(() => ({ orientation: props.orientation.value, variant: props.variant.value })),\n syncSlideVariants,\n { immediate: true },\n );\n\n // ── Sync active index → slides — dispatch table ───────────────────────────\n\n const syncTable: Record<CarouselVariant, (slides: HTMLElement[], current: number) => void> = {\n default: syncDefaultSlides,\n fade: syncDefaultSlides,\n filmstrip: syncFilmstripSlides,\n gallery: syncGallerySlides,\n };\n\n const syncActiveState = (): void => {\n const variant = (props.variant.value ?? 'default') as CarouselVariant;\n\n syncTable[variant](slides, activeIndex.value);\n };\n\n watch(activeIndex, syncActiveState, { immediate: true });\n\n // ── Autoplay ─────────────────────────────────────────────────────────────\n\n const startAutoplay = (): void => {\n if (autoplayTimer !== null) return;\n\n autoplayTimer = setInterval(next, props['autoplay-interval'].value ?? 5000);\n };\n\n const stopAutoplay = (): void => {\n if (autoplayTimer !== null) {\n clearInterval(autoplayTimer);\n autoplayTimer = null;\n }\n };\n\n watch(\n computed(() => ({ enabled: props.autoplay.value, interval: props['autoplay-interval'].value })),\n ({ enabled }) => {\n stopAutoplay();\n\n if (enabled) startAutoplay();\n },\n );\n\n // ── Keyboard navigation ──────────────────────────────────────────────────\n\n const handleKeydown = (e: KeyboardEvent): void => {\n const isH = isHorizontal.value;\n const prevKey = isH ? 'ArrowLeft' : 'ArrowUp';\n const nextKey = isH ? 'ArrowRight' : 'ArrowDown';\n\n if (e.key === prevKey) {\n e.preventDefault();\n prev();\n } else if (e.key === nextKey) {\n e.preventDefault();\n next();\n } else if (e.key === 'Home') {\n e.preventDefault();\n goTo(0);\n } else if (e.key === 'End') {\n e.preventDefault();\n goTo(slideCount.value - 1);\n }\n };\n\n const indicatorNav = createListControl<number>({\n getItems: () => Array.from({ length: slideCount.value }, (_, index) => index),\n loop: true,\n onNavigate: ({ index }) => {\n goTo(index);\n const dot = el.shadowRoot?.querySelector<HTMLElement>(`.indicator[data-index=\"${index}\"]`);\n\n dot?.focus();\n },\n orientation: () => (isHorizontal.value ? 'horizontal' : 'vertical'),\n signal: lifecycleSignal(onCleanup),\n });\n\n // ── Host bindings ────────────────────────────────────────────────────────\n\n bind({\n attr: {\n 'aria-label': () => props.label.value ?? 'Carousel',\n 'aria-roledescription': () => 'carousel',\n orientation: () => props.orientation.value ?? 'horizontal',\n role: () => 'region',\n style: () => `touch-action:${isHorizontal.value ? 'pan-y' : 'pan-x'}`,\n variant: () => props.variant.value ?? 'default',\n },\n on: {\n focusin: () => {\n stopAutoplay();\n },\n focusout: () => {\n if (props.autoplay.value) startAutoplay();\n },\n keydown: handleKeydown,\n pointerenter: () => {\n stopAutoplay();\n },\n pointerleave: () => {\n if (props.autoplay.value) startAutoplay();\n },\n },\n });\n\n // ── Lifecycle ────────────────────────────────────────────────────────────\n\n onMounted(() => {\n const pan = createPanGesture(el, {\n axis: () => (isHorizontal.value ? 'x' : 'y'),\n onEnd: ({ distance, reason }) => {\n if (reason !== 'release' || Math.abs(distance) < 48) return;\n\n if (distance < 0) next();\n else prev();\n },\n shouldStart: (event) => {\n const path = event.composedPath();\n\n return !path.some(\n (node) =>\n node instanceof HTMLElement &&\n (node.tagName === 'BUTTON' || node.tagName === 'ORE-BUTTON' || node.tagName === 'ORE-PROGRESS'),\n );\n },\n });\n const shadowRoot = el.shadowRoot!;\n const slot = shadowRoot.querySelector<HTMLSlotElement>('slot')!;\n\n // slotchange replaces MutationObserver — fires exactly when assigned\n // nodes change, scoped to this component's slot, no polling overhead.\n const onSlotChange = (): void => {\n refreshSlides();\n syncSlideVariants();\n syncActiveState();\n };\n\n slot.addEventListener('slotchange', onSlotChange);\n\n if (props.autoplay.value) {\n startAutoplay();\n }\n\n return () => {\n pan.dispose();\n stopAutoplay();\n slot.removeEventListener('slotchange', onSlotChange);\n };\n });\n\n // ── Template ─────────────────────────────────────────────────────────────\n\n const renderControls = () =>\n showControls.value\n ? html`\n <div class=\"controls\" part=\"controls\">\n <ore-button\n class=\"nav-btn prev-btn\"\n part=\"prev-btn\"\n variant=\"ghost\"\n color=${() => props.color.value}\n rounded\n icon-only\n aria-label=\"Previous slide\"\n disabled=${() => (!canGoPrev.value ? true : undefined)}\n @click=${(e: Event) => {\n e.stopPropagation();\n prev();\n }}>\n <ore-icon\n name=${() => (isHorizontal.value ? 'chevron-left' : 'chevron-up')}\n size=\"16\"\n stroke-width=\"2\"\n aria-hidden=\"true\"></ore-icon>\n </ore-button>\n <ore-button\n class=\"nav-btn next-btn\"\n part=\"next-btn\"\n variant=\"ghost\"\n color=${() => props.color.value}\n rounded\n icon-only\n aria-label=\"Next slide\"\n disabled=${() => (!canGoNext.value ? true : undefined)}\n @click=${(e: Event) => {\n e.stopPropagation();\n next();\n }}>\n <ore-icon\n name=${() => (isHorizontal.value ? 'chevron-right' : 'chevron-down')}\n size=\"16\"\n stroke-width=\"2\"\n aria-hidden=\"true\"></ore-icon>\n </ore-button>\n </div>\n `\n : html``;\n\n return html`\n <div class=\"track\" part=\"track\" aria-live=${() => (props.autoplay.value ? 'off' : 'polite')}>\n <slot></slot>\n </div>\n\n ${() =>\n showIndicators.value && slideCount.value > 1\n ? html`\n <div class=\"indicators\" part=\"indicators\" role=\"tablist\" aria-label=\"Slide indicators\">\n ${() =>\n Array.from(\n { length: slideCount.value },\n (_, i) => html`\n <button\n type=\"button\"\n role=\"tab\"\n data-index=\"${i}\"\n class=${() => `indicator${i === activeIndex.value ? ' indicator-active' : ''}`}\n aria-selected=${() => String(i === activeIndex.value)}\n tabindex=${() => (i === activeIndex.value ? '0' : '-1')}\n aria-label=\"${`Go to slide ${i + 1}`}\"\n @focus=${() => indicatorNav.set(i)}\n @click=${() => goTo(i)}\n @keydown=${(e: KeyboardEvent) => {\n const target = e.currentTarget;\n\n if (target instanceof HTMLElement) {\n const index = Number(target.dataset.index);\n\n if (Number.isInteger(index)) indicatorNav.set(index);\n }\n\n indicatorNav.handleKeydown(e);\n }}>\n <ore-progress\n aria-hidden=\"true\"\n tabindex=\"-1\"\n color=${() => props.color.value}\n type=${() => (isHorizontal.value ? 'linear' : 'vertical')}\n value=${() => (i === activeIndex.value ? 100 : 0)}\n style=${() => {\n const fillAnim = isHorizontal.value ? 'carousel-fill' : 'carousel-fill-v';\n\n return `--carousel-timeout:${props['autoplay-interval'].value ?? 5000};--carousel-animation-name:${i === activeIndex.value && props.autoplay.value ? fillAnim : 'none'}`;\n }}></ore-progress>\n </button>\n `,\n )}\n ${renderControls}\n </div>\n `\n : html`\n ${renderControls}\n `}\n `;\n },\n styles: [componentStyles],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"carousel.js","names":[],"sources":["../src/content/carousel/carousel.ts"],"sourcesContent":["import { createPanGesture } from '@vielzeug/gesture';\nimport { bind, define, getHost, html, onCleanup, onMounted, prop, useEmit } from '@vielzeug/ore';\nimport { computed, signal, watch } from '@vielzeug/ripple';\nimport { announce, createListControl, lifecycleSignal } from '../../core';\nimport type { ThemeColor } from '../../types';\nimport componentStyles from './carousel.css?inline';\nimport './carousel-slide';\n\nexport { ICON_TAG } from '../../content/icon/icon';\nexport { PROGRESS_TAG } from '../../feedback/progress/progress';\nexport { CAROUSEL_SLIDE_TAG } from './carousel-slide';\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type CarouselOrientation = 'horizontal' | 'vertical';\nexport type CarouselVariant = 'default' | 'fade' | 'filmstrip' | 'gallery';\n\nexport type OreCarouselEvents = {\n /** Fired when the active slide changes. */\n change: { index: number };\n};\n\nexport type OreCarouselProps = {\n /**\n * Whether to advance slides automatically. Defaults to `false`.\n * Opt in explicitly: autoplay on by default is a WCAG 2.1 SC 2.2.2 violation for many use cases.\n */\n autoplay?: boolean;\n /** Interval in milliseconds between automatic slide advances. Defaults to `5000`. */\n 'autoplay-interval'?: number;\n /** Theme color passed to the prev/next navigation buttons. */\n color?: ThemeColor;\n /** Accessible label for the carousel region. */\n label?: string;\n /** Whether the carousel loops from the last slide back to the first. Defaults to `true`. */\n loop?: boolean;\n /** Carousel orientation. Defaults to `'horizontal'`. */\n orientation?: CarouselOrientation;\n /** Show next/prev navigation buttons. Defaults to `true`. */\n 'show-controls'?: boolean;\n /** Show dot/indicator navigation. Defaults to `true`. */\n 'show-indicators'?: boolean;\n /** Index of the currently active slide (zero-based). Defaults to `0`. */\n 'slide-index'?: number;\n /**\n * Layout variant.\n * - `'default'`: slides translate in/out (default)\n * - `'fade'`: slides crossfade; no movement\n * - `'filmstrip'`: all slides visible side-by-side; active expands\n * - `'gallery'`: active slide fills the majority; adjacent slides show as thumbnails\n */\n variant?: CarouselVariant;\n};\n\n/**\n * An accessible, keyboard-navigable carousel / slideshow with optional\n * autoplay, swipe support, and indicator dots.\n *\n * Place `<ore-carousel-slide>` elements as direct children.\n *\n * @element ore-carousel\n * @element ore-carousel-slide - Child element for individual slides\n *\n * @attr {string} color - Theme color for navigation buttons: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'\n * @attr {boolean} autoplay - Advance slides automatically (default false)\n * @attr {number} autoplay-interval - Milliseconds between automatic advances (default 5000)\n * @attr {string} label - Accessible label for the carousel region\n * @attr {boolean} loop - Loop from last slide to first (default true)\n * @attr {string} orientation - 'horizontal' (default) | 'vertical'\n * @attr {string} variant - 'default' | 'fade' | 'filmstrip' | 'gallery'\n * @attr {boolean} show-controls - Show prev/next buttons (default true)\n * @attr {boolean} show-indicators - Show indicator dots (default true)\n * @attr {number} slide-index - Active slide index (zero-based, default 0)\n *\n * @fires change - Fires when the active slide changes. detail: { index: number }\n *\n * @slot - Place `<ore-carousel-slide>` elements here\n *\n * @cssprop --carousel-bg - Slide area background\n * @cssprop --carousel-radius - Border radius of the carousel\n * @cssprop --carousel-dot-bg - Inactive indicator color (default: var(--color-contrast-300))\n * @cssprop --carousel-dot-active-bg - Active indicator / fill color (default: var(--color-contrast-700))\n * @cssprop --carousel-transition-duration - Slide transition duration (default 0.35s; 0s under prefers-reduced-motion).\n * @cssprop --carousel-min-height - Minimum height when no explicit height is set (default 240px)\n * @cssprop --carousel-filmstrip-inactive - Width (horizontal) or height (vertical) of inactive slides in filmstrip mode\n * @cssprop --carousel-filmstrip-gap - Gap between slides in filmstrip mode\n * @cssprop --carousel-gallery-thumbnail - Width (horizontal) or height (vertical) of thumbnail slides in gallery mode (default var(--size-24))\n * @cssprop --carousel-gallery-gap - Gap between slides in gallery mode (default var(--size-2))\n *\n * @part track - The scrolling slide track element\n * @part controls - The prev/next button container\n * @part indicators - The indicator dots container\n * @part prev-btn - The previous-slide button\n * @part next-btn - The next-slide button\n *\n * @example\n * ```html\n * <ore-carousel label=\"Product highlights\" loop>\n * <ore-carousel-slide>Slide 1</ore-carousel-slide>\n * <ore-carousel-slide>Slide 2</ore-carousel-slide>\n * <ore-carousel-slide>Slide 3</ore-carousel-slide>\n * </ore-carousel>\n * ```\n */\nexport const CAROUSEL_TAG = 'ore-carousel' as const;\n\n// ── Slide-state sync helpers ──────────────────────────────────────────────────\n// Each function handles exactly one variant's attribute bookkeeping.\n// Called from a dispatch table in syncActiveState.\n\nconst syncDefaultSlides = (slides: HTMLElement[], current: number): void => {\n const count = slides.length;\n\n slides.forEach((slide, i) => {\n const active = i === current;\n\n slide.toggleAttribute('data-active', active);\n slide.setAttribute('aria-hidden', String(!active));\n slide.setAttribute('aria-label', slide.getAttribute('aria-label') ?? `Slide ${i + 1} of ${count}`);\n slide.removeAttribute('data-gallery-visible');\n slide.toggleAttribute('data-before', i < current);\n slide.toggleAttribute('data-after', i > current);\n });\n};\n\nconst syncFilmstripSlides = (slides: HTMLElement[], current: number): void => {\n const count = slides.length;\n\n slides.forEach((slide, i) => {\n const active = i === current;\n\n slide.toggleAttribute('data-active', active);\n slide.setAttribute('aria-hidden', String(!active));\n slide.setAttribute('aria-label', slide.getAttribute('aria-label') ?? `Slide ${i + 1} of ${count}`);\n slide.removeAttribute('data-gallery-visible');\n slide.removeAttribute('data-before');\n slide.removeAttribute('data-after');\n });\n};\n\nconst syncGallerySlides = (slides: HTMLElement[], current: number): void => {\n const count = slides.length;\n\n slides.forEach((slide, i) => {\n const active = i === current;\n const isPrev = current > 0 && i === current - 1;\n const isNext = current < count - 1 && i === current + 1;\n\n slide.toggleAttribute('data-active', active);\n slide.setAttribute('aria-hidden', String(!active));\n slide.setAttribute('aria-label', slide.getAttribute('aria-label') ?? `Slide ${i + 1} of ${count}`);\n slide.removeAttribute('data-before');\n slide.removeAttribute('data-after');\n slide.toggleAttribute('data-gallery-visible', active || isPrev || isNext);\n });\n};\n\n// ── ore-carousel ──────────────────────────────────────────────────────────────\n\ndefine<OreCarouselProps>(CAROUSEL_TAG, {\n props: {\n autoplay: prop.bool(false),\n 'autoplay-interval': prop.number(5000),\n color: prop.string<ThemeColor>(),\n label: prop.string('Carousel'),\n loop: prop.bool(true),\n orientation: prop.string<CarouselOrientation>('horizontal'),\n 'show-controls': prop.bool(true),\n 'show-indicators': prop.bool(true),\n 'slide-index': prop.number(0),\n variant: prop.string<CarouselVariant>('default'),\n },\n\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreCarouselEvents>();\n\n // ── State ────────────────────────────────────────────────────────────────\n\n const activeIndex = signal<number>(props['slide-index'].value ?? 0);\n let autoplayTimer: ReturnType<typeof setInterval> | null = null;\n\n // Slide cache: populated immediately, refreshed on slotchange.\n let slides: HTMLElement[] = Array.from(el.querySelectorAll<HTMLElement>(':scope > ore-carousel-slide'));\n const slideCount = signal(slides.length);\n\n const refreshSlides = (): void => {\n slides = Array.from(el.querySelectorAll<HTMLElement>(':scope > ore-carousel-slide'));\n slideCount.value = slides.length;\n };\n\n const isHorizontal = computed(() => props.orientation.value !== 'vertical');\n const looping = computed(() => props.loop.value !== false);\n const showControls = computed(() => props['show-controls'].value !== false);\n const showIndicators = computed(() => props['show-indicators'].value !== false);\n\n const canGoPrev = computed(() => looping.value || activeIndex.value > 0);\n const canGoNext = computed(() => looping.value || activeIndex.value < slideCount.value - 1);\n\n // ── Navigation ───────────────────────────────────────────────────────────\n\n const goTo = (index: number, announce_: boolean = true): void => {\n const count = slideCount.value;\n\n if (count === 0) return;\n\n const next = looping.value ? ((index % count) + count) % count : Math.max(0, Math.min(index, count - 1));\n\n if (next === activeIndex.value) return;\n\n activeIndex.value = next;\n el.setAttribute('slide-index', String(next));\n emit('change', { index: next });\n\n if (announce_) {\n announce(slides[next]?.getAttribute('aria-label') ?? `Slide ${next + 1} of ${count}`, {\n document: el.ownerDocument,\n });\n }\n };\n\n const prev = (): void => goTo(activeIndex.value - 1);\n const next = (): void => goTo(activeIndex.value + 1);\n\n // ── Sync prop → state ────────────────────────────────────────────────────\n\n watch(props['slide-index'], (v) => {\n if (typeof v === 'number' && v !== activeIndex.value) goTo(v, false);\n });\n\n // ── Sync variant + orientation → slides ──────────────────────────────────\n\n const syncSlideVariants = (): void => {\n const variant = props.variant.value ?? 'default';\n const orientation = props.orientation.value ?? 'horizontal';\n\n slides.forEach((slide) => {\n slide.setAttribute('data-variant', variant);\n slide.setAttribute('data-orientation', orientation);\n });\n };\n\n watch(\n computed(() => ({ orientation: props.orientation.value, variant: props.variant.value })),\n syncSlideVariants,\n { immediate: true },\n );\n\n // ── Sync active index → slides: dispatch table ───────────────────────────\n\n const syncTable: Record<CarouselVariant, (slides: HTMLElement[], current: number) => void> = {\n default: syncDefaultSlides,\n fade: syncDefaultSlides,\n filmstrip: syncFilmstripSlides,\n gallery: syncGallerySlides,\n };\n\n const syncActiveState = (): void => {\n const variant = (props.variant.value ?? 'default') as CarouselVariant;\n\n syncTable[variant](slides, activeIndex.value);\n };\n\n watch(activeIndex, syncActiveState, { immediate: true });\n\n // ── Autoplay ─────────────────────────────────────────────────────────────\n\n const startAutoplay = (): void => {\n if (autoplayTimer !== null) return;\n\n autoplayTimer = setInterval(next, props['autoplay-interval'].value ?? 5000);\n };\n\n const stopAutoplay = (): void => {\n if (autoplayTimer !== null) {\n clearInterval(autoplayTimer);\n autoplayTimer = null;\n }\n };\n\n watch(\n computed(() => ({ enabled: props.autoplay.value, interval: props['autoplay-interval'].value })),\n ({ enabled }) => {\n stopAutoplay();\n\n if (enabled) startAutoplay();\n },\n );\n\n // ── Keyboard navigation ──────────────────────────────────────────────────\n\n const handleKeydown = (e: KeyboardEvent): void => {\n const isH = isHorizontal.value;\n const prevKey = isH ? 'ArrowLeft' : 'ArrowUp';\n const nextKey = isH ? 'ArrowRight' : 'ArrowDown';\n\n if (e.key === prevKey) {\n e.preventDefault();\n prev();\n } else if (e.key === nextKey) {\n e.preventDefault();\n next();\n } else if (e.key === 'Home') {\n e.preventDefault();\n goTo(0);\n } else if (e.key === 'End') {\n e.preventDefault();\n goTo(slideCount.value - 1);\n }\n };\n\n const indicatorNav = createListControl<number>({\n getItems: () => Array.from({ length: slideCount.value }, (_, index) => index),\n loop: true,\n onNavigate: ({ index }) => {\n goTo(index);\n const dot = el.shadowRoot?.querySelector<HTMLElement>(`.indicator[data-index=\"${index}\"]`);\n\n dot?.focus();\n },\n orientation: () => (isHorizontal.value ? 'horizontal' : 'vertical'),\n signal: lifecycleSignal(onCleanup),\n });\n\n // ── Host bindings ────────────────────────────────────────────────────────\n\n bind({\n attr: {\n 'aria-label': () => props.label.value ?? 'Carousel',\n 'aria-roledescription': () => 'carousel',\n orientation: () => props.orientation.value ?? 'horizontal',\n role: () => 'region',\n style: () => `touch-action:${isHorizontal.value ? 'pan-y' : 'pan-x'}`,\n variant: () => props.variant.value ?? 'default',\n },\n on: {\n focusin: () => {\n stopAutoplay();\n },\n focusout: () => {\n if (props.autoplay.value) startAutoplay();\n },\n keydown: handleKeydown,\n pointerenter: () => {\n stopAutoplay();\n },\n pointerleave: () => {\n if (props.autoplay.value) startAutoplay();\n },\n },\n });\n\n // ── Lifecycle ────────────────────────────────────────────────────────────\n\n onMounted(() => {\n const pan = createPanGesture(el, {\n axis: () => (isHorizontal.value ? 'x' : 'y'),\n onEnd: ({ distance, reason }) => {\n if (reason !== 'release' || Math.abs(distance) < 48) return;\n\n if (distance < 0) next();\n else prev();\n },\n shouldStart: (event) => {\n const path = event.composedPath();\n\n return !path.some(\n (node) =>\n node instanceof HTMLElement &&\n (node.tagName === 'BUTTON' || node.tagName === 'ORE-BUTTON' || node.tagName === 'ORE-PROGRESS'),\n );\n },\n });\n const shadowRoot = el.shadowRoot!;\n const slot = shadowRoot.querySelector<HTMLSlotElement>('slot')!;\n\n // slotchange replaces MutationObserver: fires exactly when assigned\n // nodes change, scoped to this component's slot, no polling overhead.\n const onSlotChange = (): void => {\n refreshSlides();\n syncSlideVariants();\n syncActiveState();\n };\n\n slot.addEventListener('slotchange', onSlotChange);\n\n if (props.autoplay.value) {\n startAutoplay();\n }\n\n return () => {\n pan.dispose();\n stopAutoplay();\n slot.removeEventListener('slotchange', onSlotChange);\n };\n });\n\n // ── Template ─────────────────────────────────────────────────────────────\n\n const renderControls = () =>\n showControls.value\n ? html`\n <div class=\"controls\" part=\"controls\">\n <ore-button\n class=\"nav-btn prev-btn\"\n part=\"prev-btn\"\n variant=\"ghost\"\n color=${() => props.color.value}\n rounded\n icon-only\n aria-label=\"Previous slide\"\n disabled=${() => (!canGoPrev.value ? true : undefined)}\n @click=${(e: Event) => {\n e.stopPropagation();\n prev();\n }}>\n <ore-icon\n name=${() => (isHorizontal.value ? 'chevron-left' : 'chevron-up')}\n size=\"16\"\n stroke-width=\"2\"\n aria-hidden=\"true\"></ore-icon>\n </ore-button>\n <ore-button\n class=\"nav-btn next-btn\"\n part=\"next-btn\"\n variant=\"ghost\"\n color=${() => props.color.value}\n rounded\n icon-only\n aria-label=\"Next slide\"\n disabled=${() => (!canGoNext.value ? true : undefined)}\n @click=${(e: Event) => {\n e.stopPropagation();\n next();\n }}>\n <ore-icon\n name=${() => (isHorizontal.value ? 'chevron-right' : 'chevron-down')}\n size=\"16\"\n stroke-width=\"2\"\n aria-hidden=\"true\"></ore-icon>\n </ore-button>\n </div>\n `\n : html``;\n\n return html`\n <div class=\"track\" part=\"track\" aria-live=${() => (props.autoplay.value ? 'off' : 'polite')}>\n <slot></slot>\n </div>\n\n ${() =>\n showIndicators.value && slideCount.value > 1\n ? html`\n <div class=\"indicators\" part=\"indicators\" role=\"tablist\" aria-label=\"Slide indicators\">\n ${() =>\n Array.from(\n { length: slideCount.value },\n (_, i) => html`\n <button\n type=\"button\"\n role=\"tab\"\n data-index=\"${i}\"\n class=${() => `indicator${i === activeIndex.value ? ' indicator-active' : ''}`}\n aria-selected=${() => String(i === activeIndex.value)}\n tabindex=${() => (i === activeIndex.value ? '0' : '-1')}\n aria-label=\"${`Go to slide ${i + 1}`}\"\n @focus=${() => indicatorNav.set(i)}\n @click=${() => goTo(i)}\n @keydown=${(e: KeyboardEvent) => {\n const target = e.currentTarget;\n\n if (target instanceof HTMLElement) {\n const index = Number(target.dataset.index);\n\n if (Number.isInteger(index)) indicatorNav.set(index);\n }\n\n indicatorNav.handleKeydown(e);\n }}>\n <ore-progress\n aria-hidden=\"true\"\n tabindex=\"-1\"\n color=${() => props.color.value}\n type=${() => (isHorizontal.value ? 'linear' : 'vertical')}\n value=${() => (i === activeIndex.value ? 100 : 0)}\n style=${() => {\n const fillAnim = isHorizontal.value ? 'carousel-fill' : 'carousel-fill-v';\n\n return `--carousel-timeout:${props['autoplay-interval'].value ?? 5000};--carousel-animation-name:${i === activeIndex.value && props.autoplay.value ? fillAnim : 'none'}`;\n }}></ore-progress>\n </button>\n `,\n )}\n ${renderControls}\n </div>\n `\n : html`\n ${renderControls}\n `}\n `;\n },\n styles: [componentStyles],\n});\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-message.cjs","names":[],"sources":["../src/content/chat-message/chat-message.ts"],"sourcesContent":["import { define, getHost, html, onEvent, onMounted, prop, ref, useEmit, useSlots } from '@vielzeug/ore';\nimport { computed, watch } from '@vielzeug/ripple';\n\nimport { announce } from '../../core';\nimport { reducedMotionMixin } from '../../styles';\n\nimport componentStyles from './chat-message.css?inline';\n\nexport { ICON_TAG } from '../icon/icon';\nexport type ChatMessageSender = 'assistant' | 'system' | 'user';\nexport type ChatMessageStatus = 'error' | 'sending' | 'sent';\n\nconst SENDER_LABELS: Record<ChatMessageSender, string> = {\n assistant: 'Assistant',\n system: 'System',\n user: 'You',\n};\n\n/** Events emitted by the chat-message component */\nexport type OreChatMessageEvents = {\n /** Fired when the retry action is activated on a `status=\"error\"` message */\n retry: { originalEvent?: Event };\n};\n\n/** Chat message component properties */\nexport type OreChatMessageProps = {\n /** Error detail shown beneath the bubble; only visible when `status=\"error\"` */\n error?: string;\n /** Display name shown above the bubble (falls back to a generic label per `sender`) */\n name?: string;\n /** Who sent the message — controls alignment and bubble styling */\n sender?: ChatMessageSender;\n /** Delivery status for outgoing messages — shows an inline indicator and, for `\"error\"`, a retry action */\n status?: ChatMessageStatus;\n /** Append a blinking cursor after the content, for a message still streaming in */\n streaming?: boolean;\n /** ISO 8601 timestamp; rendered as a localized short time in a semantic `<time>` element */\n timestamp?: string;\n};\n\n/**\n * A single message bubble for chat/conversation UIs — sender-aware alignment, an optional\n * avatar slot, delivery status (sending/sent/error with retry), and a streaming cursor for\n * assistant messages still generating. Content is provided via the default slot, so any\n * markdown-to-HTML rendering stays the consumer's choice.\n *\n * The default slot's leading/trailing whitespace-only text nodes are trimmed in place once\n * per slot assignment (mount, and again on `slotchange`) — pretty-printed HTML's\n * indentation would otherwise render as blank lines, since the bubble preserves line breaks\n * (`white-space: pre-wrap`) for genuine multi-paragraph replies. This mutates those specific\n * text nodes' `textContent` directly; it never touches nodes appended afterward (e.g. by\n * `el.textContent += token` while streaming), only the ones present at assignment time.\n *\n * @element ore-chat-message\n *\n * @attr {string} sender - Who sent the message: 'user' | 'assistant' | 'system' (default 'assistant')\n * @attr {string} name - Display name shown above the bubble\n * @attr {string} timestamp - ISO 8601 timestamp, rendered as a localized short time\n * @attr {string} status - Delivery status: 'sending' | 'sent' | 'error'\n * @attr {string} error - Error detail shown beneath the bubble when status=\"error\"\n * @attr {boolean} streaming - Append a blinking cursor after the content\n *\n * @fires retry - Fired when the retry action is activated. detail: { originalEvent }\n *\n * @slot - Message content\n * @slot avatar - Avatar element (e.g. `<ore-avatar>`)\n * @slot actions - Action buttons shown beneath the message (copy, regenerate, feedback, …)\n *\n * @cssprop --chat-message-bg - Bubble background color (assistant/system)\n * @cssprop --chat-message-color - Bubble text color (assistant/system)\n * @cssprop --chat-message-user-bg - Bubble background color (user)\n * @cssprop --chat-message-user-color - Bubble text color (user)\n * @cssprop --chat-message-radius - Bubble border radius\n * @cssprop --chat-message-max-width - Maximum bubble width\n *\n * @part row - Root row container (avatar + column)\n * @part avatar - Avatar slot container\n * @part column - Column containing name, bubble, meta, and actions\n * @part name - Sender display name\n * @part bubble - Message bubble container\n * @part content - Content container inside the bubble\n * @part cursor - Blinking streaming cursor\n * @part meta - Row of timestamp, status, and error text below the bubble\n * @part timestamp - Timestamp `<time>` element\n * @part status - Status indicator\n * @part error - Error text\n * @part retry - Retry button (status=\"error\" only)\n * @part actions - Actions slot container\n *\n * @example\n * ```html\n * <ore-chat-message sender=\"user\" timestamp=\"2024-01-01T12:00:00Z\" status=\"sent\">\n * What's the weather like today?\n * </ore-chat-message>\n *\n * <ore-chat-message sender=\"assistant\" name=\"Assistant\" streaming>\n * Let me check that for you\n * </ore-chat-message>\n * ```\n */\nexport const CHAT_MESSAGE_TAG = 'ore-chat-message' as const;\ndefine<OreChatMessageProps>(CHAT_MESSAGE_TAG, {\n props: {\n error: prop.string(),\n name: prop.string(),\n sender: prop.oneOf(['user', 'assistant', 'system'] as const, 'assistant'),\n status: prop.string<ChatMessageStatus>(),\n streaming: prop.bool(false),\n timestamp: prop.string(),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreChatMessageEvents>();\n const slots = useSlots();\n\n const senderLabel = () => SENDER_LABELS[props.sender.value ?? 'assistant'];\n\n const bubbleLabel = computed(() => `Message from ${props.name.value || senderLabel()}`);\n\n const formattedTime = computed(() => {\n const raw = props.timestamp.value;\n\n if (!raw) return '';\n\n const date = new Date(raw);\n\n if (Number.isNaN(date.getTime())) return '';\n\n return new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' }).format(date);\n });\n\n const hasMeta = computed(\n () => Boolean(formattedTime.value) || Boolean(props.status.value) || Boolean(props.error.value),\n );\n\n // Announce delivery failures — the visible retry button and error text are always present\n // for sighted users, but a failed send is exactly the kind of state change a screen\n // reader user could otherwise miss entirely. Keyed on `error` text too (not just\n // `status`) so a retry that fails again with a *different* reason re-announces even\n // though `status` never left `\"error\"` in between; a second failure with the identical\n // message doesn't re-fire, since the source value hasn't actually changed.\n watch(\n () => (props.status.value === 'error' ? (props.error.value ?? '') : null),\n (errorText) => {\n if (errorText !== null) {\n announce(`Message failed to send${errorText ? `: ${errorText}` : ''}`, {\n document: el.ownerDocument,\n politeness: 'assertive',\n });\n }\n },\n );\n\n function handleRetry(e: Event): void {\n emit('retry', { originalEvent: e });\n }\n\n // ── Trim author-time indentation whitespace from the default slot ──\n // `.content` uses `white-space: pre-wrap` so genuine multi-paragraph replies keep their\n // line breaks — but that also preserves the leading/trailing newline + indentation from\n // pretty-printed HTML (`<ore-chat-message>\\n Hello\\n</ore-chat-message>`), rendering as\n // visible blank lines around the text. Trim only the outermost edges once per slot\n // assignment; appending tokens to an existing text node for streaming doesn't re-fire\n // `slotchange`, so this never interferes with in-progress streaming updates.\n //\n // A named-slotted sibling (e.g. `<ore-avatar slot=\"avatar\">` between the opening tag and\n // the message text) splits the default slot's light-DOM text into *multiple* text nodes\n // — text nodes can't target a named slot, so each run on either side of the element is\n // assigned separately. The \"real\" content can start on a later node than index 0 (it's\n // still preceded by its own leading indentation), so this walks inward from each end,\n // fully clearing whitespace-only nodes and stopping at the first node with real content\n // on each side — rather than only touching the very first/last assigned node.\n const contentSlotRef = ref<HTMLSlotElement>();\n\n function trimSlotEdgeWhitespace(): void {\n const nodes = contentSlotRef.value?.assignedNodes({ flatten: true });\n\n if (!nodes || nodes.length === 0) return;\n\n for (const node of nodes) {\n if (node.nodeType !== Node.TEXT_NODE) break;\n\n const trimmed = (node.textContent ?? '').replace(/^\\s+/, '');\n\n node.textContent = trimmed;\n\n if (trimmed !== '') break;\n }\n\n for (let i = nodes.length - 1; i >= 0; i--) {\n const node = nodes[i];\n\n if (node.nodeType !== Node.TEXT_NODE) break;\n\n const trimmed = (node.textContent ?? '').replace(/\\s+$/, '');\n\n node.textContent = trimmed;\n\n if (trimmed !== '') break;\n }\n }\n\n onMounted(() => {\n trimSlotEdgeWhitespace();\n\n const slotEl = contentSlotRef.value;\n\n if (slotEl) onEvent(slotEl, 'slotchange', trimSlotEdgeWhitespace);\n });\n\n return html`\n <div class=\"row\" part=\"row\">\n <span class=\"avatar\" part=\"avatar\" ?hidden=\"${() => !slots.has('avatar').value}\">\n <slot name=\"avatar\"></slot>\n </span>\n <div class=\"column\" part=\"column\">\n <span class=\"name\" part=\"name\" ?hidden=\"${() => !props.name.value}\">${props.name}</span>\n <div class=\"bubble\" part=\"bubble\" role=\"article\" aria-label=\"${bubbleLabel}\">\n <div class=\"content\" part=\"content\">\n <slot class=\"content-slot\" ref=\"${contentSlotRef}\"></slot>\n <span class=\"cursor\" part=\"cursor\" aria-hidden=\"true\" ?hidden=\"${() => !props.streaming.value}\"></span>\n </div>\n </div>\n <div class=\"meta\" part=\"meta\" ?hidden=\"${() => !hasMeta.value}\">\n <time\n class=\"timestamp\"\n part=\"timestamp\"\n datetime=\"${props.timestamp}\"\n ?hidden=\"${() => !formattedTime.value}\">\n ${formattedTime}\n </time>\n <span class=\"status\" part=\"status\" data-status=\"${props.status}\" ?hidden=\"${() => !props.status.value}\">\n ${() => {\n switch (props.status.value) {\n case 'error':\n return html`\n <ore-icon name=\"alert-circle\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n `;\n case 'sending':\n return html`\n <span class=\"spinner\" aria-hidden=\"true\"></span>\n `;\n case 'sent':\n return html`\n <ore-icon name=\"check\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n `;\n default:\n return '';\n }\n }}\n </span>\n <span class=\"error-text\" part=\"error\" role=\"alert\" ?hidden=\"${() => !props.error.value}\">\n ${props.error}\n </span>\n <button\n class=\"retry\"\n part=\"retry\"\n type=\"button\"\n ?hidden=\"${() => props.status.value !== 'error'}\"\n @click=\"${handleRetry}\">\n Retry\n </button>\n </div>\n <div class=\"actions\" part=\"actions\" ?hidden=\"${() => !slots.has('actions').value}\">\n <slot name=\"actions\"></slot>\n </div>\n </div>\n </div>\n `;\n },\n styles: [reducedMotionMixin, componentStyles],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat-message.cjs","names":[],"sources":["../src/content/chat-message/chat-message.ts"],"sourcesContent":["import { define, getHost, html, onEvent, onMounted, prop, ref, useEmit, useSlots } from '@vielzeug/ore';\nimport { computed, watch } from '@vielzeug/ripple';\n\nimport { announce } from '../../core';\nimport { reducedMotionMixin } from '../../styles';\n\nimport componentStyles from './chat-message.css?inline';\n\nexport { ICON_TAG } from '../icon/icon';\nexport type ChatMessageSender = 'assistant' | 'system' | 'user';\nexport type ChatMessageStatus = 'error' | 'sending' | 'sent';\n\nconst SENDER_LABELS: Record<ChatMessageSender, string> = {\n assistant: 'Assistant',\n system: 'System',\n user: 'You',\n};\n\n/** Events emitted by the chat-message component */\nexport type OreChatMessageEvents = {\n /** Fired when the retry action is activated on a `status=\"error\"` message */\n retry: { originalEvent?: Event };\n};\n\n/** Chat message component properties */\nexport type OreChatMessageProps = {\n /** Error detail shown beneath the bubble; only visible when `status=\"error\"` */\n error?: string;\n /** Display name shown above the bubble (falls back to a generic label per `sender`) */\n name?: string;\n /** Who sent the message: controls alignment and bubble styling */\n sender?: ChatMessageSender;\n /** Delivery status for outgoing messages: shows an inline indicator and, for `\"error\"`, a retry action */\n status?: ChatMessageStatus;\n /** Append a blinking cursor after the content, for a message still streaming in */\n streaming?: boolean;\n /** ISO 8601 timestamp; rendered as a localized short time in a semantic `<time>` element */\n timestamp?: string;\n};\n\n/**\n * A single message bubble for chat/conversation UIs: sender-aware alignment, an optional\n * avatar slot, delivery status (sending/sent/error with retry), and a streaming cursor for\n * assistant messages still generating. Content is provided via the default slot, so any\n * markdown-to-HTML rendering stays the consumer's choice.\n *\n * The default slot's leading/trailing whitespace-only text nodes are trimmed in place once\n * per slot assignment (mount, and again on `slotchange`): pretty-printed HTML's\n * indentation would otherwise render as blank lines, since the bubble preserves line breaks\n * (`white-space: pre-wrap`) for genuine multi-paragraph replies. This mutates those specific\n * text nodes' `textContent` directly; it never touches nodes appended afterward (e.g. by\n * `el.textContent += token` while streaming), only the ones present at assignment time.\n *\n * @element ore-chat-message\n *\n * @attr {string} sender - Who sent the message: 'user' | 'assistant' | 'system' (default 'assistant')\n * @attr {string} name - Display name shown above the bubble\n * @attr {string} timestamp - ISO 8601 timestamp, rendered as a localized short time\n * @attr {string} status - Delivery status: 'sending' | 'sent' | 'error'\n * @attr {string} error - Error detail shown beneath the bubble when status=\"error\"\n * @attr {boolean} streaming - Append a blinking cursor after the content\n *\n * @fires retry - Fired when the retry action is activated. detail: { originalEvent }\n *\n * @slot - Message content\n * @slot avatar - Avatar element (e.g. `<ore-avatar>`)\n * @slot actions - Action buttons shown beneath the message (copy, regenerate, feedback, …)\n *\n * @cssprop --chat-message-bg - Bubble background color (assistant/system)\n * @cssprop --chat-message-color - Bubble text color (assistant/system)\n * @cssprop --chat-message-user-bg - Bubble background color (user)\n * @cssprop --chat-message-user-color - Bubble text color (user)\n * @cssprop --chat-message-radius - Bubble border radius\n * @cssprop --chat-message-max-width - Maximum bubble width\n *\n * @part row - Root row container (avatar + column)\n * @part avatar - Avatar slot container\n * @part column - Column containing name, bubble, meta, and actions\n * @part name - Sender display name\n * @part bubble - Message bubble container\n * @part content - Content container inside the bubble\n * @part cursor - Blinking streaming cursor\n * @part meta - Row of timestamp, status, and error text below the bubble\n * @part timestamp - Timestamp `<time>` element\n * @part status - Status indicator\n * @part error - Error text\n * @part retry - Retry button (status=\"error\" only)\n * @part actions - Actions slot container\n *\n * @example\n * ```html\n * <ore-chat-message sender=\"user\" timestamp=\"2024-01-01T12:00:00Z\" status=\"sent\">\n * What's the weather like today?\n * </ore-chat-message>\n *\n * <ore-chat-message sender=\"assistant\" name=\"Assistant\" streaming>\n * Let me check that for you\n * </ore-chat-message>\n * ```\n */\nexport const CHAT_MESSAGE_TAG = 'ore-chat-message' as const;\ndefine<OreChatMessageProps>(CHAT_MESSAGE_TAG, {\n props: {\n error: prop.string(),\n name: prop.string(),\n sender: prop.oneOf(['user', 'assistant', 'system'] as const, 'assistant'),\n status: prop.string<ChatMessageStatus>(),\n streaming: prop.bool(false),\n timestamp: prop.string(),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreChatMessageEvents>();\n const slots = useSlots();\n\n const senderLabel = () => SENDER_LABELS[props.sender.value ?? 'assistant'];\n\n const bubbleLabel = computed(() => `Message from ${props.name.value || senderLabel()}`);\n\n const formattedTime = computed(() => {\n const raw = props.timestamp.value;\n\n if (!raw) return '';\n\n const date = new Date(raw);\n\n if (Number.isNaN(date.getTime())) return '';\n\n return new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' }).format(date);\n });\n\n const hasMeta = computed(\n () => Boolean(formattedTime.value) || Boolean(props.status.value) || Boolean(props.error.value),\n );\n\n // Announce delivery failures: the visible retry button and error text are always present\n // for sighted users, but a failed send is exactly the kind of state change a screen\n // reader user could otherwise miss entirely. Keyed on `error` text too (not just\n // `status`) so a retry that fails again with a *different* reason re-announces even\n // though `status` never left `\"error\"` in between; a second failure with the identical\n // message doesn't re-fire, since the source value hasn't actually changed.\n watch(\n () => (props.status.value === 'error' ? (props.error.value ?? '') : null),\n (errorText) => {\n if (errorText !== null) {\n announce(`Message failed to send${errorText ? `: ${errorText}` : ''}`, {\n document: el.ownerDocument,\n politeness: 'assertive',\n });\n }\n },\n );\n\n function handleRetry(e: Event): void {\n emit('retry', { originalEvent: e });\n }\n\n // ── Trim author-time indentation whitespace from the default slot ──\n // `.content` uses `white-space: pre-wrap` so genuine multi-paragraph replies keep their\n // line breaks, but that also preserves the leading/trailing newline + indentation from\n // pretty-printed HTML (`<ore-chat-message>\\n Hello\\n</ore-chat-message>`), rendering as\n // visible blank lines around the text. Trim only the outermost edges once per slot\n // assignment; appending tokens to an existing text node for streaming doesn't re-fire\n // `slotchange`, so this never interferes with in-progress streaming updates.\n //\n // A named-slotted sibling (e.g. `<ore-avatar slot=\"avatar\">` between the opening tag and\n // the message text) splits the default slot's light-DOM text into *multiple* text nodes\n //: text nodes can't target a named slot, so each run on either side of the element is\n // assigned separately. The \"real\" content can start on a later node than index 0 (it's\n // still preceded by its own leading indentation), so this walks inward from each end,\n // fully clearing whitespace-only nodes and stopping at the first node with real content\n // on each side, rather than only touching the very first/last assigned node.\n const contentSlotRef = ref<HTMLSlotElement>();\n\n function trimSlotEdgeWhitespace(): void {\n const nodes = contentSlotRef.value?.assignedNodes({ flatten: true });\n\n if (!nodes || nodes.length === 0) return;\n\n for (const node of nodes) {\n if (node.nodeType !== Node.TEXT_NODE) break;\n\n const trimmed = (node.textContent ?? '').replace(/^\\s+/, '');\n\n node.textContent = trimmed;\n\n if (trimmed !== '') break;\n }\n\n for (let i = nodes.length - 1; i >= 0; i--) {\n const node = nodes[i];\n\n if (node.nodeType !== Node.TEXT_NODE) break;\n\n const trimmed = (node.textContent ?? '').replace(/\\s+$/, '');\n\n node.textContent = trimmed;\n\n if (trimmed !== '') break;\n }\n }\n\n onMounted(() => {\n trimSlotEdgeWhitespace();\n\n const slotEl = contentSlotRef.value;\n\n if (slotEl) onEvent(slotEl, 'slotchange', trimSlotEdgeWhitespace);\n });\n\n return html`\n <div class=\"row\" part=\"row\">\n <span class=\"avatar\" part=\"avatar\" ?hidden=\"${() => !slots.has('avatar').value}\">\n <slot name=\"avatar\"></slot>\n </span>\n <div class=\"column\" part=\"column\">\n <span class=\"name\" part=\"name\" ?hidden=\"${() => !props.name.value}\">${props.name}</span>\n <div class=\"bubble\" part=\"bubble\" role=\"article\" aria-label=\"${bubbleLabel}\">\n <div class=\"content\" part=\"content\">\n <slot class=\"content-slot\" ref=\"${contentSlotRef}\"></slot>\n <span class=\"cursor\" part=\"cursor\" aria-hidden=\"true\" ?hidden=\"${() => !props.streaming.value}\"></span>\n </div>\n </div>\n <div class=\"meta\" part=\"meta\" ?hidden=\"${() => !hasMeta.value}\">\n <time\n class=\"timestamp\"\n part=\"timestamp\"\n datetime=\"${props.timestamp}\"\n ?hidden=\"${() => !formattedTime.value}\">\n ${formattedTime}\n </time>\n <span class=\"status\" part=\"status\" data-status=\"${props.status}\" ?hidden=\"${() => !props.status.value}\">\n ${() => {\n switch (props.status.value) {\n case 'error':\n return html`\n <ore-icon name=\"alert-circle\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n `;\n case 'sending':\n return html`\n <span class=\"spinner\" aria-hidden=\"true\"></span>\n `;\n case 'sent':\n return html`\n <ore-icon name=\"check\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n `;\n default:\n return '';\n }\n }}\n </span>\n <span class=\"error-text\" part=\"error\" role=\"alert\" ?hidden=\"${() => !props.error.value}\">\n ${props.error}\n </span>\n <button\n class=\"retry\"\n part=\"retry\"\n type=\"button\"\n ?hidden=\"${() => props.status.value !== 'error'}\"\n @click=\"${handleRetry}\">\n Retry\n </button>\n </div>\n <div class=\"actions\" part=\"actions\" ?hidden=\"${() => !slots.has('actions').value}\">\n <slot name=\"actions\"></slot>\n </div>\n </div>\n </div>\n `;\n },\n styles: [reducedMotionMixin, componentStyles],\n});\n"],"mappings":"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"}
|
package/dist/chat-message.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-message.js","names":[],"sources":["../src/content/chat-message/chat-message.ts"],"sourcesContent":["import { define, getHost, html, onEvent, onMounted, prop, ref, useEmit, useSlots } from '@vielzeug/ore';\nimport { computed, watch } from '@vielzeug/ripple';\n\nimport { announce } from '../../core';\nimport { reducedMotionMixin } from '../../styles';\n\nimport componentStyles from './chat-message.css?inline';\n\nexport { ICON_TAG } from '../icon/icon';\nexport type ChatMessageSender = 'assistant' | 'system' | 'user';\nexport type ChatMessageStatus = 'error' | 'sending' | 'sent';\n\nconst SENDER_LABELS: Record<ChatMessageSender, string> = {\n assistant: 'Assistant',\n system: 'System',\n user: 'You',\n};\n\n/** Events emitted by the chat-message component */\nexport type OreChatMessageEvents = {\n /** Fired when the retry action is activated on a `status=\"error\"` message */\n retry: { originalEvent?: Event };\n};\n\n/** Chat message component properties */\nexport type OreChatMessageProps = {\n /** Error detail shown beneath the bubble; only visible when `status=\"error\"` */\n error?: string;\n /** Display name shown above the bubble (falls back to a generic label per `sender`) */\n name?: string;\n /** Who sent the message — controls alignment and bubble styling */\n sender?: ChatMessageSender;\n /** Delivery status for outgoing messages — shows an inline indicator and, for `\"error\"`, a retry action */\n status?: ChatMessageStatus;\n /** Append a blinking cursor after the content, for a message still streaming in */\n streaming?: boolean;\n /** ISO 8601 timestamp; rendered as a localized short time in a semantic `<time>` element */\n timestamp?: string;\n};\n\n/**\n * A single message bubble for chat/conversation UIs — sender-aware alignment, an optional\n * avatar slot, delivery status (sending/sent/error with retry), and a streaming cursor for\n * assistant messages still generating. Content is provided via the default slot, so any\n * markdown-to-HTML rendering stays the consumer's choice.\n *\n * The default slot's leading/trailing whitespace-only text nodes are trimmed in place once\n * per slot assignment (mount, and again on `slotchange`) — pretty-printed HTML's\n * indentation would otherwise render as blank lines, since the bubble preserves line breaks\n * (`white-space: pre-wrap`) for genuine multi-paragraph replies. This mutates those specific\n * text nodes' `textContent` directly; it never touches nodes appended afterward (e.g. by\n * `el.textContent += token` while streaming), only the ones present at assignment time.\n *\n * @element ore-chat-message\n *\n * @attr {string} sender - Who sent the message: 'user' | 'assistant' | 'system' (default 'assistant')\n * @attr {string} name - Display name shown above the bubble\n * @attr {string} timestamp - ISO 8601 timestamp, rendered as a localized short time\n * @attr {string} status - Delivery status: 'sending' | 'sent' | 'error'\n * @attr {string} error - Error detail shown beneath the bubble when status=\"error\"\n * @attr {boolean} streaming - Append a blinking cursor after the content\n *\n * @fires retry - Fired when the retry action is activated. detail: { originalEvent }\n *\n * @slot - Message content\n * @slot avatar - Avatar element (e.g. `<ore-avatar>`)\n * @slot actions - Action buttons shown beneath the message (copy, regenerate, feedback, …)\n *\n * @cssprop --chat-message-bg - Bubble background color (assistant/system)\n * @cssprop --chat-message-color - Bubble text color (assistant/system)\n * @cssprop --chat-message-user-bg - Bubble background color (user)\n * @cssprop --chat-message-user-color - Bubble text color (user)\n * @cssprop --chat-message-radius - Bubble border radius\n * @cssprop --chat-message-max-width - Maximum bubble width\n *\n * @part row - Root row container (avatar + column)\n * @part avatar - Avatar slot container\n * @part column - Column containing name, bubble, meta, and actions\n * @part name - Sender display name\n * @part bubble - Message bubble container\n * @part content - Content container inside the bubble\n * @part cursor - Blinking streaming cursor\n * @part meta - Row of timestamp, status, and error text below the bubble\n * @part timestamp - Timestamp `<time>` element\n * @part status - Status indicator\n * @part error - Error text\n * @part retry - Retry button (status=\"error\" only)\n * @part actions - Actions slot container\n *\n * @example\n * ```html\n * <ore-chat-message sender=\"user\" timestamp=\"2024-01-01T12:00:00Z\" status=\"sent\">\n * What's the weather like today?\n * </ore-chat-message>\n *\n * <ore-chat-message sender=\"assistant\" name=\"Assistant\" streaming>\n * Let me check that for you\n * </ore-chat-message>\n * ```\n */\nexport const CHAT_MESSAGE_TAG = 'ore-chat-message' as const;\ndefine<OreChatMessageProps>(CHAT_MESSAGE_TAG, {\n props: {\n error: prop.string(),\n name: prop.string(),\n sender: prop.oneOf(['user', 'assistant', 'system'] as const, 'assistant'),\n status: prop.string<ChatMessageStatus>(),\n streaming: prop.bool(false),\n timestamp: prop.string(),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreChatMessageEvents>();\n const slots = useSlots();\n\n const senderLabel = () => SENDER_LABELS[props.sender.value ?? 'assistant'];\n\n const bubbleLabel = computed(() => `Message from ${props.name.value || senderLabel()}`);\n\n const formattedTime = computed(() => {\n const raw = props.timestamp.value;\n\n if (!raw) return '';\n\n const date = new Date(raw);\n\n if (Number.isNaN(date.getTime())) return '';\n\n return new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' }).format(date);\n });\n\n const hasMeta = computed(\n () => Boolean(formattedTime.value) || Boolean(props.status.value) || Boolean(props.error.value),\n );\n\n // Announce delivery failures — the visible retry button and error text are always present\n // for sighted users, but a failed send is exactly the kind of state change a screen\n // reader user could otherwise miss entirely. Keyed on `error` text too (not just\n // `status`) so a retry that fails again with a *different* reason re-announces even\n // though `status` never left `\"error\"` in between; a second failure with the identical\n // message doesn't re-fire, since the source value hasn't actually changed.\n watch(\n () => (props.status.value === 'error' ? (props.error.value ?? '') : null),\n (errorText) => {\n if (errorText !== null) {\n announce(`Message failed to send${errorText ? `: ${errorText}` : ''}`, {\n document: el.ownerDocument,\n politeness: 'assertive',\n });\n }\n },\n );\n\n function handleRetry(e: Event): void {\n emit('retry', { originalEvent: e });\n }\n\n // ── Trim author-time indentation whitespace from the default slot ──\n // `.content` uses `white-space: pre-wrap` so genuine multi-paragraph replies keep their\n // line breaks — but that also preserves the leading/trailing newline + indentation from\n // pretty-printed HTML (`<ore-chat-message>\\n Hello\\n</ore-chat-message>`), rendering as\n // visible blank lines around the text. Trim only the outermost edges once per slot\n // assignment; appending tokens to an existing text node for streaming doesn't re-fire\n // `slotchange`, so this never interferes with in-progress streaming updates.\n //\n // A named-slotted sibling (e.g. `<ore-avatar slot=\"avatar\">` between the opening tag and\n // the message text) splits the default slot's light-DOM text into *multiple* text nodes\n // — text nodes can't target a named slot, so each run on either side of the element is\n // assigned separately. The \"real\" content can start on a later node than index 0 (it's\n // still preceded by its own leading indentation), so this walks inward from each end,\n // fully clearing whitespace-only nodes and stopping at the first node with real content\n // on each side — rather than only touching the very first/last assigned node.\n const contentSlotRef = ref<HTMLSlotElement>();\n\n function trimSlotEdgeWhitespace(): void {\n const nodes = contentSlotRef.value?.assignedNodes({ flatten: true });\n\n if (!nodes || nodes.length === 0) return;\n\n for (const node of nodes) {\n if (node.nodeType !== Node.TEXT_NODE) break;\n\n const trimmed = (node.textContent ?? '').replace(/^\\s+/, '');\n\n node.textContent = trimmed;\n\n if (trimmed !== '') break;\n }\n\n for (let i = nodes.length - 1; i >= 0; i--) {\n const node = nodes[i];\n\n if (node.nodeType !== Node.TEXT_NODE) break;\n\n const trimmed = (node.textContent ?? '').replace(/\\s+$/, '');\n\n node.textContent = trimmed;\n\n if (trimmed !== '') break;\n }\n }\n\n onMounted(() => {\n trimSlotEdgeWhitespace();\n\n const slotEl = contentSlotRef.value;\n\n if (slotEl) onEvent(slotEl, 'slotchange', trimSlotEdgeWhitespace);\n });\n\n return html`\n <div class=\"row\" part=\"row\">\n <span class=\"avatar\" part=\"avatar\" ?hidden=\"${() => !slots.has('avatar').value}\">\n <slot name=\"avatar\"></slot>\n </span>\n <div class=\"column\" part=\"column\">\n <span class=\"name\" part=\"name\" ?hidden=\"${() => !props.name.value}\">${props.name}</span>\n <div class=\"bubble\" part=\"bubble\" role=\"article\" aria-label=\"${bubbleLabel}\">\n <div class=\"content\" part=\"content\">\n <slot class=\"content-slot\" ref=\"${contentSlotRef}\"></slot>\n <span class=\"cursor\" part=\"cursor\" aria-hidden=\"true\" ?hidden=\"${() => !props.streaming.value}\"></span>\n </div>\n </div>\n <div class=\"meta\" part=\"meta\" ?hidden=\"${() => !hasMeta.value}\">\n <time\n class=\"timestamp\"\n part=\"timestamp\"\n datetime=\"${props.timestamp}\"\n ?hidden=\"${() => !formattedTime.value}\">\n ${formattedTime}\n </time>\n <span class=\"status\" part=\"status\" data-status=\"${props.status}\" ?hidden=\"${() => !props.status.value}\">\n ${() => {\n switch (props.status.value) {\n case 'error':\n return html`\n <ore-icon name=\"alert-circle\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n `;\n case 'sending':\n return html`\n <span class=\"spinner\" aria-hidden=\"true\"></span>\n `;\n case 'sent':\n return html`\n <ore-icon name=\"check\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n `;\n default:\n return '';\n }\n }}\n </span>\n <span class=\"error-text\" part=\"error\" role=\"alert\" ?hidden=\"${() => !props.error.value}\">\n ${props.error}\n </span>\n <button\n class=\"retry\"\n part=\"retry\"\n type=\"button\"\n ?hidden=\"${() => props.status.value !== 'error'}\"\n @click=\"${handleRetry}\">\n Retry\n </button>\n </div>\n <div class=\"actions\" part=\"actions\" ?hidden=\"${() => !slots.has('actions').value}\">\n <slot name=\"actions\"></slot>\n </div>\n </div>\n </div>\n `;\n },\n styles: [reducedMotionMixin, componentStyles],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat-message.js","names":[],"sources":["../src/content/chat-message/chat-message.ts"],"sourcesContent":["import { define, getHost, html, onEvent, onMounted, prop, ref, useEmit, useSlots } from '@vielzeug/ore';\nimport { computed, watch } from '@vielzeug/ripple';\n\nimport { announce } from '../../core';\nimport { reducedMotionMixin } from '../../styles';\n\nimport componentStyles from './chat-message.css?inline';\n\nexport { ICON_TAG } from '../icon/icon';\nexport type ChatMessageSender = 'assistant' | 'system' | 'user';\nexport type ChatMessageStatus = 'error' | 'sending' | 'sent';\n\nconst SENDER_LABELS: Record<ChatMessageSender, string> = {\n assistant: 'Assistant',\n system: 'System',\n user: 'You',\n};\n\n/** Events emitted by the chat-message component */\nexport type OreChatMessageEvents = {\n /** Fired when the retry action is activated on a `status=\"error\"` message */\n retry: { originalEvent?: Event };\n};\n\n/** Chat message component properties */\nexport type OreChatMessageProps = {\n /** Error detail shown beneath the bubble; only visible when `status=\"error\"` */\n error?: string;\n /** Display name shown above the bubble (falls back to a generic label per `sender`) */\n name?: string;\n /** Who sent the message: controls alignment and bubble styling */\n sender?: ChatMessageSender;\n /** Delivery status for outgoing messages: shows an inline indicator and, for `\"error\"`, a retry action */\n status?: ChatMessageStatus;\n /** Append a blinking cursor after the content, for a message still streaming in */\n streaming?: boolean;\n /** ISO 8601 timestamp; rendered as a localized short time in a semantic `<time>` element */\n timestamp?: string;\n};\n\n/**\n * A single message bubble for chat/conversation UIs: sender-aware alignment, an optional\n * avatar slot, delivery status (sending/sent/error with retry), and a streaming cursor for\n * assistant messages still generating. Content is provided via the default slot, so any\n * markdown-to-HTML rendering stays the consumer's choice.\n *\n * The default slot's leading/trailing whitespace-only text nodes are trimmed in place once\n * per slot assignment (mount, and again on `slotchange`): pretty-printed HTML's\n * indentation would otherwise render as blank lines, since the bubble preserves line breaks\n * (`white-space: pre-wrap`) for genuine multi-paragraph replies. This mutates those specific\n * text nodes' `textContent` directly; it never touches nodes appended afterward (e.g. by\n * `el.textContent += token` while streaming), only the ones present at assignment time.\n *\n * @element ore-chat-message\n *\n * @attr {string} sender - Who sent the message: 'user' | 'assistant' | 'system' (default 'assistant')\n * @attr {string} name - Display name shown above the bubble\n * @attr {string} timestamp - ISO 8601 timestamp, rendered as a localized short time\n * @attr {string} status - Delivery status: 'sending' | 'sent' | 'error'\n * @attr {string} error - Error detail shown beneath the bubble when status=\"error\"\n * @attr {boolean} streaming - Append a blinking cursor after the content\n *\n * @fires retry - Fired when the retry action is activated. detail: { originalEvent }\n *\n * @slot - Message content\n * @slot avatar - Avatar element (e.g. `<ore-avatar>`)\n * @slot actions - Action buttons shown beneath the message (copy, regenerate, feedback, …)\n *\n * @cssprop --chat-message-bg - Bubble background color (assistant/system)\n * @cssprop --chat-message-color - Bubble text color (assistant/system)\n * @cssprop --chat-message-user-bg - Bubble background color (user)\n * @cssprop --chat-message-user-color - Bubble text color (user)\n * @cssprop --chat-message-radius - Bubble border radius\n * @cssprop --chat-message-max-width - Maximum bubble width\n *\n * @part row - Root row container (avatar + column)\n * @part avatar - Avatar slot container\n * @part column - Column containing name, bubble, meta, and actions\n * @part name - Sender display name\n * @part bubble - Message bubble container\n * @part content - Content container inside the bubble\n * @part cursor - Blinking streaming cursor\n * @part meta - Row of timestamp, status, and error text below the bubble\n * @part timestamp - Timestamp `<time>` element\n * @part status - Status indicator\n * @part error - Error text\n * @part retry - Retry button (status=\"error\" only)\n * @part actions - Actions slot container\n *\n * @example\n * ```html\n * <ore-chat-message sender=\"user\" timestamp=\"2024-01-01T12:00:00Z\" status=\"sent\">\n * What's the weather like today?\n * </ore-chat-message>\n *\n * <ore-chat-message sender=\"assistant\" name=\"Assistant\" streaming>\n * Let me check that for you\n * </ore-chat-message>\n * ```\n */\nexport const CHAT_MESSAGE_TAG = 'ore-chat-message' as const;\ndefine<OreChatMessageProps>(CHAT_MESSAGE_TAG, {\n props: {\n error: prop.string(),\n name: prop.string(),\n sender: prop.oneOf(['user', 'assistant', 'system'] as const, 'assistant'),\n status: prop.string<ChatMessageStatus>(),\n streaming: prop.bool(false),\n timestamp: prop.string(),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreChatMessageEvents>();\n const slots = useSlots();\n\n const senderLabel = () => SENDER_LABELS[props.sender.value ?? 'assistant'];\n\n const bubbleLabel = computed(() => `Message from ${props.name.value || senderLabel()}`);\n\n const formattedTime = computed(() => {\n const raw = props.timestamp.value;\n\n if (!raw) return '';\n\n const date = new Date(raw);\n\n if (Number.isNaN(date.getTime())) return '';\n\n return new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' }).format(date);\n });\n\n const hasMeta = computed(\n () => Boolean(formattedTime.value) || Boolean(props.status.value) || Boolean(props.error.value),\n );\n\n // Announce delivery failures: the visible retry button and error text are always present\n // for sighted users, but a failed send is exactly the kind of state change a screen\n // reader user could otherwise miss entirely. Keyed on `error` text too (not just\n // `status`) so a retry that fails again with a *different* reason re-announces even\n // though `status` never left `\"error\"` in between; a second failure with the identical\n // message doesn't re-fire, since the source value hasn't actually changed.\n watch(\n () => (props.status.value === 'error' ? (props.error.value ?? '') : null),\n (errorText) => {\n if (errorText !== null) {\n announce(`Message failed to send${errorText ? `: ${errorText}` : ''}`, {\n document: el.ownerDocument,\n politeness: 'assertive',\n });\n }\n },\n );\n\n function handleRetry(e: Event): void {\n emit('retry', { originalEvent: e });\n }\n\n // ── Trim author-time indentation whitespace from the default slot ──\n // `.content` uses `white-space: pre-wrap` so genuine multi-paragraph replies keep their\n // line breaks, but that also preserves the leading/trailing newline + indentation from\n // pretty-printed HTML (`<ore-chat-message>\\n Hello\\n</ore-chat-message>`), rendering as\n // visible blank lines around the text. Trim only the outermost edges once per slot\n // assignment; appending tokens to an existing text node for streaming doesn't re-fire\n // `slotchange`, so this never interferes with in-progress streaming updates.\n //\n // A named-slotted sibling (e.g. `<ore-avatar slot=\"avatar\">` between the opening tag and\n // the message text) splits the default slot's light-DOM text into *multiple* text nodes\n //: text nodes can't target a named slot, so each run on either side of the element is\n // assigned separately. The \"real\" content can start on a later node than index 0 (it's\n // still preceded by its own leading indentation), so this walks inward from each end,\n // fully clearing whitespace-only nodes and stopping at the first node with real content\n // on each side, rather than only touching the very first/last assigned node.\n const contentSlotRef = ref<HTMLSlotElement>();\n\n function trimSlotEdgeWhitespace(): void {\n const nodes = contentSlotRef.value?.assignedNodes({ flatten: true });\n\n if (!nodes || nodes.length === 0) return;\n\n for (const node of nodes) {\n if (node.nodeType !== Node.TEXT_NODE) break;\n\n const trimmed = (node.textContent ?? '').replace(/^\\s+/, '');\n\n node.textContent = trimmed;\n\n if (trimmed !== '') break;\n }\n\n for (let i = nodes.length - 1; i >= 0; i--) {\n const node = nodes[i];\n\n if (node.nodeType !== Node.TEXT_NODE) break;\n\n const trimmed = (node.textContent ?? '').replace(/\\s+$/, '');\n\n node.textContent = trimmed;\n\n if (trimmed !== '') break;\n }\n }\n\n onMounted(() => {\n trimSlotEdgeWhitespace();\n\n const slotEl = contentSlotRef.value;\n\n if (slotEl) onEvent(slotEl, 'slotchange', trimSlotEdgeWhitespace);\n });\n\n return html`\n <div class=\"row\" part=\"row\">\n <span class=\"avatar\" part=\"avatar\" ?hidden=\"${() => !slots.has('avatar').value}\">\n <slot name=\"avatar\"></slot>\n </span>\n <div class=\"column\" part=\"column\">\n <span class=\"name\" part=\"name\" ?hidden=\"${() => !props.name.value}\">${props.name}</span>\n <div class=\"bubble\" part=\"bubble\" role=\"article\" aria-label=\"${bubbleLabel}\">\n <div class=\"content\" part=\"content\">\n <slot class=\"content-slot\" ref=\"${contentSlotRef}\"></slot>\n <span class=\"cursor\" part=\"cursor\" aria-hidden=\"true\" ?hidden=\"${() => !props.streaming.value}\"></span>\n </div>\n </div>\n <div class=\"meta\" part=\"meta\" ?hidden=\"${() => !hasMeta.value}\">\n <time\n class=\"timestamp\"\n part=\"timestamp\"\n datetime=\"${props.timestamp}\"\n ?hidden=\"${() => !formattedTime.value}\">\n ${formattedTime}\n </time>\n <span class=\"status\" part=\"status\" data-status=\"${props.status}\" ?hidden=\"${() => !props.status.value}\">\n ${() => {\n switch (props.status.value) {\n case 'error':\n return html`\n <ore-icon name=\"alert-circle\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n `;\n case 'sending':\n return html`\n <span class=\"spinner\" aria-hidden=\"true\"></span>\n `;\n case 'sent':\n return html`\n <ore-icon name=\"check\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n `;\n default:\n return '';\n }\n }}\n </span>\n <span class=\"error-text\" part=\"error\" role=\"alert\" ?hidden=\"${() => !props.error.value}\">\n ${props.error}\n </span>\n <button\n class=\"retry\"\n part=\"retry\"\n type=\"button\"\n ?hidden=\"${() => props.status.value !== 'error'}\"\n @click=\"${handleRetry}\">\n Retry\n </button>\n </div>\n <div class=\"actions\" part=\"actions\" ?hidden=\"${() => !slots.has('actions').value}\">\n <slot name=\"actions\"></slot>\n </div>\n </div>\n </div>\n `;\n },\n styles: [reducedMotionMixin, componentStyles],\n});\n"],"mappings":"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"}
|
package/dist/chat-panel.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-panel.cjs","names":[],"sources":["../src/content/chat-panel/chat-panel.ts"],"sourcesContent":["import { define, each, getHost, html, onCleanup, onMounted, prop, ref, useEmit, when } from '@vielzeug/ore';\nimport { watch } from '@vielzeug/ripple';\n\nimport type { DialogCloseReason, OverlayOpenChangeDetail, OverlayOpenReason } from '../../core';\nimport { reducedMotionMixin } from '../../styles';\n\nimport componentStyles from './chat-panel.css?inline';\n\nexport { CHIP_TAG } from '../../feedback/chip/chip';\nexport { BUTTON_TAG } from '../../inputs/button/button';\nexport { MESSAGE_COMPOSER_TAG } from '../../inputs/message-composer/message-composer';\nexport { AVATAR_TAG } from '../avatar/avatar';\nexport { CHAT_MESSAGE_TAG } from '../chat-message/chat-message';\nexport { ICON_TAG } from '../icon/icon';\n/** Who authored a panel message. */\nexport type OreChatPanelSender = 'assistant' | 'user';\n\n/** A message in the panel's transcript. `action` renders a follow-up button under assistant replies. */\nexport type OreChatPanelMessage = {\n /** Optional follow-up action shown as a button beneath the message. */\n action?: { label: string; payload?: unknown };\n sender: OreChatPanelSender;\n text: string;\n};\n\n/** A suggested question shown while the transcript holds only the greeting. */\nexport type OreChatPanelSuggestion = { label: string; value: string };\n\n/** Labels the panel renders; every one has an English default for consumers without i18n. */\nexport type OreChatPanelLabels = Partial<{\n assistantName: string;\n close: string;\n composerLabel: string;\n composerPlaceholder: string;\n conversation: string;\n startOver: string;\n status: string;\n suggestions: string;\n}>;\n\n/** Element interface exposing the imperative API for `ore-chat-panel`. */\nexport interface ChatPanelElement extends HTMLElement, OreChatPanelProps {\n /** Closes the panel and restores focus to the element passed to `show()`. */\n hide(): void;\n /** Opens the panel, remembering `trigger` to restore focus to when it closes. */\n show(trigger?: HTMLElement): void;\n}\n\nexport type OreChatPanelEvents = {\n /** A composer send or suggestion click was submitted; append the assistant reply to `messages`. */\n send: { text: string };\n /** The action button on a message was activated; `payload` is whatever the message carried. */\n action: { payload: unknown };\n /** The transcript was cleared back to `messages` via the Start over button. */\n reset: Record<string, never>;\n /** Open state changed, with the reason it changed. */\n 'open-change': OverlayOpenChangeDetail;\n};\n\nexport type OreChatPanelProps = {\n /** Accessible name for the panel region. */\n label?: string;\n /** Suggested questions shown while the transcript holds only the greeting. */\n suggestions?: OreChatPanelSuggestion[];\n /** Transcript. Mutate the array and reassign to update the panel. */\n messages?: OreChatPanelMessage[];\n /** Initials for the assistant avatar (default `'V'`). */\n initials?: string;\n /** Maximum composer length (default `240`). */\n maxlength?: number;\n /** All rendered labels, each with an English default. */\n labels?: OreChatPanelLabels;\n /** Open the panel. */\n open?: boolean;\n};\n\nconst DEFAULT_LABELS = {\n assistantName: 'Assistant',\n close: 'Close',\n composerLabel: 'Message',\n composerPlaceholder: 'Ask a question…',\n conversation: 'Conversation',\n startOver: 'Start over',\n status: 'Demo assistant · replies instantly',\n suggestions: 'Suggested questions',\n} as const;\n\nconst DEFAULT_INITIALS = 'V';\nconst DEFAULT_MAXLENGTH = 240;\n\n/**\n * A self-contained assistant chat surface — the fixed corner window a demo or support\n * flow needs: header with title, status, Start over and close; a live-region message\n * transcript built from `ore-chat-message`; suggested questions while the transcript\n * holds only the greeting; and an `ore-message-composer` footer.\n *\n * The panel owns presentation and interaction only. Reply generation, persistence, and\n * action semantics stay with the consumer: listen for `send`, append the assistant reply\n * to `messages`, and reassign the array; handle `action` for whatever a message's action\n * payload means in your app.\n *\n * @element ore-chat-panel\n *\n * @attr {string} label - Accessible name for the panel region\n * @attr {boolean} open - Open state\n * @attr {string} initials - Assistant avatar initials (default 'V')\n * @attr {number} maxlength - Composer character limit (default 240)\n *\n * @fires send - A send or suggestion was submitted. detail: { text }\n * @fires action - A message action button was activated. detail: { payload }\n * @fires reset - The transcript was cleared via Start over.\n * @fires open-change - Open state changed. detail: { open, reason }\n *\n * @slot header - Replaces the default title/status header content\n *\n * @cssprop --chat-panel-width - Panel width (default min(420px, viewport-fit))\n * @cssprop --chat-panel-height - Panel height (default min(640px, viewport-fit))\n * @cssprop --chat-panel-inset - Distance from the viewport corner (default --size-6)\n * @cssprop --chat-panel-z-index - Stacking order (default --z-fixed)\n *\n * @part window - The fixed panel window\n * @part header - Header row\n * @part body - Scrollable transcript area\n * @part messages - Message list (role=log)\n * @part suggestions - Suggestion chips row\n * @part composer - Composer footer\n *\n * @example\n * ```html\n * <ore-chat-panel id=\"advisor\" label=\"Model advisor\"></ore-chat-panel>\n * <script type=\"module\">\n * import '@vielzeug/refine/chat-panel';\n *\n * const panel = document.getElementById('advisor');\n * panel.messages = [{ sender: 'assistant', text: 'Hi! What can I help with?' }];\n * panel.addEventListener('send', (event) => {\n * panel.messages = [\n * ...panel.messages,\n * { sender: 'user', text: event.detail.text },\n * { sender: 'assistant', text: 'Let me look into that.' },\n * ];\n * });\n * panel.show();\n * </script>\n * ```\n */\nexport const CHAT_PANEL_TAG = 'ore-chat-panel' as const;\ndefine<OreChatPanelProps>(CHAT_PANEL_TAG, {\n props: {\n initials: prop.string(DEFAULT_INITIALS),\n label: prop.string(),\n labels: prop.data<Partial<OreChatPanelLabels>>({}),\n maxlength: prop.number(DEFAULT_MAXLENGTH),\n messages: prop.data<OreChatPanelMessage[]>([]),\n open: prop.bool(false),\n suggestions: prop.data<OreChatPanelSuggestion[]>([]),\n },\n setup(props) {\n const host = getHost() as ChatPanelElement;\n const emit = useEmit<OreChatPanelEvents>();\n const messageList = ref<HTMLElement>();\n const panel = ref<HTMLElement>();\n let returnFocus: HTMLElement | undefined;\n // Why the panel last changed open state, so the `open-change` emitted by the\n // reactive `open` watch can report the reason even when the change came from a\n // button, Escape, or `show()`/`hide()` rather than a direct `open` assignment.\n let pendingReason: OverlayOpenReason | DialogCloseReason = 'programmatic';\n\n const label = (key: keyof typeof DEFAULT_LABELS): string => props.labels.value?.[key] ?? DEFAULT_LABELS[key];\n // `prop.data` props are typed optional (the props interface is the public\n // shape), so reads fall back to the same defaults the prop definitions set.\n const messages = (): OreChatPanelMessage[] => props.messages.value ?? [];\n const suggestions = (): OreChatPanelSuggestion[] => props.suggestions.value ?? [];\n\n // `props.open` is a read-only Readable in setup; the element's own `open`\n // accessor (installed by `define`) is the writable side of the same signal and\n // reflects the attribute, so every close path routes through it.\n const requestClose = (reason: DialogCloseReason): void => {\n pendingReason = reason;\n host.open = false;\n };\n\n const requestOpen = (reason: OverlayOpenReason, trigger?: HTMLElement): void => {\n returnFocus = trigger;\n pendingReason = reason;\n host.open = true;\n };\n\n host.show = (trigger) => requestOpen('programmatic', trigger);\n host.hide = () => requestClose('programmatic');\n\n // Single source of truth: `open` is the only reactive open state, so attribute\n // changes, property writes, and show()/hide() all flow through here.\n watch(\n () => props.open.value,\n (open) => {\n emit('open-change', { open: open ?? false, reason: pendingReason });\n\n if (open) {\n requestAnimationFrame(() => panel.value?.focus());\n } else {\n requestAnimationFrame(() => returnFocus?.focus());\n }\n },\n );\n\n const scrollToLatest = (): void => {\n requestAnimationFrame(() =>\n messageList.value?.scrollTo({ behavior: 'auto', top: messageList.value.scrollHeight }),\n );\n };\n\n const send = (text: string): void => {\n const trimmed = text.trim();\n if (!trimmed) return;\n emit('send', { text: trimmed });\n requestAnimationFrame(scrollToLatest);\n };\n\n const reset = (): void => {\n emit('reset', {});\n requestAnimationFrame(scrollToLatest);\n };\n\n // The composer's own `send` is composed and would otherwise bubble past the\n // panel; the panel re-emits its own `send` with a stable `{ text }` detail, so\n // the raw event is swallowed at the panel boundary.\n const handleComposerSend = (event: CustomEvent<{ value: string }>): void => {\n event.stopPropagation();\n send(event.detail.value);\n };\n\n const closeOnEscape = (event: KeyboardEvent): void => {\n if (event.key === 'Escape' && props.open.value) requestClose('escape');\n };\n onMounted(() => {\n document.addEventListener('keydown', closeOnEscape);\n });\n onCleanup(() => document.removeEventListener('keydown', closeOnEscape));\n\n return html`\n <aside\n class=\"window\"\n part=\"window\"\n role=\"region\"\n aria-label=\"${() => props.label.value ?? ''}\"\n tabindex=\"-1\"\n ?hidden=\"${() => !props.open.value}\"\n ref=\"${panel}\"\n @keydown=\"${(event: KeyboardEvent) => {\n if (event.key === 'Escape') requestClose('escape');\n }}\">\n <header class=\"header\" part=\"header\">\n <slot name=\"header\">\n <div class=\"identity\">\n <strong>${() => props.label.value ?? ''}</strong>\n <span class=\"status\"><i class=\"status-dot\" aria-hidden=\"true\"></i>${() => label('status')}</span>\n </div>\n </slot>\n <div class=\"tools\">\n ${when(\n () => messages().length > 1,\n () =>\n html`<ore-button size=\"sm\" variant=\"text\" @click=\"${reset}\">${() => label('startOver')}</ore-button>`,\n )}\n <ore-button\n icon-only\n label=\"${() => `${label('close')} ${props.label.value ?? ''}`.trim()}\"\n size=\"sm\"\n variant=\"ghost\"\n @click=\"${() => requestClose('trigger')}\">\n <ore-icon name=\"x\" size=\"17\" aria-hidden=\"true\"></ore-icon>\n </ore-button>\n </div>\n </header>\n <div class=\"body\" part=\"body\">\n <div\n class=\"messages\"\n part=\"messages\"\n role=\"log\"\n aria-live=\"polite\"\n aria-label=\"${() => label('conversation')}\"\n tabindex=\"0\"\n ref=\"${messageList}\">\n ${each(\n messages,\n (message, index) => `${index}:${message.sender}:${message.text.slice(0, 24)}`,\n (message) => html`\n <ore-chat-message\n sender=\"${() => (message.value.sender === 'user' ? 'user' : 'assistant')}\"\n name=\"${() => (message.value.sender === 'user' ? null : label('assistantName'))}\">\n ${() =>\n message.value.sender === 'assistant'\n ? html`<ore-avatar\n slot=\"avatar\"\n initials=\"${() => props.initials.value}\"\n size=\"sm\"\n color=\"primary\"></ore-avatar>`\n : ''}\n ${() => message.value.text}\n ${() =>\n message.value.action\n ? html`<ore-button\n slot=\"actions\"\n size=\"sm\"\n variant=\"text\"\n @click=\"${() => emit('action', { payload: message.value.action?.payload })}\">\n ${() => message.value.action?.label}\n <ore-icon slot=\"suffix\" name=\"arrow-right\" size=\"14\" aria-hidden=\"true\"></ore-icon>\n </ore-button>`\n : ''}\n </ore-chat-message>\n `,\n )}\n </div>\n ${when(\n () => messages().length === 1 && suggestions().length > 0,\n () => html`\n <div class=\"suggestions\" part=\"suggestions\" role=\"group\" aria-label=\"${() => label('suggestions')}\">\n ${each(\n suggestions,\n (suggestion) => suggestion.label,\n (suggestion) => html`\n <ore-chip mode=\"action\" variant=\"outline\" @click=\"${() => send(suggestion.value.value)}\">\n ${() => suggestion.value.label}\n </ore-chip>\n `,\n )}\n </div>\n `,\n )}\n </div>\n <div class=\"composer\" part=\"composer\">\n <ore-message-composer\n color=\"primary\"\n fullwidth\n label=\"${() => label('composerLabel')}\"\n maxlength=\"${() => props.maxlength.value}\"\n placeholder=\"${() => label('composerPlaceholder')}\"\n variant=\"flat\"\n @send=\"${handleComposerSend}\"></ore-message-composer>\n </div>\n </aside>\n `;\n },\n styles: [reducedMotionMixin, componentStyles],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat-panel.cjs","names":[],"sources":["../src/content/chat-panel/chat-panel.ts"],"sourcesContent":["import { define, each, getHost, html, onCleanup, onMounted, prop, ref, useEmit, when } from '@vielzeug/ore';\nimport { watch } from '@vielzeug/ripple';\n\nimport type { DialogCloseReason, OverlayOpenChangeDetail, OverlayOpenReason } from '../../core';\nimport { reducedMotionMixin } from '../../styles';\n\nimport componentStyles from './chat-panel.css?inline';\n\nexport { CHIP_TAG } from '../../feedback/chip/chip';\nexport { BUTTON_TAG } from '../../inputs/button/button';\nexport { MESSAGE_COMPOSER_TAG } from '../../inputs/message-composer/message-composer';\nexport { AVATAR_TAG } from '../avatar/avatar';\nexport { CHAT_MESSAGE_TAG } from '../chat-message/chat-message';\nexport { ICON_TAG } from '../icon/icon';\n/** Who authored a panel message. */\nexport type OreChatPanelSender = 'assistant' | 'user';\n\n/** A message in the panel's transcript. `action` renders a follow-up button under assistant replies. */\nexport type OreChatPanelMessage = {\n /** Optional follow-up action shown as a button beneath the message. */\n action?: { label: string; payload?: unknown };\n sender: OreChatPanelSender;\n text: string;\n};\n\n/** A suggested question shown while the transcript holds only the greeting. */\nexport type OreChatPanelSuggestion = { label: string; value: string };\n\n/** Labels the panel renders; every one has an English default for consumers without i18n. */\nexport type OreChatPanelLabels = Partial<{\n assistantName: string;\n close: string;\n composerLabel: string;\n composerPlaceholder: string;\n conversation: string;\n startOver: string;\n status: string;\n suggestions: string;\n}>;\n\n/** Element interface exposing the imperative API for `ore-chat-panel`. */\nexport interface ChatPanelElement extends HTMLElement, OreChatPanelProps {\n /** Closes the panel and restores focus to the element passed to `show()`. */\n hide(): void;\n /** Opens the panel, remembering `trigger` to restore focus to when it closes. */\n show(trigger?: HTMLElement): void;\n}\n\nexport type OreChatPanelEvents = {\n /** A composer send or suggestion click was submitted; append the assistant reply to `messages`. */\n send: { text: string };\n /** The action button on a message was activated; `payload` is whatever the message carried. */\n action: { payload: unknown };\n /** The transcript was cleared back to `messages` via the Start over button. */\n reset: Record<string, never>;\n /** Open state changed, with the reason it changed. */\n 'open-change': OverlayOpenChangeDetail;\n};\n\nexport type OreChatPanelProps = {\n /** Accessible name for the panel region. */\n label?: string;\n /** Suggested questions shown while the transcript holds only the greeting. */\n suggestions?: OreChatPanelSuggestion[];\n /** Transcript. Mutate the array and reassign to update the panel. */\n messages?: OreChatPanelMessage[];\n /** Initials for the assistant avatar (default `'V'`). */\n initials?: string;\n /** Maximum composer length (default `240`). */\n maxlength?: number;\n /** All rendered labels, each with an English default. */\n labels?: OreChatPanelLabels;\n /** Open the panel. */\n open?: boolean;\n};\n\nconst DEFAULT_LABELS = {\n assistantName: 'Assistant',\n close: 'Close',\n composerLabel: 'Message',\n composerPlaceholder: 'Ask a question…',\n conversation: 'Conversation',\n startOver: 'Start over',\n status: 'Demo assistant · replies instantly',\n suggestions: 'Suggested questions',\n} as const;\n\nconst DEFAULT_INITIALS = 'V';\nconst DEFAULT_MAXLENGTH = 240;\n\n/**\n * A self-contained assistant chat surface: the fixed corner window a demo or support\n * flow needs: header with title, status, Start over and close; a live-region message\n * transcript built from `ore-chat-message`; suggested questions while the transcript\n * holds only the greeting; and an `ore-message-composer` footer.\n *\n * The panel owns presentation and interaction only. Reply generation, persistence, and\n * action semantics stay with the consumer: listen for `send`, append the assistant reply\n * to `messages`, and reassign the array; handle `action` for whatever a message's action\n * payload means in your app.\n *\n * @element ore-chat-panel\n *\n * @attr {string} label - Accessible name for the panel region\n * @attr {boolean} open - Open state\n * @attr {string} initials - Assistant avatar initials (default 'V')\n * @attr {number} maxlength - Composer character limit (default 240)\n *\n * @fires send - A send or suggestion was submitted. detail: { text }\n * @fires action - A message action button was activated. detail: { payload }\n * @fires reset - The transcript was cleared via Start over.\n * @fires open-change - Open state changed. detail: { open, reason }\n *\n * @slot header - Replaces the default title/status header content\n *\n * @cssprop --chat-panel-width - Panel width (default min(420px, viewport-fit))\n * @cssprop --chat-panel-height - Panel height (default min(640px, viewport-fit))\n * @cssprop --chat-panel-inset - Distance from the viewport corner (default --size-6)\n * @cssprop --chat-panel-z-index - Stacking order (default --z-fixed)\n *\n * @part window - The fixed panel window\n * @part header - Header row\n * @part body - Scrollable transcript area\n * @part messages - Message list (role=log)\n * @part suggestions - Suggestion chips row\n * @part composer - Composer footer\n *\n * @example\n * ```html\n * <ore-chat-panel id=\"advisor\" label=\"Model advisor\"></ore-chat-panel>\n * <script type=\"module\">\n * import '@vielzeug/refine/chat-panel';\n *\n * const panel = document.getElementById('advisor');\n * panel.messages = [{ sender: 'assistant', text: 'Hi! What can I help with?' }];\n * panel.addEventListener('send', (event) => {\n * panel.messages = [\n * ...panel.messages,\n * { sender: 'user', text: event.detail.text },\n * { sender: 'assistant', text: 'Let me look into that.' },\n * ];\n * });\n * panel.show();\n * </script>\n * ```\n */\nexport const CHAT_PANEL_TAG = 'ore-chat-panel' as const;\ndefine<OreChatPanelProps>(CHAT_PANEL_TAG, {\n props: {\n initials: prop.string(DEFAULT_INITIALS),\n label: prop.string(),\n labels: prop.data<Partial<OreChatPanelLabels>>({}),\n maxlength: prop.number(DEFAULT_MAXLENGTH),\n messages: prop.data<OreChatPanelMessage[]>([]),\n open: prop.bool(false),\n suggestions: prop.data<OreChatPanelSuggestion[]>([]),\n },\n setup(props) {\n const host = getHost() as ChatPanelElement;\n const emit = useEmit<OreChatPanelEvents>();\n const messageList = ref<HTMLElement>();\n const panel = ref<HTMLElement>();\n let returnFocus: HTMLElement | undefined;\n // Why the panel last changed open state, so the `open-change` emitted by the\n // reactive `open` watch can report the reason even when the change came from a\n // button, Escape, or `show()`/`hide()` rather than a direct `open` assignment.\n let pendingReason: OverlayOpenReason | DialogCloseReason = 'programmatic';\n\n const label = (key: keyof typeof DEFAULT_LABELS): string => props.labels.value?.[key] ?? DEFAULT_LABELS[key];\n // `prop.data` props are typed optional (the props interface is the public\n // shape), so reads fall back to the same defaults the prop definitions set.\n const messages = (): OreChatPanelMessage[] => props.messages.value ?? [];\n const suggestions = (): OreChatPanelSuggestion[] => props.suggestions.value ?? [];\n\n // `props.open` is a read-only Readable in setup; the element's own `open`\n // accessor (installed by `define`) is the writable side of the same signal and\n // reflects the attribute, so every close path routes through it.\n const requestClose = (reason: DialogCloseReason): void => {\n pendingReason = reason;\n host.open = false;\n };\n\n const requestOpen = (reason: OverlayOpenReason, trigger?: HTMLElement): void => {\n returnFocus = trigger;\n pendingReason = reason;\n host.open = true;\n };\n\n host.show = (trigger) => requestOpen('programmatic', trigger);\n host.hide = () => requestClose('programmatic');\n\n // Single source of truth: `open` is the only reactive open state, so attribute\n // changes, property writes, and show()/hide() all flow through here.\n watch(\n () => props.open.value,\n (open) => {\n emit('open-change', { open: open ?? false, reason: pendingReason });\n\n if (open) {\n requestAnimationFrame(() => panel.value?.focus());\n } else {\n requestAnimationFrame(() => returnFocus?.focus());\n }\n },\n );\n\n const scrollToLatest = (): void => {\n requestAnimationFrame(() =>\n messageList.value?.scrollTo({ behavior: 'auto', top: messageList.value.scrollHeight }),\n );\n };\n\n const send = (text: string): void => {\n const trimmed = text.trim();\n if (!trimmed) return;\n emit('send', { text: trimmed });\n requestAnimationFrame(scrollToLatest);\n };\n\n const reset = (): void => {\n emit('reset', {});\n requestAnimationFrame(scrollToLatest);\n };\n\n // The composer's own `send` is composed and would otherwise bubble past the\n // panel; the panel re-emits its own `send` with a stable `{ text }` detail, so\n // the raw event is swallowed at the panel boundary.\n const handleComposerSend = (event: CustomEvent<{ value: string }>): void => {\n event.stopPropagation();\n send(event.detail.value);\n };\n\n const closeOnEscape = (event: KeyboardEvent): void => {\n if (event.key === 'Escape' && props.open.value) requestClose('escape');\n };\n onMounted(() => {\n document.addEventListener('keydown', closeOnEscape);\n });\n onCleanup(() => document.removeEventListener('keydown', closeOnEscape));\n\n return html`\n <aside\n class=\"window\"\n part=\"window\"\n role=\"region\"\n aria-label=\"${() => props.label.value ?? ''}\"\n tabindex=\"-1\"\n ?hidden=\"${() => !props.open.value}\"\n ref=\"${panel}\"\n @keydown=\"${(event: KeyboardEvent) => {\n if (event.key === 'Escape') requestClose('escape');\n }}\">\n <header class=\"header\" part=\"header\">\n <slot name=\"header\">\n <div class=\"identity\">\n <strong>${() => props.label.value ?? ''}</strong>\n <span class=\"status\"><i class=\"status-dot\" aria-hidden=\"true\"></i>${() => label('status')}</span>\n </div>\n </slot>\n <div class=\"tools\">\n ${when(\n () => messages().length > 1,\n () =>\n html`<ore-button size=\"sm\" variant=\"text\" @click=\"${reset}\">${() => label('startOver')}</ore-button>`,\n )}\n <ore-button\n icon-only\n label=\"${() => `${label('close')} ${props.label.value ?? ''}`.trim()}\"\n size=\"sm\"\n variant=\"ghost\"\n @click=\"${() => requestClose('trigger')}\">\n <ore-icon name=\"x\" size=\"17\" aria-hidden=\"true\"></ore-icon>\n </ore-button>\n </div>\n </header>\n <div class=\"body\" part=\"body\">\n <div\n class=\"messages\"\n part=\"messages\"\n role=\"log\"\n aria-live=\"polite\"\n aria-label=\"${() => label('conversation')}\"\n tabindex=\"0\"\n ref=\"${messageList}\">\n ${each(\n messages,\n (message, index) => `${index}:${message.sender}:${message.text.slice(0, 24)}`,\n (message) => html`\n <ore-chat-message\n sender=\"${() => (message.value.sender === 'user' ? 'user' : 'assistant')}\"\n name=\"${() => (message.value.sender === 'user' ? null : label('assistantName'))}\">\n ${() =>\n message.value.sender === 'assistant'\n ? html`<ore-avatar\n slot=\"avatar\"\n initials=\"${() => props.initials.value}\"\n size=\"sm\"\n color=\"primary\"></ore-avatar>`\n : ''}\n ${() => message.value.text}\n ${() =>\n message.value.action\n ? html`<ore-button\n slot=\"actions\"\n size=\"sm\"\n variant=\"text\"\n @click=\"${() => emit('action', { payload: message.value.action?.payload })}\">\n ${() => message.value.action?.label}\n <ore-icon slot=\"suffix\" name=\"arrow-right\" size=\"14\" aria-hidden=\"true\"></ore-icon>\n </ore-button>`\n : ''}\n </ore-chat-message>\n `,\n )}\n </div>\n ${when(\n () => messages().length === 1 && suggestions().length > 0,\n () => html`\n <div class=\"suggestions\" part=\"suggestions\" role=\"group\" aria-label=\"${() => label('suggestions')}\">\n ${each(\n suggestions,\n (suggestion) => suggestion.label,\n (suggestion) => html`\n <ore-chip mode=\"action\" variant=\"outline\" @click=\"${() => send(suggestion.value.value)}\">\n ${() => suggestion.value.label}\n </ore-chip>\n `,\n )}\n </div>\n `,\n )}\n </div>\n <div class=\"composer\" part=\"composer\">\n <ore-message-composer\n color=\"primary\"\n fullwidth\n label=\"${() => label('composerLabel')}\"\n maxlength=\"${() => props.maxlength.value}\"\n placeholder=\"${() => label('composerPlaceholder')}\"\n variant=\"flat\"\n @send=\"${handleComposerSend}\"></ore-message-composer>\n </div>\n </aside>\n `;\n },\n styles: [reducedMotionMixin, componentStyles],\n});\n"],"mappings":"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"}
|
package/dist/chat-panel.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-panel.js","names":[],"sources":["../src/content/chat-panel/chat-panel.ts"],"sourcesContent":["import { define, each, getHost, html, onCleanup, onMounted, prop, ref, useEmit, when } from '@vielzeug/ore';\nimport { watch } from '@vielzeug/ripple';\n\nimport type { DialogCloseReason, OverlayOpenChangeDetail, OverlayOpenReason } from '../../core';\nimport { reducedMotionMixin } from '../../styles';\n\nimport componentStyles from './chat-panel.css?inline';\n\nexport { CHIP_TAG } from '../../feedback/chip/chip';\nexport { BUTTON_TAG } from '../../inputs/button/button';\nexport { MESSAGE_COMPOSER_TAG } from '../../inputs/message-composer/message-composer';\nexport { AVATAR_TAG } from '../avatar/avatar';\nexport { CHAT_MESSAGE_TAG } from '../chat-message/chat-message';\nexport { ICON_TAG } from '../icon/icon';\n/** Who authored a panel message. */\nexport type OreChatPanelSender = 'assistant' | 'user';\n\n/** A message in the panel's transcript. `action` renders a follow-up button under assistant replies. */\nexport type OreChatPanelMessage = {\n /** Optional follow-up action shown as a button beneath the message. */\n action?: { label: string; payload?: unknown };\n sender: OreChatPanelSender;\n text: string;\n};\n\n/** A suggested question shown while the transcript holds only the greeting. */\nexport type OreChatPanelSuggestion = { label: string; value: string };\n\n/** Labels the panel renders; every one has an English default for consumers without i18n. */\nexport type OreChatPanelLabels = Partial<{\n assistantName: string;\n close: string;\n composerLabel: string;\n composerPlaceholder: string;\n conversation: string;\n startOver: string;\n status: string;\n suggestions: string;\n}>;\n\n/** Element interface exposing the imperative API for `ore-chat-panel`. */\nexport interface ChatPanelElement extends HTMLElement, OreChatPanelProps {\n /** Closes the panel and restores focus to the element passed to `show()`. */\n hide(): void;\n /** Opens the panel, remembering `trigger` to restore focus to when it closes. */\n show(trigger?: HTMLElement): void;\n}\n\nexport type OreChatPanelEvents = {\n /** A composer send or suggestion click was submitted; append the assistant reply to `messages`. */\n send: { text: string };\n /** The action button on a message was activated; `payload` is whatever the message carried. */\n action: { payload: unknown };\n /** The transcript was cleared back to `messages` via the Start over button. */\n reset: Record<string, never>;\n /** Open state changed, with the reason it changed. */\n 'open-change': OverlayOpenChangeDetail;\n};\n\nexport type OreChatPanelProps = {\n /** Accessible name for the panel region. */\n label?: string;\n /** Suggested questions shown while the transcript holds only the greeting. */\n suggestions?: OreChatPanelSuggestion[];\n /** Transcript. Mutate the array and reassign to update the panel. */\n messages?: OreChatPanelMessage[];\n /** Initials for the assistant avatar (default `'V'`). */\n initials?: string;\n /** Maximum composer length (default `240`). */\n maxlength?: number;\n /** All rendered labels, each with an English default. */\n labels?: OreChatPanelLabels;\n /** Open the panel. */\n open?: boolean;\n};\n\nconst DEFAULT_LABELS = {\n assistantName: 'Assistant',\n close: 'Close',\n composerLabel: 'Message',\n composerPlaceholder: 'Ask a question…',\n conversation: 'Conversation',\n startOver: 'Start over',\n status: 'Demo assistant · replies instantly',\n suggestions: 'Suggested questions',\n} as const;\n\nconst DEFAULT_INITIALS = 'V';\nconst DEFAULT_MAXLENGTH = 240;\n\n/**\n * A self-contained assistant chat surface — the fixed corner window a demo or support\n * flow needs: header with title, status, Start over and close; a live-region message\n * transcript built from `ore-chat-message`; suggested questions while the transcript\n * holds only the greeting; and an `ore-message-composer` footer.\n *\n * The panel owns presentation and interaction only. Reply generation, persistence, and\n * action semantics stay with the consumer: listen for `send`, append the assistant reply\n * to `messages`, and reassign the array; handle `action` for whatever a message's action\n * payload means in your app.\n *\n * @element ore-chat-panel\n *\n * @attr {string} label - Accessible name for the panel region\n * @attr {boolean} open - Open state\n * @attr {string} initials - Assistant avatar initials (default 'V')\n * @attr {number} maxlength - Composer character limit (default 240)\n *\n * @fires send - A send or suggestion was submitted. detail: { text }\n * @fires action - A message action button was activated. detail: { payload }\n * @fires reset - The transcript was cleared via Start over.\n * @fires open-change - Open state changed. detail: { open, reason }\n *\n * @slot header - Replaces the default title/status header content\n *\n * @cssprop --chat-panel-width - Panel width (default min(420px, viewport-fit))\n * @cssprop --chat-panel-height - Panel height (default min(640px, viewport-fit))\n * @cssprop --chat-panel-inset - Distance from the viewport corner (default --size-6)\n * @cssprop --chat-panel-z-index - Stacking order (default --z-fixed)\n *\n * @part window - The fixed panel window\n * @part header - Header row\n * @part body - Scrollable transcript area\n * @part messages - Message list (role=log)\n * @part suggestions - Suggestion chips row\n * @part composer - Composer footer\n *\n * @example\n * ```html\n * <ore-chat-panel id=\"advisor\" label=\"Model advisor\"></ore-chat-panel>\n * <script type=\"module\">\n * import '@vielzeug/refine/chat-panel';\n *\n * const panel = document.getElementById('advisor');\n * panel.messages = [{ sender: 'assistant', text: 'Hi! What can I help with?' }];\n * panel.addEventListener('send', (event) => {\n * panel.messages = [\n * ...panel.messages,\n * { sender: 'user', text: event.detail.text },\n * { sender: 'assistant', text: 'Let me look into that.' },\n * ];\n * });\n * panel.show();\n * </script>\n * ```\n */\nexport const CHAT_PANEL_TAG = 'ore-chat-panel' as const;\ndefine<OreChatPanelProps>(CHAT_PANEL_TAG, {\n props: {\n initials: prop.string(DEFAULT_INITIALS),\n label: prop.string(),\n labels: prop.data<Partial<OreChatPanelLabels>>({}),\n maxlength: prop.number(DEFAULT_MAXLENGTH),\n messages: prop.data<OreChatPanelMessage[]>([]),\n open: prop.bool(false),\n suggestions: prop.data<OreChatPanelSuggestion[]>([]),\n },\n setup(props) {\n const host = getHost() as ChatPanelElement;\n const emit = useEmit<OreChatPanelEvents>();\n const messageList = ref<HTMLElement>();\n const panel = ref<HTMLElement>();\n let returnFocus: HTMLElement | undefined;\n // Why the panel last changed open state, so the `open-change` emitted by the\n // reactive `open` watch can report the reason even when the change came from a\n // button, Escape, or `show()`/`hide()` rather than a direct `open` assignment.\n let pendingReason: OverlayOpenReason | DialogCloseReason = 'programmatic';\n\n const label = (key: keyof typeof DEFAULT_LABELS): string => props.labels.value?.[key] ?? DEFAULT_LABELS[key];\n // `prop.data` props are typed optional (the props interface is the public\n // shape), so reads fall back to the same defaults the prop definitions set.\n const messages = (): OreChatPanelMessage[] => props.messages.value ?? [];\n const suggestions = (): OreChatPanelSuggestion[] => props.suggestions.value ?? [];\n\n // `props.open` is a read-only Readable in setup; the element's own `open`\n // accessor (installed by `define`) is the writable side of the same signal and\n // reflects the attribute, so every close path routes through it.\n const requestClose = (reason: DialogCloseReason): void => {\n pendingReason = reason;\n host.open = false;\n };\n\n const requestOpen = (reason: OverlayOpenReason, trigger?: HTMLElement): void => {\n returnFocus = trigger;\n pendingReason = reason;\n host.open = true;\n };\n\n host.show = (trigger) => requestOpen('programmatic', trigger);\n host.hide = () => requestClose('programmatic');\n\n // Single source of truth: `open` is the only reactive open state, so attribute\n // changes, property writes, and show()/hide() all flow through here.\n watch(\n () => props.open.value,\n (open) => {\n emit('open-change', { open: open ?? false, reason: pendingReason });\n\n if (open) {\n requestAnimationFrame(() => panel.value?.focus());\n } else {\n requestAnimationFrame(() => returnFocus?.focus());\n }\n },\n );\n\n const scrollToLatest = (): void => {\n requestAnimationFrame(() =>\n messageList.value?.scrollTo({ behavior: 'auto', top: messageList.value.scrollHeight }),\n );\n };\n\n const send = (text: string): void => {\n const trimmed = text.trim();\n if (!trimmed) return;\n emit('send', { text: trimmed });\n requestAnimationFrame(scrollToLatest);\n };\n\n const reset = (): void => {\n emit('reset', {});\n requestAnimationFrame(scrollToLatest);\n };\n\n // The composer's own `send` is composed and would otherwise bubble past the\n // panel; the panel re-emits its own `send` with a stable `{ text }` detail, so\n // the raw event is swallowed at the panel boundary.\n const handleComposerSend = (event: CustomEvent<{ value: string }>): void => {\n event.stopPropagation();\n send(event.detail.value);\n };\n\n const closeOnEscape = (event: KeyboardEvent): void => {\n if (event.key === 'Escape' && props.open.value) requestClose('escape');\n };\n onMounted(() => {\n document.addEventListener('keydown', closeOnEscape);\n });\n onCleanup(() => document.removeEventListener('keydown', closeOnEscape));\n\n return html`\n <aside\n class=\"window\"\n part=\"window\"\n role=\"region\"\n aria-label=\"${() => props.label.value ?? ''}\"\n tabindex=\"-1\"\n ?hidden=\"${() => !props.open.value}\"\n ref=\"${panel}\"\n @keydown=\"${(event: KeyboardEvent) => {\n if (event.key === 'Escape') requestClose('escape');\n }}\">\n <header class=\"header\" part=\"header\">\n <slot name=\"header\">\n <div class=\"identity\">\n <strong>${() => props.label.value ?? ''}</strong>\n <span class=\"status\"><i class=\"status-dot\" aria-hidden=\"true\"></i>${() => label('status')}</span>\n </div>\n </slot>\n <div class=\"tools\">\n ${when(\n () => messages().length > 1,\n () =>\n html`<ore-button size=\"sm\" variant=\"text\" @click=\"${reset}\">${() => label('startOver')}</ore-button>`,\n )}\n <ore-button\n icon-only\n label=\"${() => `${label('close')} ${props.label.value ?? ''}`.trim()}\"\n size=\"sm\"\n variant=\"ghost\"\n @click=\"${() => requestClose('trigger')}\">\n <ore-icon name=\"x\" size=\"17\" aria-hidden=\"true\"></ore-icon>\n </ore-button>\n </div>\n </header>\n <div class=\"body\" part=\"body\">\n <div\n class=\"messages\"\n part=\"messages\"\n role=\"log\"\n aria-live=\"polite\"\n aria-label=\"${() => label('conversation')}\"\n tabindex=\"0\"\n ref=\"${messageList}\">\n ${each(\n messages,\n (message, index) => `${index}:${message.sender}:${message.text.slice(0, 24)}`,\n (message) => html`\n <ore-chat-message\n sender=\"${() => (message.value.sender === 'user' ? 'user' : 'assistant')}\"\n name=\"${() => (message.value.sender === 'user' ? null : label('assistantName'))}\">\n ${() =>\n message.value.sender === 'assistant'\n ? html`<ore-avatar\n slot=\"avatar\"\n initials=\"${() => props.initials.value}\"\n size=\"sm\"\n color=\"primary\"></ore-avatar>`\n : ''}\n ${() => message.value.text}\n ${() =>\n message.value.action\n ? html`<ore-button\n slot=\"actions\"\n size=\"sm\"\n variant=\"text\"\n @click=\"${() => emit('action', { payload: message.value.action?.payload })}\">\n ${() => message.value.action?.label}\n <ore-icon slot=\"suffix\" name=\"arrow-right\" size=\"14\" aria-hidden=\"true\"></ore-icon>\n </ore-button>`\n : ''}\n </ore-chat-message>\n `,\n )}\n </div>\n ${when(\n () => messages().length === 1 && suggestions().length > 0,\n () => html`\n <div class=\"suggestions\" part=\"suggestions\" role=\"group\" aria-label=\"${() => label('suggestions')}\">\n ${each(\n suggestions,\n (suggestion) => suggestion.label,\n (suggestion) => html`\n <ore-chip mode=\"action\" variant=\"outline\" @click=\"${() => send(suggestion.value.value)}\">\n ${() => suggestion.value.label}\n </ore-chip>\n `,\n )}\n </div>\n `,\n )}\n </div>\n <div class=\"composer\" part=\"composer\">\n <ore-message-composer\n color=\"primary\"\n fullwidth\n label=\"${() => label('composerLabel')}\"\n maxlength=\"${() => props.maxlength.value}\"\n placeholder=\"${() => label('composerPlaceholder')}\"\n variant=\"flat\"\n @send=\"${handleComposerSend}\"></ore-message-composer>\n </div>\n </aside>\n `;\n },\n styles: [reducedMotionMixin, componentStyles],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat-panel.js","names":[],"sources":["../src/content/chat-panel/chat-panel.ts"],"sourcesContent":["import { define, each, getHost, html, onCleanup, onMounted, prop, ref, useEmit, when } from '@vielzeug/ore';\nimport { watch } from '@vielzeug/ripple';\n\nimport type { DialogCloseReason, OverlayOpenChangeDetail, OverlayOpenReason } from '../../core';\nimport { reducedMotionMixin } from '../../styles';\n\nimport componentStyles from './chat-panel.css?inline';\n\nexport { CHIP_TAG } from '../../feedback/chip/chip';\nexport { BUTTON_TAG } from '../../inputs/button/button';\nexport { MESSAGE_COMPOSER_TAG } from '../../inputs/message-composer/message-composer';\nexport { AVATAR_TAG } from '../avatar/avatar';\nexport { CHAT_MESSAGE_TAG } from '../chat-message/chat-message';\nexport { ICON_TAG } from '../icon/icon';\n/** Who authored a panel message. */\nexport type OreChatPanelSender = 'assistant' | 'user';\n\n/** A message in the panel's transcript. `action` renders a follow-up button under assistant replies. */\nexport type OreChatPanelMessage = {\n /** Optional follow-up action shown as a button beneath the message. */\n action?: { label: string; payload?: unknown };\n sender: OreChatPanelSender;\n text: string;\n};\n\n/** A suggested question shown while the transcript holds only the greeting. */\nexport type OreChatPanelSuggestion = { label: string; value: string };\n\n/** Labels the panel renders; every one has an English default for consumers without i18n. */\nexport type OreChatPanelLabels = Partial<{\n assistantName: string;\n close: string;\n composerLabel: string;\n composerPlaceholder: string;\n conversation: string;\n startOver: string;\n status: string;\n suggestions: string;\n}>;\n\n/** Element interface exposing the imperative API for `ore-chat-panel`. */\nexport interface ChatPanelElement extends HTMLElement, OreChatPanelProps {\n /** Closes the panel and restores focus to the element passed to `show()`. */\n hide(): void;\n /** Opens the panel, remembering `trigger` to restore focus to when it closes. */\n show(trigger?: HTMLElement): void;\n}\n\nexport type OreChatPanelEvents = {\n /** A composer send or suggestion click was submitted; append the assistant reply to `messages`. */\n send: { text: string };\n /** The action button on a message was activated; `payload` is whatever the message carried. */\n action: { payload: unknown };\n /** The transcript was cleared back to `messages` via the Start over button. */\n reset: Record<string, never>;\n /** Open state changed, with the reason it changed. */\n 'open-change': OverlayOpenChangeDetail;\n};\n\nexport type OreChatPanelProps = {\n /** Accessible name for the panel region. */\n label?: string;\n /** Suggested questions shown while the transcript holds only the greeting. */\n suggestions?: OreChatPanelSuggestion[];\n /** Transcript. Mutate the array and reassign to update the panel. */\n messages?: OreChatPanelMessage[];\n /** Initials for the assistant avatar (default `'V'`). */\n initials?: string;\n /** Maximum composer length (default `240`). */\n maxlength?: number;\n /** All rendered labels, each with an English default. */\n labels?: OreChatPanelLabels;\n /** Open the panel. */\n open?: boolean;\n};\n\nconst DEFAULT_LABELS = {\n assistantName: 'Assistant',\n close: 'Close',\n composerLabel: 'Message',\n composerPlaceholder: 'Ask a question…',\n conversation: 'Conversation',\n startOver: 'Start over',\n status: 'Demo assistant · replies instantly',\n suggestions: 'Suggested questions',\n} as const;\n\nconst DEFAULT_INITIALS = 'V';\nconst DEFAULT_MAXLENGTH = 240;\n\n/**\n * A self-contained assistant chat surface: the fixed corner window a demo or support\n * flow needs: header with title, status, Start over and close; a live-region message\n * transcript built from `ore-chat-message`; suggested questions while the transcript\n * holds only the greeting; and an `ore-message-composer` footer.\n *\n * The panel owns presentation and interaction only. Reply generation, persistence, and\n * action semantics stay with the consumer: listen for `send`, append the assistant reply\n * to `messages`, and reassign the array; handle `action` for whatever a message's action\n * payload means in your app.\n *\n * @element ore-chat-panel\n *\n * @attr {string} label - Accessible name for the panel region\n * @attr {boolean} open - Open state\n * @attr {string} initials - Assistant avatar initials (default 'V')\n * @attr {number} maxlength - Composer character limit (default 240)\n *\n * @fires send - A send or suggestion was submitted. detail: { text }\n * @fires action - A message action button was activated. detail: { payload }\n * @fires reset - The transcript was cleared via Start over.\n * @fires open-change - Open state changed. detail: { open, reason }\n *\n * @slot header - Replaces the default title/status header content\n *\n * @cssprop --chat-panel-width - Panel width (default min(420px, viewport-fit))\n * @cssprop --chat-panel-height - Panel height (default min(640px, viewport-fit))\n * @cssprop --chat-panel-inset - Distance from the viewport corner (default --size-6)\n * @cssprop --chat-panel-z-index - Stacking order (default --z-fixed)\n *\n * @part window - The fixed panel window\n * @part header - Header row\n * @part body - Scrollable transcript area\n * @part messages - Message list (role=log)\n * @part suggestions - Suggestion chips row\n * @part composer - Composer footer\n *\n * @example\n * ```html\n * <ore-chat-panel id=\"advisor\" label=\"Model advisor\"></ore-chat-panel>\n * <script type=\"module\">\n * import '@vielzeug/refine/chat-panel';\n *\n * const panel = document.getElementById('advisor');\n * panel.messages = [{ sender: 'assistant', text: 'Hi! What can I help with?' }];\n * panel.addEventListener('send', (event) => {\n * panel.messages = [\n * ...panel.messages,\n * { sender: 'user', text: event.detail.text },\n * { sender: 'assistant', text: 'Let me look into that.' },\n * ];\n * });\n * panel.show();\n * </script>\n * ```\n */\nexport const CHAT_PANEL_TAG = 'ore-chat-panel' as const;\ndefine<OreChatPanelProps>(CHAT_PANEL_TAG, {\n props: {\n initials: prop.string(DEFAULT_INITIALS),\n label: prop.string(),\n labels: prop.data<Partial<OreChatPanelLabels>>({}),\n maxlength: prop.number(DEFAULT_MAXLENGTH),\n messages: prop.data<OreChatPanelMessage[]>([]),\n open: prop.bool(false),\n suggestions: prop.data<OreChatPanelSuggestion[]>([]),\n },\n setup(props) {\n const host = getHost() as ChatPanelElement;\n const emit = useEmit<OreChatPanelEvents>();\n const messageList = ref<HTMLElement>();\n const panel = ref<HTMLElement>();\n let returnFocus: HTMLElement | undefined;\n // Why the panel last changed open state, so the `open-change` emitted by the\n // reactive `open` watch can report the reason even when the change came from a\n // button, Escape, or `show()`/`hide()` rather than a direct `open` assignment.\n let pendingReason: OverlayOpenReason | DialogCloseReason = 'programmatic';\n\n const label = (key: keyof typeof DEFAULT_LABELS): string => props.labels.value?.[key] ?? DEFAULT_LABELS[key];\n // `prop.data` props are typed optional (the props interface is the public\n // shape), so reads fall back to the same defaults the prop definitions set.\n const messages = (): OreChatPanelMessage[] => props.messages.value ?? [];\n const suggestions = (): OreChatPanelSuggestion[] => props.suggestions.value ?? [];\n\n // `props.open` is a read-only Readable in setup; the element's own `open`\n // accessor (installed by `define`) is the writable side of the same signal and\n // reflects the attribute, so every close path routes through it.\n const requestClose = (reason: DialogCloseReason): void => {\n pendingReason = reason;\n host.open = false;\n };\n\n const requestOpen = (reason: OverlayOpenReason, trigger?: HTMLElement): void => {\n returnFocus = trigger;\n pendingReason = reason;\n host.open = true;\n };\n\n host.show = (trigger) => requestOpen('programmatic', trigger);\n host.hide = () => requestClose('programmatic');\n\n // Single source of truth: `open` is the only reactive open state, so attribute\n // changes, property writes, and show()/hide() all flow through here.\n watch(\n () => props.open.value,\n (open) => {\n emit('open-change', { open: open ?? false, reason: pendingReason });\n\n if (open) {\n requestAnimationFrame(() => panel.value?.focus());\n } else {\n requestAnimationFrame(() => returnFocus?.focus());\n }\n },\n );\n\n const scrollToLatest = (): void => {\n requestAnimationFrame(() =>\n messageList.value?.scrollTo({ behavior: 'auto', top: messageList.value.scrollHeight }),\n );\n };\n\n const send = (text: string): void => {\n const trimmed = text.trim();\n if (!trimmed) return;\n emit('send', { text: trimmed });\n requestAnimationFrame(scrollToLatest);\n };\n\n const reset = (): void => {\n emit('reset', {});\n requestAnimationFrame(scrollToLatest);\n };\n\n // The composer's own `send` is composed and would otherwise bubble past the\n // panel; the panel re-emits its own `send` with a stable `{ text }` detail, so\n // the raw event is swallowed at the panel boundary.\n const handleComposerSend = (event: CustomEvent<{ value: string }>): void => {\n event.stopPropagation();\n send(event.detail.value);\n };\n\n const closeOnEscape = (event: KeyboardEvent): void => {\n if (event.key === 'Escape' && props.open.value) requestClose('escape');\n };\n onMounted(() => {\n document.addEventListener('keydown', closeOnEscape);\n });\n onCleanup(() => document.removeEventListener('keydown', closeOnEscape));\n\n return html`\n <aside\n class=\"window\"\n part=\"window\"\n role=\"region\"\n aria-label=\"${() => props.label.value ?? ''}\"\n tabindex=\"-1\"\n ?hidden=\"${() => !props.open.value}\"\n ref=\"${panel}\"\n @keydown=\"${(event: KeyboardEvent) => {\n if (event.key === 'Escape') requestClose('escape');\n }}\">\n <header class=\"header\" part=\"header\">\n <slot name=\"header\">\n <div class=\"identity\">\n <strong>${() => props.label.value ?? ''}</strong>\n <span class=\"status\"><i class=\"status-dot\" aria-hidden=\"true\"></i>${() => label('status')}</span>\n </div>\n </slot>\n <div class=\"tools\">\n ${when(\n () => messages().length > 1,\n () =>\n html`<ore-button size=\"sm\" variant=\"text\" @click=\"${reset}\">${() => label('startOver')}</ore-button>`,\n )}\n <ore-button\n icon-only\n label=\"${() => `${label('close')} ${props.label.value ?? ''}`.trim()}\"\n size=\"sm\"\n variant=\"ghost\"\n @click=\"${() => requestClose('trigger')}\">\n <ore-icon name=\"x\" size=\"17\" aria-hidden=\"true\"></ore-icon>\n </ore-button>\n </div>\n </header>\n <div class=\"body\" part=\"body\">\n <div\n class=\"messages\"\n part=\"messages\"\n role=\"log\"\n aria-live=\"polite\"\n aria-label=\"${() => label('conversation')}\"\n tabindex=\"0\"\n ref=\"${messageList}\">\n ${each(\n messages,\n (message, index) => `${index}:${message.sender}:${message.text.slice(0, 24)}`,\n (message) => html`\n <ore-chat-message\n sender=\"${() => (message.value.sender === 'user' ? 'user' : 'assistant')}\"\n name=\"${() => (message.value.sender === 'user' ? null : label('assistantName'))}\">\n ${() =>\n message.value.sender === 'assistant'\n ? html`<ore-avatar\n slot=\"avatar\"\n initials=\"${() => props.initials.value}\"\n size=\"sm\"\n color=\"primary\"></ore-avatar>`\n : ''}\n ${() => message.value.text}\n ${() =>\n message.value.action\n ? html`<ore-button\n slot=\"actions\"\n size=\"sm\"\n variant=\"text\"\n @click=\"${() => emit('action', { payload: message.value.action?.payload })}\">\n ${() => message.value.action?.label}\n <ore-icon slot=\"suffix\" name=\"arrow-right\" size=\"14\" aria-hidden=\"true\"></ore-icon>\n </ore-button>`\n : ''}\n </ore-chat-message>\n `,\n )}\n </div>\n ${when(\n () => messages().length === 1 && suggestions().length > 0,\n () => html`\n <div class=\"suggestions\" part=\"suggestions\" role=\"group\" aria-label=\"${() => label('suggestions')}\">\n ${each(\n suggestions,\n (suggestion) => suggestion.label,\n (suggestion) => html`\n <ore-chip mode=\"action\" variant=\"outline\" @click=\"${() => send(suggestion.value.value)}\">\n ${() => suggestion.value.label}\n </ore-chip>\n `,\n )}\n </div>\n `,\n )}\n </div>\n <div class=\"composer\" part=\"composer\">\n <ore-message-composer\n color=\"primary\"\n fullwidth\n label=\"${() => label('composerLabel')}\"\n maxlength=\"${() => props.maxlength.value}\"\n placeholder=\"${() => label('composerPlaceholder')}\"\n variant=\"flat\"\n @send=\"${handleComposerSend}\"></ore-message-composer>\n </div>\n </aside>\n `;\n },\n styles: [reducedMotionMixin, componentStyles],\n});\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkbox-group.cjs","names":[],"sources":["../src/inputs/checkbox-group/checkbox-group.ts"],"sourcesContent":["import {\n bind,\n createContext,\n createId,\n define,\n getHost,\n html,\n onCleanup,\n prop,\n provide,\n useEmit,\n useField,\n useSlots,\n watchEffect,\n when,\n} from '@vielzeug/ore';\nimport { computed, type Readable, signal } from '@vielzeug/ripple';\nimport {\n type ChoiceChangeDetail,\n createChoiceField,\n getChoiceLabel,\n getLightChildrenByTag,\n lifecycleSignal,\n} from '../../core';\nimport { disablableBundle, sizableBundle, themableBundle } from '../../shared';\nimport { disabledStateMixin } from '../../styles';\nimport type { ComponentSize, ThemeColor } from '../../types';\nimport componentStyles from './checkbox-group.css?inline';\n\n// ─── Context ──────────────────────────────────────────────────────────────────\n\nexport type CheckboxGroupContext = {\n color: Readable<ThemeColor | undefined>;\n disabled: Readable<boolean>;\n size: Readable<ComponentSize | undefined>;\n toggle: (value: string, originalEvent?: Event) => void;\n values: Readable<string[]>;\n};\n\nexport const CHECKBOX_GROUP_CTX = createContext<CheckboxGroupContext>('CheckboxGroupContext');\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type OreCheckboxGroupProps = {\n /** Theme color — propagated to all child ore-checkbox elements */\n color?: ThemeColor;\n /** Disable all checkboxes in the group */\n disabled?: boolean;\n /** Error message shown below the group */\n error?: string;\n /** Helper text shown below the group */\n helper?: string;\n /** Legend / label for the fieldset. Required for accessibility. */\n label?: string;\n /** Form field name used during submission */\n name?: string;\n /** Layout direction of the checkbox options */\n orientation?: 'vertical' | 'horizontal';\n /** Mark the group as required */\n required?: boolean;\n /** Size — propagated to all child ore-checkbox elements */\n size?: ComponentSize;\n /** Comma-separated list of currently checked values */\n values?: string;\n};\n\nexport type OreCheckboxGroupEvents = {\n change: ChoiceChangeDetail;\n};\n\n/**\n * A fieldset wrapper that groups `ore-checkbox` elements, provides shared\n * `color` and `size` via context, and manages multi-value selection state.\n *\n * @element ore-checkbox-group\n *\n * @attr {string} label - Legend text (required for a11y)\n * @attr {string} values - Comma-separated list of checked values\n * @attr {boolean} disabled - Disable all checkboxes in the group\n * @attr {string} error - Error message\n * @attr {string} helper - Helper text\n * @attr {string} name - Form field name\n * @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'\n * @attr {string} size - Component size: 'sm' | 'md' | 'lg'\n * @attr {string} orientation - Layout: 'vertical' | 'horizontal'\n * @attr {boolean} required - Required field\n *\n * @fires change - Emitted when selection changes. detail: { value: string, values: string[], labels: string[], originalEvent?: Event }\n *\n * @slot - Place `ore-checkbox` elements here\n *\n * @cssprop --checkbox-group-direction - Flex direction of the items list ('row' | 'column')\n * @cssprop --checkbox-group-gap - Gap between checkbox items\n * @part items - Items container.\n * @example\n * ```html\n * <ore-checkbox-group name=\"fruits\" label=\"Favourite fruits\" required>\n * <ore-checkbox value=\"apple\">Apple</ore-checkbox>\n * <ore-checkbox value=\"banana\">Banana</ore-checkbox>\n * <ore-checkbox value=\"cherry\">Cherry</ore-checkbox>\n * </ore-checkbox-group>\n * <ore-checkbox-group name=\"options\" orientation=\"horizontal\" color=\"primary\">\n * <ore-checkbox value=\"a\">Option A</ore-checkbox>\n * <ore-checkbox value=\"b\">Option B</ore-checkbox>\n * </ore-checkbox-group>\n * ```\n */\nexport const CHECKBOX_GROUP_TAG = 'ore-checkbox-group' as const;\ndefine<OreCheckboxGroupProps>(CHECKBOX_GROUP_TAG, {\n formAssociated: true,\n props: {\n ...themableBundle,\n ...sizableBundle,\n ...disablableBundle,\n error: prop.string(),\n helper: prop.string(),\n label: prop.string(),\n name: prop.string(),\n orientation: prop.string('vertical'),\n required: prop.bool(false),\n // Not auto-reflected (`reflect: false`) — the derived, interaction-updated selection\n // (`choice.formValue`) is the single writer for this attribute, via `bind()` below;\n // letting `prop.string()`'s own default reflection also write the raw incoming value\n // would leave two effects racing to set the same attribute from different sources.\n values: { ...prop.string(), reflect: false },\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreCheckboxGroupEvents>();\n const slots = useSlots();\n const watch = watchEffect;\n\n const choice = createChoiceField({\n disabled: props.disabled,\n error: props.error,\n helper: props.helper,\n multiple: signal(true),\n prefix: 'checkbox-group',\n required: props.required,\n signal: lifecycleSignal(onCleanup),\n value: props.values,\n });\n\n choice.attachFormField(\n useField<string>({\n disabled: choice.disabled,\n onReset: choice.reset,\n toFormValue: (v) => v,\n validationMessage: choice.validationMessage,\n validity: choice.validity,\n value: choice.formValue,\n }),\n );\n\n const checkedValues = choice.selectedValues;\n\n const getCheckboxes = (): HTMLElement[] => getLightChildrenByTag(el, 'ore-checkbox');\n const getLabelForValue = (value: string): string => getChoiceLabel(getCheckboxes(), value);\n const emitChange = (originalEvent?: Event) => {\n const values = checkedValues.value;\n\n const labels = values.map(getLabelForValue);\n\n emit('change', { labels, originalEvent, values });\n };\n\n const toggleCheckbox = (val: string, originalEvent?: Event) => {\n choice.toggleValue(val);\n choice.triggerValidation('change');\n emitChange(originalEvent);\n };\n\n provide(CHECKBOX_GROUP_CTX, {\n color: props.color,\n disabled: computed(() => Boolean(props.disabled.value)),\n size: props.size,\n toggle: toggleCheckbox,\n values: checkedValues,\n });\n\n // Sync checked state + color/size/disabled onto slotted ore-checkbox children\n const syncChildren = () => {\n const values = checkedValues.value;\n const color = props.color.value;\n const size = props.size.value;\n const disabled = props.disabled.value;\n const checkboxes = getCheckboxes();\n\n for (const checkbox of checkboxes) {\n const val = checkbox.getAttribute('value') ?? '';\n\n checkbox.toggleAttribute('checked', values.includes(val));\n\n if (color) checkbox.setAttribute('color', color);\n else checkbox.removeAttribute('color');\n\n if (size) checkbox.setAttribute('size', size);\n else checkbox.removeAttribute('size');\n\n checkbox.toggleAttribute('disabled', Boolean(disabled));\n }\n };\n\n watch(() => {\n void slots.elements().value;\n syncChildren();\n });\n\n watch(() => {\n void slots.elements().value;\n\n const listeners = getCheckboxes().map((checkbox) => {\n const handler = (event: Event) => {\n event.stopPropagation();\n\n const val = (checkbox.getAttribute('value') ?? '').trim();\n\n if (!val) return;\n\n toggleCheckbox(val, event);\n };\n\n checkbox.addEventListener('change', handler);\n\n return () => {\n checkbox.removeEventListener('change', handler);\n };\n });\n\n return () => {\n for (const dispose of listeners) dispose();\n };\n });\n\n const legendId = createId('checkbox-group-legend');\n const errorId = `${legendId}-error`;\n const helperId = `${legendId}-helper`;\n const hasError = () => Boolean(props.error.value);\n const hasHelper = () => Boolean(props.helper.value) && !hasError();\n\n // Reactive, not a one-off `el.setAttribute()` inside the click handler: the host's `values`\n // attribute must stay in sync with the selection regardless of *why* it changed (a click,\n // or `reset()` on ancestor form reset — neither of which should need its own copy of this).\n bind({ attr: { size: props.size, values: choice.formValue } });\n\n return html`\n <fieldset\n role=\"group\"\n aria-required=\"${() => String(Boolean(props.required.value))}\"\n aria-invalid=\"${() => String(hasError())}\"\n aria-errormessage=\"${() => (hasError() ? errorId : null)}\"\n aria-describedby=\"${() => (hasError() ? errorId : hasHelper() ? helperId : null)}\">\n <legend id=\"${legendId}\" ?hidden=${() => !props.label.value}>\n ${props.label}${when(\n () => Boolean(props.required.value),\n () => html`\n <span aria-hidden=\"true\">*</span>\n `,\n )}\n </legend>\n <div class=\"checkbox-group-items\" part=\"items\">\n <slot></slot>\n </div>\n <div class=\"error-text\" id=\"${errorId}\" role=\"alert\" ?hidden=${() => !hasError()}>${props.error}</div>\n <div class=\"helper-text\" id=\"${helperId}\" ?hidden=${() => !hasHelper()}>${props.helper}</div>\n </fieldset>\n `;\n },\n styles: [disabledStateMixin, componentStyles],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"checkbox-group.cjs","names":[],"sources":["../src/inputs/checkbox-group/checkbox-group.ts"],"sourcesContent":["import {\n bind,\n createContext,\n createId,\n define,\n getHost,\n html,\n onCleanup,\n prop,\n provide,\n useEmit,\n useField,\n useSlots,\n watchEffect,\n when,\n} from '@vielzeug/ore';\nimport { computed, type Readable, signal } from '@vielzeug/ripple';\nimport {\n type ChoiceChangeDetail,\n createChoiceField,\n getChoiceLabel,\n getLightChildrenByTag,\n lifecycleSignal,\n} from '../../core';\nimport { disablableBundle, sizableBundle, themableBundle } from '../../shared';\nimport { disabledStateMixin } from '../../styles';\nimport type { ComponentSize, ThemeColor } from '../../types';\nimport componentStyles from './checkbox-group.css?inline';\n\n// ─── Context ──────────────────────────────────────────────────────────────────\n\nexport type CheckboxGroupContext = {\n color: Readable<ThemeColor | undefined>;\n disabled: Readable<boolean>;\n size: Readable<ComponentSize | undefined>;\n toggle: (value: string, originalEvent?: Event) => void;\n values: Readable<string[]>;\n};\n\nexport const CHECKBOX_GROUP_CTX = createContext<CheckboxGroupContext>('CheckboxGroupContext');\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type OreCheckboxGroupProps = {\n /** Theme color: propagated to all child ore-checkbox elements */\n color?: ThemeColor;\n /** Disable all checkboxes in the group */\n disabled?: boolean;\n /** Error message shown below the group */\n error?: string;\n /** Helper text shown below the group */\n helper?: string;\n /** Legend / label for the fieldset. Required for accessibility. */\n label?: string;\n /** Form field name used during submission */\n name?: string;\n /** Layout direction of the checkbox options */\n orientation?: 'vertical' | 'horizontal';\n /** Mark the group as required */\n required?: boolean;\n /** Size: propagated to all child ore-checkbox elements */\n size?: ComponentSize;\n /** Comma-separated list of currently checked values */\n values?: string;\n};\n\nexport type OreCheckboxGroupEvents = {\n change: ChoiceChangeDetail;\n};\n\n/**\n * A fieldset wrapper that groups `ore-checkbox` elements, provides shared\n * `color` and `size` via context, and manages multi-value selection state.\n *\n * @element ore-checkbox-group\n *\n * @attr {string} label - Legend text (required for a11y)\n * @attr {string} values - Comma-separated list of checked values\n * @attr {boolean} disabled - Disable all checkboxes in the group\n * @attr {string} error - Error message\n * @attr {string} helper - Helper text\n * @attr {string} name - Form field name\n * @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'\n * @attr {string} size - Component size: 'sm' | 'md' | 'lg'\n * @attr {string} orientation - Layout: 'vertical' | 'horizontal'\n * @attr {boolean} required - Required field\n *\n * @fires change - Emitted when selection changes. detail: { value: string, values: string[], labels: string[], originalEvent?: Event }\n *\n * @slot - Place `ore-checkbox` elements here\n *\n * @cssprop --checkbox-group-direction - Flex direction of the items list ('row' | 'column')\n * @cssprop --checkbox-group-gap - Gap between checkbox items\n * @part items - Items container.\n * @example\n * ```html\n * <ore-checkbox-group name=\"fruits\" label=\"Favourite fruits\" required>\n * <ore-checkbox value=\"apple\">Apple</ore-checkbox>\n * <ore-checkbox value=\"banana\">Banana</ore-checkbox>\n * <ore-checkbox value=\"cherry\">Cherry</ore-checkbox>\n * </ore-checkbox-group>\n * <ore-checkbox-group name=\"options\" orientation=\"horizontal\" color=\"primary\">\n * <ore-checkbox value=\"a\">Option A</ore-checkbox>\n * <ore-checkbox value=\"b\">Option B</ore-checkbox>\n * </ore-checkbox-group>\n * ```\n */\nexport const CHECKBOX_GROUP_TAG = 'ore-checkbox-group' as const;\ndefine<OreCheckboxGroupProps>(CHECKBOX_GROUP_TAG, {\n formAssociated: true,\n props: {\n ...themableBundle,\n ...sizableBundle,\n ...disablableBundle,\n error: prop.string(),\n helper: prop.string(),\n label: prop.string(),\n name: prop.string(),\n orientation: prop.string('vertical'),\n required: prop.bool(false),\n // Not auto-reflected (`reflect: false`): the derived, interaction-updated selection\n // (`choice.formValue`) is the single writer for this attribute, via `bind()` below;\n // letting `prop.string()`'s own default reflection also write the raw incoming value\n // would leave two effects racing to set the same attribute from different sources.\n values: { ...prop.string(), reflect: false },\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreCheckboxGroupEvents>();\n const slots = useSlots();\n const watch = watchEffect;\n\n const choice = createChoiceField({\n disabled: props.disabled,\n error: props.error,\n helper: props.helper,\n multiple: signal(true),\n prefix: 'checkbox-group',\n required: props.required,\n signal: lifecycleSignal(onCleanup),\n value: props.values,\n });\n\n choice.attachFormField(\n useField<string>({\n disabled: choice.disabled,\n onReset: choice.reset,\n toFormValue: (v) => v,\n validationMessage: choice.validationMessage,\n validity: choice.validity,\n value: choice.formValue,\n }),\n );\n\n const checkedValues = choice.selectedValues;\n\n const getCheckboxes = (): HTMLElement[] => getLightChildrenByTag(el, 'ore-checkbox');\n const getLabelForValue = (value: string): string => getChoiceLabel(getCheckboxes(), value);\n const emitChange = (originalEvent?: Event) => {\n const values = checkedValues.value;\n\n const labels = values.map(getLabelForValue);\n\n emit('change', { labels, originalEvent, values });\n };\n\n const toggleCheckbox = (val: string, originalEvent?: Event) => {\n choice.toggleValue(val);\n choice.triggerValidation('change');\n emitChange(originalEvent);\n };\n\n provide(CHECKBOX_GROUP_CTX, {\n color: props.color,\n disabled: computed(() => Boolean(props.disabled.value)),\n size: props.size,\n toggle: toggleCheckbox,\n values: checkedValues,\n });\n\n // Sync checked state + color/size/disabled onto slotted ore-checkbox children\n const syncChildren = () => {\n const values = checkedValues.value;\n const color = props.color.value;\n const size = props.size.value;\n const disabled = props.disabled.value;\n const checkboxes = getCheckboxes();\n\n for (const checkbox of checkboxes) {\n const val = checkbox.getAttribute('value') ?? '';\n\n checkbox.toggleAttribute('checked', values.includes(val));\n\n if (color) checkbox.setAttribute('color', color);\n else checkbox.removeAttribute('color');\n\n if (size) checkbox.setAttribute('size', size);\n else checkbox.removeAttribute('size');\n\n checkbox.toggleAttribute('disabled', Boolean(disabled));\n }\n };\n\n watch(() => {\n void slots.elements().value;\n syncChildren();\n });\n\n watch(() => {\n void slots.elements().value;\n\n const listeners = getCheckboxes().map((checkbox) => {\n const handler = (event: Event) => {\n event.stopPropagation();\n\n const val = (checkbox.getAttribute('value') ?? '').trim();\n\n if (!val) return;\n\n toggleCheckbox(val, event);\n };\n\n checkbox.addEventListener('change', handler);\n\n return () => {\n checkbox.removeEventListener('change', handler);\n };\n });\n\n return () => {\n for (const dispose of listeners) dispose();\n };\n });\n\n const legendId = createId('checkbox-group-legend');\n const errorId = `${legendId}-error`;\n const helperId = `${legendId}-helper`;\n const hasError = () => Boolean(props.error.value);\n const hasHelper = () => Boolean(props.helper.value) && !hasError();\n\n // Reactive, not a one-off `el.setAttribute()` inside the click handler: the host's `values`\n // attribute must stay in sync with the selection regardless of *why* it changed (a click,\n // or `reset()` on ancestor form reset: neither of which should need its own copy of this).\n bind({ attr: { size: props.size, values: choice.formValue } });\n\n return html`\n <fieldset\n role=\"group\"\n aria-required=\"${() => String(Boolean(props.required.value))}\"\n aria-invalid=\"${() => String(hasError())}\"\n aria-errormessage=\"${() => (hasError() ? errorId : null)}\"\n aria-describedby=\"${() => (hasError() ? errorId : hasHelper() ? helperId : null)}\">\n <legend id=\"${legendId}\" ?hidden=${() => !props.label.value}>\n ${props.label}${when(\n () => Boolean(props.required.value),\n () => html`\n <span aria-hidden=\"true\">*</span>\n `,\n )}\n </legend>\n <div class=\"checkbox-group-items\" part=\"items\">\n <slot></slot>\n </div>\n <div class=\"error-text\" id=\"${errorId}\" role=\"alert\" ?hidden=${() => !hasError()}>${props.error}</div>\n <div class=\"helper-text\" id=\"${helperId}\" ?hidden=${() => !hasHelper()}>${props.helper}</div>\n </fieldset>\n `;\n },\n styles: [disabledStateMixin, componentStyles],\n});\n"],"mappings":"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"}
|