@sableui/react 0.6.1 → 0.7.0
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/AGENTS.md +2 -2
- package/CHANGELOG.md +285 -0
- package/CONTRIBUTING.md +16 -10
- package/DESIGN.md +3 -3
- package/README.md +3 -3
- package/bin/sable-lint.mjs +18 -0
- package/bin/sable.mjs +470 -62
- package/dist/blocks/api-keys-block.js +44 -0
- package/dist/blocks/api-keys-block.meta.js +21 -0
- package/dist/blocks/chat-thread-block.js +17 -0
- package/dist/blocks/chat-thread-block.meta.js +20 -0
- package/dist/blocks/command-palette-block.js +24 -0
- package/dist/blocks/command-palette-block.meta.js +21 -0
- package/dist/blocks/cta-band-block.js +27 -0
- package/dist/blocks/cta-band-block.meta.js +20 -0
- package/dist/blocks/danger-zone-block.js +39 -0
- package/dist/blocks/danger-zone-block.meta.js +21 -0
- package/dist/blocks/device-comparison-block.js +69 -0
- package/dist/blocks/device-comparison-block.meta.js +21 -0
- package/dist/blocks/event-card-block.js +40 -0
- package/dist/blocks/event-card-block.meta.js +21 -0
- package/dist/blocks/faq-block.js +38 -0
- package/dist/blocks/faq-block.meta.js +20 -0
- package/dist/blocks/feature-grid-block.js +38 -0
- package/dist/blocks/feature-grid-block.meta.js +21 -0
- package/dist/blocks/feedback-form-block.js +30 -0
- package/dist/blocks/feedback-form-block.meta.js +21 -0
- package/dist/blocks/goal-progress-block.js +24 -0
- package/dist/blocks/goal-progress-block.meta.js +21 -0
- package/dist/blocks/hero-block.js +33 -0
- package/dist/blocks/hero-block.meta.js +21 -0
- package/dist/blocks/index.js +162 -1964
- package/dist/blocks/invite-team-block.js +36 -0
- package/dist/blocks/invite-team-block.meta.js +20 -0
- package/dist/blocks/invoice-history-block.js +41 -0
- package/dist/blocks/invoice-history-block.meta.js +21 -0
- package/dist/blocks/kpi-sparkline-block.js +29 -0
- package/dist/blocks/kpi-sparkline-block.meta.js +20 -0
- package/dist/blocks/media-player-block.js +68 -0
- package/dist/blocks/media-player-block.meta.js +21 -0
- package/dist/blocks/newsletter-block.js +35 -0
- package/dist/blocks/newsletter-block.meta.js +21 -0
- package/dist/blocks/not-found-block.js +27 -0
- package/dist/blocks/not-found-block.meta.js +20 -0
- package/dist/blocks/notification-list-block.js +32 -0
- package/dist/blocks/notification-list-block.meta.js +21 -0
- package/dist/blocks/notification-settings-block.js +25 -0
- package/dist/blocks/notification-settings-block.meta.js +20 -0
- package/dist/blocks/order-tracking-block.js +26 -0
- package/dist/blocks/order-tracking-block.meta.js +20 -0
- package/dist/blocks/pricing-card-block.js +46 -0
- package/dist/blocks/pricing-card-block.meta.js +21 -0
- package/dist/blocks/pricing-table-block.js +88 -0
- package/dist/blocks/pricing-table-block.meta.js +20 -0
- package/dist/blocks/product-card-block.js +26 -0
- package/dist/blocks/product-card-block.meta.js +21 -0
- package/dist/blocks/profile-card-block.js +58 -0
- package/dist/blocks/profile-card-block.meta.js +21 -0
- package/dist/blocks/profile-form-block.js +70 -0
- package/dist/blocks/profile-form-block.meta.js +21 -0
- package/dist/blocks/revenue-breakdown-block.js +38 -0
- package/dist/blocks/revenue-breakdown-block.meta.js +21 -0
- package/dist/blocks/sign-in-block.js +38 -0
- package/dist/blocks/sign-in-block.meta.js +21 -0
- package/dist/blocks/sign-up-block.js +48 -0
- package/dist/blocks/sign-up-block.meta.js +21 -0
- package/dist/blocks/site-footer-block.js +54 -0
- package/dist/blocks/site-footer-block.meta.js +20 -0
- package/dist/blocks/stat-row-block.js +28 -0
- package/dist/blocks/stat-row-block.meta.js +21 -0
- package/dist/blocks/steps-block.js +37 -0
- package/dist/blocks/steps-block.meta.js +20 -0
- package/dist/blocks/task-list-block.js +52 -0
- package/dist/blocks/task-list-block.meta.js +21 -0
- package/dist/blocks/team-members-block.js +49 -0
- package/dist/blocks/team-members-block.meta.js +21 -0
- package/dist/blocks/testimonial-block.js +49 -0
- package/dist/blocks/testimonial-block.meta.js +20 -0
- package/dist/blocks/traffic-sources-block.js +39 -0
- package/dist/blocks/traffic-sources-block.meta.js +21 -0
- package/dist/blocks/traffic-trend-block.js +61 -0
- package/dist/blocks/traffic-trend-block.meta.js +21 -0
- package/dist/blocks/upload-list-block.js +39 -0
- package/dist/blocks/upload-list-block.meta.js +21 -0
- package/dist/blocks/verify-code-block.js +28 -0
- package/dist/blocks/verify-code-block.meta.js +21 -0
- package/dist/blocks/wallet-block.js +48 -0
- package/dist/blocks/wallet-block.meta.js +21 -0
- package/dist/components/accordion/accordion.js +63 -0
- package/dist/components/accordion/accordion.meta.js +155 -0
- package/dist/components/accordion/index.js +15 -0
- package/dist/components/app-header/app-header.js +30 -0
- package/dist/components/app-header/app-header.meta.js +88 -0
- package/dist/components/app-header/index.js +7 -0
- package/dist/components/app-shell/app-shell-dock.js +37 -0
- package/dist/components/app-shell/app-shell.js +67 -0
- package/dist/components/app-shell/app-shell.meta.js +176 -0
- package/dist/components/app-shell/index.js +7 -0
- package/dist/components/area-chart/area-chart.js +10 -0
- package/dist/components/area-chart/area-chart.meta.js +218 -0
- package/dist/components/area-chart/index.js +7 -0
- package/dist/components/attachment/attachment.js +176 -0
- package/dist/components/attachment/attachment.meta.js +179 -0
- package/dist/components/attachment/attachment.variants.js +47 -0
- package/dist/components/attachment/index.js +7 -0
- package/dist/components/avatar/avatar.js +192 -0
- package/dist/components/avatar/avatar.meta.js +233 -0
- package/dist/components/avatar/avatar.variants.js +44 -0
- package/dist/components/avatar/index.js +22 -0
- package/dist/components/badge/badge.js +26 -0
- package/dist/components/badge/badge.meta.js +171 -0
- package/dist/components/badge/badge.variants.js +103 -0
- package/dist/components/badge/index.js +11 -0
- package/dist/components/banner/banner.js +76 -0
- package/dist/components/banner/banner.meta.js +172 -0
- package/dist/components/banner/banner.variants.js +44 -0
- package/dist/components/banner/index.js +9 -0
- package/dist/components/bar-chart/bar-chart.js +10 -0
- package/dist/components/bar-chart/bar-chart.meta.js +245 -0
- package/dist/components/bar-chart/index.js +7 -0
- package/dist/components/bottom-nav/bottom-nav.js +63 -0
- package/dist/components/bottom-nav/bottom-nav.meta.js +95 -0
- package/dist/components/bottom-nav/index.js +11 -0
- package/dist/components/breadcrumbs/breadcrumbs.js +90 -0
- package/dist/components/breadcrumbs/breadcrumbs.meta.js +111 -0
- package/dist/components/breadcrumbs/index.js +17 -0
- package/dist/components/browser/browser.js +39 -0
- package/dist/components/browser/browser.meta.js +80 -0
- package/dist/components/browser/index.js +7 -0
- package/dist/components/bubble/bubble.js +110 -0
- package/dist/components/bubble/bubble.meta.js +158 -0
- package/dist/components/bubble/bubble.variants.js +90 -0
- package/dist/components/bubble/index.js +13 -0
- package/dist/components/button/button.js +87 -0
- package/dist/components/button/button.meta.js +302 -0
- package/dist/components/button/button.variants.js +137 -0
- package/dist/components/button/index.js +9 -0
- package/dist/components/calendar/calendar.js +89 -0
- package/dist/components/calendar/calendar.meta.js +136 -0
- package/dist/components/calendar/index.js +7 -0
- package/dist/components/card/card.js +200 -0
- package/dist/components/card/card.meta.js +212 -0
- package/dist/components/card/index.js +23 -0
- package/dist/components/carousel/carousel.js +428 -0
- package/dist/components/carousel/carousel.meta.js +100 -0
- package/dist/components/carousel/index.js +7 -0
- package/dist/components/chat-composer/chat-composer.js +174 -0
- package/dist/components/chat-composer/chat-composer.meta.js +241 -0
- package/dist/components/chat-composer/chat-composer.variants.js +17 -0
- package/dist/components/chat-composer/index.js +7 -0
- package/dist/components/checkbox/checkbox-mark.js +28 -0
- package/dist/components/checkbox/checkbox.js +30 -0
- package/dist/components/checkbox/checkbox.meta.js +127 -0
- package/dist/components/checkbox/index.js +7 -0
- package/dist/components/checkbox-card/checkbox-card.js +94 -0
- package/dist/components/checkbox-card/checkbox-card.meta.js +154 -0
- package/dist/components/checkbox-card/checkbox-card.variants.js +43 -0
- package/dist/components/checkbox-card/index.js +11 -0
- package/dist/components/code/code.js +19 -0
- package/dist/components/code/code.meta.js +71 -0
- package/dist/components/code/index.js +7 -0
- package/dist/components/code-block/code-block.js +84 -0
- package/dist/components/code-block/code-block.meta.js +103 -0
- package/dist/components/code-block/index.js +8 -0
- package/dist/components/collapsible/collapsible.js +38 -0
- package/dist/components/collapsible/collapsible.meta.js +117 -0
- package/dist/components/collapsible/index.js +15 -0
- package/dist/components/color-field/color-field.js +293 -0
- package/dist/components/color-field/color-field.meta.js +142 -0
- package/dist/components/color-field/color.js +69 -0
- package/dist/components/color-field/index.js +15 -0
- package/dist/components/combobox/combobox.js +486 -0
- package/dist/components/combobox/combobox.meta.js +217 -0
- package/dist/components/combobox/index.js +37 -0
- package/dist/components/command-menu/command-menu-trigger.js +122 -0
- package/dist/components/command-menu/command-menu.js +433 -0
- package/dist/components/command-menu/command-menu.meta.js +279 -0
- package/dist/components/command-menu/index.js +21 -0
- package/dist/components/context-card/context-card.js +62 -0
- package/dist/components/context-card/context-card.meta.js +106 -0
- package/dist/components/context-card/index.js +17 -0
- package/dist/components/context-menu/context-menu.js +92 -0
- package/dist/components/context-menu/context-menu.meta.js +138 -0
- package/dist/components/context-menu/index.js +23 -0
- package/dist/components/data-table/data-table.js +142 -0
- package/dist/components/data-table/data-table.meta.js +146 -0
- package/dist/components/data-table/index.js +9 -0
- package/dist/components/date-picker/date-picker.js +196 -0
- package/dist/components/date-picker/date-picker.meta.js +164 -0
- package/dist/components/date-picker/index.js +7 -0
- package/dist/components/date-tile/date-tile.js +29 -0
- package/dist/components/date-tile/date-tile.meta.js +72 -0
- package/dist/components/date-tile/index.js +7 -0
- package/dist/components/destructive-action-modal/destructive-action-modal.js +130 -0
- package/dist/components/destructive-action-modal/destructive-action-modal.meta.js +136 -0
- package/dist/components/destructive-action-modal/index.js +9 -0
- package/dist/components/dialog/dialog.js +207 -0
- package/dist/components/dialog/dialog.meta.js +239 -0
- package/dist/components/dialog/index.js +45 -0
- package/dist/components/disclosure-picker/disclosure-picker.js +417 -0
- package/dist/components/disclosure-picker/disclosure-picker.meta.js +213 -0
- package/dist/components/disclosure-picker/index.js +21 -0
- package/dist/components/empty-state/empty-state.js +52 -0
- package/dist/components/empty-state/empty-state.meta.js +158 -0
- package/dist/components/empty-state/index.js +7 -0
- package/dist/components/entity/entity.js +70 -0
- package/dist/components/entity/entity.meta.js +108 -0
- package/dist/components/entity/entity.variants.js +16 -0
- package/dist/components/entity/index.js +7 -0
- package/dist/components/feedback/feedback.js +93 -0
- package/dist/components/feedback/feedback.meta.js +109 -0
- package/dist/components/feedback/index.js +7 -0
- package/dist/components/fieldset/fieldset.js +80 -0
- package/dist/components/fieldset/fieldset.meta.js +160 -0
- package/dist/components/fieldset/index.js +17 -0
- package/dist/components/file-dropzone/file-dropzone.js +193 -0
- package/dist/components/file-dropzone/file-dropzone.meta.js +210 -0
- package/dist/components/file-dropzone/file-dropzone.variants.js +26 -0
- package/dist/components/file-dropzone/index.js +13 -0
- package/dist/components/font-picker/font-picker.js +77 -0
- package/dist/components/font-picker/font-picker.meta.js +119 -0
- package/dist/components/font-picker/index.js +7 -0
- package/dist/components/form/form.js +145 -0
- package/dist/components/form/form.meta.js +142 -0
- package/dist/components/form/index.js +11 -0
- package/dist/components/gliding-highlight/gliding-highlight.js +50 -0
- package/dist/components/gliding-highlight/gliding-highlight.meta.js +135 -0
- package/dist/components/gliding-highlight/index.js +9 -0
- package/dist/components/gradient-art/gradient-art.js +168 -0
- package/dist/components/gradient-art/gradient-art.meta.js +81 -0
- package/dist/components/gradient-art/index.js +7 -0
- package/dist/components/gradient-art/renderer.js +336 -0
- package/dist/components/inline-status/index.js +7 -0
- package/dist/components/inline-status/inline-status.js +33 -0
- package/dist/components/inline-status/inline-status.meta.js +83 -0
- package/dist/components/inline-status/inline-status.variants.js +18 -0
- package/dist/components/input/index.js +7 -0
- package/dist/components/input/input.js +33 -0
- package/dist/components/input/input.meta.js +155 -0
- package/dist/components/kbd/index.js +8 -0
- package/dist/components/kbd/kbd.js +54 -0
- package/dist/components/kbd/kbd.meta.js +95 -0
- package/dist/components/label/index.js +7 -0
- package/dist/components/label/label.js +51 -0
- package/dist/components/label/label.meta.js +98 -0
- package/dist/components/line-chart/index.js +7 -0
- package/dist/components/line-chart/line-chart.js +10 -0
- package/dist/components/line-chart/line-chart.meta.js +222 -0
- package/dist/components/marker/index.js +7 -0
- package/dist/components/marker/marker.js +39 -0
- package/dist/components/marker/marker.meta.js +132 -0
- package/dist/components/marker/marker.variants.js +25 -0
- package/dist/components/marquee/index.js +7 -0
- package/dist/components/marquee/marquee.js +146 -0
- package/dist/components/marquee/marquee.meta.js +106 -0
- package/dist/components/media-image/index.js +7 -0
- package/dist/components/media-image/media-image.js +56 -0
- package/dist/components/media-image/media-image.meta.js +92 -0
- package/dist/components/media-tile/index.js +7 -0
- package/dist/components/media-tile/media-tile.js +135 -0
- package/dist/components/media-tile/media-tile.meta.js +133 -0
- package/dist/components/media-tile/media-tile.variants.js +48 -0
- package/dist/components/menu/index.js +33 -0
- package/dist/components/menu/menu.js +168 -0
- package/dist/components/menu/menu.meta.js +176 -0
- package/dist/components/message/index.js +7 -0
- package/dist/components/message/message.js +83 -0
- package/dist/components/message/message.meta.js +150 -0
- package/dist/components/message/message.variants.js +41 -0
- package/dist/components/message-scroller/index.js +9 -0
- package/dist/components/message-scroller/message-scroller.js +300 -0
- package/dist/components/message-scroller/message-scroller.meta.js +172 -0
- package/dist/components/meter/index.js +20 -0
- package/dist/components/meter/meter.js +112 -0
- package/dist/components/meter/meter.meta.js +227 -0
- package/dist/components/meter/meter.variants.js +61 -0
- package/dist/components/middle-truncate/index.js +7 -0
- package/dist/components/middle-truncate/middle-truncate.js +28 -0
- package/dist/components/middle-truncate/middle-truncate.meta.js +77 -0
- package/dist/components/note/index.js +9 -0
- package/dist/components/note/note.js +73 -0
- package/dist/components/note/note.meta.js +112 -0
- package/dist/components/note/note.variants.js +22 -0
- package/dist/components/notification-badge/index.js +15 -0
- package/dist/components/notification-badge/notification-badge.js +48 -0
- package/dist/components/notification-badge/notification-badge.meta.js +107 -0
- package/dist/components/notification-badge/notification-badge.variants.js +57 -0
- package/dist/components/number-field/index.js +7 -0
- package/dist/components/number-field/number-field.js +70 -0
- package/dist/components/number-field/number-field.meta.js +109 -0
- package/dist/components/otp-field/index.js +7 -0
- package/dist/components/otp-field/otp-field.js +70 -0
- package/dist/components/otp-field/otp-field.meta.js +197 -0
- package/dist/components/overflow-menu/index.js +7 -0
- package/dist/components/overflow-menu/overflow-menu.js +26 -0
- package/dist/components/overflow-menu/overflow-menu.meta.js +100 -0
- package/dist/components/page-header/index.js +7 -0
- package/dist/components/page-header/page-header.js +34 -0
- package/dist/components/page-header/page-header.meta.js +104 -0
- package/dist/components/page-loader/index.js +7 -0
- package/dist/components/page-loader/page-loader.js +37 -0
- package/dist/components/page-loader/page-loader.meta.js +85 -0
- package/dist/components/pagination/index.js +7 -0
- package/dist/components/pagination/pagination.js +105 -0
- package/dist/components/pagination/pagination.meta.js +141 -0
- package/dist/components/panel-group/index.js +10 -0
- package/dist/components/panel-group/panel-group.js +168 -0
- package/dist/components/panel-group/panel-group.meta.js +153 -0
- package/dist/components/panel-section/index.js +9 -0
- package/dist/components/panel-section/panel-section.js +115 -0
- package/dist/components/panel-section/panel-section.meta.js +156 -0
- package/dist/components/phone/index.js +7 -0
- package/dist/components/phone/phone.js +50 -0
- package/dist/{chunk-KFZ43RKU.js → components/phone/phone.meta.js} +3 -50
- package/dist/{chunk-NEQNXI2G.js → components/phone/status-icons.js} +4 -4
- package/dist/components/pie-chart/index.js +7 -0
- package/dist/components/pie-chart/pie-chart.js +191 -0
- package/dist/components/pie-chart/pie-chart.meta.js +210 -0
- package/dist/components/pill/index.js +9 -0
- package/dist/components/pill/pill.js +24 -0
- package/dist/components/pill/pill.meta.js +108 -0
- package/dist/components/pill/pill.variants.js +65 -0
- package/dist/components/popover/index.js +23 -0
- package/dist/components/popover/popover.js +100 -0
- package/dist/components/popover/popover.meta.js +136 -0
- package/dist/components/progress/index.js +19 -0
- package/dist/components/progress/progress.js +70 -0
- package/dist/components/progress/progress.meta.js +171 -0
- package/dist/components/progress/progress.variants.js +17 -0
- package/dist/components/radar-chart/index.js +7 -0
- package/dist/components/radar-chart/radar-chart.js +161 -0
- package/dist/components/radar-chart/radar-chart.meta.js +204 -0
- package/dist/components/radial-chart/index.js +7 -0
- package/dist/components/radial-chart/radial-chart.js +175 -0
- package/dist/components/radial-chart/radial-chart.meta.js +216 -0
- package/dist/components/radio/index.js +8 -0
- package/dist/components/radio/radio.js +39 -0
- package/dist/components/radio/radio.meta.js +104 -0
- package/dist/components/radio-card/index.js +7 -0
- package/dist/components/radio-card/radio-card.js +81 -0
- package/dist/components/radio-card/radio-card.meta.js +144 -0
- package/dist/components/radio-card/radio-card.variants.js +43 -0
- package/dist/components/relative-time/index.js +7 -0
- package/dist/components/relative-time/relative-time.js +47 -0
- package/dist/components/relative-time/relative-time.meta.js +77 -0
- package/dist/components/scroller/index.js +7 -0
- package/dist/components/scroller/scroller.js +111 -0
- package/dist/components/scroller/scroller.meta.js +118 -0
- package/dist/components/scrub-field/index.js +7 -0
- package/dist/components/scrub-field/scrub-field.js +124 -0
- package/dist/components/scrub-field/scrub-field.meta.js +139 -0
- package/dist/components/search-input/index.js +7 -0
- package/dist/components/search-input/search-input.js +91 -0
- package/dist/components/search-input/search-input.meta.js +129 -0
- package/dist/components/section-header/index.js +7 -0
- package/dist/components/section-header/section-header.js +39 -0
- package/dist/components/section-header/section-header.meta.js +174 -0
- package/dist/components/segmented-bar/index.js +14 -0
- package/dist/components/segmented-bar/segmented-bar.js +26 -0
- package/dist/components/segmented-bar/segmented-bar.meta.js +108 -0
- package/dist/components/segmented-bar/segmented-bar.variants.js +39 -0
- package/dist/components/select/index.js +25 -0
- package/dist/components/select/select.js +191 -0
- package/dist/components/select/select.meta.js +189 -0
- package/dist/components/separator/index.js +7 -0
- package/dist/components/separator/separator.js +42 -0
- package/dist/components/separator/separator.meta.js +84 -0
- package/dist/components/settings-menu/index.js +11 -0
- package/dist/components/settings-menu/settings-menu.js +101 -0
- package/dist/components/settings-menu/settings-menu.meta.js +177 -0
- package/dist/components/settings-menu/settings-menu.variants.js +15 -0
- package/dist/components/sheet/index.js +29 -0
- package/dist/components/sheet/sheet.js +121 -0
- package/dist/components/sheet/sheet.meta.js +167 -0
- package/dist/components/sheet/sheet.variants.js +21 -0
- package/dist/components/show-more/index.js +7 -0
- package/dist/components/show-more/show-more.js +87 -0
- package/dist/components/show-more/show-more.meta.js +79 -0
- package/dist/components/side-nav/index.js +13 -0
- package/dist/components/side-nav/side-nav.js +75 -0
- package/dist/components/side-nav/side-nav.meta.js +139 -0
- package/dist/components/side-nav/side-nav.variants.js +33 -0
- package/dist/components/sidebar/index.js +21 -0
- package/dist/components/sidebar/sidebar.js +196 -0
- package/dist/components/sidebar/sidebar.meta.js +118 -0
- package/dist/components/skeleton/index.js +7 -0
- package/dist/components/skeleton/skeleton.js +16 -0
- package/dist/components/skeleton/skeleton.meta.js +90 -0
- package/dist/components/slider/index.js +21 -0
- package/dist/components/slider/slider.js +115 -0
- package/dist/components/slider/slider.meta.js +214 -0
- package/dist/components/sparkline/index.js +7 -0
- package/dist/components/sparkline/sparkline.js +84 -0
- package/dist/components/sparkline/sparkline.meta.js +147 -0
- package/dist/components/spinner/index.js +7 -0
- package/dist/components/spinner/spinner.js +59 -0
- package/dist/components/spinner/spinner.meta.js +118 -0
- package/dist/components/status-bar/index.js +9 -0
- package/dist/components/status-bar/status-bar.js +40 -0
- package/dist/components/status-bar/status-bar.meta.js +117 -0
- package/dist/components/status-dot/index.js +9 -0
- package/dist/components/status-dot/status-dot.js +40 -0
- package/dist/components/status-dot/status-dot.meta.js +113 -0
- package/dist/components/status-dot/status-dot.variants.js +28 -0
- package/dist/components/stepper/index.js +17 -0
- package/dist/components/stepper/stepper.js +161 -0
- package/dist/components/stepper/stepper.meta.js +190 -0
- package/dist/components/stepper/stepper.variants.js +72 -0
- package/dist/components/streaming-text/index.js +7 -0
- package/dist/components/streaming-text/streaming-text.js +55 -0
- package/dist/components/streaming-text/streaming-text.meta.js +147 -0
- package/dist/components/switch/index.js +7 -0
- package/dist/components/switch/switch.js +26 -0
- package/dist/components/switch/switch.meta.js +133 -0
- package/dist/components/table/index.js +23 -0
- package/dist/components/table/table.js +117 -0
- package/dist/components/table/table.meta.js +143 -0
- package/dist/components/tabs/index.js +17 -0
- package/dist/components/tabs/tabs.js +58 -0
- package/dist/components/tabs/tabs.meta.js +120 -0
- package/dist/components/textarea/index.js +7 -0
- package/dist/components/textarea/textarea.js +38 -0
- package/dist/components/textarea/textarea.meta.js +117 -0
- package/dist/components/theme-selector/index.js +11 -0
- package/dist/components/theme-selector/theme-selector.js +145 -0
- package/dist/components/theme-selector/theme-selector.meta.js +181 -0
- package/dist/components/theme-switcher/index.js +7 -0
- package/dist/components/theme-switcher/theme-switcher.js +106 -0
- package/dist/components/theme-switcher/theme-switcher.meta.js +99 -0
- package/dist/components/thinking-orb/index.js +9 -0
- package/dist/components/thinking-orb/thinking-orb.js +126 -0
- package/dist/components/thinking-orb/thinking-orb.meta.js +142 -0
- package/dist/components/thinking-orb/thinking-orb.render.js +252 -0
- package/dist/components/toast/index.js +29 -0
- package/dist/components/toast/toast.js +254 -0
- package/dist/components/toast/toast.meta.js +261 -0
- package/dist/components/toggle/index.js +13 -0
- package/dist/components/toggle/toggle.js +51 -0
- package/dist/components/toggle/toggle.meta.js +129 -0
- package/dist/components/toolbar/index.js +27 -0
- package/dist/components/toolbar/toolbar.js +80 -0
- package/dist/components/toolbar/toolbar.meta.js +272 -0
- package/dist/components/toolbar/toolbar.variants.js +16 -0
- package/dist/components/tooltip/index.js +21 -0
- package/dist/components/tooltip/tooltip.js +74 -0
- package/dist/components/tooltip/tooltip.meta.js +156 -0
- package/dist/components/tree/index.js +12 -0
- package/dist/components/tree/tree.js +364 -0
- package/dist/components/tree/tree.meta.js +334 -0
- package/dist/components/video/index.js +7 -0
- package/dist/components/video/video.js +59 -0
- package/dist/components/video/video.meta.js +81 -0
- package/dist/components/workspace/index.js +9 -0
- package/dist/components/workspace/workspace.js +79 -0
- package/dist/components/workspace/workspace.meta.js +183 -0
- package/dist/foundations/accessibility.js +107 -0
- package/dist/foundations/index.js +25 -792
- package/dist/foundations/motion.js +135 -0
- package/dist/foundations/scrolling.js +62 -0
- package/dist/foundations/shape.js +113 -0
- package/dist/foundations/spacing.js +121 -0
- package/dist/foundations/surfaces.js +107 -0
- package/dist/foundations/types.js +0 -0
- package/dist/foundations/typography.js +152 -0
- package/dist/icon/Icon.js +30 -0
- package/dist/icon/IconProvider.js +27 -0
- package/dist/icon/compose.js +13 -0
- package/dist/icon/index.d.ts +13 -5
- package/dist/icon/index.js +13 -2
- package/dist/icon/registry.js +0 -0
- package/dist/icon-sets/brands.generated.js +123 -0
- package/dist/icon-sets/brands.js +10 -123
- package/dist/icon-sets/reicon.d.ts +3 -1
- package/dist/icon-sets/reicon.js +209 -1
- package/dist/index.d.ts +611 -33
- package/dist/index.js +211 -11351
- package/dist/ios/chrome.js +36 -0
- package/dist/ios/index.js +19 -315
- package/dist/ios/liquid-glass.js +103 -0
- package/dist/ios/native-app.js +46 -0
- package/dist/ios/navigation.js +63 -0
- package/dist/ios/phone-frame.js +64 -0
- package/dist/ios/preview-theme.js +83 -0
- package/dist/ios/preview.js +10 -137
- package/dist/ios/sheet.js +85 -0
- package/dist/ios/theme.js +19 -0
- package/dist/layout/AspectBox.js +27 -0
- package/dist/layout/Bleed.js +15 -0
- package/dist/layout/Center.js +21 -0
- package/dist/layout/Columns.js +70 -0
- package/dist/layout/Grid.js +50 -0
- package/dist/layout/GridOverlay.js +41 -0
- package/dist/layout/Page.js +11 -0
- package/dist/layout/Region.js +39 -0
- package/dist/layout/Row.js +45 -0
- package/dist/layout/SafeArea.js +26 -0
- package/dist/layout/Spacer.js +9 -0
- package/dist/layout/SplitPane.js +55 -0
- package/dist/layout/Stack.js +42 -0
- package/dist/layout/_responsive.js +16 -0
- package/dist/layout/_safeArea.js +18 -0
- package/dist/layout/index.d.ts +25 -6
- package/dist/layout/index.js +14 -2
- package/dist/lib/anchored-scroll.js +7 -0
- package/dist/lib/bleed.js +7 -0
- package/dist/lib/chart/cartesian.js +365 -0
- package/dist/lib/chart/chart-frame.js +163 -0
- package/dist/lib/chart/chart-legend.js +13 -0
- package/dist/lib/chart/chart-tooltip.js +38 -0
- package/dist/lib/chart/format.js +19 -0
- package/dist/lib/chart/geometry.js +114 -0
- package/dist/lib/chart/motion.js +34 -0
- package/dist/lib/chart/scale.js +72 -0
- package/dist/lib/chart/types.js +45 -0
- package/dist/lib/chart/use-arrival.js +41 -0
- package/dist/{chunk-SRQWQUTW.js → lib/cn.js} +5 -5
- package/dist/lib/component-guidance.js +210 -0
- package/dist/lib/density.js +54 -0
- package/dist/lib/field-shell.js +11 -0
- package/dist/lib/glyphs.js +9 -0
- package/dist/lib/heading-row.js +36 -0
- package/dist/lib/highlight-glide.js +84 -0
- package/dist/lib/hit-target.js +5 -0
- package/dist/lib/meta.js +0 -0
- package/dist/lib/portal-theme-scope.js +74 -0
- package/dist/{chunk-FKZXAFIY.js → lib/presentation.js} +8 -3
- package/dist/lib/refs.js +14 -0
- package/dist/lib/scrub-gesture.js +50 -0
- package/dist/lib/segmented.js +13 -0
- package/dist/lib/selection-card.js +41 -0
- package/dist/lib/status-tone.js +13 -0
- package/dist/lib/taxonomy.js +214 -0
- package/dist/lib/trim-label.js +20 -0
- package/dist/lib/use-copy.js +27 -0
- package/dist/lib/use-edge-fade.js +32 -0
- package/dist/lib/use-isomorphic-layout-effect.js +8 -0
- package/dist/lib/use-resize-observer.js +18 -0
- package/dist/lib/use-scroll-activity.js +52 -0
- package/dist/lib/use-wheel-to-horizontal.js +65 -0
- package/dist/motion/MotionProvider.js +75 -0
- package/dist/motion/Presence.js +112 -0
- package/dist/motion/Reveal.js +42 -0
- package/dist/motion/TextReveal.js +61 -0
- package/dist/motion/index.js +24 -0
- package/dist/motion/useInView.js +36 -0
- package/dist/motion/useReducedMotion.js +25 -0
- package/dist/motion/viewTransition.js +47 -0
- package/dist/patterns/auth-page.js +90 -0
- package/dist/patterns/auth-page.meta.js +27 -0
- package/dist/patterns/brand-landing-page.js +341 -0
- package/dist/patterns/brand-landing-page.meta.js +28 -0
- package/dist/patterns/chat-panel.js +185 -0
- package/dist/patterns/chat-panel.meta.js +31 -0
- package/dist/patterns/dashboard-page.js +332 -0
- package/dist/patterns/dashboard-page.meta.js +29 -0
- package/dist/patterns/feed-page.js +205 -0
- package/dist/patterns/feed-page.meta.js +25 -0
- package/dist/patterns/index.js +34 -2140
- package/dist/patterns/landing-page.js +356 -0
- package/dist/patterns/landing-page.meta.js +26 -0
- package/dist/patterns/list-detail-page.js +373 -0
- package/dist/patterns/list-detail-page.meta.js +24 -0
- package/dist/patterns/settings-page.js +268 -0
- package/dist/patterns/settings-page.meta.js +27 -0
- package/dist/presets/index.js +43 -185
- package/dist/styles.css +118 -3
- package/dist/{theme-provider-kQszHYji.d.ts → theme-provider-S0NVJs2Y.d.ts} +6 -1
- package/dist/{chunk-NETUFI34.js → themes/_contract.js} +14 -2
- package/dist/themes/atelier/index.js +17 -1
- package/dist/themes/contrast.js +288 -0
- package/dist/themes/ember/ember.css +6 -6
- package/dist/themes/ember/index.js +16 -1
- package/dist/themes/index.d.ts +1 -1
- package/dist/themes/index.js +3 -8
- package/dist/themes/meridian/index.js +16 -1
- package/dist/themes/poppy/index.js +17 -1
- package/dist/themes/prism/index.js +17 -1
- package/dist/themes/prism/prism.css +2 -2
- package/dist/themes/registry.js +59 -0
- package/dist/themes/sable/index.js +16 -1
- package/dist/{chunk-EFLC4NA3.js → themes/theme-provider.js} +38 -54
- package/dist/themes/verdant/index.js +17 -1
- package/llms/blocks/media-player-block.md +20 -7
- package/llms/blocks/product-card-block.md +6 -4
- package/llms/blocks/task-list-block.md +1 -1
- package/llms/blocks/verify-code-block.md +1 -1
- package/llms/components/app-shell.md +5 -2
- package/llms/components/avatar.md +25 -3
- package/llms/components/button.md +20 -1
- package/llms/components/card.md +1 -1
- package/llms/components/disclosure-picker.md +168 -0
- package/llms/components/font-picker.md +104 -0
- package/llms/components/media-image.md +98 -0
- package/llms/components/media-tile.md +143 -0
- package/llms/components/page-header.md +6 -5
- package/llms/components/section-header.md +144 -0
- package/llms/components/sheet.md +10 -0
- package/llms/components/sidebar.md +4 -3
- package/llms/components/slider.md +14 -2
- package/llms/components/theme-selector.md +44 -4
- package/llms/components/tree.md +3 -3
- package/llms/layout/grid.md +24 -3
- package/llms/patterns/dashboard-page.md +3 -6
- package/llms/patterns/feed-page.md +4 -4
- package/llms/patterns/list-detail-page.md +1 -0
- package/llms/patterns/settings-page.md +2 -5
- package/llms/reference/utilities.md +84 -4
- package/llms/tasks/converting-an-app.md +14 -4
- package/llms/tasks/reviewing-a-design.md +15 -0
- package/llms-full.txt +981 -65
- package/llms.txt +27 -10
- package/package.json +1 -1
- package/registry.json +2324 -171
- package/scripts/agent-setup.mjs +90 -0
- package/scripts/design-presets.mjs +46 -4
- package/scripts/grade.mjs +195 -29
- package/scripts/lint-rules.mjs +486 -10
- package/scripts/review-evidence.mjs +28 -1
- package/skills/sable/SKILL.md +56 -22
- package/targets/ios.json +45 -0
- package/dist/chunk-3IWZYBGH.js +0 -398
- package/dist/chunk-7MG4SHYX.js +0 -15
- package/dist/chunk-7QSDYBXO.js +0 -7421
- package/dist/chunk-ADLLDWLW.js +0 -15
- package/dist/chunk-C4Z3CF54.js +0 -15
- package/dist/chunk-EDXLM7YL.js +0 -5078
- package/dist/chunk-EHH4MHO6.js +0 -14
- package/dist/chunk-K7RUHUGC.js +0 -14
- package/dist/chunk-KO3NAEBF.js +0 -15
- package/dist/chunk-L4IEIQQA.js +0 -14
- package/dist/chunk-QJ6FWTUG.js +0 -40
- package/dist/chunk-VVW4UCDI.js +0 -4908
- package/dist/chunk-XAZGMY73.js +0 -187
package/llms-full.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sable (@sableui/react)
|
|
2
2
|
|
|
3
|
-
> An owned, themeable React component system. Default theme: **
|
|
3
|
+
> An owned, themeable React component system. Default theme: **Sable** (dark-by-default, content-first, fluid product UI). Behavior from Base UI; styling via Tailwind v4 + semantic CSS-variable tokens; Reicon icons via a pluggable adapter. 111 components, 13 layout primitives, and a **patterns** tier of canonical page shells. The full, per-component contract is the generated `registry.json`.
|
|
4
4
|
|
|
5
5
|
## Start here — brief, composition, evidence
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ validates a record and `--css <path>` generates its scoped stylesheet.
|
|
|
13
13
|
|
|
14
14
|
For a new surface, first read `sable docs designing-a-surface`: define the user
|
|
15
15
|
outcome, content, visual direction, story, interactions, motion, and adaptation.
|
|
16
|
-
|
|
16
|
+
Sable is the default identity; a consumer's brief can call for a different
|
|
17
17
|
visual world. Retrieve conditional precedents with `sable learn --search
|
|
18
18
|
"<context>"`. After building, inspect the rendered result using `sable docs
|
|
19
19
|
reviewing-a-design` and preserve a useful discovery through `sable docs
|
|
@@ -1207,12 +1207,22 @@ probably keeping a layout that Sable would have given you.
|
|
|
1207
1207
|
|
|
1208
1208
|
## 4. Icons
|
|
1209
1209
|
|
|
1210
|
-
**
|
|
1211
|
-
|
|
1212
|
-
IconProvider. To add domain-specific names while retaining Reicon:
|
|
1210
|
+
**Mount an icon set once at the root.** `<Icon>` has no built-in fallback:
|
|
1211
|
+
without an `IconProvider`, icons render nothing (a development build warns once).
|
|
1213
1212
|
|
|
1214
1213
|
```tsx
|
|
1215
|
-
import { IconProvider
|
|
1214
|
+
import { IconProvider } from "@sableui/react"
|
|
1215
|
+
import { reiconIconSet } from "@sableui/react/icons/reicon"
|
|
1216
|
+
<IconProvider set={reiconIconSet}>{app}</IconProvider>
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
To use another icon package, map Sable's semantic names in an app-owned IconSet
|
|
1220
|
+
and pass it instead; the app then ships none of Reicon. To add domain-specific
|
|
1221
|
+
names while retaining Reicon:
|
|
1222
|
+
|
|
1223
|
+
```tsx
|
|
1224
|
+
import { IconProvider, composeIconSets } from "@sableui/react"
|
|
1225
|
+
import { reiconIconSet } from "@sableui/react/icons/reicon"
|
|
1216
1226
|
<IconProvider set={composeIconSets(appExtras, reiconIconSet)} />
|
|
1217
1227
|
```
|
|
1218
1228
|
|
|
@@ -1937,6 +1947,21 @@ those values into artifact records only after inspecting the corresponding files
|
|
|
1937
1947
|
replacing a capture at the same path then makes the review incomplete. Neither
|
|
1938
1948
|
the source fingerprint nor an artifact hash proves someone inspected the screen.
|
|
1939
1949
|
|
|
1950
|
+
The template starts with one example screenshot and one example report artifact.
|
|
1951
|
+
Replace them with your own captures; as shipped they point at no file and fail
|
|
1952
|
+
validation. Each artifact record has these fields:
|
|
1953
|
+
|
|
1954
|
+
| Field | Meaning |
|
|
1955
|
+
| --- | --- |
|
|
1956
|
+
| `id` | A unique name that dimensions, checks and iterations cite in `evidence`. |
|
|
1957
|
+
| `kind` | `screenshot`, `recording`, or `report` (a written observation, such as a keyboard walkthrough). |
|
|
1958
|
+
| `path` | The capture file, relative to the review JSON. It must exist and be non-empty. |
|
|
1959
|
+
| `sha256` | The file's hash, copied from `review.artifactHashes` after you inspect the file. |
|
|
1960
|
+
| `state` | The route and state captured, such as "checkout, payment error". |
|
|
1961
|
+
| `viewport` | `{ width, height }` in pixels. Screenshots and recordings only. You need one at 1024 wide or more and one at 480 wide or less. |
|
|
1962
|
+
| `colorMode` | `light` or `dark`. Screenshots and recordings only. |
|
|
1963
|
+
| `motion` | `full` or `reduced`. Screenshots and recordings only. At least one artifact must be captured with reduced motion. |
|
|
1964
|
+
|
|
1940
1965
|
## Review these eight dimensions
|
|
1941
1966
|
|
|
1942
1967
|
| Dimension id | Look for concrete evidence |
|
|
@@ -3943,7 +3968,7 @@ App-shell creator dashboard: a branded Sidebar with an account footer (AppHeader
|
|
|
3943
3968
|
## Source
|
|
3944
3969
|
|
|
3945
3970
|
```tsx
|
|
3946
|
-
import { AppHeader, AppShell, Avatar, Badge, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, DataTable, Entity, Grid, Icon, MenuItem, MenuSeparator, Meter, NotificationBadge, OverflowMenu, Page, PageHeader, RelativeTime, Row, Sidebar, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, Stack } from "@sableui/react"
|
|
3971
|
+
import { AppHeader, AppShell, Avatar, Badge, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, DataTable, Entity, Grid, Icon, MenuItem, MenuSeparator, Meter, NotificationBadge, OverflowMenu, Page, PageHeader, RelativeTime, Row, Sidebar, SidebarBrand, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, Stack } from "@sableui/react"
|
|
3947
3972
|
|
|
3948
3973
|
// Edition status → Badge tone. Sable has no status vocabulary of its own;
|
|
3949
3974
|
// an app maps ITS states onto tones once, here, and every badge follows.
|
|
@@ -4040,10 +4065,7 @@ export function DashboardPage() {
|
|
|
4040
4065
|
sidebar={
|
|
4041
4066
|
<Sidebar>
|
|
4042
4067
|
<SidebarHeader>
|
|
4043
|
-
<
|
|
4044
|
-
<Icon name="gem" variant="solid" className="text-xl" />
|
|
4045
|
-
<span className="text-title-sm">Atelier</span>
|
|
4046
|
-
</Row>
|
|
4068
|
+
<SidebarBrand mark={<Icon name="gem" variant="solid" />} label="Atelier" render={<a href="/" />} />
|
|
4047
4069
|
</SidebarHeader>
|
|
4048
4070
|
<SidebarNav>
|
|
4049
4071
|
<SidebarItem icon={<Icon name="grid" />} label="Overview" active render={<a href="/" />} />
|
|
@@ -4188,7 +4210,7 @@ export function DashboardPage() {
|
|
|
4188
4210
|
<span className="text-caption text-muted-foreground">
|
|
4189
4211
|
Showing {editions.length} of {editionCount} editions
|
|
4190
4212
|
</span>
|
|
4191
|
-
<Button variant="ghost" size="sm" render={<a href="/editions" />}>
|
|
4213
|
+
<Button variant="ghost" size="sm" bleed="end" render={<a href="/editions" />}>
|
|
4192
4214
|
View all editions
|
|
4193
4215
|
<Icon name="arrow-right" />
|
|
4194
4216
|
</Button>
|
|
@@ -4445,10 +4467,10 @@ const renderPost = (post: Post) => (
|
|
|
4445
4467
|
</Meter>
|
|
4446
4468
|
</Stack>
|
|
4447
4469
|
) : null}
|
|
4448
|
-
{/*
|
|
4449
|
-
|
|
4450
|
-
<Row gap={0.5}
|
|
4451
|
-
<Button variant="ghost" size="sm" aria-label={`Like, ${post.likes} likes`}>
|
|
4470
|
+
{/* bleed="start" pulls the first button out by exactly its own
|
|
4471
|
+
padding, so its icon lands on the text edge above it. */}
|
|
4472
|
+
<Row gap={0.5}>
|
|
4473
|
+
<Button variant="ghost" size="sm" bleed="start" aria-label={`Like, ${post.likes} likes`}>
|
|
4452
4474
|
<Icon name="heart" />
|
|
4453
4475
|
{post.likes}
|
|
4454
4476
|
</Button>
|
|
@@ -5365,6 +5387,7 @@ export function ListDetailPage() {
|
|
|
5365
5387
|
ref={backRef}
|
|
5366
5388
|
variant="ghost"
|
|
5367
5389
|
size="sm"
|
|
5390
|
+
bleed="start"
|
|
5368
5391
|
iconStart={<Icon name="chevron-left" />}
|
|
5369
5392
|
onClick={backToList}
|
|
5370
5393
|
className="self-start lg:hidden"
|
|
@@ -5549,7 +5572,7 @@ Account settings inside the app shell: a sticky SideNav section rail beside a co
|
|
|
5549
5572
|
|
|
5550
5573
|
```tsx
|
|
5551
5574
|
import { useState } from "react"
|
|
5552
|
-
import { AppHeader, AppShell, Avatar, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, Col, Columns, DestructiveActionModal, Entity, Field, Form, Grid, Icon, Input, MenuItem, MenuSeparator, NotificationBadge, OverflowMenu, Page, PageHeader, Row, Select, SelectContent, SelectItem, SelectTrigger, Sidebar, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, SideNav, SideNavItem, Stack, Switch, Textarea } from "@sableui/react"
|
|
5575
|
+
import { AppHeader, AppShell, Avatar, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, Col, Columns, DestructiveActionModal, Entity, Field, Form, Grid, Icon, Input, MenuItem, MenuSeparator, NotificationBadge, OverflowMenu, Page, PageHeader, Row, Select, SelectContent, SelectItem, SelectTrigger, Sidebar, SidebarBrand, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, SideNav, SideNavItem, Stack, Switch, Textarea } from "@sableui/react"
|
|
5553
5576
|
|
|
5554
5577
|
const sections = [
|
|
5555
5578
|
{ id: "profile", label: "Profile" },
|
|
@@ -5611,10 +5634,7 @@ export function SettingsPage() {
|
|
|
5611
5634
|
sidebar={
|
|
5612
5635
|
<Sidebar>
|
|
5613
5636
|
<SidebarHeader>
|
|
5614
|
-
<
|
|
5615
|
-
<Icon name="gem" variant="solid" className="text-xl" />
|
|
5616
|
-
<span className="text-title-sm">Atelier</span>
|
|
5617
|
-
</Row>
|
|
5637
|
+
<SidebarBrand mark={<Icon name="gem" variant="solid" />} label="Atelier" render={<a href="/" />} />
|
|
5618
5638
|
</SidebarHeader>
|
|
5619
5639
|
<SidebarNav>
|
|
5620
5640
|
<SidebarItem icon={<Icon name="grid" />} label="Overview" render={<a href="/" />} />
|
|
@@ -6930,9 +6950,15 @@ A now-playing card: cover, title and artist as an Entity with a Like action, a p
|
|
|
6930
6950
|
|
|
6931
6951
|
```tsx
|
|
6932
6952
|
import { useState } from "react"
|
|
6933
|
-
import { Button, Card, CardContent, cn, Entity,
|
|
6953
|
+
import { Button, Card, CardContent, cn, Entity, Icon, MediaImage, Slider } from "@sableui/react"
|
|
6954
|
+
|
|
6955
|
+
// `cover` is the artwork URL; left empty, the cover paints art seeded by the title.
|
|
6956
|
+
const track = { title: "Low tide, high noon", artist: "Juno Avery", cover: "", duration: 220, position: 84 }
|
|
6934
6957
|
|
|
6935
|
-
|
|
6958
|
+
/** Seconds → m:ss, the way the readout and the screen reader both say it. */
|
|
6959
|
+
function formatTime(seconds: number) {
|
|
6960
|
+
return `${Math.floor(seconds / 60)}:${String(Math.floor(seconds % 60)).padStart(2, "0")}`
|
|
6961
|
+
}
|
|
6936
6962
|
|
|
6937
6963
|
|
|
6938
6964
|
export interface MediaPlayerBlockProps {
|
|
@@ -6947,6 +6973,7 @@ export interface MediaPlayerBlockProps {
|
|
|
6947
6973
|
|
|
6948
6974
|
export function MediaPlayerBlock({ className, framed = true, onPlayingChange, onPrevious, onNext }: MediaPlayerBlockProps) {
|
|
6949
6975
|
const [playing, setPlaying] = useState(true)
|
|
6976
|
+
const [position, setPosition] = useState(track.position)
|
|
6950
6977
|
return (
|
|
6951
6978
|
<Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-xs", className)}>
|
|
6952
6979
|
<CardContent className="flex flex-col gap-4">
|
|
@@ -6954,8 +6981,7 @@ export function MediaPlayerBlock({ className, framed = true, onPlayingChange, on
|
|
|
6954
6981
|
card already pads it. */}
|
|
6955
6982
|
<Entity
|
|
6956
6983
|
className="p-0"
|
|
6957
|
-
|
|
6958
|
-
leading={<GradientArt aria-hidden seed={track.title} className="size-14 rounded-md" />}
|
|
6984
|
+
leading={<MediaImage src={track.cover} seed={track.title} className="size-14 shrink-0 rounded-md" />}
|
|
6959
6985
|
title={track.title}
|
|
6960
6986
|
subtitle={track.artist}
|
|
6961
6987
|
action={
|
|
@@ -6965,10 +6991,17 @@ export function MediaPlayerBlock({ className, framed = true, onPlayingChange, on
|
|
|
6965
6991
|
}
|
|
6966
6992
|
/>
|
|
6967
6993
|
<div className="flex flex-col gap-1.5">
|
|
6968
|
-
<Slider
|
|
6994
|
+
<Slider
|
|
6995
|
+
value={position}
|
|
6996
|
+
max={track.duration}
|
|
6997
|
+
onValueChange={setPosition}
|
|
6998
|
+
aria-label="Position"
|
|
6999
|
+
// Spoken as a time, not "38": the number alone means nothing.
|
|
7000
|
+
getAriaValueText={(_, value) => `${formatTime(value)} of ${formatTime(track.duration)}`}
|
|
7001
|
+
/>
|
|
6969
7002
|
<div className="flex justify-between text-mono-caption text-muted-foreground">
|
|
6970
|
-
<span>{
|
|
6971
|
-
<span
|
|
7003
|
+
<span>{formatTime(position)}</span>
|
|
7004
|
+
<span>-{formatTime(track.duration - position)}</span>
|
|
6972
7005
|
</div>
|
|
6973
7006
|
</div>
|
|
6974
7007
|
<div className="flex items-center justify-center gap-3">
|
|
@@ -7550,9 +7583,10 @@ A product card: edge-to-edge artwork, title and maker, an edition Badge, and the
|
|
|
7550
7583
|
## Source
|
|
7551
7584
|
|
|
7552
7585
|
```tsx
|
|
7553
|
-
import { Badge, Button, Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle, cn,
|
|
7586
|
+
import { AspectBox, Badge, Button, Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle, cn, MediaImage } from "@sableui/react"
|
|
7554
7587
|
|
|
7555
|
-
|
|
7588
|
+
// `image` is the product shot URL; left empty, it paints art seeded by the title.
|
|
7589
|
+
const product = { title: "Untitled (gold rush)", maker: "Mara Okafor", image: "", edition: "12 of 50", price: "0.42 SOL" }
|
|
7556
7590
|
|
|
7557
7591
|
|
|
7558
7592
|
export interface ProductCardBlockProps {
|
|
@@ -7566,8 +7600,9 @@ export interface ProductCardBlockProps {
|
|
|
7566
7600
|
export function ProductCardBlock({ className, framed = true, onCollect }: ProductCardBlockProps) {
|
|
7567
7601
|
return (
|
|
7568
7602
|
<Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-xs overflow-hidden", className)}>
|
|
7569
|
-
{
|
|
7570
|
-
|
|
7603
|
+
<AspectBox ratio={4 / 3}>
|
|
7604
|
+
<MediaImage src={product.image} seed={product.title} />
|
|
7605
|
+
</AspectBox>
|
|
7571
7606
|
<CardHeader>
|
|
7572
7607
|
<CardTitle>{product.title}</CardTitle>
|
|
7573
7608
|
<CardDescription>{product.maker}</CardDescription>
|
|
@@ -8327,7 +8362,7 @@ export function TaskListBlock({ className, framed = true, onAddTask }: TaskListB
|
|
|
8327
8362
|
</ul>
|
|
8328
8363
|
</CardContent>
|
|
8329
8364
|
<CardFooter>
|
|
8330
|
-
<Button variant="ghost" size="sm" iconStart={<Icon name="plus" />} onClick={onAddTask}>
|
|
8365
|
+
<Button variant="ghost" size="sm" bleed="start" iconStart={<Icon name="plus" />} onClick={onAddTask}>
|
|
8331
8366
|
Add task
|
|
8332
8367
|
</Button>
|
|
8333
8368
|
</CardFooter>
|
|
@@ -8797,7 +8832,7 @@ export function VerifyCodeBlock({ className, framed = true, onVerify, onResend }
|
|
|
8797
8832
|
</CardContent>
|
|
8798
8833
|
<CardFooter className="justify-between gap-3">
|
|
8799
8834
|
<span className="text-caption text-muted-foreground">Didn’t get it?</span>
|
|
8800
|
-
<Button variant="ghost" size="sm" onClick={onResend}>
|
|
8835
|
+
<Button variant="ghost" size="sm" bleed="end" onClick={onResend}>
|
|
8801
8836
|
Resend code
|
|
8802
8837
|
</Button>
|
|
8803
8838
|
</CardFooter>
|
|
@@ -8993,7 +9028,7 @@ import { Card, CardContent, Page, PageHeader } from "@sableui/react"
|
|
|
8993
9028
|
|
|
8994
9029
|
**Status:** stable
|
|
8995
9030
|
|
|
8996
|
-
CSS Grid within a region. Two modes: fixed/responsive cols count, or minItemWidth
|
|
9031
|
+
CSS Grid within a region. Two modes: fixed/responsive cols count, or minItemWidth so cells breathe with NO breakpoints (the default choice for card grids). A minItemWidth grid keeps its empty columns (track="fill"), so one result stays one cell wide; track="fit" stretches a fixed set across the row.
|
|
8997
9032
|
|
|
8998
9033
|
**Keywords:** grid, card grid, auto fit, layout
|
|
8999
9034
|
|
|
@@ -9004,16 +9039,20 @@ Use Grid for uniform grid layouts inside a Region, especially card grids that ca
|
|
|
9004
9039
|
### Best practices
|
|
9005
9040
|
|
|
9006
9041
|
- Do: Prefer minItemWidth for responsive card grids that do not need breakpoint-specific spans.
|
|
9042
|
+
- Do: Keep the default track="fill" for collections whose count is data (search results, shelves, galleries): sparse rows keep their empty columns, so one result stays one cell wide.
|
|
9043
|
+
- Do: Use track="fit" only for a fixed set designed to span the row, such as three KPI cards.
|
|
9007
9044
|
- Do: Use explicit columns only when the design truly needs a fixed track count.
|
|
9008
9045
|
- Don't: Do not use Grid to set a page's reading width; use Region or Center.
|
|
9046
|
+
- Don't: Do not use track="fit" for results or other data-driven collections: with one or two items the empty columns collapse and each item stretches across the row (a single result rendered as a full-width cover).
|
|
9009
9047
|
|
|
9010
9048
|
## Props
|
|
9011
9049
|
|
|
9012
9050
|
| Name | Type | Default | Description |
|
|
9013
9051
|
| --- | --- | --- | --- |
|
|
9014
9052
|
| cols | number \| { base?, sm?, md?, lg?, xl?, "2xl"? } | 12 | Fixed column count or a mobile-first breakpoint map ({ base: 2, md: 4 }). Ignored when minItemWidth is set. |
|
|
9015
|
-
| minItemWidth | string | — |
|
|
9016
|
-
|
|
|
9053
|
+
| minItemWidth | string | — | Breathing mode: repeat(auto-fill, minmax(minItemWidth, 1fr)) — as many columns of at least minItemWidth as fit, wrapping without breakpoints. e.g. "16rem". |
|
|
9054
|
+
| track | "fill" \| "fit" | "fill" | What a minItemWidth grid does with columns it has no items for (only visible when fewer items than fit one row). "fill" (auto-fill) keeps them, so items keep their size whatever their count — one search result is one cover wide, not a banner. Use it for collections whose count is data: results, shelves, galleries. "fit" (auto-fit) collapses them and stretches the items to share the row — only for a fixed set designed to span it (three KPI cards, plan tiers). |
|
|
9055
|
+
| gap | number \| [row, column] \| string | var(--grid-gutter) | Gap between cells. Number = rhythm units (×8px); [row, column] = two rhythm counts, for a gallery whose rows want more air than its columns; string = raw CSS. |
|
|
9017
9056
|
|
|
9018
9057
|
## Related
|
|
9019
9058
|
|
|
@@ -9029,6 +9068,23 @@ import { Card, CardHeader, CardTitle, Grid } from "@sableui/react"
|
|
|
9029
9068
|
<Grid minItemWidth="16rem" gap={3}><Card><CardHeader><CardTitle>Alpha</CardTitle></CardHeader></Card><Card><CardHeader><CardTitle>Beta</CardTitle></CardHeader></Card><Card><CardHeader><CardTitle>Gamma</CardTitle></CardHeader></Card></Grid>
|
|
9030
9069
|
```
|
|
9031
9070
|
|
|
9071
|
+
### Sparse results keep their size
|
|
9072
|
+
|
|
9073
|
+
```tsx
|
|
9074
|
+
import { Card, CardDescription, CardHeader, CardTitle, Grid } from "@sableui/react"
|
|
9075
|
+
|
|
9076
|
+
// One match still renders one cover wide: the empty columns are kept (track="fill", the default).
|
|
9077
|
+
<Grid minItemWidth="10rem" gap={2}><Card><CardHeader><CardTitle>Night Swim</CardTitle><CardDescription>Album · Undertow</CardDescription></CardHeader></Card></Grid>
|
|
9078
|
+
```
|
|
9079
|
+
|
|
9080
|
+
### A fixed set that spans the row
|
|
9081
|
+
|
|
9082
|
+
```tsx
|
|
9083
|
+
import { Card, CardDescription, CardHeader, Grid } from "@sableui/react"
|
|
9084
|
+
|
|
9085
|
+
<Grid minItemWidth="12rem" track="fit" gap={2}><Card><CardHeader><CardDescription>Listeners</CardDescription><span className="text-mono-xl">48.2k</span></CardHeader></Card><Card><CardHeader><CardDescription>Streams</CardDescription><span className="text-mono-xl">1.3M</span></CardHeader></Card><Card><CardHeader><CardDescription>Saves</CardDescription><span className="text-mono-xl">9,410</span></CardHeader></Card></Grid>
|
|
9086
|
+
```
|
|
9087
|
+
|
|
9032
9088
|
### Responsive fixed columns
|
|
9033
9089
|
|
|
9034
9090
|
```tsx
|
|
@@ -9634,9 +9690,9 @@ import { AppHeader, AppShell, PageHeader, Region } from "@sableui/react"
|
|
|
9634
9690
|
|
|
9635
9691
|
**Status:** stable
|
|
9636
9692
|
|
|
9637
|
-
The application frame: slots for status banners, a mobile header, a desktop sidebar, main content, a mobile bottom nav, and floating overlays — plus a built-in skip link and #main-content target.
|
|
9693
|
+
The application frame: slots for status banners, a mobile header, a desktop sidebar, main content, a mobile bottom nav, a persistent dock (a now-playing or call bar), and floating overlays — plus a built-in skip link and #main-content target.
|
|
9638
9694
|
|
|
9639
|
-
**Keywords:** app shell, layout, frame, sidebar, header, bottom nav, mobile, banners, main
|
|
9695
|
+
**Keywords:** app shell, layout, frame, sidebar, header, bottom nav, mobile, banners, main, dock, player bar, now playing, persistent
|
|
9640
9696
|
|
|
9641
9697
|
## Usage
|
|
9642
9698
|
|
|
@@ -9660,6 +9716,7 @@ Use AppShell as the outer product frame for authenticated or app-like experience
|
|
|
9660
9716
|
| Sidebar | no | Desktop navigation chrome, usually Sidebar. |
|
|
9661
9717
|
| Main | yes | The route content landmark with id main-content. |
|
|
9662
9718
|
| Bottom nav | no | Mobile fixed navigation with reserved bottom space. |
|
|
9719
|
+
| Dock | no | Persistent bottom chrome that outlives routes — a now-playing bar, a call bar. Sticky above the bottom nav; the desktop Sidebar shortens to clear it. |
|
|
9663
9720
|
|
|
9664
9721
|
## Props
|
|
9665
9722
|
|
|
@@ -9669,6 +9726,7 @@ Use AppShell as the outer product frame for authenticated or app-like experience
|
|
|
9669
9726
|
| header | ReactNode | — | Mobile top header (e.g. AppHeader); expected to hide itself at lg. |
|
|
9670
9727
|
| sidebar | ReactNode | — | Desktop side navigation (e.g. Sidebar); expected to hide itself below lg. Leading column of the body row. |
|
|
9671
9728
|
| bottomNav | ReactNode | — | Mobile bottom navigation (e.g. BottomNav); fixed to the bottom edge. main reserves space for it. |
|
|
9729
|
+
| dock | ReactNode | — | Persistent chrome kept on screen across routes (a now-playing bar, an active-call bar, an upload tray). Full width, sticky to the bottom edge and above bottomNav on mobile; its height is published as --dock-height and the app Sidebar subtracts it. In flow, so main needs no reserved space. The contents paint their own surface and top border, and name themselves (e.g. <section aria-label="Player">). Mount AppShell in a layout that survives navigation, or the playback restarts. |
|
|
9672
9730
|
| overlays | ReactNode | — | Floating overlays — a message launcher, FAB, or modal mount points. |
|
|
9673
9731
|
|
|
9674
9732
|
## Tokens used
|
|
@@ -9680,6 +9738,7 @@ Use AppShell as the outer product frame for authenticated or app-like experience
|
|
|
9680
9738
|
- Renders a skip link to #main-content as the first focusable element.
|
|
9681
9739
|
- Wraps content in a <main id="main-content"> landmark.
|
|
9682
9740
|
- Slot components own their own ARIA (nav landmarks, labels).
|
|
9741
|
+
- The dock follows <main> in DOM order, so keyboard and screen-reader users reach the player after the page content, not before it.
|
|
9683
9742
|
|
|
9684
9743
|
## Related
|
|
9685
9744
|
|
|
@@ -9990,11 +10049,12 @@ Use Avatar to represent a person, team, or account with a compact visual identit
|
|
|
9990
10049
|
- Do: Use `variant="art"` where people without photos sit side by side — a member list, an activity feed, a chat. Seeded by the person, each gets their own colour, so faces stay tellable apart at a glance; the neutral fallback makes everyone the same grey circle.
|
|
9991
10050
|
- Do: Provide meaningful alt text for profile images, or an empty alt when the adjacent text already names the person.
|
|
9992
10051
|
- Do: Use initials or a simple glyph fallback so missing images still communicate identity.
|
|
9993
|
-
- Do:
|
|
10052
|
+
- Do: Pick the size by the job: sm in dense rows and tables, md (the default) beside one line of text, lg in a row with a title and subtitle, xl (96px) for a profile header beside the person's name, 2xl (160px) for an artist or profile hero and roster portraits. Keep one size within a list or group.
|
|
9994
10053
|
- Do: Use AvatarGroup for a stack of people, and give it an aria-label with the real count ("12 collaborators"). Set `surface` (or `--avatar-group-ring`) to whatever the group sits on, so the separating ring matches a card or popover, not the page. When it shares a row with actions, set `fit` so it gives up avatars to the "+N" count before the row has to wrap.
|
|
9995
10054
|
- Don't: Hand-build a stack with negative margins and per-avatar ring classes, or fake the overflow as an Avatar with "+4" initials — screen readers hear "plus 4" with no context. Use AvatarGroup's `max`/`total`, or AvatarGroupCount.
|
|
9996
10055
|
- Don't: Use avatars as decoration without nearby identity context. They should help users recognize a person or account.
|
|
9997
10056
|
- Don't: Overload a dense table with large avatars; use sm or omit images when scanning data is more important.
|
|
10057
|
+
- Don't: Build a portrait by stretching an Avatar with size classes (`className="size-40"`) — the initials, the status indicator and the image fade stay at row scale. Use size="xl" or "2xl", which scale all three. Past 160px, or for a non-circular cover, it is media: an AspectBox, not an avatar.
|
|
9998
10058
|
|
|
9999
10059
|
### Anatomy
|
|
10000
10060
|
|
|
@@ -10025,19 +10085,26 @@ Use Avatar to represent a person, team, or account with a compact visual identit
|
|
|
10025
10085
|
| fallback | ReactNode | — | Initials or a glyph rendered when no image is displayed. |
|
|
10026
10086
|
| fallbackDelay | number | — | Delay before showing the fallback, in ms — avoids a flash on fast loads. |
|
|
10027
10087
|
| status | ReactNode | — | A presence indicator pinned to the bottom-end edge, usually <StatusDot tone="success" />. It sits outside the clipped circle and wears a ring in the surface colour (--avatar-group-ring, else the page background). Use this instead of absolutely positioning a dot over an Avatar. |
|
|
10028
|
-
| size | "sm" \| "md" \| "lg" | "md" | Square footprint: 32px / 40px / 48px. |
|
|
10088
|
+
| size | "sm" \| "md" \| "lg" \| "xl" \| "2xl" | "md" | Square footprint: 32px / 40px / 48px for rows; 96px / 160px for portraits (a profile header, an artist hero, a roster tile). The initials, a status indicator and its cut-out ring scale with it, and at the portrait sizes the photo fades in over the resting surface. |
|
|
10029
10089
|
| variant | "neutral" \| "art" | "neutral" | What the fallback looks like. neutral: initials on the muted surface. art: initials over GradientArt seeded by the person, so each person without a photo gets their own stable colour from the theme. |
|
|
10030
10090
|
| seed | string \| number | — | The art variant's seed. Defaults to the fallback text, then `alt`, so a person keeps one colour wherever they appear; pass a user id to tell apart two people who share initials. |
|
|
10031
10091
|
|
|
10032
10092
|
## Variants
|
|
10033
10093
|
|
|
10034
|
-
- **size**: sm, md, lg
|
|
10094
|
+
- **size**: sm, md, lg, xl, 2xl
|
|
10035
10095
|
- **variant**: neutral, art
|
|
10036
10096
|
|
|
10037
10097
|
## Tokens used
|
|
10038
10098
|
|
|
10039
10099
|
`muted`, `muted-foreground`, `background`, `card`, `popover`, `overlay`, `overlay-foreground`
|
|
10040
10100
|
|
|
10101
|
+
## Motion
|
|
10102
|
+
|
|
10103
|
+
- **arrival** · Image · `motion-fade` — At the portrait sizes (xl, 2xl) the photo replacing the initials is a visible pop across one of the page's largest elements; fading it in reads as the picture arriving, not the layout glitching. Row sizes swap instantly — too small to register, and a list of fading faces would shimmer.
|
|
10104
|
+
- **response** · Root · _no motion_ — An avatar is not interactive itself; a link or button wrapping it owns the response.
|
|
10105
|
+
- **transition** · Root · _no motion_ — It has no states of its own to move between beyond the image arriving.
|
|
10106
|
+
- **activity** · Root · _no motion_ — Loading shows the fallback, which is already a finished resting state rather than a spinner.
|
|
10107
|
+
|
|
10041
10108
|
## Accessibility
|
|
10042
10109
|
|
|
10043
10110
|
- Base UI tracks the image loading status and swaps to the fallback on missing/loading/error — no manual onError wiring.
|
|
@@ -10087,6 +10154,20 @@ import { Avatar } from "@sableui/react"
|
|
|
10087
10154
|
<Avatar src="/pfp.png" alt="Alex Morgan" fallback="AM" size="lg" />
|
|
10088
10155
|
```
|
|
10089
10156
|
|
|
10157
|
+
### Profile portrait
|
|
10158
|
+
|
|
10159
|
+
```tsx
|
|
10160
|
+
import { Avatar, StatusDot } from "@sableui/react"
|
|
10161
|
+
|
|
10162
|
+
<div className="flex flex-col items-center gap-3 text-center">
|
|
10163
|
+
<Avatar variant="art" size="2xl" fallback="LV" alt="Lena Vale" status={<StatusDot tone="success" />} />
|
|
10164
|
+
<div className="flex flex-col gap-1">
|
|
10165
|
+
<h1 className="text-heading-1">Lena Vale</h1>
|
|
10166
|
+
<p className="text-body-sm text-muted-foreground">1.2M monthly listeners</p>
|
|
10167
|
+
</div>
|
|
10168
|
+
</div>
|
|
10169
|
+
```
|
|
10170
|
+
|
|
10090
10171
|
### Low-level parts
|
|
10091
10172
|
|
|
10092
10173
|
```tsx
|
|
@@ -10864,7 +10945,7 @@ import { Bubble, Message } from "@sableui/react"
|
|
|
10864
10945
|
|
|
10865
10946
|
Pill-shaped action primitive (theme-driven radius via --button-radius, defaults to full). Neutral tiers (primary/secondary/ghost/outline), plus destructive and link, and three on-color tiers (on-color/on-color-outline/on-color-ghost) that paint with the surrounding ink so a button reads on any tone or brand fill. Optional loading state shows a spinner and disables the button.
|
|
10866
10947
|
|
|
10867
|
-
**Keywords:** button, action, cta, submit, save, cancel, destructive, loading, link, icon button, on color, inverse, band, brand fill, tone fill
|
|
10948
|
+
**Keywords:** button, action, cta, submit, save, cancel, destructive, loading, link, icon button, on color, inverse, band, brand fill, tone fill, bleed, optical alignment, align label to edge, negative margin, view all
|
|
10868
10949
|
|
|
10869
10950
|
## Usage
|
|
10870
10951
|
|
|
@@ -10876,7 +10957,9 @@ Use Button when the user commits an action, submits a form, opens UI, or confirm
|
|
|
10876
10957
|
- Do: Write labels as actions, such as "Save changes" or "Delete draft", instead of vague labels like "OK".
|
|
10877
10958
|
- Do: Use loading with loadingLabel for async work so the action cannot double-fire and the state is announced.
|
|
10878
10959
|
- Do: Put icons in iconStart / iconEnd rather than as children, so the button applies its optical padding. Start icons say what the action is (plus, share); end icons say where it goes (chevron-right, external).
|
|
10960
|
+
- Do: Set bleed on a ghost or link button that sits at the start or end of a row, so its LABEL lines up with the content edge the row shares ("View all" over a grid, "Add task" under a list, "Resend code" at a card footer's end). The box moves out by exactly its own padding; the hit area and hover fill stay full size and run past the edge. SectionHeader, PageHeader (back and actions) and CardAction already do this for their outermost ghost/link child — don't add it there, and use bleed="none" to opt one out.
|
|
10879
10961
|
- Do: On a coloured fill — a solid Banner, a bg-tone-* or bg-primary band, an inverse (bg-foreground) section, a brand panel — use variant="on-color" for the main action and on-color-outline / on-color-ghost for the rest. They paint with the band's inherited ink (currentColor), so the surface must set its paired ink (text-tone-info-foreground, text-primary-foreground, text-background…), which it already should. Name the fill once on the surface, e.g. [--on-color-fill:var(--tone-info)], and the solid button's label takes the fill colour; unnamed, it falls back to black or white against the ink. Both clear 4.5:1 in every shipped theme and mode.
|
|
10962
|
+
- Don't: Pull a ghost button onto an edge with -ms-3 / -me-3 classNames. That number is one size's padding: it is wrong at xs, md and cta, wrong on a side carrying an icon, and wrong once a preset retunes the spacing. bleed reads the button's own padding var. And don't bleed a filled or outlined button — its visible edge is what aligns, so bleed is a no-op there.
|
|
10880
10963
|
- Don't: Use destructive for reversible or low-risk actions; reserve it for operations with real loss or danger.
|
|
10881
10964
|
- Don't: Use a button for plain navigation. Compose with render={<a />} when the element should behave like a link.
|
|
10882
10965
|
- Don't: Use secondary on a tinted surface — inside a Note, a soft Banner, or any tone fill. `secondary` is a near-background surface token (in some themes within 2% of the page), so on a tint it reads as a hole punched in the fill rather than as a control sitting on it. On a soft tint use outline or ghost; on a full-strength fill use the on-color tiers.
|
|
@@ -10903,6 +10986,7 @@ Use Button when the user commits an action, submits a form, opens UI, or confirm
|
|
|
10903
10986
|
| loadingLabel | ReactNode | — | Optional text shown in place of children while loading. |
|
|
10904
10987
|
| iconStart | ReactNode | — | Icon before the label. The button takes one 2px step off its start padding so icon + label read as centred. Replaced by the spinner while loading. |
|
|
10905
10988
|
| iconEnd | ReactNode | — | Icon after the label (disclosure, external link, next). Same optical correction on the end side. |
|
|
10989
|
+
| bleed | "start" \| "end" \| "both" \| "none" | — | Optical alignment for a borderless button (ghost, link, on-color-ghost): a negative inline margin of exactly its own padding on that side — --button-px, or the icon side's optical padding when an icon sits there; an icon-only size bleeds by its inset to the glyph — so the label (or glyph) lands on the content edge. Hit area and hover/focus fill keep their full size. Logical sides, so it mirrors in RTL. A no-op on variants with a visible edge (primary, secondary, outline, destructive, floating, field, on-color, on-color-outline): a filled button aligns its edge, not its label. Unset, a borderless button renders data-bleed="auto" and SectionHeader, PageHeader and CardAction bleed it when it is their outermost child; none opts out. |
|
|
10906
10990
|
|
|
10907
10991
|
## Variants
|
|
10908
10992
|
|
|
@@ -10928,6 +11012,7 @@ Use Button when the user commits an action, submits a form, opens UI, or confirm
|
|
|
10928
11012
|
- Use render={<a/>} for link semantics rather than onClick navigation.
|
|
10929
11013
|
- On-color tiers replace the --ring focus ring with one drawn in the band's ink past a transparent 2px gap, so focus stays visible on any fill (ink-on-fill is the theme's own tested pair).
|
|
10930
11014
|
- On-color contrast is measured in button.test.tsx for every shipped theme and mode on each tone, primary, destructive, success, highlight and inverse fill: solid label at rest and hover, and outline/ghost label on the hover wash, all at least 4.5:1.
|
|
11015
|
+
- bleed moves only the box, never the hit target: the full padded area stays clickable and the focus ring still draws around the whole button, past the content edge.
|
|
10931
11016
|
- loading sets aria-busy and disables activation (disabled on a native button, aria-disabled on a render element) so the action can't double-fire.
|
|
10932
11017
|
|
|
10933
11018
|
## Related
|
|
@@ -11034,6 +11119,21 @@ import { Button, Icon } from "@sableui/react"
|
|
|
11034
11119
|
<Button size="icon" variant="ghost" aria-label="Share"><Icon name="share" /></Button>
|
|
11035
11120
|
```
|
|
11036
11121
|
|
|
11122
|
+
### Optically aligned to the row's edges
|
|
11123
|
+
|
|
11124
|
+
```tsx
|
|
11125
|
+
import { Button, Icon, Row, Stack } from "@sableui/react"
|
|
11126
|
+
|
|
11127
|
+
<Stack gap={1.5}>
|
|
11128
|
+
<Row justify="between">
|
|
11129
|
+
<span className="text-title-md">Tasks</span>
|
|
11130
|
+
<Button variant="ghost" size="sm" bleed="end">View all</Button>
|
|
11131
|
+
</Row>
|
|
11132
|
+
<span className="text-body-sm text-muted-foreground">Ship the release notes</span>
|
|
11133
|
+
<Button variant="ghost" size="sm" bleed="start" iconStart={<Icon name="plus" />} className="self-start">Add task</Button>
|
|
11134
|
+
</Stack>
|
|
11135
|
+
```
|
|
11136
|
+
|
|
11037
11137
|
### Icon button with an unread badge
|
|
11038
11138
|
|
|
11039
11139
|
```tsx
|
|
@@ -11188,7 +11288,7 @@ Use Card for a discrete thing with its own boundary, such as one product, one no
|
|
|
11188
11288
|
| CardHeader | ComponentPropsWithRef<"div"> | Top section holding CardTitle/CardDescription (and optionally CardAction). Padding follows the density knob. |
|
|
11189
11289
|
| CardTitle | ComponentPropsWithRef<"div"> & { render?: useRender.RenderProp } | The card's name (text-title-lg). A div by default; pass `render={<h2 />}` (or h3…) to make it a heading in the page outline, at the level the card's position calls for. |
|
|
11190
11290
|
| CardDescription | ComponentPropsWithRef<"div"> | Muted supporting line under the title. |
|
|
11191
|
-
| CardAction | ComponentPropsWithRef<"div"> | Header-aligned top-right action slot (e.g. an overflow menu, switch, or close button). Place inside CardHeader, which becomes a 2-column grid when present and collapses to a single column otherwise. |
|
|
11291
|
+
| CardAction | ComponentPropsWithRef<"div"> | Header-aligned top-right action slot (e.g. an overflow menu, switch, or close button). Place inside CardHeader, which becomes a 2-column grid when present and collapses to a single column otherwise. A ghost or link Button that ends the slot bleeds by its own padding at the end, so a "View all" label or a "⋯" glyph sits on the card's content edge; filled buttons keep their edge there. |
|
|
11192
11292
|
| CardContent | ComponentPropsWithRef<"div"> & { flush?: boolean } | The body region. Padding follows the density knob on all sides — does not assume a preceding CardHeader, so it is safe to use standalone (e.g. no CardHeader in the card). When paired with CardHeader, the seam stays tight because CardHeader zeroes its own bottom padding. `flush` runs it edge to edge for a table: the Table insets its first and last columns by exactly the card padding, so it lines up with the header — never hand-set pl-6/pr-6 on columns. |
|
|
11193
11293
|
| CardList | ComponentPropsWithRef<"div"> | Rows that run edge to edge in the card (Entity rows, most often), divided by hairlines. Insets each row to the card's own padding, so a list card lines up with every other card at any density — no hand-tuned px-* on the rows. Under a CardHeader it opens with a hairline and a gap; alone it fills the card. |
|
|
11194
11294
|
| CardFooter | ComponentPropsWithRef<"div"> & { actions?: boolean } | Bottom row for actions/meta. Padding follows the density knob. `actions` marks it as the card's action row (buttons only): in a sheet (IosSheet, a bottom SheetContent) it becomes a full-width stack pinned to the bottom, primary on top; on a page nothing changes. |
|
|
@@ -12973,6 +13073,177 @@ import { AlertDialog, AlertDialogClose, AlertDialogContent, AlertDialogDescripti
|
|
|
12973
13073
|
|
|
12974
13074
|
---
|
|
12975
13075
|
|
|
13076
|
+
# DisclosurePicker
|
|
13077
|
+
|
|
13078
|
+
**Status:** stable
|
|
13079
|
+
|
|
13080
|
+
A property row that names the current value and opens its list in place, beneath the row, instead of in a popup — for a tool panel or settings column where the choice is made while looking at what it changes. Built on Collapsible; the list is a listbox — arrow keys move, Enter or a click picks, applies and closes — and an optional search field filters long lists.
|
|
13081
|
+
|
|
13082
|
+
**Keywords:** disclosure picker, inline picker, in-place select, expanding list, property row, font picker, preset picker, inspector, picker row
|
|
13083
|
+
|
|
13084
|
+
## Usage
|
|
13085
|
+
|
|
13086
|
+
Use DisclosurePicker for one-of-many choices in a docked panel or inspector where a popup would cover the preview the choice is about: the row shows the current value (in its own face, with a tile, as the content warrants), and the list opens beneath it, pushing the panel down. Use Select for a form field or anywhere a popup is fine; use Combobox when people type to find an option among hundreds; use RadioGroup or RadioCard when every option should stay visible. Nothing applies while arrowing through the list — only Enter, Space or a click picks — so it suits choices with a cost (a confirm, a font load, a reset); where each option should apply as focus lands on it, that is a radio group (ThemeSelector, RadioGroup).
|
|
13087
|
+
|
|
13088
|
+
### Best practices
|
|
13089
|
+
|
|
13090
|
+
- Do: Enable search (`search` on DisclosurePickerPanel) only for long lists — roughly a dozen options or more, i.e. once the list scrolls. A short list (a handful of presets) reads faster with no field to skip past; the panel then has no search row and no gap.
|
|
13091
|
+
- Do: Show the value as the thing it is: a font row renders the family in its own face, a theme row leads with a ThemePreview tile in `media`. The row is a preview of the choice, not a label for it.
|
|
13092
|
+
- Do: Use `size="xs"` in a property grid (a compact Density / PanelSection), where it sits on the inset panel-field fill beside ScrubField and an xs SelectTrigger. Keep `md` in a settings column or a simple panel.
|
|
13093
|
+
- Do: When several rows share a panel, control `open` from the parent so only one list is open at a time — two open lists push each other and the preview out of view.
|
|
13094
|
+
- Do: Use it for a choice with a cost — one that opens a confirm, loads something, or resets other settings. Arrow keys only move focus; onValueChange fires once, on Enter, Space or a click, and only when the pick differs from `value`.
|
|
13095
|
+
- Do: Give options a leading visual with `media` (a ThemePreview tile, a swatch) instead of nesting another component's list: ThemeSelector's radio list applies each theme as the arrow keys land on it, which defeats commit-on-Enter. A list the picker cannot express (a Tree) can still close the row with `useDisclosurePicker().close()`.
|
|
13096
|
+
- Do: Render DisclosurePickerEmpty in place of the list when a search matches nothing, not inside it — a listbox holds only options.
|
|
13097
|
+
- Don't: Use it in a page-register form. A form field that pushes the form down on open reads as a layout jump; that is Select's job.
|
|
13098
|
+
- Don't: Rebuild the row from a Collapsible with hand-copied classes. The disclosure, the well, the hover step, the xs field shell, focus return and scroll-into-view all live here; two hand copies of this row in one panel is how it came to exist.
|
|
13099
|
+
- Don't: Turn search on for a short list just because the other rows have it. A search field over six options is a control with nothing to do.
|
|
13100
|
+
|
|
13101
|
+
### Anatomy
|
|
13102
|
+
|
|
13103
|
+
| Part | Required | Description |
|
|
13104
|
+
| --- | --- | --- |
|
|
13105
|
+
| DisclosurePicker | yes | Root. Owns open state, size, focus return and scroll-into-view. |
|
|
13106
|
+
| DisclosurePickerTrigger | yes | The row: optional `label` caption, optional `media`, the value, and a chevron that flips when open. |
|
|
13107
|
+
| DisclosurePickerPanel | yes | What opens beneath the row: the opt-in search field, then the list. |
|
|
13108
|
+
| DisclosurePickerList | no | A listbox of options that scrolls past 16rem. Arrow keys move; Enter, Space or a click picks. |
|
|
13109
|
+
| DisclosurePickerOption | no | One option row, with optional leading media and trailing note. Chosen, it lifts out of the well; focused, it takes the ring. |
|
|
13110
|
+
| DisclosurePickerEmpty | no | The no-match line, rendered in place of the list when a search filters everything out. |
|
|
13111
|
+
|
|
13112
|
+
## Subcomponents
|
|
13113
|
+
|
|
13114
|
+
| Name | Type | Description |
|
|
13115
|
+
| --- | --- | --- |
|
|
13116
|
+
| DisclosurePickerTrigger | { label?: ReactNode; media?: ReactNode; children?: ReactNode } + Base UI Collapsible.Trigger props | The row. `label` is a muted leading caption on a fixed column so stacked rows align; `media` a leading visual (a ThemePreview tile); children the value, truncated on one line. ArrowDown opens it, and steps into the list once open. |
|
|
13117
|
+
| DisclosurePickerPanel | { search?: { value: string; onValueChange(value: string): void; placeholder?: string; label?: string } } + Collapsible.Panel props | Opens beneath the row. `search` opts in to a SearchInput above the list (off by default); it is controlled because the consumer filters, and it is cleared when the row closes. Escape clears a query first, then closes; ArrowDown in the field steps into the list. |
|
|
13118
|
+
| DisclosurePickerList | { value: string; onValueChange(value: string): void; aria-label?: string } + div props | A listbox. ArrowUp/ArrowDown, Home/End and type-ahead move focus without applying anything (ArrowUp from the first option returns to the search field); Enter, Space or a click picks the focused option — onValueChange fires once, only when it differs from `value` — and closes the row. Escape closes without a pick. |
|
|
13119
|
+
| DisclosurePickerOption | { value: string; children?: ReactNode; media?: ReactNode; note?: ReactNode; disabled?: boolean; textValue?: string } | One option row. `media` is a leading visual (a ThemePreview tile); `note` a muted trailing annotation — options without one show a check when chosen. `disabled` skips it for the keyboard and refuses the pick; `textValue` is what type-ahead matches when the name is not plain text. |
|
|
13120
|
+
| DisclosurePickerEmpty | { children?: ReactNode } | The no-match line, rendered in place of the list. |
|
|
13121
|
+
| useDisclosurePicker | () => { open: boolean; close(): void; size: "md" \| "xs" } | For a list the picker does not own (a Tree) that should close the row when something is picked. |
|
|
13122
|
+
|
|
13123
|
+
## Props
|
|
13124
|
+
|
|
13125
|
+
| Name | Type | Default | Description |
|
|
13126
|
+
| --- | --- | --- | --- |
|
|
13127
|
+
| open | boolean | — | Controlled open state. Pair with onOpenChange. |
|
|
13128
|
+
| defaultOpen | boolean | false | Uncontrolled initial open state. |
|
|
13129
|
+
| onOpenChange | (open: boolean) => void | — | Called with the next open state — from the trigger, a pick, or Escape. |
|
|
13130
|
+
| size | "md" \| "xs" | "md" | md is a --control-md row (40px comfortable) in a settings column, its own well when closed. xs is the property-grid rung: a --control-xs row on the inset panel-field fill with ui-tier type. Height, padding, radius, type and surface move together. |
|
|
13131
|
+
| className | string | — | Merged onto the root (layout only — the look comes from size). |
|
|
13132
|
+
|
|
13133
|
+
## Tokens used
|
|
13134
|
+
|
|
13135
|
+
`foreground`, `muted-foreground`, `popover`, `hairline`, `panel-field`, `panel-field-hover`, `ring`, `accent`
|
|
13136
|
+
|
|
13137
|
+
## Motion
|
|
13138
|
+
|
|
13139
|
+
- **arrival** · root · _no motion_ — A property row is present from first paint; animating it in would stage chrome the user is already scanning.
|
|
13140
|
+
- **response** · DisclosurePickerTrigger · `motion-interactive` — The row reads as a value, not a button; the hover step is what says it opens before it is clicked.
|
|
13141
|
+
- **transition** · DisclosurePickerPanel · `motion-disclosure` — The list pushes the panel down; growing its height ties the movement below to this row rather than to a layout jump.
|
|
13142
|
+
- **activity** · root · _no motion_ — Nothing is indeterminate: the options are resident. A list that loads should show its own loader inside the panel.
|
|
13143
|
+
|
|
13144
|
+
## Accessibility
|
|
13145
|
+
|
|
13146
|
+
- The trigger is a real <button>; Base UI wires aria-expanded and aria-controls to the panel. Its accessible name is its text (caption + value) unless an aria-label is given.
|
|
13147
|
+
- The list is role="listbox" with role="option" rows; aria-selected marks the committed value, not the focused row, so a screen reader hears both where focus is and what is chosen. Selection does not follow focus: ArrowUp/ArrowDown, Home/End and type-ahead move focus (roving tabindex — one tab stop, so Tab leaves the list in one step) and apply nothing; Enter, Space or a click picks, applies and closes. Disabled options carry aria-disabled and are skipped.
|
|
13148
|
+
- Why a listbox and not a radio group: a radio group's checked state follows focus, so every arrow press would be a pick — wrong for a choice that opens a confirm, loads a font or resets settings. Base UI ships no standalone listbox (Select is popup-only, Combobox's inline list is focus-in-the-input, Composite is internal), so the keyboard model lives here.
|
|
13149
|
+
- Opening from the keyboard (Enter, Space, ArrowDown) moves focus into the panel: the search field when there is one, otherwise the chosen option (or the first). A pointer open focuses the search field on a fine pointer only, so touch does not raise the on-screen keyboard.
|
|
13150
|
+
- Escape closes the row without changing the value (clearing a query first) and returns focus to the trigger; a pick does the same. Focus is not pulled back when it has already moved on, e.g. to a confirm dialog the pick opened.
|
|
13151
|
+
- The panel animates with motion-disclosure, which is static under prefers-reduced-motion or MotionProvider off; the scroll that keeps the row in view is instant, never smooth.
|
|
13152
|
+
|
|
13153
|
+
## Related
|
|
13154
|
+
|
|
13155
|
+
- Components: Collapsible, Select, Combobox, SearchInput, RadioGroup, ThemeSelector, FontPicker, PropertyRow
|
|
13156
|
+
|
|
13157
|
+
## Examples
|
|
13158
|
+
|
|
13159
|
+
### A short list — no search
|
|
13160
|
+
|
|
13161
|
+
```tsx
|
|
13162
|
+
import { DisclosurePicker, DisclosurePickerList, DisclosurePickerOption, DisclosurePickerPanel, DisclosurePickerTrigger } from "@sableui/react"
|
|
13163
|
+
|
|
13164
|
+
const [preset, setPreset] = useState("comfortable")
|
|
13165
|
+
const [open, setOpen] = useState(false)
|
|
13166
|
+
<DisclosurePicker open={open} onOpenChange={setOpen} className="w-72">
|
|
13167
|
+
<DisclosurePickerTrigger label="Density">{preset === "compact" ? "Compact" : "Comfortable"}</DisclosurePickerTrigger>
|
|
13168
|
+
<DisclosurePickerPanel>
|
|
13169
|
+
<DisclosurePickerList aria-label="Density" value={preset} onValueChange={setPreset}>
|
|
13170
|
+
<DisclosurePickerOption value="comfortable">Comfortable</DisclosurePickerOption>
|
|
13171
|
+
<DisclosurePickerOption value="compact">Compact</DisclosurePickerOption>
|
|
13172
|
+
</DisclosurePickerList>
|
|
13173
|
+
</DisclosurePickerPanel>
|
|
13174
|
+
</DisclosurePicker>
|
|
13175
|
+
```
|
|
13176
|
+
|
|
13177
|
+
### A long list — search opted in
|
|
13178
|
+
|
|
13179
|
+
```tsx
|
|
13180
|
+
import { DisclosurePicker, DisclosurePickerEmpty, DisclosurePickerList, DisclosurePickerOption, DisclosurePickerPanel, DisclosurePickerTrigger } from "@sableui/react"
|
|
13181
|
+
|
|
13182
|
+
const COUNTRIES = ["Argentina", "Australia", "Austria", "Belgium", "Brazil", "Canada", "Chile", "Denmark", "Finland", "France", "Germany", "Ireland", "Italy", "Japan", "Mexico", "Norway", "Portugal", "Spain", "Sweden"]
|
|
13183
|
+
const [country, setCountry] = useState("Canada")
|
|
13184
|
+
const [query, setQuery] = useState("")
|
|
13185
|
+
const shown = COUNTRIES.filter((name) => name.toLowerCase().includes(query.trim().toLowerCase()))
|
|
13186
|
+
<DisclosurePicker className="w-72">
|
|
13187
|
+
<DisclosurePickerTrigger label="Country">{country}</DisclosurePickerTrigger>
|
|
13188
|
+
<DisclosurePickerPanel search={{ value: query, onValueChange: setQuery, placeholder: `Search ${COUNTRIES.length} countries` }}>
|
|
13189
|
+
{shown.length > 0 ? (
|
|
13190
|
+
<DisclosurePickerList aria-label="Country" value={country} onValueChange={setCountry}>
|
|
13191
|
+
{shown.map((name) => (
|
|
13192
|
+
<DisclosurePickerOption key={name} value={name}>{name}</DisclosurePickerOption>
|
|
13193
|
+
))}
|
|
13194
|
+
</DisclosurePickerList>
|
|
13195
|
+
) : (
|
|
13196
|
+
<DisclosurePickerEmpty>No countries match “{query}”.</DisclosurePickerEmpty>
|
|
13197
|
+
)}
|
|
13198
|
+
</DisclosurePickerPanel>
|
|
13199
|
+
</DisclosurePicker>
|
|
13200
|
+
```
|
|
13201
|
+
|
|
13202
|
+
### A theme picker — media tiles, commit on Enter
|
|
13203
|
+
|
|
13204
|
+
```tsx
|
|
13205
|
+
import { DisclosurePicker, DisclosurePickerList, DisclosurePickerOption, DisclosurePickerPanel, DisclosurePickerTrigger, ThemePreview, useSableTheme } from "@sableui/react"
|
|
13206
|
+
|
|
13207
|
+
const { themeId, setThemeId, themes, mode } = useSableTheme()
|
|
13208
|
+
const current = themes.find((theme) => theme.id === themeId)
|
|
13209
|
+
<DisclosurePicker>
|
|
13210
|
+
<DisclosurePickerTrigger aria-label={`Theme: ${current?.label}`} media={current ? <ThemePreview theme={current} mode={mode} size="sm" /> : null}>
|
|
13211
|
+
{current?.label}
|
|
13212
|
+
</DisclosurePickerTrigger>
|
|
13213
|
+
<DisclosurePickerPanel>
|
|
13214
|
+
<DisclosurePickerList aria-label="Theme" value={themeId} onValueChange={setThemeId}>
|
|
13215
|
+
{themes.map((theme) => (
|
|
13216
|
+
<DisclosurePickerOption key={theme.id} value={theme.id} media={<ThemePreview theme={theme} mode={mode} size="sm" />}>
|
|
13217
|
+
{theme.label}
|
|
13218
|
+
</DisclosurePickerOption>
|
|
13219
|
+
))}
|
|
13220
|
+
</DisclosurePickerList>
|
|
13221
|
+
</DisclosurePickerPanel>
|
|
13222
|
+
</DisclosurePicker>
|
|
13223
|
+
```
|
|
13224
|
+
|
|
13225
|
+
### Property-grid rung
|
|
13226
|
+
|
|
13227
|
+
```tsx
|
|
13228
|
+
import { Density, DisclosurePicker, DisclosurePickerList, DisclosurePickerOption, DisclosurePickerPanel, DisclosurePickerTrigger } from "@sableui/react"
|
|
13229
|
+
|
|
13230
|
+
const [blend, setBlend] = useState("normal")
|
|
13231
|
+
<Density value="compact" className="w-64">
|
|
13232
|
+
<DisclosurePicker size="xs">
|
|
13233
|
+
<DisclosurePickerTrigger label="Blend">{blend}</DisclosurePickerTrigger>
|
|
13234
|
+
<DisclosurePickerPanel>
|
|
13235
|
+
<DisclosurePickerList aria-label="Blend mode" value={blend} onValueChange={setBlend}>
|
|
13236
|
+
<DisclosurePickerOption value="normal">normal</DisclosurePickerOption>
|
|
13237
|
+
<DisclosurePickerOption value="multiply">multiply</DisclosurePickerOption>
|
|
13238
|
+
<DisclosurePickerOption value="screen">screen</DisclosurePickerOption>
|
|
13239
|
+
</DisclosurePickerList>
|
|
13240
|
+
</DisclosurePickerPanel>
|
|
13241
|
+
</DisclosurePicker>
|
|
13242
|
+
</Density>
|
|
13243
|
+
```
|
|
13244
|
+
|
|
13245
|
+
---
|
|
13246
|
+
|
|
12976
13247
|
# EmptyState
|
|
12977
13248
|
|
|
12978
13249
|
**Status:** stable
|
|
@@ -13442,6 +13713,113 @@ import { Field, FileDropzone } from "@sableui/react"
|
|
|
13442
13713
|
|
|
13443
13714
|
---
|
|
13444
13715
|
|
|
13716
|
+
# FontPicker
|
|
13717
|
+
|
|
13718
|
+
**Status:** stable
|
|
13719
|
+
|
|
13720
|
+
A DisclosurePicker for a typeface: the row names the chosen family in its own face and opens in place into a searchable list of families, each drawn in itself. The app supplies the families and loads the faces; Sable bundles no catalog.
|
|
13721
|
+
|
|
13722
|
+
**Keywords:** font picker, typeface, font family, font selector, typography, google fonts, inspector, customizer
|
|
13723
|
+
|
|
13724
|
+
## Usage
|
|
13725
|
+
|
|
13726
|
+
Use FontPicker wherever someone chooses a typeface for a role (body, headings, code) in a panel that shows the result: a theme editor, a document inspector, a brand customizer. It is a DisclosurePicker, so the list opens beneath the row rather than over the preview; arrow keys walk the families (each drawn in its own face) without applying or loading anything, and Enter, Space or a click picks one, loads it and closes the row.
|
|
13727
|
+
|
|
13728
|
+
### Best practices
|
|
13729
|
+
|
|
13730
|
+
- Do: Pass `loadFont` and load faces lazily: `"preview"` fires for every family when the list opens (load just the glyphs of its name — Google Fonts' `text=` parameter makes a 20-font list cost a few KB), `"use"` for the chosen one (load its real weights). Make it idempotent and define it once, outside render.
|
|
13731
|
+
- Do: Put non-font choices — "Preset (Figtree)", "Same as body" — in `options`, with a short `valueLabel` for the row. They list first and render in the app's face, so they never read as a family.
|
|
13732
|
+
- Do: Give each row a role `label` ("Body", "Headings", "Mono") and stack them; the caption column aligns. Control `open` from the parent so only one list is open at a time.
|
|
13733
|
+
- Don't: Leave search on for a short curated list — under a dozen families, pass `searchable={false}`. Search is on by default because font lists are usually long.
|
|
13734
|
+
- Don't: Use a Select of family names for this. A popup covers the text it changes, and a family name set in the app's face says nothing about the face.
|
|
13735
|
+
|
|
13736
|
+
## Props
|
|
13737
|
+
|
|
13738
|
+
| Name | Type | Default | Description |
|
|
13739
|
+
| --- | --- | --- | --- |
|
|
13740
|
+
| value | string | — | The chosen family, or the value of one of `options`. |
|
|
13741
|
+
| onValueChange | (value: string) => void | — | Called once a family (or option) is picked — Enter, Space or a click — and only when it differs from `value`; the row then closes. Arrow keys and type-ahead only move focus; Escape closes without a pick. |
|
|
13742
|
+
| fonts | readonly { family: string; note?: string; stack?: string }[] | — | The families to offer, in order. `note` is a muted annotation (searched too); `stack` the CSS font-family to draw it in (default `"<family>"`). |
|
|
13743
|
+
| options | readonly { value: string; label: string; valueLabel?: string }[] | [] | Non-font choices listed above the fonts, in the app's face. `valueLabel` is what the row shows while it is chosen. |
|
|
13744
|
+
| label | ReactNode | — | The row's leading caption — the role the font plays. |
|
|
13745
|
+
| listLabel | string | "Font" | Accessible name of the list. |
|
|
13746
|
+
| searchable | boolean | true | A search field above the list, filtering by family and note. Pass false for a short curated list (under a dozen). |
|
|
13747
|
+
| searchLabel | string | "Search fonts" | Accessible name of the search field. |
|
|
13748
|
+
| loadFont | (font, purpose: "preview" \| "use") => void | — | Loads a face on demand: "preview" for every family when the list opens, "use" for the chosen one once it is picked (never while the list is only being walked). Receives your own font objects. Idempotent. |
|
|
13749
|
+
| open | boolean | — | Controlled open state, as DisclosurePicker. Control it to keep one of several rows open at a time. |
|
|
13750
|
+
| defaultOpen | boolean | false | Uncontrolled initial open state. |
|
|
13751
|
+
| onOpenChange | (open: boolean) => void | — | Called with the next open state. |
|
|
13752
|
+
| size | "md" \| "xs" | "md" | md for a settings column; xs for the property-grid rung (see DisclosurePicker). |
|
|
13753
|
+
|
|
13754
|
+
## Tokens used
|
|
13755
|
+
|
|
13756
|
+
`foreground`, `muted-foreground`, `popover`, `hairline`, `panel-field`, `ring`
|
|
13757
|
+
|
|
13758
|
+
## Motion
|
|
13759
|
+
|
|
13760
|
+
- **arrival** · root · _no motion_ — A property row is present from first paint.
|
|
13761
|
+
- **response** · Trigger · `motion-interactive` — Inherited from DisclosurePicker: the hover step says the value row opens.
|
|
13762
|
+
- **transition** · Panel · `motion-disclosure` — Inherited from DisclosurePicker: the list grows in place, tying the panel's movement to this row.
|
|
13763
|
+
- **activity** · root · _no motion_ — Faces load in the background and swap in when ready; the names are legible in the fallback meanwhile, so there is nothing to wait on.
|
|
13764
|
+
|
|
13765
|
+
## Accessibility
|
|
13766
|
+
|
|
13767
|
+
- Inherits DisclosurePicker: a button trigger with aria-expanded/controls whose name is the caption plus the family; a listbox named by `listLabel`, aria-selected on the chosen family.
|
|
13768
|
+
- Keyboard open focuses the search field; ArrowDown steps from it into the list (ArrowUp from the first option steps back); arrows, Home/End and type-ahead move without applying; Enter or Space picks and closes; Escape clears the query, then closes without a pick and returns focus to the row.
|
|
13769
|
+
- Each family is set in its own face, but its name is still plain text, so a screen reader reads the name, not the glyphs.
|
|
13770
|
+
|
|
13771
|
+
## Related
|
|
13772
|
+
|
|
13773
|
+
- Components: DisclosurePicker, ThemeSelector, Select, Combobox
|
|
13774
|
+
|
|
13775
|
+
## Examples
|
|
13776
|
+
|
|
13777
|
+
### Body font with lazy loading
|
|
13778
|
+
|
|
13779
|
+
```tsx
|
|
13780
|
+
import { FontPicker } from "@sableui/react"
|
|
13781
|
+
|
|
13782
|
+
const FONTS = [
|
|
13783
|
+
{ family: "Inter", note: "Neutral, dense UI" },
|
|
13784
|
+
{ family: "Figtree", note: "Friendly geometric" },
|
|
13785
|
+
{ family: "Fraunces", note: "Soft, wonky serif", stack: '"Fraunces", serif' },
|
|
13786
|
+
]
|
|
13787
|
+
// Your loader; Sable never fetches fonts itself.
|
|
13788
|
+
const loadFont = (font: { family: string }, purpose: "preview" | "use") => {
|
|
13789
|
+
const id = `font-${purpose}-${font.family}`
|
|
13790
|
+
if (document.getElementById(id)) return
|
|
13791
|
+
const link = Object.assign(document.createElement("link"), { id, rel: "stylesheet" })
|
|
13792
|
+
const family = font.family.replace(/ /g, "+")
|
|
13793
|
+
link.href = purpose === "preview"
|
|
13794
|
+
? `https://fonts.googleapis.com/css2?family=${family}&text=${encodeURIComponent(font.family)}`
|
|
13795
|
+
: `https://fonts.googleapis.com/css2?family=${family}:wght@400..700&display=swap`
|
|
13796
|
+
document.head.appendChild(link)
|
|
13797
|
+
}
|
|
13798
|
+
const [family, setFamily] = useState("Inter")
|
|
13799
|
+
<FontPicker label="Body" listLabel="Body font" value={family} onValueChange={setFamily} fonts={FONTS} loadFont={loadFont} searchable={false} />
|
|
13800
|
+
```
|
|
13801
|
+
|
|
13802
|
+
### With a preset option, one row of several
|
|
13803
|
+
|
|
13804
|
+
```tsx
|
|
13805
|
+
import { FontPicker } from "@sableui/react"
|
|
13806
|
+
|
|
13807
|
+
const [open, setOpen] = useState<string | null>(null)
|
|
13808
|
+
const [body, setBody] = useState("theme")
|
|
13809
|
+
<FontPicker
|
|
13810
|
+
label="Body"
|
|
13811
|
+
listLabel="Body font"
|
|
13812
|
+
value={body}
|
|
13813
|
+
onValueChange={setBody}
|
|
13814
|
+
options={[{ value: "theme", label: "Preset (Figtree)", valueLabel: "Figtree" }]}
|
|
13815
|
+
fonts={[{ family: "Inter" }, { family: "Figtree" }, { family: "Geist" }]}
|
|
13816
|
+
open={open === "body"}
|
|
13817
|
+
onOpenChange={(next) => setOpen(next ? "body" : null)}
|
|
13818
|
+
/>
|
|
13819
|
+
```
|
|
13820
|
+
|
|
13821
|
+
---
|
|
13822
|
+
|
|
13445
13823
|
# Form
|
|
13446
13824
|
|
|
13447
13825
|
**Status:** stable
|
|
@@ -14344,6 +14722,253 @@ import { Marquee } from "@sableui/react"
|
|
|
14344
14722
|
|
|
14345
14723
|
---
|
|
14346
14724
|
|
|
14725
|
+
# MediaImage
|
|
14726
|
+
|
|
14727
|
+
**Status:** beta
|
|
14728
|
+
|
|
14729
|
+
An image that fills its box and falls back to GradientArt seeded by the item when it has no source or the source fails — Avatar's image-load tracking, for covers, thumbnails and product shots instead of faces.
|
|
14730
|
+
|
|
14731
|
+
**Keywords:** image, img, image fallback, broken image, cover art, album art, thumbnail, placeholder image, onError, missing image
|
|
14732
|
+
|
|
14733
|
+
## Usage
|
|
14734
|
+
|
|
14735
|
+
Use MediaImage wherever content imagery may be missing or broken: a cover in a grid, a thumbnail in a row, a product shot. It fills the box it is given — an AspectBox for a cover, a sized and rounded box for a thumbnail — shows a quiet muted surface while the image loads, fades the image in once decoded, and paints GradientArt seeded by the item when there is no image to show, so a grid of missing covers still reads as distinct things. For a person use Avatar; for a whole tile with title and actions use MediaTile, which composes this.
|
|
14736
|
+
|
|
14737
|
+
### Best practices
|
|
14738
|
+
|
|
14739
|
+
- Do: Give the box the shape: `<AspectBox ratio={1} className="rounded-md"><MediaImage … /></AspectBox>`, or `size-14 rounded-md` on MediaImage itself for a thumbnail. It clips to whatever radius the box has.
|
|
14740
|
+
- Do: Pass `seed` (the title or an id) when `alt` is empty, so the fallback art is the item's own and stays the same in every list it appears in.
|
|
14741
|
+
- Do: Leave `alt` empty when a visible title sits next to the image — a tile, a row — and describe it when the image stands alone. A set `alt` also names the fallback, so the description survives a broken URL.
|
|
14742
|
+
- Don't: Do not hand-write `<img onError={() => setBroken(true)}>`. An image that fails before hydration never fires the handler on a prerendered page; MediaImage preloads off the DOM and catches it.
|
|
14743
|
+
- Don't: Do not use it for profile pictures. A person is a circle with initials — Avatar, whose `variant="art"` is the same fallback with the person's initials over it.
|
|
14744
|
+
|
|
14745
|
+
## Props
|
|
14746
|
+
|
|
14747
|
+
| Name | Type | Default | Description |
|
|
14748
|
+
| --- | --- | --- | --- |
|
|
14749
|
+
| src | string | — | Image URL. Omitted, empty or failing to load ⇒ the fallback shows. |
|
|
14750
|
+
| alt | string | "" | Alt text. Empty is decorative — right beside a visible title. When set and no image shows, it names the fallback instead. |
|
|
14751
|
+
| seed | string \| number | — | Seeds the fallback art. Defaults to `alt`; pass the title or an id when `alt` is empty. |
|
|
14752
|
+
| fallback | ReactNode | — | Replaces the GradientArt fallback — a glyph or monogram. Fills the frame under the same clipping. |
|
|
14753
|
+
| fit | "cover" \| "contain" | "cover" | `cover` crops to fill (artwork, photos); `contain` letterboxes on the muted surface (a logo, a product that must not be cropped). |
|
|
14754
|
+
| srcSet | string | — | Responsive sources, passed to the `<img>`. |
|
|
14755
|
+
| sizes | string | — | The `sizes` attribute for `srcSet`. |
|
|
14756
|
+
| loading | "lazy" \| "eager" | "lazy" | Native lazy loading. Use `eager` for the first row of a grid above the fold. |
|
|
14757
|
+
| className | string | — | Sizing and rounding when MediaImage is the box itself (`size-14 rounded-md`). Inside an AspectBox it already fills. |
|
|
14758
|
+
|
|
14759
|
+
## Variants
|
|
14760
|
+
|
|
14761
|
+
- **fit**: cover, contain
|
|
14762
|
+
|
|
14763
|
+
## Tokens used
|
|
14764
|
+
|
|
14765
|
+
`muted`, `art-1`, `art-2`, `art-3`, `art-4`, `art-5`, `art-6`, `art-ground`
|
|
14766
|
+
|
|
14767
|
+
## Motion
|
|
14768
|
+
|
|
14769
|
+
- **arrival** · image · `motion-fade` — The image fades in over the muted surface once it has decoded, so a slow cover settles in instead of popping row by row as the grid loads.
|
|
14770
|
+
- **response** · root · _no motion_ — Imagery, not a control. The tile or row around it answers the pointer.
|
|
14771
|
+
- **transition** · root · _no motion_ — Its only state change is loading → loaded or failed, which the arrival covers.
|
|
14772
|
+
- **activity** · root · _no motion_ — A loading image rests on a static muted surface; a shimmer per cover across a long grid would be noise.
|
|
14773
|
+
|
|
14774
|
+
## Accessibility
|
|
14775
|
+
|
|
14776
|
+
- `alt` defaults to empty (decorative), because the usual neighbour is a visible title that already names the item; describe the image when it stands alone.
|
|
14777
|
+
- When no image is on screen and `alt` is set, the frame takes `role="img"` with that label, so a broken URL does not erase the description. The fallback art itself is always aria-hidden.
|
|
14778
|
+
- The `<img>` mounts only once it has loaded, so assistive technology never meets a broken-image icon.
|
|
14779
|
+
|
|
14780
|
+
## Related
|
|
14781
|
+
|
|
14782
|
+
- Components: MediaTile, Avatar, GradientArt, AspectBox
|
|
14783
|
+
|
|
14784
|
+
## Examples
|
|
14785
|
+
|
|
14786
|
+
### Cover with fallback
|
|
14787
|
+
|
|
14788
|
+
```tsx
|
|
14789
|
+
import { AspectBox, MediaImage } from "@sableui/react"
|
|
14790
|
+
|
|
14791
|
+
<AspectBox ratio={1} className="rounded-md">
|
|
14792
|
+
<MediaImage src={album.cover} seed={album.title} />
|
|
14793
|
+
</AspectBox>
|
|
14794
|
+
```
|
|
14795
|
+
|
|
14796
|
+
### No image yet
|
|
14797
|
+
|
|
14798
|
+
```tsx
|
|
14799
|
+
import { AspectBox, MediaImage } from "@sableui/react"
|
|
14800
|
+
|
|
14801
|
+
<AspectBox ratio={1} className="rounded-md">
|
|
14802
|
+
<MediaImage seed="Low tide, high noon" />
|
|
14803
|
+
</AspectBox>
|
|
14804
|
+
```
|
|
14805
|
+
|
|
14806
|
+
### Row thumbnail
|
|
14807
|
+
|
|
14808
|
+
```tsx
|
|
14809
|
+
import { MediaImage } from "@sableui/react"
|
|
14810
|
+
|
|
14811
|
+
<MediaImage src={track.art} seed={track.title} className="size-12 shrink-0 rounded-sm" />
|
|
14812
|
+
```
|
|
14813
|
+
|
|
14814
|
+
### Logo, never cropped
|
|
14815
|
+
|
|
14816
|
+
```tsx
|
|
14817
|
+
import { AspectBox, MediaImage } from "@sableui/react"
|
|
14818
|
+
|
|
14819
|
+
<AspectBox ratio={16 / 9} className="rounded-lg">
|
|
14820
|
+
<MediaImage src={brand.logo} alt={brand.name} fit="contain" />
|
|
14821
|
+
</AspectBox>
|
|
14822
|
+
```
|
|
14823
|
+
|
|
14824
|
+
---
|
|
14825
|
+
|
|
14826
|
+
# MediaTile
|
|
14827
|
+
|
|
14828
|
+
**Status:** beta
|
|
14829
|
+
|
|
14830
|
+
A cover-first tile — square, 16:9 or poster artwork with an optional play-style action over it, then a title and a subtitle that can each be links — that is one link as a whole without nesting its other controls inside one.
|
|
14831
|
+
|
|
14832
|
+
**Keywords:** album, album art, cover, tile, media card, video thumbnail, podcast, playlist, gallery, product tile, poster, grid item, play button, music
|
|
14833
|
+
|
|
14834
|
+
## Usage
|
|
14835
|
+
|
|
14836
|
+
The item in a media grid or shelf: an album, a playlist, a video, a podcast, a film, a photo, a product. The cover leads and carries the weight; the title and subtitle sit underneath without a card around them. The whole tile follows the title's link, the subtitle can link somewhere else (the artist, the channel), and an action — usually play — floats over the cover's corner, revealed on hover and focus and always shown on touch. A missing or broken cover becomes GradientArt seeded by the title, so a grid of items without artwork still reads as distinct things.
|
|
14837
|
+
|
|
14838
|
+
### Best practices
|
|
14839
|
+
|
|
14840
|
+
- Do: Lay tiles out with `Grid` (`<Grid minItemWidth="10rem">`) or a horizontal `Scroller` for a shelf. The tile fills its cell; the grid decides the width.
|
|
14841
|
+
- Do: Pass `render={<Link to="…" />}` (or `href`) for where the tile goes, and `subtitleRender` / `subtitleHref` for a second destination. Each stays a separate control; never wrap the tile in your own link.
|
|
14842
|
+
- Do: Name the action for its item — `aria-label="Play Low tide, high noon"`, not "Play". A grid of twenty identical "Play" buttons is unusable from a screen reader's list of controls.
|
|
14843
|
+
- Do: Use `titleAs="h3"` when the tiles sit under a section heading, so screen-reader users can move tile to tile by heading.
|
|
14844
|
+
- Don't: Do not build this from `Card render={<Link />}`. A card that is a link cannot hold a second link or a button — interactive content cannot nest — which is exactly what a media tile needs.
|
|
14845
|
+
- Don't: Do not put more than one action on the cover. A second control over the art is a toolbar on a thumbnail; move secondary actions (add to queue, share) into an OverflowMenu in the action slot or onto the item's page.
|
|
14846
|
+
- Don't: Do not use it for a framed commerce card with a price and a buy button — that is ProductCardBlock — or for a person, which is an Avatar.
|
|
14847
|
+
|
|
14848
|
+
### Anatomy
|
|
14849
|
+
|
|
14850
|
+
| Part | Required | Description |
|
|
14851
|
+
| --- | --- | --- |
|
|
14852
|
+
| Cover | yes | An AspectBox holding MediaImage: the image, or GradientArt seeded by the title. Drawn first, read last. |
|
|
14853
|
+
| Title | yes | The item's name. When the tile links, this is the link, stretched over the whole tile. |
|
|
14854
|
+
| Subtitle | no | The second line. Optionally its own link, raised above the stretched one. |
|
|
14855
|
+
| Action | no | One control over the cover's bottom-end corner — usually play. Revealed on hover and focus; always visible on touch. |
|
|
14856
|
+
| Badge | no | A static label over the cover's top-start corner — a duration, "New". Click-through. |
|
|
14857
|
+
|
|
14858
|
+
## Props
|
|
14859
|
+
|
|
14860
|
+
| Name | Type | Default | Description |
|
|
14861
|
+
| --- | --- | --- | --- |
|
|
14862
|
+
| title | ReactNode | — | The item's name. Seeds the fallback art when it is a string. |
|
|
14863
|
+
| subtitle | ReactNode | — | The second line — an artist, a channel, a year. |
|
|
14864
|
+
| src | string | — | Cover image URL. Without one, or when it fails, the cover is GradientArt seeded by the item. |
|
|
14865
|
+
| alt | string | "" | Cover alt text. Empty by default: the title below already names the item. |
|
|
14866
|
+
| seed | string \| number | — | Seeds the fallback art. Defaults to `title` when it is a string. |
|
|
14867
|
+
| aspect | "square" \| "video" \| "poster" \| number | "square" | The cover's shape: square (albums, podcasts), video (16:9), poster (2:3 — films, books), or any width / height ratio. |
|
|
14868
|
+
| size | "sm" \| "md" \| "lg" | "md" | Moves the type tier, the cover-to-text gap, the corner and the overlay inset together. |
|
|
14869
|
+
| href | string | — | Where the tile goes. The title becomes the link, stretched over the whole tile. |
|
|
14870
|
+
| render | useRender.RenderProp | — | The title link's element, for a router: `render={<Link to="/albums/1" />}`. Takes precedence over `href`. |
|
|
14871
|
+
| subtitleHref | string | — | Makes the subtitle its own link — a separate tab stop above the tile's. |
|
|
14872
|
+
| subtitleRender | useRender.RenderProp | — | The subtitle link's element, for a router. |
|
|
14873
|
+
| action | ReactNode | — | One control over the cover's bottom-end corner, usually a play Button. Hidden until hover or focus on hover-capable devices; always shown on touch. |
|
|
14874
|
+
| badge | ReactNode | — | A static label on the cover's top-start corner — `<Badge appearance="overlay">`. |
|
|
14875
|
+
| titleAs | "h2" \| "h3" \| "h4" \| "h5" \| "p" | "p" | The title's element. A heading one level below the section's lets screen-reader users move tile to tile. |
|
|
14876
|
+
| loading | "lazy" \| "eager" | "lazy" | Native image loading. `eager` for the first row above the fold. |
|
|
14877
|
+
| ...div props | Omit<ComponentPropsWithRef<"div">, "title"> | — | Spread onto the root. |
|
|
14878
|
+
|
|
14879
|
+
## Variants
|
|
14880
|
+
|
|
14881
|
+
- **size**: sm, md, lg
|
|
14882
|
+
- **aspect**: square, video, poster
|
|
14883
|
+
|
|
14884
|
+
## Tokens used
|
|
14885
|
+
|
|
14886
|
+
`foreground`, `muted-foreground`, `muted`, `overlay`, `ring`
|
|
14887
|
+
|
|
14888
|
+
## Motion
|
|
14889
|
+
|
|
14890
|
+
- **arrival** · Cover · `motion-fade` — The cover image fades in over a quiet surface once it decodes, so a grid loading on a slow connection settles tile by tile instead of popping.
|
|
14891
|
+
- **response** · Cover · `motion-interactive` — Hovering the tile deepens the cover with a light scrim and lifts the action in (`reveal-on-hover`), so it is clear the whole tile is one target and where its action is.
|
|
14892
|
+
- **transition** · root · _no motion_ — A tile has no states of its own to move between; the action carries any (play / pause).
|
|
14893
|
+
- **activity** · root · _no motion_ — Nothing runs inside a tile; a now-playing indicator belongs to the player, not the grid.
|
|
14894
|
+
|
|
14895
|
+
## Accessibility
|
|
14896
|
+
|
|
14897
|
+
- No nested interactive content: the title is the tile's link, and its `stretched-link` overlay covers the tile, while the subtitle link and the action sit above it as separate controls.
|
|
14898
|
+
- Tab order is title → subtitle → action. The text comes first in the DOM and the cover is drawn above it with CSS `order`, so focus follows how a reader names the item.
|
|
14899
|
+
- Focusing the title link rings the whole tile, rounded like the cover, because the whole tile is what the link is; the subtitle link and the action ring themselves.
|
|
14900
|
+
- The action is hidden with opacity only, and only where the device can hover; it stays in the tab order and the accessibility tree, appears when anything in the tile has focus, and is always visible on touch.
|
|
14901
|
+
- The cover is decorative by default (`alt=""`), since the title names the item; the fallback art is aria-hidden.
|
|
14902
|
+
- Truncated titles keep their full text in the link's accessible name and in a `title` tooltip.
|
|
14903
|
+
|
|
14904
|
+
## Related
|
|
14905
|
+
|
|
14906
|
+
- Components: MediaImage, GradientArt, Grid, AspectBox, Card, Scroller
|
|
14907
|
+
|
|
14908
|
+
## Examples
|
|
14909
|
+
|
|
14910
|
+
### Album grid
|
|
14911
|
+
|
|
14912
|
+
```tsx
|
|
14913
|
+
import { Button, Grid, Icon, MediaTile } from "@sableui/react"
|
|
14914
|
+
// <Link> = your router's link (react-router, next/link, @tanstack/router, …)
|
|
14915
|
+
|
|
14916
|
+
<Grid minItemWidth="10rem" gap={3}>
|
|
14917
|
+
{albums.map((album) => (
|
|
14918
|
+
<MediaTile
|
|
14919
|
+
key={album.id}
|
|
14920
|
+
title={album.title}
|
|
14921
|
+
subtitle={album.artist}
|
|
14922
|
+
src={album.cover}
|
|
14923
|
+
render={<Link to={`/albums/${album.id}`} />}
|
|
14924
|
+
subtitleRender={<Link to={`/artists/${album.artistId}`} />}
|
|
14925
|
+
action={
|
|
14926
|
+
<Button size="icon" aria-label={`Play ${album.title}`} onClick={() => play(album)}>
|
|
14927
|
+
<Icon name="play" variant="solid" />
|
|
14928
|
+
</Button>
|
|
14929
|
+
}
|
|
14930
|
+
/>
|
|
14931
|
+
))}
|
|
14932
|
+
</Grid>
|
|
14933
|
+
```
|
|
14934
|
+
|
|
14935
|
+
### Video tile with a duration
|
|
14936
|
+
|
|
14937
|
+
```tsx
|
|
14938
|
+
import { Badge, MediaTile } from "@sableui/react"
|
|
14939
|
+
|
|
14940
|
+
<MediaTile
|
|
14941
|
+
aspect="video"
|
|
14942
|
+
title="Building a synth from scratch"
|
|
14943
|
+
subtitle="Undertow Sessions · 2 days ago"
|
|
14944
|
+
src={video.thumbnail}
|
|
14945
|
+
href={`/watch/${video.id}`}
|
|
14946
|
+
badge={<Badge appearance="overlay">12:04</Badge>}
|
|
14947
|
+
/>
|
|
14948
|
+
```
|
|
14949
|
+
|
|
14950
|
+
### Gallery, no links
|
|
14951
|
+
|
|
14952
|
+
```tsx
|
|
14953
|
+
import { Grid, MediaTile } from "@sableui/react"
|
|
14954
|
+
|
|
14955
|
+
<Grid minItemWidth="8rem" gap={2}>
|
|
14956
|
+
{photos.map((photo) => (
|
|
14957
|
+
<MediaTile key={photo.id} size="sm" title={photo.caption} src={photo.url} alt={photo.description} />
|
|
14958
|
+
))}
|
|
14959
|
+
</Grid>
|
|
14960
|
+
```
|
|
14961
|
+
|
|
14962
|
+
### Poster shelf
|
|
14963
|
+
|
|
14964
|
+
```tsx
|
|
14965
|
+
import { MediaTile } from "@sableui/react"
|
|
14966
|
+
|
|
14967
|
+
<MediaTile aspect="poster" size="lg" titleAs="h3" title="The Long Dark" subtitle="2026 · Drama" href="/films/the-long-dark" />
|
|
14968
|
+
```
|
|
14969
|
+
|
|
14970
|
+
---
|
|
14971
|
+
|
|
14347
14972
|
# Menu
|
|
14348
14973
|
|
|
14349
14974
|
**Status:** stable
|
|
@@ -15306,6 +15931,7 @@ Use PageHeader to establish a page's title, supporting description, and primary
|
|
|
15306
15931
|
- Do: Use one PageHeader per major page or route.
|
|
15307
15932
|
- Do: Keep the title literal and put explanation in the lede or nearby content.
|
|
15308
15933
|
- Don't: Do not use PageHeader inside repeated cards, dialogs, or compact panels.
|
|
15934
|
+
- Don't: Do not use a second PageHeader for a section of the page. It is the route's one h1; each section below it takes a SectionHeader (an h2 with the same title row, description and actions).
|
|
15309
15935
|
- Don't: Do not put it outside the Region that holds the page content. It then sits at a different measure from what it introduces, and the page reads as two documents.
|
|
15310
15936
|
|
|
15311
15937
|
## Props
|
|
@@ -15314,8 +15940,8 @@ Use PageHeader to establish a page's title, supporting description, and primary
|
|
|
15314
15940
|
| --- | --- | --- | --- |
|
|
15315
15941
|
| title | ReactNode | — | The page title (renders an <h1>). |
|
|
15316
15942
|
| description | ReactNode | — | Supporting copy beneath the title. |
|
|
15317
|
-
| back | ReactNode | — | Leading element before the title — typically a back control on drill-in routes. Keep it a ghost icon Button (size="icon", variant="ghost", aria-label="Back") so it stays quieter than the title. |
|
|
15318
|
-
| actions | ReactNode | — | Trailing actions aligned to the title row's end. They drop under the title when the row runs out of room, and the title wraps rather than truncating. |
|
|
15943
|
+
| back | ReactNode | — | Leading element before the title — typically a back control on drill-in routes. Keep it a ghost icon Button (size="icon", variant="ghost", aria-label="Back") so it stays quieter than the title. A ghost back control bleeds by its inset at the start, so its glyph lines up with the content edge below (Button bleed="none" opts out). |
|
|
15944
|
+
| actions | ReactNode | — | Trailing actions aligned to the title row's end. They drop under the title when the row runs out of room, and the title wraps rather than truncating. A ghost or link Button at either end of the group bleeds by its own padding, so its label meets the content edge (the end on the title row, the start once wrapped); filled buttons keep their edge on the line. Direct children only. |
|
|
15319
15945
|
| titleAs | "h1" \| "h2" | "h1" | The title's heading element. Keep h1 — the route's one h1. Use h2 only when something above already owns the h1: a shell that renders the route title, or a page showing a PageHeader as an example. |
|
|
15320
15946
|
|
|
15321
15947
|
## Tokens used
|
|
@@ -15329,7 +15955,7 @@ Use PageHeader to establish a page's title, supporting description, and primary
|
|
|
15329
15955
|
|
|
15330
15956
|
## Related
|
|
15331
15957
|
|
|
15332
|
-
- Components: AppHeader, Breadcrumbs, Button
|
|
15958
|
+
- Components: AppHeader, SectionHeader, Breadcrumbs, Button
|
|
15333
15959
|
|
|
15334
15960
|
## Examples
|
|
15335
15961
|
|
|
@@ -15360,8 +15986,8 @@ import { Button, PageHeader, Region } from "@sableui/react"
|
|
|
15360
15986
|
|
|
15361
15987
|
// ✗ Outside the Region. The header then sits at a different measure from the
|
|
15362
15988
|
// content it introduces, and the page reads as two documents.
|
|
15363
|
-
// ✗ Two on one page. A second one is a section heading
|
|
15364
|
-
//
|
|
15989
|
+
// ✗ Two on one page. A second one is a section heading: a SectionHeader
|
|
15990
|
+
// (an h2 on text-heading-3, same title row and actions), not another PageHeader.
|
|
15365
15991
|
// ✗ Global actions in it (account, search). Those persist across routes and
|
|
15366
15992
|
// belong in AppHeader.
|
|
15367
15993
|
```
|
|
@@ -16956,6 +17582,153 @@ import { SearchInput } from "@sableui/react"
|
|
|
16956
17582
|
|
|
16957
17583
|
---
|
|
16958
17584
|
|
|
17585
|
+
# SectionHeader
|
|
17586
|
+
|
|
17587
|
+
**Status:** stable
|
|
17588
|
+
|
|
17589
|
+
The heading block for one section of a page: a title (h2 by default), an optional line of description, and actions on the title row — a "View all" link or a couple of buttons — that drop beneath the title only when the row runs out of room.
|
|
17590
|
+
|
|
17591
|
+
**Keywords:** section header, section heading, section title, shelf, view all, see all, show all, h2, subsection, optical alignment, bleed
|
|
17592
|
+
|
|
17593
|
+
## Usage
|
|
17594
|
+
|
|
17595
|
+
Use SectionHeader to head each section below the page title — a home-page shelf with View all, a settings group, a list with its own actions. PageHeader is the route's single h1; every section under it takes a SectionHeader, with titleId wired to the section's aria-labelledby.
|
|
17596
|
+
|
|
17597
|
+
### Best practices
|
|
17598
|
+
|
|
17599
|
+
- Do: Wrap the header and its content in a `<section aria-labelledby={titleId}>` (a flex column with a gap, or a Stack inside it) so the section is a named region and the header's title is its name.
|
|
17600
|
+
- Do: Pick titleAs by the document outline (h2 under the page h1, h3 inside an h2 section) and size by visual weight: md for a page's sections and shelves, sm for denser groups like settings.
|
|
17601
|
+
- Do: Keep actions to one or two `size="sm"` buttons — the title is centred on that height. A shelf's "View all" is a ghost Button rendered as a link (`render={<a href=… />}`), named for what it opens when the label alone is ambiguous (aria-label="View all new releases").
|
|
17602
|
+
- Do: Pass a ghost or link action as a plain Button and let the header align it: the last direct child bleeds by its own padding at the end, so "View all" sits on the edge of the grid below, and the first bleeds at the start, which is the edge once the actions wrap beneath the title. Filled buttons keep their edge on the line. Set bleed="none" on the Button to opt one out.
|
|
17603
|
+
- Don't: Nudge a View all onto the edge with -me-3, or wrap the actions in your own div: the header aligns direct children only, and only by each button's real padding. If you must wrap them, set bleed on the Button inside instead.
|
|
17604
|
+
- Don't: Use a second PageHeader for a section — that is a second h1, and the outline then reports two documents. And do not hand-build `<div className="flex justify-between"><h2 className="text-heading-3">…` per shelf: it misses the wrapping, the aria wiring and the shared rhythm.
|
|
17605
|
+
- Don't: Use SectionHeader to title a Card (use CardHeader + CardTitle) or a docked tool panel (use PanelSection). It heads a section of the page, not a surface.
|
|
17606
|
+
- Don't: Add margin to the header to space it from its content. It has no outer spacing on purpose; the section's gap owns that rhythm, so every section on the page keeps the same one.
|
|
17607
|
+
|
|
17608
|
+
### Anatomy
|
|
17609
|
+
|
|
17610
|
+
| Part | Required | Description |
|
|
17611
|
+
| --- | --- | --- |
|
|
17612
|
+
| Title | yes | The section's heading (h2 by default), wrapping rather than truncating. Its id names the section. |
|
|
17613
|
+
| Description | no | One line of supporting copy under the title, capped at the prose measure. |
|
|
17614
|
+
| Actions | no | Buttons or links at the title row's end; they move beneath the title when the row is too narrow for both. A borderless (ghost/link) Button at either end of the group is optically aligned: its label, not its invisible padding, meets the content edge. |
|
|
17615
|
+
|
|
17616
|
+
## Props
|
|
17617
|
+
|
|
17618
|
+
| Name | Type | Default | Description |
|
|
17619
|
+
| --- | --- | --- | --- |
|
|
17620
|
+
| title | ReactNode | — | The section's title. |
|
|
17621
|
+
| description | ReactNode | — | One line of supporting copy beneath the title, capped at the prose measure. |
|
|
17622
|
+
| actions | ReactNode | — | Trailing actions on the title row — a View all link or a couple of buttons, size="sm". They stay on the title's row and drop beneath it only when the row runs out of room; the title wraps rather than truncating. A ghost or link Button that ends the group bleeds by its padding at the end (and one that starts it, at the start — the edge once wrapped), so its label lines up with the content below; Button bleed="none" opts out. Direct children only. |
|
|
17623
|
+
| titleAs | "h2" \| "h3" \| "h4" | "h2" | The heading element, chosen by the document outline: h2 for a section under the page's h1, h3 for a subsection inside one. |
|
|
17624
|
+
| titleId | string | — | The title's id, so the section it heads can point at it: <section aria-labelledby={titleId}>. Generated when omitted. |
|
|
17625
|
+
| size | "md" \| "sm" | "md" | Visual weight, independent of the heading level: md (text-heading-3) for a page's sections and shelves; sm (text-heading-4) for denser sets — settings groups, subsections. |
|
|
17626
|
+
|
|
17627
|
+
## Variants
|
|
17628
|
+
|
|
17629
|
+
- **size**: md, sm
|
|
17630
|
+
|
|
17631
|
+
## Tokens used
|
|
17632
|
+
|
|
17633
|
+
`foreground`, `muted-foreground`
|
|
17634
|
+
|
|
17635
|
+
## Motion
|
|
17636
|
+
|
|
17637
|
+
- **arrival** · root · _no motion_ — A heading arrives with the section it names; animating it separately would make the section's title lag its content.
|
|
17638
|
+
- **response** · root · _no motion_ — The header itself is not interactive; the Buttons passed as actions carry their own response.
|
|
17639
|
+
- **transition** · root · _no motion_ — It has no states of its own to move between.
|
|
17640
|
+
- **activity** · root · _no motion_ — It never does indeterminate work.
|
|
17641
|
+
|
|
17642
|
+
## Accessibility
|
|
17643
|
+
|
|
17644
|
+
- Renders a real heading (h2 by default); choose titleAs by the outline, never to get a size — size sets the look.
|
|
17645
|
+
- The title always carries an id (titleId, or a generated one). Point the section at it with aria-labelledby so the section is announced as a named region and appears in landmark navigation.
|
|
17646
|
+
- A repeated "View all" is ambiguous in a links list; name each one for its section (aria-label="View all new releases") or keep the visible label specific.
|
|
17647
|
+
|
|
17648
|
+
## Related
|
|
17649
|
+
|
|
17650
|
+
- Components: PageHeader, CardHeader, PanelSection, Grid, Stack, Button
|
|
17651
|
+
- Patterns: SettingsPage, DashboardPage
|
|
17652
|
+
|
|
17653
|
+
## Examples
|
|
17654
|
+
|
|
17655
|
+
### A shelf with View all
|
|
17656
|
+
|
|
17657
|
+
```tsx
|
|
17658
|
+
import { Button, Card, CardDescription, CardHeader, CardTitle, Grid, SectionHeader } from "@sableui/react"
|
|
17659
|
+
|
|
17660
|
+
<section aria-labelledby="new-releases" className="flex flex-col gap-4">
|
|
17661
|
+
<SectionHeader
|
|
17662
|
+
titleId="new-releases"
|
|
17663
|
+
title="New releases"
|
|
17664
|
+
description="Fresh from artists you follow."
|
|
17665
|
+
actions={
|
|
17666
|
+
<Button variant="ghost" size="sm" render={<a href="/new" />} aria-label="View all new releases">
|
|
17667
|
+
View all
|
|
17668
|
+
</Button>
|
|
17669
|
+
}
|
|
17670
|
+
/>
|
|
17671
|
+
<Grid minItemWidth="10rem" gap={2}>
|
|
17672
|
+
<Card><CardHeader><CardTitle>Night Swim</CardTitle><CardDescription>Lena Vale</CardDescription></CardHeader></Card>
|
|
17673
|
+
<Card><CardHeader><CardTitle>Low Tide</CardTitle><CardDescription>Harbour Lights</CardDescription></CardHeader></Card>
|
|
17674
|
+
</Grid>
|
|
17675
|
+
</section>
|
|
17676
|
+
```
|
|
17677
|
+
|
|
17678
|
+
### A settings section
|
|
17679
|
+
|
|
17680
|
+
```tsx
|
|
17681
|
+
import { Field, SectionHeader, Switch } from "@sableui/react"
|
|
17682
|
+
|
|
17683
|
+
<section aria-labelledby="playback" className="flex flex-col gap-4">
|
|
17684
|
+
<SectionHeader
|
|
17685
|
+
titleId="playback"
|
|
17686
|
+
size="sm"
|
|
17687
|
+
title="Playback"
|
|
17688
|
+
description="How tracks start, end and blend into each other."
|
|
17689
|
+
/>
|
|
17690
|
+
<Field label="Crossfade" orientation="horizontal">
|
|
17691
|
+
<Switch defaultChecked />
|
|
17692
|
+
</Field>
|
|
17693
|
+
<Field label="Normalize volume" orientation="horizontal">
|
|
17694
|
+
<Switch />
|
|
17695
|
+
</Field>
|
|
17696
|
+
</section>
|
|
17697
|
+
```
|
|
17698
|
+
|
|
17699
|
+
### A section with two actions
|
|
17700
|
+
|
|
17701
|
+
```tsx
|
|
17702
|
+
import { Button, Entity, Icon, SectionHeader, Stack } from "@sableui/react"
|
|
17703
|
+
|
|
17704
|
+
<section aria-labelledby="playlists" className="flex flex-col gap-4">
|
|
17705
|
+
<SectionHeader
|
|
17706
|
+
titleId="playlists"
|
|
17707
|
+
title="Your playlists"
|
|
17708
|
+
actions={
|
|
17709
|
+
<>
|
|
17710
|
+
<Button variant="ghost" size="sm" iconStart={<Icon name="filter" />}>Filter</Button>
|
|
17711
|
+
<Button size="sm" iconStart={<Icon name="plus" />}>New playlist</Button>
|
|
17712
|
+
</>
|
|
17713
|
+
}
|
|
17714
|
+
/>
|
|
17715
|
+
<Stack gap={1}>
|
|
17716
|
+
<Entity className="[--entity-px:0]" title="Late drives" subtitle="42 tracks" />
|
|
17717
|
+
<Entity className="[--entity-px:0]" title="Focus" subtitle="18 tracks" />
|
|
17718
|
+
</Stack>
|
|
17719
|
+
</section>
|
|
17720
|
+
```
|
|
17721
|
+
|
|
17722
|
+
### A subsection inside a section
|
|
17723
|
+
|
|
17724
|
+
```tsx
|
|
17725
|
+
import { SectionHeader } from "@sableui/react"
|
|
17726
|
+
|
|
17727
|
+
<SectionHeader titleAs="h3" size="sm" title="Appears on" />
|
|
17728
|
+
```
|
|
17729
|
+
|
|
17730
|
+
---
|
|
17731
|
+
|
|
16959
17732
|
# SegmentedBar
|
|
16960
17733
|
|
|
16961
17734
|
**Status:** stable
|
|
@@ -17414,6 +18187,7 @@ Use Sheet for secondary workflows that slide in while preserving context from th
|
|
|
17414
18187
|
| SheetHeader | no | The title and description region. |
|
|
17415
18188
|
| SheetTitle | no | The sheet heading. |
|
|
17416
18189
|
| SheetDescription | no | Supporting context for the sheet. |
|
|
18190
|
+
| SheetList | no | Edge-to-edge rows (Entity) aligned with the header — a queue, notifications, a picker. |
|
|
17417
18191
|
| SheetFooter | no | Footer actions for the workflow. |
|
|
17418
18192
|
|
|
17419
18193
|
## Subcomponents
|
|
@@ -17426,6 +18200,7 @@ Use Sheet for secondary workflows that slide in while preserving context from th
|
|
|
17426
18200
|
| SheetHeader | ComponentProps<"div"> | Top section holding SheetTitle/SheetDescription. |
|
|
17427
18201
|
| SheetTitle | Base UI Drawer.Title props + className | The sheet's accessible name — wires aria-labelledby on the popup. |
|
|
17428
18202
|
| SheetDescription | Base UI Drawer.Description props + className | Muted supporting line; wires aria-describedby. |
|
|
18203
|
+
| SheetList | ComponentProps<"div"> | Rows that run edge to edge in the sheet, Entity most often. It bleeds through the sheet's padding (--sheet-p) and hands it back to each row as --entity-px, so row content lines up with SheetHeader and a row's hover fill reaches the panel's edges. Without it a list of Entity rows sits 12px in from the header. The Sheet equivalent of CardList. |
|
|
17429
18204
|
| SheetFooter | ComponentProps<"div"> | Bottom row for actions, pinned after the scrollable body. In a bottom sheet it stacks: full width, the primary on top, every button at the cta height. |
|
|
17430
18205
|
|
|
17431
18206
|
## Props
|
|
@@ -17459,6 +18234,14 @@ Use Sheet for secondary workflows that slide in while preserving context from th
|
|
|
17459
18234
|
|
|
17460
18235
|
## Examples
|
|
17461
18236
|
|
|
18237
|
+
### List of rows (a queue)
|
|
18238
|
+
|
|
18239
|
+
```tsx
|
|
18240
|
+
import { Button, Entity, GradientArt, Sheet, SheetContent, SheetHeader, SheetList, SheetTitle, SheetTrigger } from "@sableui/react"
|
|
18241
|
+
|
|
18242
|
+
<Sheet><SheetTrigger render={<Button variant="secondary">Queue</Button>} /><SheetContent><SheetHeader><SheetTitle>Up next</SheetTitle></SheetHeader><SheetList>{queue.map((track) => <Entity key={track.id} leading={<GradientArt aria-hidden seed={track.title} className="size-10 rounded-sm" />} title={track.title} subtitle={track.artist} />)}</SheetList></SheetContent></Sheet>
|
|
18243
|
+
```
|
|
18244
|
+
|
|
17462
18245
|
### Basic (right)
|
|
17463
18246
|
|
|
17464
18247
|
```tsx
|
|
@@ -17551,7 +18334,7 @@ import { ShowMore } from "@sableui/react"
|
|
|
17551
18334
|
|
|
17552
18335
|
**Status:** stable
|
|
17553
18336
|
|
|
17554
|
-
Desktop side-navigation rail (in-flow sticky, hidden below lg) with SidebarHeader / SidebarNav / SidebarItem / SidebarFooter parts. Items are router-agnostic via the `render` prop; an optional collapsed icon-rail mode reveals labels in tooltips.
|
|
18337
|
+
Desktop side-navigation rail (in-flow sticky, hidden below lg) with SidebarHeader / SidebarBrand / SidebarNav / SidebarItem / SidebarFooter parts. Items are router-agnostic via the `render` prop; an optional collapsed icon-rail mode reveals labels in tooltips.
|
|
17555
18338
|
|
|
17556
18339
|
**Keywords:** sidebar, navigation, app chrome, rail
|
|
17557
18340
|
|
|
@@ -17577,6 +18360,7 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
|
|
|
17577
18360
|
|
|
17578
18361
|
| Name | Type | Description |
|
|
17579
18362
|
| --- | --- | --- |
|
|
18363
|
+
| SidebarBrand | { mark: ReactNode; label: ReactNode; render?: useRender.RenderProp } + div props | The brand row, inside SidebarHeader, on the same grid as SidebarItem: mark in the icons' 24px slot, label on the labels' column, so a logo lines up with the nav below it without copying the rail's inset numbers. Collapsed, it shows the mark alone (named by a string label). render={<Link to="/" />} makes it the home link. |
|
|
17580
18364
|
| SidebarItem | { label: ReactNode; icon?: ReactNode; active?: boolean; badge?: ReactNode; render?: useRender.RenderProp } + button props | A single nav row. label is the row text (inline when expanded, in a hover tooltip when collapsed); icon is the leading icon node (the consumer swaps fill weight for the active state); active marks the current route (accent fill + aria-current="page"); badge is a trailing node (e.g. a count) pushed to the row end and hidden when collapsed; render composes another element, e.g. render={<Link to="/x" />}. |
|
|
17581
18365
|
|
|
17582
18366
|
## Props
|
|
@@ -17611,10 +18395,10 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
|
|
|
17611
18395
|
### Rail with nav
|
|
17612
18396
|
|
|
17613
18397
|
```tsx
|
|
17614
|
-
import { Icon, NotificationBadge, Sidebar, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav } from "@sableui/react"
|
|
18398
|
+
import { Icon, NotificationBadge, Sidebar, SidebarBrand, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav } from "@sableui/react"
|
|
17615
18399
|
// <Link> = your router's link (react-router, next/link, @tanstack/router, …)
|
|
17616
18400
|
|
|
17617
|
-
<Sidebar><SidebarHeader
|
|
18401
|
+
<Sidebar><SidebarHeader><SidebarBrand mark={<Icon name="music" variant="solid" />} label="Undertow" render={<Link to="/" />} /></SidebarHeader><SidebarNav><SidebarItem icon={<Icon name="bell" />} label="Notifications" badge={<NotificationBadge count={3} />} render={<Link to="/notifications" />} /></SidebarNav><SidebarFooter>{wallet}</SidebarFooter></Sidebar>
|
|
17618
18402
|
```
|
|
17619
18403
|
|
|
17620
18404
|
### Collapsible (icon rail)
|
|
@@ -17865,6 +18649,9 @@ Use Slider for bounded numeric choices where approximate adjustment is faster th
|
|
|
17865
18649
|
| step | number | 1 | Granularity the slider steps through. Snaps to multiples. |
|
|
17866
18650
|
| disabled | boolean | false | Ignore user interaction and dim the control. |
|
|
17867
18651
|
| orientation | 'horizontal' \| 'vertical' | horizontal | Axis the slider tracks along. A vertical slider has no intrinsic length — give it a height (h-40, or flex-1 in a column). |
|
|
18652
|
+
| aria-label | string | — | Accessible name, applied to each thumb's input (the element with role=slider). A range also names its group with it. |
|
|
18653
|
+
| getAriaLabel | (index) => string | — | Per-thumb name for a range ("Minimum price" / "Maximum price"). Wins over aria-label on the thumbs. |
|
|
18654
|
+
| getAriaValueText | (formattedValue, value, index) => string | — | What a screen reader announces for the value when the number alone is wrong — a seek bar's "1:23 of 3:45". For units or percent, `format` (Intl.NumberFormatOptions) is enough. |
|
|
17868
18655
|
|
|
17869
18656
|
## Tokens used
|
|
17870
18657
|
|
|
@@ -17877,7 +18664,8 @@ Use Slider for bounded numeric choices where approximate adjustment is faster th
|
|
|
17877
18664
|
- The thumb's hit area is a fixed 16px box at every state. Only the decorative inner dot resizes — rest 12px, full 16px on hover, keyboard focus, and drag — so the target a pointer or finger must land on never moves or changes size. Keyboard users, who never hover, get the enlarged dot from focus-visible.
|
|
17878
18665
|
- The rest-to-full transition uses the motion duration/easing tokens, so it collapses to an instant state change under prefers-reduced-motion or MotionProvider off. The size change itself still happens: it reports interaction state, it is not decoration.
|
|
17879
18666
|
- Range sliders get one thumb per value with minStepsBetweenValues honored.
|
|
17880
|
-
- Pass
|
|
18667
|
+
- Pass aria-label (or getAriaLabel for a range) so each handle has an accessible name. On Slider it reaches the thumb inputs; on a bare SliderRoot it only names the role=group — when composing, put aria-label / getAriaLabel on each SliderThumb, or label the Root with aria-labelledby or a Field, which Base UI does carry to the thumbs.
|
|
18668
|
+
- getAriaValueText sets aria-valuetext for values a number misreads (time, named steps).
|
|
17881
18669
|
|
|
17882
18670
|
## Related
|
|
17883
18671
|
|
|
@@ -17899,7 +18687,15 @@ import { Slider } from "@sableui/react"
|
|
|
17899
18687
|
import { Slider } from "@sableui/react"
|
|
17900
18688
|
|
|
17901
18689
|
const [range, setRange] = useState([20, 60])
|
|
17902
|
-
<Slider value={range} onValueChange={setRange} aria-label="Price range" />
|
|
18690
|
+
<Slider value={range} onValueChange={setRange} aria-label="Price range" getAriaLabel={(i) => (i ? "Maximum price" : "Minimum price")} />
|
|
18691
|
+
```
|
|
18692
|
+
|
|
18693
|
+
### Seek bar with spoken time
|
|
18694
|
+
|
|
18695
|
+
```tsx
|
|
18696
|
+
import { Slider } from "@sableui/react"
|
|
18697
|
+
|
|
18698
|
+
<Slider value={position} max={duration} onValueChange={seek} aria-label="Seek" getAriaValueText={(_, v) => `${formatTime(v)} of ${formatTime(duration)}`} />
|
|
17903
18699
|
```
|
|
17904
18700
|
|
|
17905
18701
|
### Stepped
|
|
@@ -18872,23 +19668,37 @@ import { Textarea } from "@sableui/react"
|
|
|
18872
19668
|
|
|
18873
19669
|
**Status:** stable
|
|
18874
19670
|
|
|
18875
|
-
A presentational radiogroup for choosing a Sable design theme from theme manifests —
|
|
19671
|
+
A presentational radiogroup for choosing a Sable design theme from theme manifests — tiles that draw each theme as a miniature of its own interface (its colours, type and corner shape) for a settings page, or compact rows for tool chrome. Pair it with useSableTheme().themeId and setThemeId when the app uses SableThemeProvider.
|
|
18876
19672
|
|
|
18877
19673
|
**Keywords:** theme selector, theme, appearance, skin, design theme, preset
|
|
18878
19674
|
|
|
18879
19675
|
## Usage
|
|
18880
19676
|
|
|
18881
|
-
Use ThemeSelector wherever a person picks a theme. The default `cards` layout is page-register furniture: a
|
|
19677
|
+
Use ThemeSelector wherever a person picks a theme. The default `cards` layout is page-register furniture: a miniature of each theme's interface (its background, "Aa" in its display face, a card, and a primary button in its own corner shape), with the name and a caption only where one distinguishes it (the default theme, or a single-mode theme). Use `layout="list"` in tool chrome, a popover, or any docked panel — five cards is the wrong object for a 256px surface, however it is sized. For colour mode (light/dark/system) use ThemeSwitcher, which is a segmented control and belongs in chrome as-is.
|
|
18882
19678
|
|
|
18883
19679
|
### Best practices
|
|
18884
19680
|
|
|
18885
|
-
- Do: Use layout="list" in a popover, a menu, or a docked panel. Rows sit on the --control-sm ladder and still show each theme
|
|
19681
|
+
- Do: Use layout="list" in a popover, a menu, or a docked panel. Rows sit on the --control-sm ladder and still show a small tile of each theme, so the choice stays visible rather than described. Rows take `sable-frame-inner`, so host the list in a sable-frame (`sable-frame sable-surface sable-surface-popover sable-frame-popover` — PopoverContent is not one; give it that class list, or its p-4 leaves the rows unnested) and their corners nest concentrically.
|
|
18886
19682
|
- Do: Pass mode={useSableTheme().mode} so every preview renders in the colour mode actually in use, rather than in each theme's manifest default.
|
|
19683
|
+
- Do: Use ThemeSelector when switching is free: it is a radio group, so arrow keys check the next theme and onValueChange fires on every press — the page restyles as you go, which is the point of a live theme switcher. When a switch has a cost (it opens a "discard changes?" confirm, loads fonts, or resets other settings), use a DisclosurePicker list instead, with a `ThemePreview size="sm"` as each option's `media`: arrows only move there, and Enter, Space or a click commits.
|
|
18887
19684
|
- Do: Load each theme's fonts in the host app. Every card carries a type specimen in that theme's own face so people can compare typography as well as color. A theme whose fonts are not loaded falls back to system-ui, and its specimen stops telling the truth.
|
|
18888
19685
|
- Don't: Add a viewport breakpoint to force a column count — `className="sm:grid-cols-1"` or similar. `sm:` keys off the VIEWPORT, not this component's container, so on a wide screen it does nothing for a narrow parent. The cards grid already reflows on its own width via auto-fit.
|
|
18889
19686
|
- Don't: Put the cards layout in workspace chrome. It was built for a settings page; in a 256px popover the cards clip their labels to a single character and grow to roughly twice their intended height.
|
|
18890
19687
|
- Don't: Use ThemeSelector for light / dark / system colour mode. That is ThemeSwitcher — a segmented control. This picks the design theme, which is a different axis entirely, and the two are frequently confused.
|
|
18891
19688
|
|
|
19689
|
+
### Anatomy
|
|
19690
|
+
|
|
19691
|
+
| Part | Required | Description |
|
|
19692
|
+
| --- | --- | --- |
|
|
19693
|
+
| ThemeSelector | yes | The radiogroup of themes, as cards or list rows. |
|
|
19694
|
+
| ThemePreview | no | The tile each option draws, exported for showing a theme outside the picker — the row naming the current theme, or the `media` of each option in a commit-on-Enter DisclosurePicker list. |
|
|
19695
|
+
|
|
19696
|
+
## Subcomponents
|
|
19697
|
+
|
|
19698
|
+
| Name | Type | Description |
|
|
19699
|
+
| --- | --- | --- |
|
|
19700
|
+
| ThemePreview | { theme: ThemeManifest; mode?: "light" \| "dark"; size?: "md" \| "sm" } | The tile each option draws: the theme as a miniature of itself (background, "Aa" in its display face, a card, a primary button in its own corner shape), in its own scope. Exported for anywhere that shows a theme outside the picker, e.g. a row naming the current preset, or each option of a DisclosurePicker list where a pick has a cost. Sized in the caller's spacing unit, so it lines up with the row it sits in. |
|
|
19701
|
+
|
|
18892
19702
|
## Props
|
|
18893
19703
|
|
|
18894
19704
|
| Name | Type | Default | Description |
|
|
@@ -18896,7 +19706,7 @@ Use ThemeSelector wherever a person picks a theme. The default `cards` layout is
|
|
|
18896
19706
|
| value | ThemeId | — | The currently selected Sable design theme id. |
|
|
18897
19707
|
| onValueChange | (themeId: ThemeId) => void | — | Called with the next design theme id when an option is chosen. |
|
|
18898
19708
|
| themes | readonly ThemeManifest[] | themeManifests | Theme manifests to render. Defaults to Sable's built-in themes. |
|
|
18899
|
-
| mode | "light" \| "dark" | — | Preview mode for each
|
|
19709
|
+
| mode | "light" \| "dark" | — | Preview mode for each theme's tile. Pass useSableTheme().mode to match the current color mode; omitted cards use their manifest defaultMode. |
|
|
18900
19710
|
| label | string | "Design theme" | Accessible label for the radiogroup. |
|
|
18901
19711
|
| layout | "cards" \| "list" | "cards" | Presentation. "cards" is the page-register picker (~144px per card); "list" is one compact --control-sm row per theme, for chrome and popovers. A presentation variant, not a size — "list" is not "cards, smaller" — which is why it is a prop rather than a density scope. |
|
|
18902
19712
|
| className | string | — | Merged onto the outer radiogroup grid. |
|
|
@@ -18908,6 +19718,7 @@ Use ThemeSelector wherever a person picks a theme. The default `cards` layout is
|
|
|
18908
19718
|
## Accessibility
|
|
18909
19719
|
|
|
18910
19720
|
- The container is role="radiogroup" with an aria-label; each card is a role="radio" button with aria-checked reflecting the selected theme.
|
|
19721
|
+
- Standard radio keyboard: arrow keys move the check — calling onValueChange on each press — and Space checks the focused option. That instant switching is right for a free, reversible choice; where a switch has a cost, use a DisclosurePicker list (a listbox: arrows move, Enter commits) with ThemePreview tiles instead.
|
|
18911
19722
|
- Focus-visible draws a 2px ring on the focused card.
|
|
18912
19723
|
- The selected card wears the shared selection-card checked ring (the same as RadioCard and CheckboxCard), a muted fill and a check icon, so selection does not rely on colour alone.
|
|
18913
19724
|
- Presentational only: it does not change the document theme; pass useSableTheme().setThemeId or apply the change in onValueChange.
|
|
@@ -18918,6 +19729,31 @@ Use ThemeSelector wherever a person picks a theme. The default `cards` layout is
|
|
|
18918
19729
|
|
|
18919
19730
|
## Examples
|
|
18920
19731
|
|
|
19732
|
+
### When a switch has a cost: a DisclosurePicker list of ThemePreview tiles
|
|
19733
|
+
|
|
19734
|
+
```tsx
|
|
19735
|
+
import { DisclosurePicker, DisclosurePickerList, DisclosurePickerOption, DisclosurePickerPanel, DisclosurePickerTrigger, ThemePreview, useSableTheme } from "@sableui/react"
|
|
19736
|
+
|
|
19737
|
+
// Arrows move; Enter, Space or a click commits — so whatever the switch costs
|
|
19738
|
+
// (a confirm, a font load, a reset) happens once, on the pick.
|
|
19739
|
+
const { themeId, setThemeId, themes, mode } = useSableTheme()
|
|
19740
|
+
const current = themes.find((theme) => theme.id === themeId)
|
|
19741
|
+
<DisclosurePicker>
|
|
19742
|
+
<DisclosurePickerTrigger aria-label={`Theme: ${current?.label}`} media={current ? <ThemePreview theme={current} mode={mode} size="sm" /> : null}>
|
|
19743
|
+
{current?.label}
|
|
19744
|
+
</DisclosurePickerTrigger>
|
|
19745
|
+
<DisclosurePickerPanel>
|
|
19746
|
+
<DisclosurePickerList aria-label="Theme" value={themeId} onValueChange={setThemeId}>
|
|
19747
|
+
{themes.map((theme) => (
|
|
19748
|
+
<DisclosurePickerOption key={theme.id} value={theme.id} media={<ThemePreview theme={theme} mode={mode} size="sm" />}>
|
|
19749
|
+
{theme.label}
|
|
19750
|
+
</DisclosurePickerOption>
|
|
19751
|
+
))}
|
|
19752
|
+
</DisclosurePickerList>
|
|
19753
|
+
</DisclosurePickerPanel>
|
|
19754
|
+
</DisclosurePicker>
|
|
19755
|
+
```
|
|
19756
|
+
|
|
18921
19757
|
### With SableThemeProvider
|
|
18922
19758
|
|
|
18923
19759
|
```tsx
|
|
@@ -19660,7 +20496,7 @@ Use Tree for the hierarchy panel in a workspace: layers, objects, an inspector o
|
|
|
19660
20496
|
- Do: Give Tree an aria-label naming the hierarchy ("Layers", "Files"). It is a role="tree", and an unnamed tree is announced as just "tree".
|
|
19661
20497
|
- Do: For a file tree, nest with `subtree` and use the icon convention: `icon={<Icon name="folder" />}` on a row with a subtree, `icon={<Icon name="file" />}` on a leaf. There are no folder/file presets — they would only bind those two props.
|
|
19662
20498
|
- Do: Give TreeAddRow the same depth as the rows it adds to, so its mark lands in their type column: one deeper than the parent for a child list, the parent's own depth for a sibling list.
|
|
19663
|
-
- Do: Put mutable per-row state (visibility, solo, a row menu) in `actions`, on the right rail. Show it on hover, and at rest only when its value is the non-default one.
|
|
20499
|
+
- Do: Put mutable per-row state (visibility, solo, a row menu) in `actions`, on the right rail. Show it on hover with `reveal-on-hover` (the row is its `group`), and at rest only when its value is the non-default one.
|
|
19664
20500
|
- Do: Let section headers sit on the rail at the same height as a depth-0 row. Space and a hairline separate sections; TreeSection handles both, including dropping the divider on the first.
|
|
19665
20501
|
- Don't: Hardcode a left inset anywhere in the panel. Use treeIndent(depth) or var(--tree-rail); a hand-written px value is how a tree ends up with four different left edges.
|
|
19666
20502
|
- Don't: Redefine --tree-indent to anything other than --tree-column. A step smaller than the column puts a child's content left of its parent's and the nesting reads inverted.
|
|
@@ -19703,7 +20539,7 @@ Use Tree for the hierarchy panel in a workspace: layers, objects, an inspector o
|
|
|
19703
20539
|
| selected | boolean | false | Selection fill and accent bar on a TreeRow. Inside a Tree it is exposed as aria-selected on the treeitem; a row rendered outside a Tree is a plain div and exposes aria-current instead. |
|
|
19704
20540
|
| toggleLabel | string | — | Names the thing being disclosed, for the toggle's accessible label — "Expand Background" rather than "Expand group". Every example passes it; a screen-reader user hearing twenty identical "Expand group" buttons has no way to tell them apart. |
|
|
19705
20541
|
| icon | ReactNode | — | The kind marker for a TreeRow, rendered in the type column at 1em. Use a semantic Icon so it follows the app icon set. |
|
|
19706
|
-
| disclosureIcon | ReactNode | — | Replaces TreeRow's semantic chevron-down icon. Omit to use the
|
|
20542
|
+
| disclosureIcon | ReactNode | — | Replaces TreeRow's semantic chevron-down icon. Omit to use the chevron-down from the app's IconProvider set. |
|
|
19707
20543
|
| actions | ReactNode | — | Right-rail controls for a TreeRow: visibility, solo, a row menu. |
|
|
19708
20544
|
| label | ReactNode | — | TreeAddRow's visible label. Name what gets added — 'Add effect', not 'Add'. |
|
|
19709
20545
|
|
|
@@ -19719,7 +20555,7 @@ Use Tree for the hierarchy panel in a workspace: layers, objects, an inspector o
|
|
|
19719
20555
|
- Each treeitem is named by its own label (aria-labelledby the name), never by the text of its icon or action buttons.
|
|
19720
20556
|
- The disclosure is a real button with aria-expanded and a label naming what it discloses, and it stops propagation so collapsing a group never changes the selection. Inside a Tree the treeitem carries aria-expanded and the arrow keys, so the button becomes a pointer target only: aria-hidden, out of the tab order, and it leaves focus where it was when clicked.
|
|
19721
20557
|
- The disclosure's POINTER TARGET is bigger than the mark it draws. `--tree-column` is 20px comfortable and 18px compact, both under WCAG 2.5.8's 24x24 floor, so a centred pseudo-element takes the hit at `--control-xs` (28/24) instead. The drawn box stays on the column, because that column is also the indent step — growing the button would push every name in the tree one column right.
|
|
19722
|
-
- Right-rail actions stay in the tab order, so a row with actions costs extra Tab stops. Keep actions few and
|
|
20558
|
+
- Right-rail actions stay in the tab order, so a row with actions costs extra Tab stops. Keep actions few and reveal them with `reveal-on-hover`, which also shows them on focus and always on touch.
|
|
19723
20559
|
- `dimmed` is presentation only. A row switched off must also say so in its actions' aria-pressed or its accessible name; opacity alone is invisible to a screen reader and fails contrast.
|
|
19724
20560
|
- Right-rail controls that appear on hover must also appear on focus-visible, or they are unreachable by keyboard.
|
|
19725
20561
|
|
|
@@ -20428,8 +21264,7 @@ Which token to reach for, and what it is most often confused with. Generated fro
|
|
|
20428
21264
|
|
|
20429
21265
|
The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, when to reach for it, and what you would hand-write without it. Generated from `src/utilities/utility-docs.ts`; the full data is `registry.json → utilities`; one entry at a time with `sable utility <name>`.
|
|
20430
21266
|
|
|
20431
|
-
- **Typography:** The paired `text-*` page tiers (`text-heading-1`, `text-body-md`, `text-label-sm`, `text-mono-md`, …) are documented as `registry.json → tokens.typography` (named without the `text-` prefix). `sable token typography`.
|
|
20432
|
-
- **Motion:** The `motion-*` and `animate-*` recipes are listed in `registry.json → tokens.motion.recipes`, with the slot model in `registry.json → motionLayer`. `sable docs motion`.
|
|
21267
|
+
- **Typography:** The paired `text-*` page tiers (`text-heading-1`, `text-body-md`, `text-label-sm`, `text-mono-md`, …) are documented as `registry.json → tokens.typography` (named without the `text-` prefix). Retrieve one with `sable utility text-heading-1`, all with `sable token typography`.
|
|
20433
21268
|
|
|
20434
21269
|
### relative surface (6)
|
|
20435
21270
|
|
|
@@ -20557,12 +21392,93 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
|
|
|
20557
21392
|
- **Instead of** An animated fade or a hand-written `mask-image` gradient. Use `StreamingText` first; this is for a stream rendered without it.
|
|
20558
21393
|
- `<span className="stream-tail">{latestChunk}</span>`
|
|
20559
21394
|
|
|
20560
|
-
###
|
|
21395
|
+
### motion (24)
|
|
21396
|
+
|
|
21397
|
+
- **`motion-interactive`** — The transition for interactive chrome: colour, background, border, shadow, opacity, fill, stroke, scale and translate at the fast duration and standard easing. Also removes the mobile tap delay and the platform's grey tap flash. *Use for:* On anything clickable that changes on hover, focus, press or selection: a custom row, a tile, a link, an app-owned button. It is the `response` slot, and the only transition declarer such an element needs.
|
|
21398
|
+
- **Instead of** Tailwind's colour-only or all-property transition utilities with a hand-picked duration. Those ignore --motion-scale, skip the transform properties `motion-press` needs, and leave the 300ms tap delay on touch screens. sable-lint's `static-interactive` fires on a hover style with no recipe.
|
|
21399
|
+
- `<a href="/inbox" className="motion-interactive link-underline text-primary">Inbox</a>`
|
|
21400
|
+
- **`motion-press`** — A small tactile settle (scale 0.97) while the element is pressed. Skips disabled elements. Declares only the pressed state, not a transition. *Use for:* A pressable surface that is not a Sable Button (Button already has it): a tile, a card that acts as a button, an icon control.
|
|
21401
|
+
- **Instead of** An `active:` scale utility with its own transition, which fights `motion-interactive` for the transition property and keeps animating with motion off.
|
|
21402
|
+
- `<button type="button" className="motion-interactive motion-press sable-surface sable-frame sable-frame-r-lg sable-frame-p-4">Open</button>`
|
|
21403
|
+
- **`motion-fade`** — Opacity-only enter and exit, keyed to Base UI's `data-starting-style` / `data-ending-style`: fades in at the enter duration, out at the exit duration. *Use for:* Scrims, backdrops and overlays that should appear without moving. The element rests visible; the data attributes supply the from and to.
|
|
21404
|
+
- **Instead of** A keyframe fade or a conditional opacity class, which cannot animate the exit of an element Base UI is unmounting.
|
|
21405
|
+
- `<div className="motion-fade fixed inset-0 bg-overlay" />`
|
|
21406
|
+
- **`motion-pop`** — Enter with a slight scale from the anchor edge (spring easing) and exit faster without overshoot, keyed to `data-starting-style` / `data-ending-style`. Scales from Base UI's `--transform-origin`. *Use for:* Anchored or centred floating surfaces: popovers, menus, selects, tooltips, dialogs, command menus. Sable's own overlays already carry it.
|
|
21407
|
+
- **Instead of** A custom zoom keyframe, or a spring on exit, which draws the eye to what the user just dismissed.
|
|
21408
|
+
- `<div className="motion-pop sable-surface-popover bg-popover text-popover-foreground rounded-lg shadow-lg">…</div>`
|
|
21409
|
+
- **`motion-slide-up`** — Fade plus a 0.5rem rise on enter, reversed faster on exit, keyed to `data-starting-style` / `data-ending-style`. The suffix is the travel direction on enter. *Use for:* A surface arriving from below: a toast at the bottom of the screen, a bottom bar, a banner under its trigger.
|
|
21410
|
+
- **Instead of** A translate keyframe with a literal duration, which keeps moving under reduced motion.
|
|
21411
|
+
- `<div className="motion-slide-up sable-surface rounded-lg">Saved</div>`
|
|
21412
|
+
- **`motion-slide-down`** — Fade plus a 0.5rem drop on enter, reversed faster on exit, keyed to `data-starting-style` / `data-ending-style`. *Use for:* A surface arriving from above: a top toast, a dropdown panel that reads better sliding than scaling.
|
|
21413
|
+
- **Instead of** A translate keyframe with a literal duration.
|
|
21414
|
+
- `<div className="motion-slide-down sable-surface rounded-lg">Connected</div>`
|
|
21415
|
+
- **`motion-slide-left`** — Fade plus a 0.5rem travel leftward on enter (it starts offset to the right), reversed faster on exit. *Use for:* A surface arriving from the right edge: a side panel, a right-anchored toast.
|
|
21416
|
+
- **Instead of** A translate keyframe with a literal duration.
|
|
21417
|
+
- `<aside className="motion-slide-left sable-surface">…</aside>`
|
|
21418
|
+
- **`motion-slide-right`** — Fade plus a 0.5rem travel rightward on enter (it starts offset to the left), reversed faster on exit. *Use for:* A surface arriving from the left edge: a navigation drawer, a left-anchored toast.
|
|
21419
|
+
- **Instead of** A translate keyframe with a literal duration.
|
|
21420
|
+
- `<nav className="motion-slide-right sable-surface">…</nav>`
|
|
21421
|
+
- **`motion-disclosure`** — Height plus fade for a panel that opens and closes: carries the overflow, the transition and the content fade. The component sets the target height and zeroes it on the starting/ending style. *Use for:* A hand-built expanding region. Accordion and Collapsible already use it; reach for them first.
|
|
21422
|
+
- **Instead of** Animating max-height to a guessed number, or a show/hide with no transition. Height is the one sanctioned non-compositor property.
|
|
21423
|
+
- `<div className="motion-disclosure h-[var(--collapsible-panel-height)]">…</div>`
|
|
21424
|
+
- **`motion-icon-swap`** — Cross-fades two glyphs stacked in one cell when a control's icon changes with its state: the outgoing one shrinks and blurs out, the incoming one resolves in. The inactive glyph is the one marked `aria-hidden="true"` (or `data-active="false"`). *Use for:* Play/pause, mute/unmute, outline/fill, copy/check, menu/close: one control whose icon changes. Keep both icons mounted; the correct accessibility markup is also the animation hook.
|
|
21425
|
+
- **Instead of** Rendering one icon or the other (it snaps between two frames), or absolutely positioning the second icon, which collapses the wrapper. Under reduced motion the swap is instant and the glyph still changes.
|
|
21426
|
+
- `<span className="motion-icon-swap"><Icon name="play" aria-hidden={playing} /><Icon name="pause" aria-hidden={!playing} /></span>`
|
|
21427
|
+
- **`motion-spin`** — A continuous rotation for a spinner glyph, scaled by --motion-scale so reduced motion freezes it. *Use for:* A loading glyph that is not the Spinner component (use Spinner first), such as an icon inside a busy button.
|
|
21428
|
+
- **Instead of** Tailwind's built-in spin animation, which ignores --motion-scale. sable-lint's `builtin-animate` flags it.
|
|
21429
|
+
- `<Icon name="spinner" className="motion-spin" />`
|
|
21430
|
+
- **`motion-orbit`** — A slow continuous rotation (24s by default) for ambient line art. `--motion-orbit-duration` sets the speed and `--motion-orbit-direction` the direction; it rotates about the centre of any viewBox. *Use for:* The idle loop of an illustration or schematic after it draws on, usually with `motion-draw` on the same figure.
|
|
21431
|
+
- **Instead of** `motion-spin`, which is a loader's speed, or a hand-written keyframe that keeps turning under reduced motion.
|
|
21432
|
+
- `<svg className="motion-orbit [--motion-orbit-duration:40s]" viewBox="0 0 200 200">…</svg>`
|
|
21433
|
+
- **`motion-pulse`** — A soft looping opacity pulse, scaled by --motion-scale. *Use for:* A live or pending indicator: a recording dot, an unsaved marker, a pending status.
|
|
21434
|
+
- **Instead of** Tailwind's built-in pulse animation, which ignores --motion-scale. For a loading placeholder use Skeleton or `motion-shimmer`.
|
|
21435
|
+
- `<span className="motion-pulse size-2 rounded-full bg-destructive" />`
|
|
21436
|
+
- **`motion-shimmer`** — The skeleton sweep: one band of light that passes across every skeleton on screen in sequence, because the gradient is fixed to the viewport. Rests off-screen, so with motion off a skeleton is a calm block. *Use for:* A hand-built loading placeholder. The Skeleton component already uses it.
|
|
21437
|
+
- **Instead of** A pulse on each block, or a per-box gradient, where a short line and a tall card sweep at different speeds.
|
|
21438
|
+
- `<div className="motion-shimmer h-4 w-48 rounded-md bg-muted" />`
|
|
21439
|
+
- **`motion-text-shimmer`** — The sweep run through the glyphs instead of behind them, for a live status line. The gradient goes muted to foreground and back, so the text stays readable on every frame. *Use for:* Status text while work is happening: "Thinking…", "Searching", "Generating response".
|
|
21440
|
+
- **Instead of** `motion-shimmer` on text, which paints a band behind real words and reads as a skeleton. Named `motion-*`, not `text-*`, so tailwind-merge does not drop the colour class beside it.
|
|
21441
|
+
- `<span className="motion-text-shimmer text-body-sm">Thinking…</span>`
|
|
21442
|
+
- **`motion-dot-bounce`** — A looping vertical bounce for one dot of a typing or waiting indicator. Offset each dot with an animation delay. *Use for:* A three-dot typing indicator in a chat or assistant surface.
|
|
21443
|
+
- **Instead of** Tailwind's built-in bounce animation, which ignores --motion-scale.
|
|
21444
|
+
- `<span className="motion-dot-bounce size-1.5 rounded-full bg-muted-foreground" />`
|
|
21445
|
+
- **`motion-ping`** — A looping ring that expands and fades from a point, scaled by --motion-scale. *Use for:* Drawing attention to a live point: a new-activity dot, a location marker, a beacon behind a status dot.
|
|
21446
|
+
- **Instead of** Tailwind's built-in ping animation, which ignores --motion-scale.
|
|
21447
|
+
- `<span className="motion-ping absolute inset-0 rounded-full bg-success" />`
|
|
21448
|
+
- **`animate-fade-in`** — A one-shot fade in on mount at the base duration, decelerating, with `both` fill so there is no flash before it starts. *Use for:* Content that appears once when a route or panel mounts.
|
|
21449
|
+
- **Instead of** A keyframe with a literal duration. For content entering on scroll use Reveal (`motion-reveal`).
|
|
21450
|
+
- `<section className="animate-fade-in">…</section>`
|
|
21451
|
+
- **`animate-fade-up`** — A one-shot fade plus rise on mount at the slow duration, decelerating, with `both` fill. *Use for:* A page section or list arriving from below on mount.
|
|
21452
|
+
- **Instead of** A translate keyframe with a literal duration. For scroll-triggered entrances use Reveal.
|
|
21453
|
+
- `<div className="animate-fade-up">…</div>`
|
|
21454
|
+
- **`animate-fade-down`** — A one-shot fade plus drop on mount at the slow duration, decelerating, with `both` fill. *Use for:* Content arriving from above on mount, such as a notice under a header.
|
|
21455
|
+
- **Instead of** A translate keyframe with a literal duration.
|
|
21456
|
+
- `<div className="animate-fade-down">…</div>`
|
|
21457
|
+
- **`motion-stream-in`** — Each token of a live stream resolves in from a slight blur once, on mount. Knows nothing of the final length, so there is no stagger. *Use for:* Wrapping each arriving chunk of a streamed response. StreamingText already does it.
|
|
21458
|
+
- **Instead of** The reveal recipes, which stage text that is already there, or a typewriter effect.
|
|
21459
|
+
- `<span key={chunk.id} className="motion-stream-in">{chunk.text}</span>`
|
|
21460
|
+
- **`motion-caret`** — A caret that blinks only when idle and holds steady while `data-streaming="true"`, so the blink returning signals the stream has finished. *Use for:* The cursor at the end of streamed or typed text.
|
|
21461
|
+
- **Instead of** A caret that blinks while text is arriving, which sets two competing rhythms.
|
|
21462
|
+
- `<span className="motion-caret" data-streaming={streaming} aria-hidden="true">▍</span>`
|
|
21463
|
+
- **`motion-reveal`** — Fade plus a 0.75rem rise when `data-reveal` flips from "out" to "in". Out of view it rests hidden and nudged down; with motion off the content stays visible. *Use for:* Through the Reveal and TextReveal components, which set `data-reveal` when the element scrolls into view. Use the class directly only on an element whose `data-reveal` you set yourself.
|
|
21464
|
+
- **Instead of** An IntersectionObserver plus hand-written opacity classes.
|
|
21465
|
+
- `<Reveal><Card>…</Card></Reveal>`
|
|
21466
|
+
- **`motion-draw`** — Draws an SVG path on when an ancestor's `data-reveal` flips to "in". The path needs `pathLength="1"`. `--motion-draw-duration` and `--motion-draw-delay` (both scaled by --motion-scale) set its pace and stagger. *Use for:* Line art and schematics inside a Reveal. Stagger sibling paths with `--motion-draw-delay`.
|
|
21467
|
+
- **Instead of** A hand-written dash-offset animation with literal timing, which keeps animating, or keeps its delay, under reduced motion.
|
|
21468
|
+
- `<Reveal><svg viewBox="0 0 100 100"><path className="motion-draw" pathLength="1" d="M10 50 L90 50" /></svg></Reveal>`
|
|
21469
|
+
|
|
21470
|
+
### helper (5)
|
|
20561
21471
|
|
|
20562
21472
|
- **`hit-target`** — Grows an element's tap target to 44px with a transparent `::after`, without moving its layout by a pixel. `--hit-target-size` overrides the size. *Use for:* A deliberately small mark — a tag's remove ×, a tiny disclosure, a toast close — that must still meet the WCAG touch floor. Shrink it (`[--hit-target-size:32px]`) where targets sit side by side, or the later one steals the taps.
|
|
20563
21473
|
- **Instead of** Padding the glyph up (moves it off the row's grid and pushes siblings) or a bigger icon. Never on an `<input>` or other replaced element — pseudo-elements silently do nothing there; wrap it in a label.
|
|
20564
21474
|
- `<button className="hit-target text-muted-foreground" aria-label="Remove"><Icon name="xmark" /></button>`
|
|
20565
|
-
- **`
|
|
21475
|
+
- **`reveal-on-hover`** — Hides a row or tile action until its item is hovered or holds focus — on hover-capable devices only. Touch and no-hover devices always show it, and it fades with opacity, so it never leaves the tab order or the accessibility tree. The item is the nearest ancestor with `group` (or `data-reveal-scope`); an open popup keeps its trigger visible. `--reveal-offset` adds a short rise. *Use for:* Secondary per-item actions in a dense list or grid — a tile's play button, a row's overflow menu or visibility toggle — where showing every action at rest would turn the list into a wall of controls. Put `group` on the item (the row, the tile), not on the list, and the class on the action or its wrapper.
|
|
21476
|
+
- **Instead of** `opacity-0 group-hover:opacity-100` (hidden forever on a phone), `hidden group-hover:flex` (drops it from the tab order and the accessibility tree), or a hand-written `[@media(hover:hover)]:opacity-0` pair (snaps, and misses focus and open menus). Not for a primary action — that stays visible.
|
|
21477
|
+
- `<li className="group flex items-center gap-2"><span className="flex-1">Track</span><Button variant="ghost" size="icon-sm" aria-label="Like Track" className="reveal-on-hover"><Icon name="heart" /></Button></li>`
|
|
21478
|
+
- **`stretched-link`** — Makes one link cover its whole item with a transparent `::after`, so a tile or card is clickable everywhere while its other links and buttons stay separate controls. Focus draws the ring on the overlay, rounded by `--stretched-link-radius`. *Use for:* A card, tile or row that IS a link but also holds a second control — a subtitle link to the artist, a play button, a menu. Put it on the primary link (usually the title), `relative` on the item, and `relative z-10` on every other control.
|
|
21479
|
+
- **Instead of** Wrapping the item in an `<a>` or `Card render={<a />}` — invalid as soon as it nests another link or button, which browsers re-parent and screen readers read as one long link. Not with `hit-target` on the same element (both own `::after`).
|
|
21480
|
+
- `<article className="relative [--stretched-link-radius:var(--radius-lg)]"><a href="/albums/1" className="stretched-link">Low tide</a><a href="/artists/2" className="relative z-10">Juno Avery</a></article>`
|
|
21481
|
+
- **`link-underline`** — An underline drawn from the font's own metrics (position and thickness `from-font`) with ink skipping descenders, so it retunes when a theme swaps typeface. Also carries the link's focus-visible outline in `--ring`, so a plain `<a>` meets the focus floor without app CSS. *Use for:* Every plain text link — inline in running text or standalone ("Forgot password?"). Pair with `motion-interactive` for a colour change on hover.
|
|
20566
21482
|
- **Instead of** `underline underline-offset-4 decoration-2` — tuned to one face and wrong for the next theme's. Animating thickness or offset is not supported; a growing underline is a separate pseudo-element.
|
|
20567
21483
|
- `<a href="/terms" className="link-underline motion-interactive text-primary">terms</a>`
|
|
20568
21484
|
- **`checkerboard`** — The transparency checkerboard, painted from `--panel` and a derived darker step so it reads in both modes. `--checkerboard-size` (default 16px) and `--checkerboard-alt` tune it. *Use for:* Behind anything that can be translucent: a colour swatch with alpha, an image or artboard preview, an alpha slider track.
|