mirror of
https://github.com/aphrody-labs/aphrody-m3-react.git
synced 2026-10-13 20:07:22 +00:00
chore: migrate package sources from Aphrody
This commit is contained in:
1 parent
d30b18b099
commit
52c1293981
100 files changed
+5898
-2
No files matched your search
+979
@@ -0,0 +1,979 @@
|
||||
{
|
||||
"resolved": [
|
||||
{
|
||||
"tag": "md-accordion",
|
||||
"name": "MdAccordion",
|
||||
"file": "expansion/accordion.js",
|
||||
"className": "MdAccordion",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-assist-chip",
|
||||
"name": "MdAssistChip",
|
||||
"file": "chips/assist-chip.js",
|
||||
"className": "MdAssistChip",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-autocomplete",
|
||||
"name": "MdAutocomplete",
|
||||
"file": "autocomplete/autocomplete.js",
|
||||
"className": "MdAutocomplete",
|
||||
"events": {
|
||||
"onInput": "input",
|
||||
"onSelect": "autocomplete:select"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-badge",
|
||||
"name": "MdBadge",
|
||||
"file": "badge/badge.js",
|
||||
"className": "MdBadge",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-bottom-app-bar",
|
||||
"name": "MdBottomAppBar",
|
||||
"file": "appbar/bottom-app-bar.js",
|
||||
"className": "MdBottomAppBar",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-bottom-sheet",
|
||||
"name": "MdBottomSheet",
|
||||
"file": "sheet/bottom-sheet.js",
|
||||
"className": "MdBottomSheet",
|
||||
"events": {
|
||||
"onOpening": "bottom-sheet:opening",
|
||||
"onOpened": "bottom-sheet:opened",
|
||||
"onClosing": "bottom-sheet:closing",
|
||||
"onClosed": "bottom-sheet:closed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-branded-fab",
|
||||
"name": "MdBrandedFab",
|
||||
"file": "fab/branded-fab.js",
|
||||
"className": "MdBrandedFab",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-button",
|
||||
"name": "MdButton",
|
||||
"file": "labs/gb/components/button/md-button.js",
|
||||
"className": "Button",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-button-group",
|
||||
"name": "MdButtonGroup",
|
||||
"file": "buttongroup/button-group.js",
|
||||
"className": "MdButtonGroup",
|
||||
"events": {
|
||||
"onChange": "button-group:change"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-card",
|
||||
"name": "MdCard",
|
||||
"file": "card/card.js",
|
||||
"className": "MdCard",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-carousel",
|
||||
"name": "MdCarousel",
|
||||
"file": "carousel/carousel.js",
|
||||
"className": "MdCarousel",
|
||||
"events": {
|
||||
"onChange": "carousel-change"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-carousel-item",
|
||||
"name": "MdCarouselItem",
|
||||
"file": "carousel/carousel-item.js",
|
||||
"className": "MdCarouselItem",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-checkbox",
|
||||
"name": "MdCheckbox",
|
||||
"file": "checkbox/checkbox.js",
|
||||
"className": "MdCheckbox",
|
||||
"events": {
|
||||
"onChange": "change",
|
||||
"onInput": "input"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-chip-set",
|
||||
"name": "MdChipSet",
|
||||
"file": "chips/chip-set.js",
|
||||
"className": "MdChipSet",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-circular-progress",
|
||||
"name": "MdCircularProgress",
|
||||
"file": "progress/circular-progress.js",
|
||||
"className": "MdCircularProgress",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-date-picker",
|
||||
"name": "MdDatePicker",
|
||||
"file": "datepicker/date-picker.js",
|
||||
"className": "MdDatePicker",
|
||||
"events": {
|
||||
"onChange": "date-picker:change"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-dialog",
|
||||
"name": "MdDialog",
|
||||
"file": "dialog/dialog.js",
|
||||
"className": "MdDialog",
|
||||
"events": {
|
||||
"onOpen": "open",
|
||||
"onOpened": "opened",
|
||||
"onClose": "close",
|
||||
"onClosed": "closed",
|
||||
"onCancel": "cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-divider",
|
||||
"name": "MdDivider",
|
||||
"file": "divider/divider.js",
|
||||
"className": "MdDivider",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-elevated-button",
|
||||
"name": "MdElevatedButton",
|
||||
"file": "button/elevated-button.js",
|
||||
"className": "MdElevatedButton",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-elevated-card",
|
||||
"name": "MdElevatedCard",
|
||||
"file": "labs/card/elevated-card.js",
|
||||
"className": "MdElevatedCard",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-elevation",
|
||||
"name": "MdElevation",
|
||||
"file": "elevation/elevation.js",
|
||||
"className": "MdElevation",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-expansion-panel",
|
||||
"name": "MdExpansionPanel",
|
||||
"file": "expansion/expansion-panel.js",
|
||||
"className": "MdExpansionPanel",
|
||||
"events": {
|
||||
"onToggle": "expansion:toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-fab",
|
||||
"name": "MdFab",
|
||||
"file": "fab/fab.js",
|
||||
"className": "MdFab",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-fab-menu",
|
||||
"name": "MdFabMenu",
|
||||
"file": "fabmenu/fab-menu.js",
|
||||
"className": "MdFabMenu",
|
||||
"events": {
|
||||
"onOpen": "fab-menu:open",
|
||||
"onClose": "fab-menu:close"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-fab-menu-item",
|
||||
"name": "MdFabMenuItem",
|
||||
"file": "fabmenu/fab-menu-item.js",
|
||||
"className": "MdFabMenuItem",
|
||||
"events": {
|
||||
"onClick": "fab-menu-item:click"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-filled-button",
|
||||
"name": "MdFilledButton",
|
||||
"file": "button/filled-button.js",
|
||||
"className": "MdFilledButton",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-filled-card",
|
||||
"name": "MdFilledCard",
|
||||
"file": "labs/card/filled-card.js",
|
||||
"className": "MdFilledCard",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-filled-field",
|
||||
"name": "MdFilledField",
|
||||
"file": "field/filled-field.js",
|
||||
"className": "MdFilledField",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-filled-icon-button",
|
||||
"name": "MdFilledIconButton",
|
||||
"file": "iconbutton/filled-icon-button.js",
|
||||
"className": "MdFilledIconButton",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-filled-select",
|
||||
"name": "MdFilledSelect",
|
||||
"file": "select/filled-select.js",
|
||||
"className": "MdFilledSelect",
|
||||
"events": {
|
||||
"onChange": "change",
|
||||
"onInput": "input",
|
||||
"onOpening": "opening",
|
||||
"onOpened": "opened",
|
||||
"onClosing": "closing",
|
||||
"onClosed": "closed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-filled-text-field",
|
||||
"name": "MdFilledTextField",
|
||||
"file": "textfield/filled-text-field.js",
|
||||
"className": "MdFilledTextField",
|
||||
"events": {
|
||||
"onChange": "change",
|
||||
"onInput": "input"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-filled-tonal-button",
|
||||
"name": "MdFilledTonalButton",
|
||||
"file": "button/filled-tonal-button.js",
|
||||
"className": "MdFilledTonalButton",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-filled-tonal-icon-button",
|
||||
"name": "MdFilledTonalIconButton",
|
||||
"file": "iconbutton/filled-tonal-icon-button.js",
|
||||
"className": "MdFilledTonalIconButton",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-filter-chip",
|
||||
"name": "MdFilterChip",
|
||||
"file": "chips/filter-chip.js",
|
||||
"className": "MdFilterChip",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-focus-ring",
|
||||
"name": "MdFocusRing",
|
||||
"file": "focus/md-focus-ring.js",
|
||||
"className": "MdFocusRing",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-grid-list",
|
||||
"name": "MdGridList",
|
||||
"file": "gridlist/grid-list.js",
|
||||
"className": "MdGridList",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-grid-tile",
|
||||
"name": "MdGridTile",
|
||||
"file": "gridlist/grid-tile.js",
|
||||
"className": "MdGridTile",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-icon",
|
||||
"name": "MdIcon",
|
||||
"file": "icon/icon.js",
|
||||
"className": "MdIcon",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-icon-button",
|
||||
"name": "MdIconButton",
|
||||
"file": "iconbutton/icon-button.js",
|
||||
"className": "MdIconButton",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-input-chip",
|
||||
"name": "MdInputChip",
|
||||
"file": "chips/input-chip.js",
|
||||
"className": "MdInputChip",
|
||||
"events": {
|
||||
"onRemove": "remove",
|
||||
"onUpdateFocus": "update-focus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-item",
|
||||
"name": "MdItem",
|
||||
"file": "labs/item/item.js",
|
||||
"className": "MdItem",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-linear-progress",
|
||||
"name": "MdLinearProgress",
|
||||
"file": "progress/linear-progress.js",
|
||||
"className": "MdLinearProgress",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-list",
|
||||
"name": "MdList",
|
||||
"file": "list/list.js",
|
||||
"className": "MdList",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-list-detail",
|
||||
"name": "MdListDetail",
|
||||
"file": "layout/md-list-detail.js",
|
||||
"className": "MdListDetail",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-list-item",
|
||||
"name": "MdListItem",
|
||||
"file": "list/list-item.js",
|
||||
"className": "MdListItem",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-loading-indicator",
|
||||
"name": "MdLoadingIndicator",
|
||||
"file": "loadingindicator/loading-indicator.js",
|
||||
"className": "MdLoadingIndicator",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-menu",
|
||||
"name": "MdMenu",
|
||||
"file": "menu/menu.js",
|
||||
"className": "MdMenu",
|
||||
"events": {
|
||||
"onOpening": "opening",
|
||||
"onOpened": "opened",
|
||||
"onClosing": "closing",
|
||||
"onClosed": "closed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-menu-group",
|
||||
"name": "MdMenuGroup",
|
||||
"file": "labs/gb/components/menu/md-menu-group.js",
|
||||
"className": "MenuGroup",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-menu-item",
|
||||
"name": "MdMenuItem",
|
||||
"file": "menu/menu-item.js",
|
||||
"className": "MdMenuItem",
|
||||
"events": {
|
||||
"onCloseMenu": "close-menu"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-navigation-bar",
|
||||
"name": "MdNavigationBar",
|
||||
"file": "labs/navigationbar/navigation-bar.js",
|
||||
"className": "MdNavigationBar",
|
||||
"events": {
|
||||
"onActivated": "navigation-bar-activated"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-navigation-drawer",
|
||||
"name": "MdNavigationDrawer",
|
||||
"file": "labs/navigationdrawer/navigation-drawer.js",
|
||||
"className": "MdNavigationDrawer",
|
||||
"events": {
|
||||
"onNavigationDrawerChanged": "navigation-drawer-changed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-navigation-drawer-modal",
|
||||
"name": "MdNavigationDrawerModal",
|
||||
"file": "labs/navigationdrawer/navigation-drawer-modal.js",
|
||||
"className": "MdNavigationDrawerModal",
|
||||
"events": {
|
||||
"onNavigationDrawerChanged": "navigation-drawer-changed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-navigation-rail",
|
||||
"name": "MdNavigationRail",
|
||||
"file": "navigationrail/navigation-rail.js",
|
||||
"className": "MdNavigationRail",
|
||||
"events": {
|
||||
"onChange": "navigation-rail:change",
|
||||
"onItemActivate": "navigation-rail-item:activate"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-navigation-rail-item",
|
||||
"name": "MdNavigationRailItem",
|
||||
"file": "navigationrail/navigation-rail-item.js",
|
||||
"className": "MdNavigationRailItem",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-navigation-tab",
|
||||
"name": "MdNavigationTab",
|
||||
"file": "labs/navigationtab/navigation-tab.js",
|
||||
"className": "MdNavigationTab",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-outlined-button",
|
||||
"name": "MdOutlinedButton",
|
||||
"file": "button/outlined-button.js",
|
||||
"className": "MdOutlinedButton",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-outlined-card",
|
||||
"name": "MdOutlinedCard",
|
||||
"file": "labs/card/outlined-card.js",
|
||||
"className": "MdOutlinedCard",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-outlined-field",
|
||||
"name": "MdOutlinedField",
|
||||
"file": "field/outlined-field.js",
|
||||
"className": "MdOutlinedField",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-outlined-icon-button",
|
||||
"name": "MdOutlinedIconButton",
|
||||
"file": "iconbutton/outlined-icon-button.js",
|
||||
"className": "MdOutlinedIconButton",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-outlined-segmented-button",
|
||||
"name": "MdOutlinedSegmentedButton",
|
||||
"file": "labs/segmentedbutton/outlined-segmented-button.js",
|
||||
"className": "MdOutlinedSegmentedButton",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-outlined-segmented-button-set",
|
||||
"name": "MdOutlinedSegmentedButtonSet",
|
||||
"file": "labs/segmentedbuttonset/outlined-segmented-button-set.js",
|
||||
"className": "MdOutlinedSegmentedButtonSet",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-outlined-select",
|
||||
"name": "MdOutlinedSelect",
|
||||
"file": "select/outlined-select.js",
|
||||
"className": "MdOutlinedSelect",
|
||||
"events": {
|
||||
"onChange": "change",
|
||||
"onInput": "input",
|
||||
"onOpening": "opening",
|
||||
"onOpened": "opened",
|
||||
"onClosing": "closing",
|
||||
"onClosed": "closed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-outlined-text-field",
|
||||
"name": "MdOutlinedTextField",
|
||||
"file": "textfield/outlined-text-field.js",
|
||||
"className": "MdOutlinedTextField",
|
||||
"events": {
|
||||
"onChange": "change",
|
||||
"onInput": "input"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-paginator",
|
||||
"name": "MdPaginator",
|
||||
"file": "paginator/paginator.js",
|
||||
"className": "MdPaginator",
|
||||
"events": {
|
||||
"onPage": "paginator:page"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-pane",
|
||||
"name": "MdPane",
|
||||
"file": "layout/md-pane.js",
|
||||
"className": "MdPane",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-primary-tab",
|
||||
"name": "MdPrimaryTab",
|
||||
"file": "tabs/primary-tab.js",
|
||||
"className": "MdPrimaryTab",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-radio",
|
||||
"name": "MdRadio",
|
||||
"file": "radio/radio.js",
|
||||
"className": "MdRadio",
|
||||
"events": {
|
||||
"onChange": "change",
|
||||
"onInput": "input"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-ripple",
|
||||
"name": "MdRipple",
|
||||
"file": "ripple/ripple.js",
|
||||
"className": "MdRipple",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-scaffold",
|
||||
"name": "MdScaffold",
|
||||
"file": "layout/md-scaffold.js",
|
||||
"className": "MdScaffold",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-search-bar",
|
||||
"name": "MdSearchBar",
|
||||
"file": "search/search-bar.js",
|
||||
"className": "MdSearchBar",
|
||||
"events": {
|
||||
"onInput": "input",
|
||||
"onSearch": "search",
|
||||
"onSearchOpen": "search:open",
|
||||
"onSearchClose": "search:close"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-secondary-tab",
|
||||
"name": "MdSecondaryTab",
|
||||
"file": "tabs/secondary-tab.js",
|
||||
"className": "MdSecondaryTab",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-select-option",
|
||||
"name": "MdSelectOption",
|
||||
"file": "select/select-option.js",
|
||||
"className": "MdSelectOption",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-side-sheet",
|
||||
"name": "MdSideSheet",
|
||||
"file": "sheet/side-sheet.js",
|
||||
"className": "MdSideSheet",
|
||||
"events": {
|
||||
"onOpening": "side-sheet:opening",
|
||||
"onOpened": "side-sheet:opened",
|
||||
"onClosing": "side-sheet:closing",
|
||||
"onClosed": "side-sheet:closed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-slider",
|
||||
"name": "MdSlider",
|
||||
"file": "slider/slider.js",
|
||||
"className": "MdSlider",
|
||||
"events": {
|
||||
"onChange": "change",
|
||||
"onInput": "input"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-snackbar",
|
||||
"name": "MdSnackbar",
|
||||
"file": "snackbar/snackbar.js",
|
||||
"className": "MdSnackbar",
|
||||
"events": {
|
||||
"onOpening": "opening",
|
||||
"onOpened": "opened",
|
||||
"onClosing": "closing",
|
||||
"onClosed": "closed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-split-button",
|
||||
"name": "MdSplitButton",
|
||||
"file": "labs/gb/components/splitbutton/md-split-button.js",
|
||||
"className": "SplitButton",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-step",
|
||||
"name": "MdStep",
|
||||
"file": "stepper/step.js",
|
||||
"className": "MdStep",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-stepper",
|
||||
"name": "MdStepper",
|
||||
"file": "stepper/stepper.js",
|
||||
"className": "MdStepper",
|
||||
"events": {
|
||||
"onChange": "stepper:change"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-sub-menu",
|
||||
"name": "MdSubMenu",
|
||||
"file": "menu/sub-menu.js",
|
||||
"className": "MdSubMenu",
|
||||
"events": {
|
||||
"onOpening": "opening",
|
||||
"onOpened": "opened",
|
||||
"onClosing": "closing",
|
||||
"onClosed": "closed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-suggestion-chip",
|
||||
"name": "MdSuggestionChip",
|
||||
"file": "chips/suggestion-chip.js",
|
||||
"className": "MdSuggestionChip",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-supporting-pane",
|
||||
"name": "MdSupportingPane",
|
||||
"file": "layout/md-supporting-pane.js",
|
||||
"className": "MdSupportingPane",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-switch",
|
||||
"name": "MdSwitch",
|
||||
"file": "switch/switch.js",
|
||||
"className": "MdSwitch",
|
||||
"events": {
|
||||
"onChange": "change",
|
||||
"onInput": "input"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-table",
|
||||
"name": "MdTable",
|
||||
"file": "table/table.js",
|
||||
"className": "MdTable",
|
||||
"events": {
|
||||
"onSort": "table:sort",
|
||||
"onSelectionChange": "table:selection-change"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-tabs",
|
||||
"name": "MdTabs",
|
||||
"file": "tabs/tabs.js",
|
||||
"className": "MdTabs",
|
||||
"events": {
|
||||
"onChange": "change"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-text-button",
|
||||
"name": "MdTextButton",
|
||||
"file": "button/text-button.js",
|
||||
"className": "MdTextButton",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-time-picker",
|
||||
"name": "MdTimePicker",
|
||||
"file": "timepicker/time-picker.js",
|
||||
"className": "MdTimePicker",
|
||||
"events": {
|
||||
"onChange": "time-picker:change"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-toolbar",
|
||||
"name": "MdToolbar",
|
||||
"file": "toolbar/md-toolbar.js",
|
||||
"className": "MdToolbar",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-tooltip",
|
||||
"name": "MdTooltip",
|
||||
"file": "tooltip/tooltip.js",
|
||||
"className": "MdTooltip",
|
||||
"events": {
|
||||
"onShow": "tooltip-show",
|
||||
"onHide": "tooltip-hide"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-top-app-bar",
|
||||
"name": "MdTopAppBar",
|
||||
"file": "appbar/top-app-bar.js",
|
||||
"className": "MdTopAppBar",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-tree",
|
||||
"name": "MdTree",
|
||||
"file": "tree/tree.js",
|
||||
"className": "MdTree",
|
||||
"events": {
|
||||
"onSelect": "tree:select"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-tree-item",
|
||||
"name": "MdTreeItem",
|
||||
"file": "tree/tree-item.js",
|
||||
"className": "MdTreeItem",
|
||||
"events": {
|
||||
"onToggle": "tree-item:toggle",
|
||||
"onActivate": "tree-item:activate"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-virtual-scroller",
|
||||
"name": "MdVirtualScroller",
|
||||
"file": "virtualscroll/virtual-scroller.js",
|
||||
"className": "MdVirtualScroller",
|
||||
"events": {
|
||||
"onRange": "virtual-scroll:range"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-webgpu-canvas",
|
||||
"name": "MdWebgpuCanvas",
|
||||
"file": "effects/webgpu-canvas.js",
|
||||
"className": "MdWebgpuCanvas",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-avatar",
|
||||
"name": "MdAvatar",
|
||||
"file": "avatar/avatar.js",
|
||||
"className": "MdAvatar",
|
||||
"events": {
|
||||
"onError": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-avatar-group",
|
||||
"name": "MdAvatarGroup",
|
||||
"file": "avatar/avatar-group.js",
|
||||
"className": "MdAvatarGroup",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-alert",
|
||||
"name": "MdAlert",
|
||||
"file": "alert/alert.js",
|
||||
"className": "MdAlert",
|
||||
"events": {
|
||||
"onClose": "close"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-skeleton",
|
||||
"name": "MdSkeleton",
|
||||
"file": "skeleton/skeleton.js",
|
||||
"className": "MdSkeleton",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-breadcrumbs",
|
||||
"name": "MdBreadcrumbs",
|
||||
"file": "breadcrumbs/breadcrumbs.js",
|
||||
"className": "MdBreadcrumbs",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-link",
|
||||
"name": "MdLink",
|
||||
"file": "link/link.js",
|
||||
"className": "MdLink",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-surface",
|
||||
"name": "MdSurface",
|
||||
"file": "surface/surface.js",
|
||||
"className": "MdSurface",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-backdrop",
|
||||
"name": "MdBackdrop",
|
||||
"file": "backdrop/backdrop.js",
|
||||
"className": "MdBackdrop",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-popover",
|
||||
"name": "MdPopover",
|
||||
"file": "popover/popover.js",
|
||||
"className": "MdPopover",
|
||||
"events": {
|
||||
"onOpened": "opened",
|
||||
"onClosed": "closed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-rating",
|
||||
"name": "MdRating",
|
||||
"file": "rating/rating.js",
|
||||
"className": "MdRating",
|
||||
"events": {
|
||||
"onChange": "change",
|
||||
"onInput": "input"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-line-chart",
|
||||
"name": "MdLineChart",
|
||||
"file": "charts/line-chart.js",
|
||||
"className": "MdLineChart",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-bar-chart",
|
||||
"name": "MdBarChart",
|
||||
"file": "charts/bar-chart.js",
|
||||
"className": "MdBarChart",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-pie-chart",
|
||||
"name": "MdPieChart",
|
||||
"file": "charts/pie-chart.js",
|
||||
"className": "MdPieChart",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-area-chart",
|
||||
"name": "MdAreaChart",
|
||||
"file": "charts/area-chart.js",
|
||||
"className": "MdAreaChart",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-sparkline",
|
||||
"name": "MdSparkline",
|
||||
"file": "charts/sparkline.js",
|
||||
"className": "MdSparkline",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-gauge",
|
||||
"name": "MdGauge",
|
||||
"file": "charts/gauge.js",
|
||||
"className": "MdGauge",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-date-range-picker",
|
||||
"name": "MdDateRangePicker",
|
||||
"file": "datepicker/date-range-picker.js",
|
||||
"className": "MdDateRangePicker",
|
||||
"events": {
|
||||
"onInput": "input",
|
||||
"onChange": "change",
|
||||
"onDateRangeChange": "date-range-picker:change"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-mobile-stepper",
|
||||
"name": "MdMobileStepper",
|
||||
"file": "mobilestepper/mobile-stepper.js",
|
||||
"className": "MdMobileStepper",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-scatter-chart",
|
||||
"name": "MdScatterChart",
|
||||
"file": "charts/scatter-chart.js",
|
||||
"className": "MdScatterChart",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-radar-chart",
|
||||
"name": "MdRadarChart",
|
||||
"file": "charts/radar-chart.js",
|
||||
"className": "MdRadarChart",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-date-time-picker",
|
||||
"name": "MdDateTimePicker",
|
||||
"file": "datepicker/date-time-picker.js",
|
||||
"className": "MdDateTimePicker",
|
||||
"events": {
|
||||
"onInput": "input",
|
||||
"onChange": "change",
|
||||
"onDateTimeChange": "date-time-picker:change"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-scheduler",
|
||||
"name": "MdScheduler",
|
||||
"file": "scheduler/scheduler.js",
|
||||
"className": "MdScheduler",
|
||||
"events": {
|
||||
"onNavigate": "scheduler:navigate",
|
||||
"onSelect": "scheduler:select",
|
||||
"onEventClick": "scheduler:event-click"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "md-type",
|
||||
"name": "MdType",
|
||||
"file": "typography/md-type.js",
|
||||
"className": "MdType",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-3d-canvas",
|
||||
"name": "Md3dCanvas",
|
||||
"file": "canvas3d/canvas-3d.js",
|
||||
"className": "M3Canvas3D",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-3d-globe",
|
||||
"name": "Md3dGlobe",
|
||||
"file": "globe3d/globe-3d.js",
|
||||
"className": "M3Globe3D",
|
||||
"events": {}
|
||||
},
|
||||
{
|
||||
"tag": "md-3d-card",
|
||||
"name": "Md3dCard",
|
||||
"file": "card3d/card-3d.js",
|
||||
"className": "M3Card3D",
|
||||
"events": {}
|
||||
}
|
||||
],
|
||||
"unresolved": []
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
# Intégration frameworks (SSR / RSC) — `@aphrody/m3-react`
|
||||
|
||||
Guide d'intégration des wrappers Material 3 dans un framework React à rendu
|
||||
serveur — **Next.js App Router**, Remix, ou tout setup RSC / SSR / hydration.
|
||||
Les composants sont des **web components Lit**, donc **client-only** : ils ne
|
||||
rendent rien côté serveur (le shadow DOM n'existe qu'au navigateur) et
|
||||
s'« upgradent » après l'hydration. Les exemples ci-dessous utilisent Next.js App
|
||||
Router (le cas le plus contraint), mais les principes valent pour tout SSR.
|
||||
|
||||
L'exemple du monorepo ([`examples/showcase/`](../../docs/products/examples/showcase)) est
|
||||
bun-natif (pas Next) ; ce guide couvre les spécificités **Next.js App Router**
|
||||
côté consommateur. Le package se consomme tel quel dans une app Next.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ce que le package livre
|
||||
|
||||
Le package est **pré-buildé** dans `dist/` (`bun run build`, via `tsup` + `tsc`) :
|
||||
|
||||
- **ESM** (`dist/index.js`, `dist/transitions/index.js`) — pas de `.ts` brut, donc
|
||||
consommable depuis `node_modules` **sans `transpilePackages`**.
|
||||
- Chaque chunk `.js` porte la directive **`'use client';`** en tête → Next RSC
|
||||
traite tout le package comme des **client components**, sans erreur
|
||||
« You're importing a component that needs … ».
|
||||
- **Types** (`dist/**/*.d.ts`).
|
||||
- `react`, `react-dom` et `@material/web` sont **externes** (peer deps / résolus
|
||||
chez le consommateur). Les imports profonds `@material/web/<dir>/<el>.js` sont
|
||||
conservés tels quels : c'est l'import du wrapper qui déclenche le
|
||||
`customElements.define()` (effet de bord) côté client.
|
||||
|
||||
`package.json` pointe `main`/`module`/`types`/`exports` sur `dist/`, déclare
|
||||
`sideEffects` (les imports d'éléments enregistrent les custom elements — ne pas
|
||||
tree-shaker) et `files: ["dist"]`.
|
||||
|
||||
```jsonc
|
||||
"exports": {
|
||||
".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
||||
"./transitions":{ "types": "./dist/transitions/index.d.ts", "default": "./dist/transitions/index.js" }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Boundaries client / serveur
|
||||
|
||||
Règle : **tout ce qui touche un `Md…` vit dans un fichier `'use client'`.**
|
||||
|
||||
- `layout.tsx` / `page.tsx` restent des **Server Components** (pas de directive) ;
|
||||
ils importent le CSS de thème et rendent un **îlot client**.
|
||||
- L'îlot client (ex. `demo.tsx`) porte `'use client'`, importe les wrappers,
|
||||
gère state/refs/handlers.
|
||||
|
||||
```tsx
|
||||
// app/page.tsx — Server Component
|
||||
import { Demo } from "./demo";
|
||||
export default function Home() {
|
||||
return (
|
||||
<main>
|
||||
<Demo />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// app/demo.tsx — Client island
|
||||
"use client";
|
||||
import {
|
||||
MdFilledButton,
|
||||
MdOutlinedTextField,
|
||||
MdDialog,
|
||||
MdLineChart,
|
||||
MdTabs,
|
||||
MdPrimaryTab,
|
||||
} from "@aphrody/m3-react";
|
||||
import { Fade } from "@aphrody/m3-react/transitions";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
export function Demo() {
|
||||
const dialogRef = useRef<React.ComponentRef<typeof MdDialog>>(null);
|
||||
const [name, setName] = useState("");
|
||||
return (
|
||||
<>
|
||||
<MdFilledButton onClick={() => dialogRef.current?.show()}>Ouvrir</MdFilledButton>
|
||||
<MdOutlinedTextField
|
||||
label="Nom"
|
||||
value={name}
|
||||
onInput={(e) => setName((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
<MdTabs>
|
||||
<MdPrimaryTab>A</MdPrimaryTab>
|
||||
<MdPrimaryTab>B</MdPrimaryTab>
|
||||
</MdTabs>
|
||||
<MdLineChart
|
||||
smooth
|
||||
showMarkers
|
||||
categories={["Jan", "Fév", "Mar"]}
|
||||
series={[{ label: "CA", data: [12, 19, 8] }]}
|
||||
/>
|
||||
<MdDialog ref={dialogRef}>
|
||||
<div slot="headline">Titre</div>
|
||||
<div slot="content">Ouvert via ref impérative.</div>
|
||||
<div slot="actions">
|
||||
<MdFilledButton onClick={() => dialogRef.current?.close()}>Fermer</MdFilledButton>
|
||||
</div>
|
||||
</MdDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Refs impératives (dialog, etc.)
|
||||
|
||||
Le `ref` d'un wrapper pointe sur **l'instance du custom element** (avec ses
|
||||
méthodes `show()` / `close()` / `returnValue`…). Pour typer le ref sans importer
|
||||
les sources de `@material/web`, dérivez-le du composant :
|
||||
|
||||
```tsx
|
||||
const dialogRef = useRef<React.ComponentRef<typeof MdDialog>>(null);
|
||||
dialogRef.current?.show();
|
||||
```
|
||||
|
||||
### Propriétés objet (charts, etc.)
|
||||
|
||||
`@lit/react` passe les props objet/tableau comme **propriétés** de l'élément
|
||||
(pas des attributs). `<MdLineChart series={[…]} categories={[…]} />` règle donc
|
||||
`.series` / `.categories` directement — pas de JSON à sérialiser.
|
||||
|
||||
---
|
||||
|
||||
## 3. SSR / hydration + parade FOUC
|
||||
|
||||
Côté serveur, un `<md-filled-button>` se rend en élément non-`:defined` (pas de
|
||||
shadow DOM). **Aucun crash SSR** : la page se pré-rend en statique (vérifié,
|
||||
`next build` → route `/` en `○ Static`). Pas de hydration mismatch tant que le
|
||||
markup serveur = markup client (c'est le cas, le wrapper rend juste la balise).
|
||||
|
||||
Pour éviter le **flash of unstyled content** (l'élément brut visible avant
|
||||
l'upgrade), masquez les éléments tant qu'ils ne sont pas définis :
|
||||
|
||||
```css
|
||||
/* app/theme.css — FOUC parade */
|
||||
:not(:defined) {
|
||||
visibility: hidden;
|
||||
}
|
||||
```
|
||||
|
||||
Optionnellement, révélez après upgrade complet via `customElements.whenDefined`
|
||||
dans un `useEffect` (cf. `examples/showcase`).
|
||||
|
||||
### Tokens de thème
|
||||
|
||||
Définissez les `--md-sys-*` au niveau `:root` (ou `body`) dans un CSS importé par
|
||||
le layout serveur :
|
||||
|
||||
```css
|
||||
:root {
|
||||
--md-sys-color-primary: #6750a4;
|
||||
--md-sys-color-on-primary: #fff;
|
||||
/* … générés depuis @aphrody/m3-tokens en prod … */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Fallback : consommer les **sources** (`transpilePackages`)
|
||||
|
||||
Le dist pré-buildé fonctionne sans config. Si vous préférez consommer les
|
||||
**sources TS** du package (ou épingler `@material/web` sur ses sources `.ts`,
|
||||
p. ex. en monorepo lié par symlink), activez `transpilePackages` pour que Next
|
||||
les compile :
|
||||
|
||||
```ts
|
||||
// next.config.ts
|
||||
import type { NextConfig } from "next";
|
||||
const nextConfig: NextConfig = {
|
||||
transpilePackages: ["@aphrody/m3-react", "@material/web"],
|
||||
};
|
||||
export default nextConfig;
|
||||
```
|
||||
|
||||
> En monorepo, le lien workspace expose aussi les `.ts` de `@material/web` à côté
|
||||
> des `.js`/`.d.ts`. Le `moduleResolution: "bundler"` de TS résout alors `…/x.js`
|
||||
> vers la **source `.ts`** (décorateurs → erreur de typecheck). Deux parades :
|
||||
> soit `transpilePackages` ci-dessus, soit ne typer que les artefacts publiés
|
||||
> (l'app d'exemple redirige `@material/web/*` vers une copie `.js`+`.d.ts`-only
|
||||
> via `compilerOptions.paths`, exactement ce qu'un `npm install` produirait).
|
||||
|
||||
---
|
||||
|
||||
## 5. Checklist build
|
||||
|
||||
```bash
|
||||
# 1. builder la lib + le package react (turbo)
|
||||
bun run build # à la racine du monorepo → @material/web .js + dist react
|
||||
|
||||
# 2. dans VOTRE app Next.js : importer les wrappers (déjà marqués 'use client')
|
||||
# import { MdFilledButton } from "@aphrody/m3-react";
|
||||
# next build → vert, sans transpilePackages
|
||||
```
|
||||
|
||||
> Le monorepo n'embarque plus d'exemple Next.js dédié : l'exemple de référence
|
||||
> est `examples/showcase` (bun-natif). Ce guide reste valable pour intégrer les
|
||||
> wrappers dans **votre** app Next.js App Router.
|
||||
@@ -1,2 +1,327 @@
|
||||
# aphrody-m3-react
|
||||
Aphrody package @aphrody/m3-react
|
||||
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||
|
||||
# `@aphrody/m3-react`
|
||||
|
||||
Wrappers React **par élément `md-*`** du fork aphrody `@material/web` (v2.4.1, 119 éléments),
|
||||
générés avec [`@lit/react`](https://www.npmjs.com/package/@lit/react) `createComponent`.
|
||||
|
||||
Ce package réalise le plan que le fork prévoit (`apps/m3-react`) mais ne livre pas sur disque
|
||||
(cf. `migration/00-CONVENTIONS.md` §0). Il expose un composant React typé pour chacun des
|
||||
**119 tags** listés dans `migration/scripts/md-elements.txt`.
|
||||
|
||||
- **Statut compilation : 119 / 119 wrappers générés, `tsc --noEmit` → 0 erreur.**
|
||||
- 1 wrapper React par élément `md-*`, nom = PascalCase du tag (CONVENTIONS §2).
|
||||
- Events natifs réels mappés par élément (vérifiés dans le code Lit, voir plus bas).
|
||||
|
||||
## Install
|
||||
|
||||
> **bun uniquement** (jamais npm/pnpm — CONVENTIONS §7.1).
|
||||
|
||||
Le package est déjà installé dans ce dossier (`migration/wrappers/`) :
|
||||
|
||||
```bash
|
||||
cd migration/wrappers
|
||||
bun install # @lit/react + @material/web (file:../../material-web) + react/react-dom
|
||||
bun run typecheck # tsc --noEmit → 0 erreur
|
||||
bun run generate # régénère wrappers/ + index.ts depuis md-elements.txt
|
||||
```
|
||||
|
||||
Dans un projet consommateur :
|
||||
|
||||
```jsonc
|
||||
// package.json
|
||||
{
|
||||
"dependencies": {
|
||||
"@aphrody/m3-react": "file:../migration/wrappers",
|
||||
"react": "^19",
|
||||
"react-dom": "^19",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Importer un wrapper enregistre l'élément custom correspondant **comme effet de bord**
|
||||
(le wrapper importe le module `@material/web/...` qui appelle `customElements.define`).
|
||||
Aucun import side-effect supplémentaire n'est nécessaire.
|
||||
|
||||
```tsx
|
||||
import { MdFilledButton, MdOutlinedTextField, MdCheckbox } from "@aphrody/m3-react";
|
||||
|
||||
function LoginForm() {
|
||||
const [email, setEmail] = React.useState("");
|
||||
const [accept, setAccept] = React.useState(false);
|
||||
|
||||
return (
|
||||
<form>
|
||||
{/* controlled : la signature React change — e.target.value, PAS (e, value) */}
|
||||
<MdOutlinedTextField
|
||||
label="Email"
|
||||
value={email}
|
||||
onInput={(e) => setEmail((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
|
||||
<label>
|
||||
<MdCheckbox
|
||||
checked={accept}
|
||||
onChange={(e) => setAccept((e.target as HTMLInputElement).checked)}
|
||||
/>
|
||||
J'accepte
|
||||
</label>
|
||||
|
||||
<MdFilledButton onClick={() => console.log({ email, accept })}>Valider</MdFilledButton>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Slots & icônes (CONVENTIONS §4)
|
||||
|
||||
Les sous-composants MUI (`DialogTitle`, `CardHeader`, `startIcon`…) deviennent du contenu slotté :
|
||||
|
||||
```tsx
|
||||
import { MdDialog, MdFilledButton, MdTextButton, MdIcon } from "@aphrody/m3-react";
|
||||
|
||||
<MdDialog open onClosed={(e) => console.log((e.target as any).returnValue)}>
|
||||
<div slot="headline">Supprimer ?</div>
|
||||
<form slot="content" method="dialog">
|
||||
Cette action est irréversible.
|
||||
</form>
|
||||
<div slot="actions">
|
||||
<MdTextButton value="cancel">Annuler</MdTextButton>
|
||||
<MdFilledButton value="delete">
|
||||
<MdIcon slot="icon">delete</MdIcon>
|
||||
Supprimer
|
||||
</MdFilledButton>
|
||||
</div>
|
||||
</MdDialog>;
|
||||
```
|
||||
|
||||
> **`MdIcon` exige la police Material Symbols.** La lib ne la charge pas. Une fois,
|
||||
> au démarrage de l'app :
|
||||
>
|
||||
> ```ts
|
||||
> import { ensureMaterialSymbols } from "@aphrody/material-web/icon/material-symbols.js";
|
||||
> ensureMaterialSymbols(); // ou { iconNames: ["delete", "search", …] } pour subseter
|
||||
> ```
|
||||
>
|
||||
> Le texte enfant est le **nom du glyphe** (snake_case Material Symbols). Les axes
|
||||
> variables se pilotent par token : `--md-icon-fill` (0..1), `--md-icon-wght`
|
||||
> (100..700), `--md-icon-grad`, `--md-icon-opsz`. Migration depuis
|
||||
> `@mui/icons-material` : codemod `migration/codemods/transforms/icons.ts` (96 % auto).
|
||||
|
||||
## Architecture
|
||||
|
||||
- `index.ts` — réexporte les 119 wrappers (un `export *` par catégorie).
|
||||
- `wrappers/<categorie>.ts` — 45 fichiers groupés par dossier source de `material-web/`
|
||||
(`button.ts`, `select.ts`, `labs.ts`, …). Chaque export :
|
||||
|
||||
```ts
|
||||
export const MdFilledButton = createComponent({
|
||||
react: React,
|
||||
tagName: "md-filled-button",
|
||||
elementClass: MdFilledButtonElement, // importé depuis @material/web/button/filled-button.js
|
||||
events: {}, // events natifs réels (voir tableau)
|
||||
});
|
||||
```
|
||||
|
||||
- `generate.ts` — générateur **bun** : lit `md-elements.txt`, scanne `material-web/` pour
|
||||
résoudre chaque tag → `{fichier d'entrée, classe exportée}` via le motif
|
||||
`@customElement('md-x') export class Foo`, applique la table d'events vérifiés, et émet
|
||||
les fichiers. Préfère le chemin canonique (bundlé par `all.ts` / `aphrody-components.ts` /
|
||||
`aphrody-labs.ts`), puis non-`labs/gb`, sinon `labs/gb` (pour les tags `md-button`,
|
||||
`md-card`, `md-menu-group`, `md-split-button` qui n'existent que là).
|
||||
- `material-web-shims.d.ts` — voir « Détail compilation ».
|
||||
|
||||
## Événements mappés (vérifiés dans le code Lit)
|
||||
|
||||
Noms d'events extraits par grep de `@fires` / `new (Custom)?Event('…')` dans
|
||||
`material-web/<dir>/internal/*.ts`. Les composants **upstream** utilisent des noms simples
|
||||
(`input`, `change`, `opening`…) ; les composants **fork** namespacent les leurs
|
||||
(`paginator:page`, `table:sort`, `side-sheet:opening`, `tree:select`…).
|
||||
|
||||
| Élément | Events React → event natif |
|
||||
| --------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| `MdCheckbox`, `MdRadio`, `MdSwitch`, `MdSlider`, `Md*TextField` | `onChange:change`, `onInput:input` |
|
||||
| `MdFilledSelect`, `MdOutlinedSelect` | `+onOpening/onOpened/onClosing/onClosed` |
|
||||
| `MdDialog` | `onOpen:open`, `onOpened`, `onClose:close`, `onClosed`, `onCancel:cancel` |
|
||||
| `MdMenu`, `MdSubMenu` | `onOpening/onOpened/onClosing/onClosed` |
|
||||
| `MdMenuItem` | `onCloseMenu:close-menu` |
|
||||
| `MdSnackbar` | `onOpening/onOpened/onClosing/onClosed` (bare) |
|
||||
| `MdBottomSheet` / `MdSideSheet` | `onOpening:bottom-sheet:opening` … (fork, namespacés) |
|
||||
| `MdSearchBar` | `onInput`, `onSearch:search`, `onSearchOpen/onSearchClose` |
|
||||
| `MdAutocomplete` | `onInput`, `onSelect:autocomplete:select` |
|
||||
| `MdNavigationBar` | `onActivated:navigation-bar-activated` |
|
||||
| `MdNavigationRail` | `onChange:navigation-rail:change`, `onItemActivate` |
|
||||
| `MdNavigationDrawer(Modal)` | `onNavigationDrawerChanged:navigation-drawer-changed` |
|
||||
| `MdInputChip` | `onRemove:remove`, `onUpdateFocus:update-focus` |
|
||||
| `MdButtonGroup` | `onChange:button-group:change` |
|
||||
| `MdFabMenu` / `MdFabMenuItem` | `onOpen/onClose:fab-menu:*`, `onClick:fab-menu-item:click` |
|
||||
| `MdExpansionPanel` | `onToggle:expansion:toggle` |
|
||||
| `MdStepper` | `onChange:stepper:change` |
|
||||
| `MdDatePicker` / `MdTimePicker` | `onChange:date-picker:change` / `time-picker:change` |
|
||||
| `MdTable` | `onSort:table:sort`, `onSelectionChange:table:selection-change` |
|
||||
| `MdPaginator` | `onPage:paginator:page` |
|
||||
| `MdTree` / `MdTreeItem` | `onSelect:tree:select` / `onToggle:tree-item:toggle`, `onActivate` |
|
||||
| `MdVirtualScroller` | `onRange:virtual-scroll:range` |
|
||||
| `MdTooltip` | `onShow:tooltip-show`, `onHide:tooltip-hide` |
|
||||
| `MdTabs` | `onChange:change` |
|
||||
|
||||
Les éléments purement présentationnels (`md-icon`, `md-divider`, `md-elevation`, `md-ripple`,
|
||||
`md-focus-ring`, chips assist/filter/suggestion, cards, fab, boutons, `md-item`, `md-toolbar`,
|
||||
`md-carousel`, `md-accordion`, `md-loading-indicator`, panes/scaffold…) n'émettent pas d'event
|
||||
documenté : `events: {}`.
|
||||
|
||||
## Détail compilation
|
||||
|
||||
`tsc --noEmit` compile **toute la chaîne** (les wrappers _et_ les sources `.ts` de
|
||||
`material-web/` qu'ils importent). Trois ajustements ont été nécessaires côté `tsconfig.json` /
|
||||
shims, **sans toucher** à `material-web/src` :
|
||||
|
||||
1. **`experimentalDecorators: true` + `useDefineForClassFields: false`** — les éléments Lit du
|
||||
fork utilisent les décorateurs _expérimentaux_ (`@property()`, `@customElement()`).
|
||||
2. **`material-web-shims.d.ts`** — déclare `*.cssresult.js` et `*.css` comme modules ambiants.
|
||||
Ces fichiers de styles sont des **artefacts du build SASS** (`build:sass` → `build:css-to-ts`)
|
||||
**absents de ce checkout source** du fork. Le wrapper React ne touche jamais ces valeurs
|
||||
(les styles voyagent avec l'élément au runtime), d'où un typage `any` — qui évite aussi le
|
||||
conflit nominal entre le `lit@2.8.0` imbriqué de `material-web` et le `lit@3.3.3` du package.
|
||||
3. **`moduleResolution: bundler`** — résout les imports bare `@material/web/x/y.js` vers les
|
||||
sources `.ts` (le fork n'a pas de `.js`/`.d.ts` compilés ni de champ `exports`).
|
||||
|
||||
> En contexte de **build réel** du fork (`material-web` buildé : `*.cssresult.js` et `*.css`
|
||||
> présents), les shims deviennent inutiles ; ils ne servent qu'à faire compiler le wrapper
|
||||
> contre les **sources** du fork tel que livré ici.
|
||||
|
||||
### Blocages réels
|
||||
|
||||
**Aucun.** Les 119 éléments de `md-elements.txt` résolvent vers un fichier d'entrée réel avec
|
||||
classe exportée, et compilent. Les 4 tags `md-button`, `md-card`, `md-menu-group`,
|
||||
`md-split-button` n'existent que sous `labs/gb/components/` (composants « Google Sans » du fork) :
|
||||
leurs classes exportées ne suivent pas la convention `Md*` (`Button`, `Card`, `MenuGroup`,
|
||||
`SplitButton`) — le wrapper les ré-alias correctement (`MdButton`, `MdCard`, …).
|
||||
|
||||
## Liste complète des wrappers (119)
|
||||
|
||||
> Régénérée par `generate.ts` (`.manifest.json`). 0 non résolu(s).
|
||||
|
||||
| Wrapper | Tag | Import élément | # events |
|
||||
| --- | --- | --- | --- |
|
||||
| `Md3dCanvas` | `md-3d-canvas` | `@aphrody/material-web/canvas3d/canvas-3d.js` | — |
|
||||
| `Md3dCard` | `md-3d-card` | `@aphrody/material-web/card3d/card-3d.js` | — |
|
||||
| `Md3dGlobe` | `md-3d-globe` | `@aphrody/material-web/globe3d/globe-3d.js` | — |
|
||||
| `MdAccordion` | `md-accordion` | `@aphrody/material-web/expansion/accordion.js` | — |
|
||||
| `MdAlert` | `md-alert` | `@aphrody/material-web/alert/alert.js` | 1 |
|
||||
| `MdAreaChart` | `md-area-chart` | `@aphrody/material-web/charts/area-chart.js` | — |
|
||||
| `MdAssistChip` | `md-assist-chip` | `@aphrody/material-web/chips/assist-chip.js` | — |
|
||||
| `MdAutocomplete` | `md-autocomplete` | `@aphrody/material-web/autocomplete/autocomplete.js` | 2 |
|
||||
| `MdAvatar` | `md-avatar` | `@aphrody/material-web/avatar/avatar.js` | 1 |
|
||||
| `MdAvatarGroup` | `md-avatar-group` | `@aphrody/material-web/avatar/avatar-group.js` | — |
|
||||
| `MdBackdrop` | `md-backdrop` | `@aphrody/material-web/backdrop/backdrop.js` | — |
|
||||
| `MdBadge` | `md-badge` | `@aphrody/material-web/badge/badge.js` | — |
|
||||
| `MdBarChart` | `md-bar-chart` | `@aphrody/material-web/charts/bar-chart.js` | — |
|
||||
| `MdBottomAppBar` | `md-bottom-app-bar` | `@aphrody/material-web/appbar/bottom-app-bar.js` | — |
|
||||
| `MdBottomSheet` | `md-bottom-sheet` | `@aphrody/material-web/sheet/bottom-sheet.js` | 4 |
|
||||
| `MdBrandedFab` | `md-branded-fab` | `@aphrody/material-web/fab/branded-fab.js` | — |
|
||||
| `MdBreadcrumbs` | `md-breadcrumbs` | `@aphrody/material-web/breadcrumbs/breadcrumbs.js` | — |
|
||||
| `MdButton` | `md-button` | `@aphrody/material-web/labs/gb/components/button/md-button.js` | — |
|
||||
| `MdButtonGroup` | `md-button-group` | `@aphrody/material-web/buttongroup/button-group.js` | 1 |
|
||||
| `MdCard` | `md-card` | `@aphrody/material-web/card/card.js` | — |
|
||||
| `MdCarousel` | `md-carousel` | `@aphrody/material-web/carousel/carousel.js` | 1 |
|
||||
| `MdCarouselItem` | `md-carousel-item` | `@aphrody/material-web/carousel/carousel-item.js` | — |
|
||||
| `MdCheckbox` | `md-checkbox` | `@aphrody/material-web/checkbox/checkbox.js` | 2 |
|
||||
| `MdChipSet` | `md-chip-set` | `@aphrody/material-web/chips/chip-set.js` | — |
|
||||
| `MdCircularProgress` | `md-circular-progress` | `@aphrody/material-web/progress/circular-progress.js` | — |
|
||||
| `MdDatePicker` | `md-date-picker` | `@aphrody/material-web/datepicker/date-picker.js` | 1 |
|
||||
| `MdDateRangePicker` | `md-date-range-picker` | `@aphrody/material-web/datepicker/date-range-picker.js` | 3 |
|
||||
| `MdDateTimePicker` | `md-date-time-picker` | `@aphrody/material-web/datepicker/date-time-picker.js` | 3 |
|
||||
| `MdDialog` | `md-dialog` | `@aphrody/material-web/dialog/dialog.js` | 5 |
|
||||
| `MdDivider` | `md-divider` | `@aphrody/material-web/divider/divider.js` | — |
|
||||
| `MdElevatedButton` | `md-elevated-button` | `@aphrody/material-web/button/elevated-button.js` | — |
|
||||
| `MdElevatedCard` | `md-elevated-card` | `@aphrody/material-web/labs/card/elevated-card.js` | — |
|
||||
| `MdElevation` | `md-elevation` | `@aphrody/material-web/elevation/elevation.js` | — |
|
||||
| `MdExpansionPanel` | `md-expansion-panel` | `@aphrody/material-web/expansion/expansion-panel.js` | 1 |
|
||||
| `MdFab` | `md-fab` | `@aphrody/material-web/fab/fab.js` | — |
|
||||
| `MdFabMenu` | `md-fab-menu` | `@aphrody/material-web/fabmenu/fab-menu.js` | 2 |
|
||||
| `MdFabMenuItem` | `md-fab-menu-item` | `@aphrody/material-web/fabmenu/fab-menu-item.js` | 1 |
|
||||
| `MdFilledButton` | `md-filled-button` | `@aphrody/material-web/button/filled-button.js` | — |
|
||||
| `MdFilledCard` | `md-filled-card` | `@aphrody/material-web/labs/card/filled-card.js` | — |
|
||||
| `MdFilledField` | `md-filled-field` | `@aphrody/material-web/field/filled-field.js` | — |
|
||||
| `MdFilledIconButton` | `md-filled-icon-button` | `@aphrody/material-web/iconbutton/filled-icon-button.js` | — |
|
||||
| `MdFilledSelect` | `md-filled-select` | `@aphrody/material-web/select/filled-select.js` | 6 |
|
||||
| `MdFilledTextField` | `md-filled-text-field` | `@aphrody/material-web/textfield/filled-text-field.js` | 2 |
|
||||
| `MdFilledTonalButton` | `md-filled-tonal-button` | `@aphrody/material-web/button/filled-tonal-button.js` | — |
|
||||
| `MdFilledTonalIconButton` | `md-filled-tonal-icon-button` | `@aphrody/material-web/iconbutton/filled-tonal-icon-button.js` | — |
|
||||
| `MdFilterChip` | `md-filter-chip` | `@aphrody/material-web/chips/filter-chip.js` | — |
|
||||
| `MdFocusRing` | `md-focus-ring` | `@aphrody/material-web/focus/md-focus-ring.js` | — |
|
||||
| `MdGauge` | `md-gauge` | `@aphrody/material-web/charts/gauge.js` | — |
|
||||
| `MdGridList` | `md-grid-list` | `@aphrody/material-web/gridlist/grid-list.js` | — |
|
||||
| `MdGridTile` | `md-grid-tile` | `@aphrody/material-web/gridlist/grid-tile.js` | — |
|
||||
| `MdIcon` | `md-icon` | `@aphrody/material-web/icon/icon.js` | — |
|
||||
| `MdIconButton` | `md-icon-button` | `@aphrody/material-web/iconbutton/icon-button.js` | — |
|
||||
| `MdInputChip` | `md-input-chip` | `@aphrody/material-web/chips/input-chip.js` | 2 |
|
||||
| `MdItem` | `md-item` | `@aphrody/material-web/labs/item/item.js` | — |
|
||||
| `MdLinearProgress` | `md-linear-progress` | `@aphrody/material-web/progress/linear-progress.js` | — |
|
||||
| `MdLineChart` | `md-line-chart` | `@aphrody/material-web/charts/line-chart.js` | — |
|
||||
| `MdLink` | `md-link` | `@aphrody/material-web/link/link.js` | — |
|
||||
| `MdList` | `md-list` | `@aphrody/material-web/list/list.js` | — |
|
||||
| `MdListDetail` | `md-list-detail` | `@aphrody/material-web/layout/md-list-detail.js` | — |
|
||||
| `MdListItem` | `md-list-item` | `@aphrody/material-web/list/list-item.js` | — |
|
||||
| `MdLoadingIndicator` | `md-loading-indicator` | `@aphrody/material-web/loadingindicator/loading-indicator.js` | — |
|
||||
| `MdMenu` | `md-menu` | `@aphrody/material-web/menu/menu.js` | 4 |
|
||||
| `MdMenuGroup` | `md-menu-group` | `@aphrody/material-web/labs/gb/components/menu/md-menu-group.js` | — |
|
||||
| `MdMenuItem` | `md-menu-item` | `@aphrody/material-web/menu/menu-item.js` | 1 |
|
||||
| `MdMobileStepper` | `md-mobile-stepper` | `@aphrody/material-web/mobilestepper/mobile-stepper.js` | — |
|
||||
| `MdNavigationBar` | `md-navigation-bar` | `@aphrody/material-web/labs/navigationbar/navigation-bar.js` | 1 |
|
||||
| `MdNavigationDrawer` | `md-navigation-drawer` | `@aphrody/material-web/labs/navigationdrawer/navigation-drawer.js` | 1 |
|
||||
| `MdNavigationDrawerModal` | `md-navigation-drawer-modal` | `@aphrody/material-web/labs/navigationdrawer/navigation-drawer-modal.js` | 1 |
|
||||
| `MdNavigationRail` | `md-navigation-rail` | `@aphrody/material-web/navigationrail/navigation-rail.js` | 2 |
|
||||
| `MdNavigationRailItem` | `md-navigation-rail-item` | `@aphrody/material-web/navigationrail/navigation-rail-item.js` | — |
|
||||
| `MdNavigationTab` | `md-navigation-tab` | `@aphrody/material-web/labs/navigationtab/navigation-tab.js` | — |
|
||||
| `MdOutlinedButton` | `md-outlined-button` | `@aphrody/material-web/button/outlined-button.js` | — |
|
||||
| `MdOutlinedCard` | `md-outlined-card` | `@aphrody/material-web/labs/card/outlined-card.js` | — |
|
||||
| `MdOutlinedField` | `md-outlined-field` | `@aphrody/material-web/field/outlined-field.js` | — |
|
||||
| `MdOutlinedIconButton` | `md-outlined-icon-button` | `@aphrody/material-web/iconbutton/outlined-icon-button.js` | — |
|
||||
| `MdOutlinedSegmentedButton` | `md-outlined-segmented-button` | `@aphrody/material-web/labs/segmentedbutton/outlined-segmented-button.js` | — |
|
||||
| `MdOutlinedSegmentedButtonSet` | `md-outlined-segmented-button-set` | `@aphrody/material-web/labs/segmentedbuttonset/outlined-segmented-button-set.js` | — |
|
||||
| `MdOutlinedSelect` | `md-outlined-select` | `@aphrody/material-web/select/outlined-select.js` | 6 |
|
||||
| `MdOutlinedTextField` | `md-outlined-text-field` | `@aphrody/material-web/textfield/outlined-text-field.js` | 2 |
|
||||
| `MdPaginator` | `md-paginator` | `@aphrody/material-web/paginator/paginator.js` | 1 |
|
||||
| `MdPane` | `md-pane` | `@aphrody/material-web/layout/md-pane.js` | — |
|
||||
| `MdPieChart` | `md-pie-chart` | `@aphrody/material-web/charts/pie-chart.js` | — |
|
||||
| `MdPopover` | `md-popover` | `@aphrody/material-web/popover/popover.js` | 2 |
|
||||
| `MdPrimaryTab` | `md-primary-tab` | `@aphrody/material-web/tabs/primary-tab.js` | — |
|
||||
| `MdRadarChart` | `md-radar-chart` | `@aphrody/material-web/charts/radar-chart.js` | — |
|
||||
| `MdRadio` | `md-radio` | `@aphrody/material-web/radio/radio.js` | 2 |
|
||||
| `MdRating` | `md-rating` | `@aphrody/material-web/rating/rating.js` | 2 |
|
||||
| `MdRipple` | `md-ripple` | `@aphrody/material-web/ripple/ripple.js` | — |
|
||||
| `MdScaffold` | `md-scaffold` | `@aphrody/material-web/layout/md-scaffold.js` | — |
|
||||
| `MdScatterChart` | `md-scatter-chart` | `@aphrody/material-web/charts/scatter-chart.js` | — |
|
||||
| `MdScheduler` | `md-scheduler` | `@aphrody/material-web/scheduler/scheduler.js` | 3 |
|
||||
| `MdSearchBar` | `md-search-bar` | `@aphrody/material-web/search/search-bar.js` | 4 |
|
||||
| `MdSecondaryTab` | `md-secondary-tab` | `@aphrody/material-web/tabs/secondary-tab.js` | — |
|
||||
| `MdSelectOption` | `md-select-option` | `@aphrody/material-web/select/select-option.js` | — |
|
||||
| `MdSideSheet` | `md-side-sheet` | `@aphrody/material-web/sheet/side-sheet.js` | 4 |
|
||||
| `MdSkeleton` | `md-skeleton` | `@aphrody/material-web/skeleton/skeleton.js` | — |
|
||||
| `MdSlider` | `md-slider` | `@aphrody/material-web/slider/slider.js` | 2 |
|
||||
| `MdSnackbar` | `md-snackbar` | `@aphrody/material-web/snackbar/snackbar.js` | 4 |
|
||||
| `MdSparkline` | `md-sparkline` | `@aphrody/material-web/charts/sparkline.js` | — |
|
||||
| `MdSplitButton` | `md-split-button` | `@aphrody/material-web/labs/gb/components/splitbutton/md-split-button.js` | — |
|
||||
| `MdStep` | `md-step` | `@aphrody/material-web/stepper/step.js` | — |
|
||||
| `MdStepper` | `md-stepper` | `@aphrody/material-web/stepper/stepper.js` | 1 |
|
||||
| `MdSubMenu` | `md-sub-menu` | `@aphrody/material-web/menu/sub-menu.js` | 4 |
|
||||
| `MdSuggestionChip` | `md-suggestion-chip` | `@aphrody/material-web/chips/suggestion-chip.js` | — |
|
||||
| `MdSupportingPane` | `md-supporting-pane` | `@aphrody/material-web/layout/md-supporting-pane.js` | — |
|
||||
| `MdSurface` | `md-surface` | `@aphrody/material-web/surface/surface.js` | — |
|
||||
| `MdSwitch` | `md-switch` | `@aphrody/material-web/switch/switch.js` | 2 |
|
||||
| `MdTable` | `md-table` | `@aphrody/material-web/table/table.js` | 2 |
|
||||
| `MdTabs` | `md-tabs` | `@aphrody/material-web/tabs/tabs.js` | 1 |
|
||||
| `MdTextButton` | `md-text-button` | `@aphrody/material-web/button/text-button.js` | — |
|
||||
| `MdTimePicker` | `md-time-picker` | `@aphrody/material-web/timepicker/time-picker.js` | 1 |
|
||||
| `MdToolbar` | `md-toolbar` | `@aphrody/material-web/toolbar/md-toolbar.js` | — |
|
||||
| `MdTooltip` | `md-tooltip` | `@aphrody/material-web/tooltip/tooltip.js` | 2 |
|
||||
| `MdTopAppBar` | `md-top-app-bar` | `@aphrody/material-web/appbar/top-app-bar.js` | — |
|
||||
| `MdTree` | `md-tree` | `@aphrody/material-web/tree/tree.js` | 1 |
|
||||
| `MdTreeItem` | `md-tree-item` | `@aphrody/material-web/tree/tree-item.js` | 2 |
|
||||
| `MdType` | `md-type` | `@aphrody/material-web/typography/md-type.js` | — |
|
||||
| `MdVirtualScroller` | `md-virtual-scroller` | `@aphrody/material-web/virtualscroll/virtual-scroller.js` | 1 |
|
||||
| `MdWebgpuCanvas` | `md-webgpu-canvas` | `@aphrody/material-web/effects/webgpu-canvas.js` | — |
|
||||
@@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
/**
|
||||
* @fileoverview TanStack Table adapter for @aphrody/m3-react & @aphrody/material-web.
|
||||
* Connects TanStack headless table state (sorting, pagination, column models)
|
||||
* with the Material Design 3 <md-table> custom element.
|
||||
*/
|
||||
|
||||
import type { TableColumn, TableSortCriterion } from '@aphrody/material-web/table/internal/table.js';
|
||||
|
||||
/**
|
||||
* Minimal interface representing a TanStack Column Definition.
|
||||
*/
|
||||
export interface TanStackColumnLike<_TData = unknown> {
|
||||
id?: string;
|
||||
accessorKey?: string;
|
||||
header?: string | ((...args: unknown[]) => unknown);
|
||||
enableSorting?: boolean;
|
||||
meta?: {
|
||||
numeric?: boolean;
|
||||
filter?: 'text' | 'number' | '';
|
||||
resizable?: boolean;
|
||||
width?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps TanStack Column Definitions to Aphrody Material 3 TableColumn descriptors.
|
||||
*/
|
||||
export function mapTanStackColumnsToM3<TData = unknown>(
|
||||
columns: TanStackColumnLike<TData>[]
|
||||
): TableColumn[] {
|
||||
return columns.map((col) => {
|
||||
const key = col.id || col.accessorKey || '';
|
||||
const label = typeof col.header === 'string' ? col.header : key;
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
sortable: col.enableSorting !== false,
|
||||
numeric: col.meta?.numeric ?? false,
|
||||
filter: col.meta?.filter ?? '',
|
||||
resizable: col.meta?.resizable ?? false,
|
||||
width: col.meta?.width,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps M3 Table Sort Criteria to TanStack SortingState format.
|
||||
*/
|
||||
export function mapM3SortToTanStack(
|
||||
sortCriteria: TableSortCriterion[]
|
||||
): Array<{ id: string; desc: boolean }> {
|
||||
return sortCriteria.map((item) => ({
|
||||
id: item.key,
|
||||
desc: item.direction === 'desc',
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps TanStack SortingState to M3 TableSortCriterion format.
|
||||
*/
|
||||
export function mapTanStackSortToM3(
|
||||
sortingState: Array<{ id: string; desc: boolean }>
|
||||
): TableSortCriterion[] {
|
||||
return sortingState.map((item) => ({
|
||||
key: item.id,
|
||||
direction: item.desc ? 'desc' : 'asc',
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Adaptive layout primitives for React (Material Design 3 window size classes).
|
||||
// Import: `import { useWindowSizeClass } from "@aphrody/m3-react/adaptive"`.
|
||||
export * from "./use-window-size-class.js";
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Adaptive layout hooks for React — Material Design 3 window size classes.
|
||||
*
|
||||
* The **breakpoint values and classification logic are platform-agnostic** and
|
||||
* live in `@aphrody/m3-tokens/breakpoints` (pure TypeScript, no DOM). This
|
||||
* module re-exports them and adds the *web* reactivity layer (`matchMedia` /
|
||||
* `resize`). That split is what makes the design system portable: a React
|
||||
* Native shell can share the very same `BREAKPOINTS` / `classifyWidth` from
|
||||
* `m3-tokens` and feed them `useWindowDimensions().width`, while a Tauri / web
|
||||
* app uses the hooks below unchanged.
|
||||
*
|
||||
* SSR-safe: during server render (and the first client render, to avoid a
|
||||
* hydration mismatch) the hooks return `ssrSizeClass` (default `"compact"`,
|
||||
* mobile-first), then settle to the real value after mount.
|
||||
*/
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
BREAKPOINTS,
|
||||
classifyWidth,
|
||||
mediaQuery,
|
||||
type WindowSizeClass,
|
||||
WINDOW_SIZE_CLASSES,
|
||||
} from "@aphrody/m3-tokens/breakpoints";
|
||||
|
||||
// Re-export the canonical, platform-agnostic primitives so consumers can import
|
||||
// everything adaptive from a single entrypoint (`@aphrody/m3-react/adaptive`).
|
||||
export { BREAKPOINTS, classifyWidth, WINDOW_SIZE_CLASSES };
|
||||
export type { WindowSizeClass };
|
||||
|
||||
/**
|
||||
* `(min-width: …)` media query string for the lower bound of a size class.
|
||||
* Alias of `m3-tokens`' `mediaQuery`, kept for the web-oriented hook API.
|
||||
*/
|
||||
export function minWidthQuery(sizeClass: WindowSizeClass): string {
|
||||
return mediaQuery(sizeClass);
|
||||
}
|
||||
|
||||
const isClient = typeof window !== "undefined" && typeof window.matchMedia === "function";
|
||||
|
||||
/**
|
||||
* Reactive boolean for a CSS media query. SSR-safe (returns `false` until
|
||||
* mounted on the client).
|
||||
*/
|
||||
export function useMediaQuery(query: string): boolean {
|
||||
const [matches, setMatches] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!isClient) return;
|
||||
const mql = window.matchMedia(query);
|
||||
const onChange = () => setMatches(mql.matches);
|
||||
onChange();
|
||||
mql.addEventListener("change", onChange);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, [query]);
|
||||
return matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reactive M3 window size class for the viewport. Updates on resize/orientation
|
||||
* change via `matchMedia`. SSR-safe — returns `ssrSizeClass` until mounted.
|
||||
*
|
||||
* ```tsx
|
||||
* const sizeClass = useWindowSizeClass();
|
||||
* return sizeClass === "compact" ? <MdNavigationBar/> : <MdNavigationRail/>;
|
||||
* ```
|
||||
*/
|
||||
export function useWindowSizeClass(ssrSizeClass: WindowSizeClass = "compact"): WindowSizeClass {
|
||||
const [sizeClass, setSizeClass] = useState<WindowSizeClass>(ssrSizeClass);
|
||||
useEffect(() => {
|
||||
if (!isClient) return;
|
||||
const update = () => setSizeClass(classifyWidth(window.innerWidth));
|
||||
update();
|
||||
window.addEventListener("resize", update, { passive: true });
|
||||
window.addEventListener("orientationchange", update);
|
||||
return () => {
|
||||
window.removeEventListener("resize", update);
|
||||
window.removeEventListener("orientationchange", update);
|
||||
};
|
||||
}, []);
|
||||
return sizeClass;
|
||||
}
|
||||
|
||||
/** True when the viewport is at least the given size class (e.g. tablet+). */
|
||||
export function useAtLeast(sizeClass: WindowSizeClass): boolean {
|
||||
return useMediaQuery(minWidthQuery(sizeClass));
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { m3Package } from "@aphrody/m3-config/bun-build";
|
||||
|
||||
export default m3Package({
|
||||
entry: [
|
||||
"index.ts",
|
||||
"transitions/index.ts",
|
||||
"adaptive/index.ts",
|
||||
"state/index.ts",
|
||||
"adapters/tanstack.ts",
|
||||
"data/index.ts",
|
||||
"wrappers/*.ts",
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
[test]
|
||||
preload = ["@aphrody/m3-config/test/dom"]
|
||||
@@ -0,0 +1,304 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
/**
|
||||
* M3 DataTable: TanStack Table v9 (sorting, global filter, pagination) rendered as a semantic `<table>`
|
||||
* styled with the `--md-sys-*` tokens. With `virtual`, rows are virtualized by TanStack Virtual instead of paged.
|
||||
*/
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import {
|
||||
type ColumnDef,
|
||||
columnFilteringFeature,
|
||||
createColumnHelper,
|
||||
createFilteredRowModel,
|
||||
createPaginatedRowModel,
|
||||
createSortedRowModel,
|
||||
filterFn_includesString,
|
||||
globalFilteringFeature,
|
||||
rowPaginationFeature,
|
||||
rowSortingFeature,
|
||||
sortFn_alphanumeric,
|
||||
sortFn_basic,
|
||||
sortFn_text,
|
||||
tableFeatures,
|
||||
useTable,
|
||||
type RowData,
|
||||
} from "@tanstack/react-table";
|
||||
import { type CSSProperties, type ReactNode, useId, useRef } from "react";
|
||||
|
||||
/** The features of every DataTable: sorting, column and global filtering, pagination. */
|
||||
export const dataTableFeatures = tableFeatures({
|
||||
rowSortingFeature,
|
||||
sortedRowModel: createSortedRowModel(),
|
||||
sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text, basic: sortFn_basic },
|
||||
columnFilteringFeature,
|
||||
globalFilteringFeature,
|
||||
filteredRowModel: createFilteredRowModel(),
|
||||
filterFns: { includesString: filterFn_includesString },
|
||||
rowPaginationFeature,
|
||||
paginatedRowModel: createPaginatedRowModel(),
|
||||
});
|
||||
export type DataTableFeatures = typeof dataTableFeatures;
|
||||
export type DataColumn<TData extends RowData = any> = ColumnDef<DataTableFeatures, TData, any>;
|
||||
|
||||
/** Column helper typed for DataTable: `const col = dataColumns<Person>(); col.accessor("name", { header: "Name" })`. */
|
||||
export const dataColumns = <TData extends RowData = any>() =>
|
||||
createColumnHelper<DataTableFeatures, TData>();
|
||||
|
||||
export interface DataTableProps<TData extends RowData = any> {
|
||||
data: TData[];
|
||||
columns: DataColumn<TData>[];
|
||||
/** Accessible caption (also the table label). */
|
||||
caption?: ReactNode;
|
||||
/** Show a search field filtering every column. */
|
||||
searchable?: boolean;
|
||||
searchLabel?: string;
|
||||
/** Rows per page (default 25). Ignored with `virtual`. */
|
||||
pageSize?: number;
|
||||
/** Virtualize rows in a scroll container of this height (px) instead of paging. */
|
||||
virtual?: { height: number; rowHeight?: number; overscan?: number };
|
||||
/** Initial sort, e.g. `[{ id: "name", desc: false }]`. */
|
||||
initialSorting?: { id: string; desc: boolean }[];
|
||||
getRowId?: (row: TData, index: number) => string;
|
||||
onRowClick?: (row: TData) => void;
|
||||
emptyLabel?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const EMPTY: never[] = [];
|
||||
|
||||
const s = {
|
||||
root: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
color: "var(--md-sys-color-on-surface)",
|
||||
font: "var(--md-sys-typescale-body-medium-weight, 400) var(--md-sys-typescale-body-medium-size, 14px) / var(--md-sys-typescale-body-medium-line-height, 20px) var(--md-sys-typescale-body-medium-font, inherit)",
|
||||
},
|
||||
container: {
|
||||
overflow: "auto",
|
||||
borderRadius: "var(--md-sys-shape-corner-medium, 12px)",
|
||||
border: "1px solid var(--md-sys-color-outline-variant)",
|
||||
background: "var(--md-sys-color-surface)",
|
||||
},
|
||||
table: { width: "100%", borderCollapse: "collapse" },
|
||||
caption: {
|
||||
textAlign: "start",
|
||||
padding: "12px 16px",
|
||||
font: "var(--md-sys-typescale-title-medium-weight, 500) var(--md-sys-typescale-title-medium-size, 16px) / var(--md-sys-typescale-title-medium-line-height, 24px) var(--md-sys-typescale-title-medium-font, inherit)",
|
||||
},
|
||||
th: {
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
textAlign: "start",
|
||||
padding: "0 16px",
|
||||
height: 56,
|
||||
background: "var(--md-sys-color-surface-container)",
|
||||
color: "var(--md-sys-color-on-surface-variant)",
|
||||
borderBottom: "1px solid var(--md-sys-color-outline-variant)",
|
||||
font: "var(--md-sys-typescale-title-small-weight, 500) var(--md-sys-typescale-title-small-size, 14px) / var(--md-sys-typescale-title-small-line-height, 20px) var(--md-sys-typescale-title-small-font, inherit)",
|
||||
},
|
||||
sort: {
|
||||
all: "unset",
|
||||
cursor: "pointer",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
},
|
||||
td: {
|
||||
padding: "0 16px",
|
||||
height: 52,
|
||||
borderBottom: "1px solid var(--md-sys-color-outline-variant)",
|
||||
},
|
||||
search: {
|
||||
alignSelf: "flex-start",
|
||||
minWidth: 240,
|
||||
height: 40,
|
||||
padding: "0 16px",
|
||||
borderRadius: "var(--md-sys-shape-corner-extra-large, 28px)",
|
||||
border: "1px solid var(--md-sys-color-outline)",
|
||||
background: "var(--md-sys-color-surface-container-high)",
|
||||
color: "inherit",
|
||||
font: "inherit",
|
||||
},
|
||||
pager: { display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 8 },
|
||||
button: {
|
||||
height: 40,
|
||||
padding: "0 16px",
|
||||
borderRadius: "var(--md-sys-shape-corner-full, 20px)",
|
||||
border: "1px solid var(--md-sys-color-outline)",
|
||||
background: "transparent",
|
||||
color: "var(--md-sys-color-primary)",
|
||||
font: "inherit",
|
||||
cursor: "pointer",
|
||||
},
|
||||
} satisfies Record<string, CSSProperties>;
|
||||
|
||||
const SORT_MARK = { asc: " ▲", desc: " ▼" } as const;
|
||||
|
||||
export function DataTable<TData extends RowData = any>(props: DataTableProps<TData>) {
|
||||
const {
|
||||
data,
|
||||
columns,
|
||||
caption,
|
||||
searchable = false,
|
||||
searchLabel = "Search",
|
||||
pageSize = 25,
|
||||
virtual,
|
||||
initialSorting = EMPTY,
|
||||
getRowId,
|
||||
onRowClick,
|
||||
emptyLabel = "No data",
|
||||
className,
|
||||
} = props;
|
||||
const searchId = useId();
|
||||
const table = useTable<DataTableFeatures, TData>({
|
||||
features: dataTableFeatures,
|
||||
data,
|
||||
columns,
|
||||
getRowId,
|
||||
globalFilterFn: "includesString",
|
||||
initialState: {
|
||||
sorting: initialSorting,
|
||||
pagination: { pageIndex: 0, pageSize: virtual ? Number.MAX_SAFE_INTEGER : pageSize },
|
||||
},
|
||||
});
|
||||
const rows = table.getRowModel().rows;
|
||||
const columnCount = table.getAllLeafColumns().length;
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const rowHeight = virtual?.rowHeight ?? 52;
|
||||
// oxlint-disable-next-line react/incompatible-library -- TanStack Virtual renvoie des fonctions non mémoïsables (getVirtualItems, measureElement) : le compilateur React doit l'ignorer
|
||||
const virtualizer = useVirtualizer({
|
||||
count: virtual ? rows.length : 0,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => rowHeight,
|
||||
getItemKey: (index) => rows[index]?.id ?? index,
|
||||
overscan: virtual?.overscan ?? 8,
|
||||
initialRect: virtual ? { width: 0, height: virtual.height } : undefined,
|
||||
});
|
||||
|
||||
const items = virtual ? virtualizer.getVirtualItems() : undefined;
|
||||
const visible = items ? items.map((item) => rows[item.index]!) : rows;
|
||||
const padTop = items?.[0]?.start ?? 0;
|
||||
const padBottom = items?.length ? virtualizer.getTotalSize() - items[items.length - 1]!.end : 0;
|
||||
|
||||
return (
|
||||
<div className={className} style={s.root} data-m3-data-table="">
|
||||
{searchable && (
|
||||
<input
|
||||
id={searchId}
|
||||
type="search"
|
||||
aria-label={searchLabel}
|
||||
placeholder={searchLabel}
|
||||
style={s.search}
|
||||
value={String(table.store.state.globalFilter ?? "")}
|
||||
onChange={(event) => table.setGlobalFilter(event.currentTarget.value)}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
style={virtual ? { ...s.container, height: virtual.height } : s.container}
|
||||
data-m3-scroll=""
|
||||
>
|
||||
<table style={s.table} aria-rowcount={virtual ? rows.length + 1 : undefined}>
|
||||
{caption && <caption style={s.caption}>{caption}</caption>}
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((group) => (
|
||||
<tr key={group.id}>
|
||||
{group.headers.map((header) => {
|
||||
const sorted = header.column.getIsSorted();
|
||||
const label = header.isPlaceholder ? null : <table.FlexRender header={header} />;
|
||||
return (
|
||||
<th
|
||||
key={header.id}
|
||||
scope="col"
|
||||
colSpan={header.colSpan}
|
||||
style={s.th}
|
||||
aria-sort={
|
||||
sorted === "asc"
|
||||
? "ascending"
|
||||
: sorted === "desc"
|
||||
? "descending"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{header.column.getCanSort() ? (
|
||||
<button
|
||||
type="button"
|
||||
style={s.sort}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{label}
|
||||
{sorted ? SORT_MARK[sorted] : null}
|
||||
</button>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{padTop > 0 && (
|
||||
<tr aria-hidden="true">
|
||||
{/* oxlint-disable-next-line jsx-a11y/control-has-associated-label -- cellule de remplissage de virtualisation (ligne aria-hidden, sans contenu ni contrôle) */}
|
||||
<td colSpan={columnCount} style={{ height: padTop, padding: 0 }} />
|
||||
</tr>
|
||||
)}
|
||||
{visible.map((row) => (
|
||||
<tr
|
||||
key={row.id}
|
||||
data-row-id={row.id}
|
||||
onClick={onRowClick ? () => onRowClick(row.original as TData) : undefined}
|
||||
style={onRowClick ? { cursor: "pointer" } : undefined}
|
||||
>
|
||||
{row.getAllCells().map((cell) => (
|
||||
// oxlint-disable-next-line jsx-a11y/control-has-associated-label -- cellule de données (td), pas un contrôle : son contenu est rendu par FlexRender
|
||||
<td key={cell.id} style={s.td}>
|
||||
<table.FlexRender cell={cell} />
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
{padBottom > 0 && (
|
||||
<tr aria-hidden="true">
|
||||
{/* oxlint-disable-next-line jsx-a11y/control-has-associated-label -- cellule de remplissage de virtualisation (ligne aria-hidden, sans contenu ni contrôle) */}
|
||||
<td colSpan={columnCount} style={{ height: padBottom, padding: 0 }} />
|
||||
</tr>
|
||||
)}
|
||||
{rows.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={columnCount} style={{ ...s.td, textAlign: "center" }}>
|
||||
{emptyLabel}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{!virtual && table.getPageCount() > 1 && (
|
||||
<nav style={s.pager} aria-label="Pagination">
|
||||
<button
|
||||
type="button"
|
||||
style={s.button}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
onClick={() => table.previousPage()}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span>
|
||||
{table.store.state.pagination.pageIndex + 1} / {table.getPageCount()}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
style={s.button}
|
||||
disabled={!table.getCanNextPage()}
|
||||
onClick={() => table.nextPage()}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
/** M3 VirtualList: a `role="list"` scroll container that renders only the visible items (TanStack Virtual). */
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { type CSSProperties, type Key, type ReactNode, useRef } from "react";
|
||||
|
||||
export interface VirtualListProps<T> {
|
||||
items: readonly T[];
|
||||
renderItem: (item: T, index: number) => ReactNode;
|
||||
/** Scroll container height in px (the visible window). */
|
||||
height: number;
|
||||
/** Estimated item size in px (default 56, the M3 one-line list item); items are measured once rendered. */
|
||||
estimateSize?: number;
|
||||
overscan?: number;
|
||||
getKey?: (item: T, index: number) => Key;
|
||||
/** Gap between items in px. */
|
||||
gap?: number;
|
||||
/** Called when the last item becomes visible (infinite loading). */
|
||||
onEndReached?: () => void;
|
||||
"aria-label"?: string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export function VirtualList<T>(props: VirtualListProps<T>) {
|
||||
const {
|
||||
items,
|
||||
renderItem,
|
||||
height,
|
||||
estimateSize = 56,
|
||||
overscan = 6,
|
||||
getKey,
|
||||
gap = 0,
|
||||
onEndReached,
|
||||
className,
|
||||
style,
|
||||
} = props;
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const endReported = useRef(-1);
|
||||
// oxlint-disable-next-line react/incompatible-library -- TanStack Virtual renvoie des fonctions non mémoïsables (getVirtualItems, measureElement) : le compilateur React doit l'ignorer
|
||||
const virtualizer = useVirtualizer({
|
||||
count: items.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => estimateSize,
|
||||
overscan,
|
||||
gap,
|
||||
getItemKey: getKey ? (index) => getKey(items[index]!, index) : undefined,
|
||||
initialRect: { width: 0, height },
|
||||
});
|
||||
const virtualItems = virtualizer.getVirtualItems();
|
||||
const last = virtualItems.at(-1);
|
||||
if (
|
||||
onEndReached &&
|
||||
last &&
|
||||
last.index === items.length - 1 &&
|
||||
endReported.current !== items.length
|
||||
) {
|
||||
endReported.current = items.length;
|
||||
queueMicrotask(onEndReached);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className={className}
|
||||
data-m3-virtual-list=""
|
||||
style={{
|
||||
height,
|
||||
overflow: "auto",
|
||||
contain: "strict",
|
||||
background: "var(--md-sys-color-surface)",
|
||||
color: "var(--md-sys-color-on-surface)",
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<ul
|
||||
aria-label={props["aria-label"]}
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: virtualizer.getTotalSize(),
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
listStyle: "none",
|
||||
}}
|
||||
>
|
||||
{virtualItems.map((item) => (
|
||||
<li
|
||||
key={item.key}
|
||||
aria-setsize={items.length}
|
||||
aria-posinset={item.index + 1}
|
||||
data-index={item.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
margin: 0,
|
||||
listStyle: "none",
|
||||
transform: `translateY(${item.start}px)`,
|
||||
}}
|
||||
>
|
||||
{renderItem(items[item.index]!, item.index)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
/** `@aphrody/m3-react/data`: M3 data components on TanStack Table v9 and TanStack Virtual. */
|
||||
export { DataTable, dataColumns, dataTableFeatures } from "./DataTable.tsx";
|
||||
export type { DataColumn, DataTableFeatures, DataTableProps } from "./DataTable.tsx";
|
||||
export { VirtualList } from "./VirtualList.tsx";
|
||||
export type { VirtualListProps } from "./VirtualList.tsx";
|
||||
+416
@@ -0,0 +1,416 @@
|
||||
/**
|
||||
* Generator for @aphrody/m3-react.
|
||||
*
|
||||
* Reads migration/scripts/md-elements.txt (119 tags), resolves each tag to its
|
||||
* real entry file + exported element class inside material-web/ by scanning the
|
||||
* `@customElement('md-x') export class Foo` pattern, and emits one wrapper file
|
||||
* per category directory using @lit/react `createComponent`.
|
||||
*
|
||||
* Registry archives contain compiled files only; pass the standalone fork sources.
|
||||
* Run: bun run generate.ts --source /path/to/material-web [--output /tmp/preview]
|
||||
*/
|
||||
import { mkdirSync, readdirSync, statSync } from "node:fs";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
|
||||
const HERE = import.meta.dir;
|
||||
const args = process.argv.slice(2);
|
||||
let sourceRoot: string | undefined;
|
||||
let outputRoot = HERE;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const flag = args[i];
|
||||
const value = args[++i];
|
||||
if ((flag !== "--source" && flag !== "--output") || !value || value.startsWith("--")) {
|
||||
throw new Error(
|
||||
"Usage: bun run generate.ts --source /path/to/material-web [--output /tmp/preview]",
|
||||
);
|
||||
}
|
||||
if (flag === "--source") sourceRoot = resolve(value);
|
||||
else outputRoot = resolve(value);
|
||||
}
|
||||
if (!sourceRoot) {
|
||||
throw new Error(
|
||||
"Material Web npm archives contain compiled files only. Pass --source /path/to/material-web (the standalone fork checkout).",
|
||||
);
|
||||
}
|
||||
const MW = sourceRoot;
|
||||
const BUNDLES = ["all.ts", "aphrody-components.ts", "aphrody-labs.ts"];
|
||||
const bundlePresent = await Promise.all(
|
||||
BUNDLES.map((bundle) => Bun.file(join(MW, bundle)).exists()),
|
||||
);
|
||||
for (const [index, bundle] of BUNDLES.entries()) {
|
||||
if (!bundlePresent[index]) {
|
||||
throw new Error(`Material Web source checkout is missing ${bundle}: ${MW}`);
|
||||
}
|
||||
}
|
||||
const ELEMENTS_TXT = join(HERE, "md-elements.txt");
|
||||
const OUT_DIR = join(outputRoot, "wrappers");
|
||||
|
||||
// ── 1. Tag → PascalCase wrapper name (CONVENTIONS §2) ──────────────────────
|
||||
const pascal = (tag: string): string =>
|
||||
tag
|
||||
.split("-")
|
||||
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
||||
.join("");
|
||||
|
||||
// ── 2. Walk material-web for every `@customElement('md-…')` entry file ──────
|
||||
// Skip tests, internal/, demos, catalog. Collect tag → [{file, className}].
|
||||
const SKIP_DIRS = new Set([
|
||||
"node_modules",
|
||||
"internal",
|
||||
"demo",
|
||||
"demos",
|
||||
"catalog",
|
||||
"dist-aphrody",
|
||||
"sass",
|
||||
"testing",
|
||||
]);
|
||||
const RE = /@customElement\(\s*['"]([a-z0-9-]+)['"]\s*\)\s*\nexport class (\w+)/g;
|
||||
|
||||
interface Def {
|
||||
file: string;
|
||||
className: string;
|
||||
}
|
||||
|
||||
type EventMap = Record<string, string>;
|
||||
|
||||
interface Resolved {
|
||||
tag: string;
|
||||
name: string;
|
||||
file: string;
|
||||
className: string;
|
||||
events: EventMap;
|
||||
}
|
||||
|
||||
interface Unresolved {
|
||||
tag: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
const defs: Map<string, Def[]> = new Map();
|
||||
|
||||
async function walk(dir: string): Promise<void> {
|
||||
for (const name of readdirSync(dir)) {
|
||||
const full = join(dir, name);
|
||||
const st = statSync(full);
|
||||
if (st.isDirectory()) {
|
||||
if (SKIP_DIRS.has(name)) continue;
|
||||
// oxlint-disable-next-line no-await-in-loop -- depth-first walk in readdir order: the defs lists it appends to feed the generated output order
|
||||
await walk(full);
|
||||
} else if (name.endsWith(".ts") && !name.endsWith("_test.ts") && !name.endsWith(".d.ts")) {
|
||||
// oxlint-disable-next-line no-await-in-loop -- files are registered in walk order (defs lists feed the generated output), so reads stay sequential
|
||||
const src = await Bun.file(full).text();
|
||||
let m;
|
||||
RE.lastIndex = 0;
|
||||
while ((m = RE.exec(src))) {
|
||||
const [, tag, className] = m;
|
||||
if (!tag.startsWith("md-")) continue;
|
||||
const rel = relative(MW, full).replace(/\.ts$/, ".js"); // import as .js (side-effect register)
|
||||
if (!defs.has(tag)) defs.set(tag, []);
|
||||
defs.get(tag)!.push({ file: rel, className });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await walk(MW);
|
||||
|
||||
// ── 3. Canonical import path preference ────────────────────────────────────
|
||||
// Tags re-exported by the three bundles must import from the bundled entry
|
||||
// path. Build the set of canonical relative paths from those bundles.
|
||||
const bundleImports = new Set<string>();
|
||||
const bundleSources = await Promise.all(BUNDLES.map((bundle) => Bun.file(join(MW, bundle)).text()));
|
||||
for (const src of bundleSources) {
|
||||
for (const m of src.matchAll(/from ['"]\.\/([^'"]+\.js)['"]/g)) bundleImports.add(m[1]);
|
||||
}
|
||||
|
||||
function pickDef(tag: string, candidates: Def[]): Def {
|
||||
// 1. Prefer a candidate whose path is referenced by a bundle entry.
|
||||
const bundled = candidates.find((c) => bundleImports.has(c.file));
|
||||
if (bundled) return bundled;
|
||||
// 2. Otherwise prefer a non-labs/gb path (canonical source dir).
|
||||
const nonGb = candidates.find((c) => !c.file.startsWith("labs/gb/"));
|
||||
if (nonGb) return nonGb;
|
||||
// 3. Fall back to the first (labs/gb only — e.g. md-button, md-split-button).
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
// ── 4. Event maps per category (verified against new Event/CustomEvent in MW) ─
|
||||
// Generic UI events React already types; the md elements re-dispatch the native
|
||||
// `input`/`change`. Component-specific events use their real names.
|
||||
// Every name below is the REAL event string verified by grepping
|
||||
// `@fires` / `new (Custom)?Event('…')` in material-web/<dir>/internal/*.ts.
|
||||
// Upstream components use bare names (input/change/opening/…); aphrody fork
|
||||
// components namespace theirs (paginator:page, table:sort, side-sheet:opening…).
|
||||
const EVENTS_BY_TAG: Record<string, EventMap> = {
|
||||
// ── fork: parity additions (event names verified against source dispatch) ──
|
||||
"md-rating": { onChange: "change", onInput: "input" },
|
||||
"md-carousel": { onChange: "carousel-change" },
|
||||
"md-scheduler": {
|
||||
onNavigate: "scheduler:navigate",
|
||||
onSelect: "scheduler:select",
|
||||
onEventClick: "scheduler:event-click",
|
||||
},
|
||||
"md-date-range-picker": {
|
||||
onInput: "input",
|
||||
onChange: "change",
|
||||
onDateRangeChange: "date-range-picker:change",
|
||||
},
|
||||
"md-date-time-picker": {
|
||||
onInput: "input",
|
||||
onChange: "change",
|
||||
onDateTimeChange: "date-time-picker:change",
|
||||
},
|
||||
"md-popover": { onOpened: "opened", onClosed: "closed" },
|
||||
"md-alert": { onClose: "close" },
|
||||
"md-avatar": { onError: "error" },
|
||||
// ── Form inputs (upstream: re-dispatched native input/change) ──────────
|
||||
"md-checkbox": { onChange: "change", onInput: "input" },
|
||||
"md-radio": { onChange: "change", onInput: "input" },
|
||||
"md-switch": { onChange: "change", onInput: "input" },
|
||||
"md-slider": { onChange: "change", onInput: "input" },
|
||||
"md-filled-text-field": { onChange: "change", onInput: "input" },
|
||||
"md-outlined-text-field": { onChange: "change", onInput: "input" },
|
||||
"md-filled-select": {
|
||||
onChange: "change",
|
||||
onInput: "input",
|
||||
onOpening: "opening",
|
||||
onOpened: "opened",
|
||||
onClosing: "closing",
|
||||
onClosed: "closed",
|
||||
},
|
||||
"md-outlined-select": {
|
||||
onChange: "change",
|
||||
onInput: "input",
|
||||
onOpening: "opening",
|
||||
onOpened: "opened",
|
||||
onClosing: "closing",
|
||||
onClosed: "closed",
|
||||
},
|
||||
// fork: search-bar fires input + search + search:open/close
|
||||
"md-search-bar": {
|
||||
onInput: "input",
|
||||
onSearch: "search",
|
||||
onSearchOpen: "search:open",
|
||||
onSearchClose: "search:close",
|
||||
},
|
||||
// fork: autocomplete fires input + autocomplete:select
|
||||
"md-autocomplete": { onInput: "input", onSelect: "autocomplete:select" },
|
||||
// ── Overlays (upstream) ────────────────────────────────────────────────
|
||||
"md-dialog": {
|
||||
onOpen: "open",
|
||||
onOpened: "opened",
|
||||
onClose: "close",
|
||||
onClosed: "closed",
|
||||
onCancel: "cancel",
|
||||
},
|
||||
"md-menu": {
|
||||
onOpening: "opening",
|
||||
onOpened: "opened",
|
||||
onClosing: "closing",
|
||||
onClosed: "closed",
|
||||
},
|
||||
"md-menu-item": { onCloseMenu: "close-menu" },
|
||||
"md-sub-menu": {
|
||||
onOpening: "opening",
|
||||
onOpened: "opened",
|
||||
onClosing: "closing",
|
||||
onClosed: "closed",
|
||||
},
|
||||
// fork: snackbar (bare opening/opened/closing/closed)
|
||||
"md-snackbar": {
|
||||
onOpening: "opening",
|
||||
onOpened: "opened",
|
||||
onClosing: "closing",
|
||||
onClosed: "closed",
|
||||
},
|
||||
// fork: sheets namespace their lifecycle events
|
||||
"md-bottom-sheet": {
|
||||
onOpening: "bottom-sheet:opening",
|
||||
onOpened: "bottom-sheet:opened",
|
||||
onClosing: "bottom-sheet:closing",
|
||||
onClosed: "bottom-sheet:closed",
|
||||
},
|
||||
"md-side-sheet": {
|
||||
onOpening: "side-sheet:opening",
|
||||
onOpened: "side-sheet:opened",
|
||||
onClosing: "side-sheet:closing",
|
||||
onClosed: "side-sheet:closed",
|
||||
},
|
||||
// labs navigation drawer
|
||||
"md-navigation-drawer": {
|
||||
onNavigationDrawerChanged: "navigation-drawer-changed",
|
||||
},
|
||||
"md-navigation-drawer-modal": {
|
||||
onNavigationDrawerChanged: "navigation-drawer-changed",
|
||||
},
|
||||
// ── Selection / navigation ─────────────────────────────────────────────
|
||||
"md-tabs": { onChange: "change" }, // upstream: bubbling 'change'
|
||||
"md-navigation-bar": { onActivated: "navigation-bar-activated" }, // labs
|
||||
"md-navigation-rail": {
|
||||
onChange: "navigation-rail:change",
|
||||
onItemActivate: "navigation-rail-item:activate",
|
||||
}, // fork
|
||||
"md-input-chip": { onRemove: "remove", onUpdateFocus: "update-focus" },
|
||||
"md-button-group": { onChange: "button-group:change" }, // fork
|
||||
"md-fab-menu": { onOpen: "fab-menu:open", onClose: "fab-menu:close" }, // fork
|
||||
"md-fab-menu-item": { onClick: "fab-menu-item:click" }, // fork
|
||||
// ── Disclosure (fork) ──────────────────────────────────────────────────
|
||||
"md-expansion-panel": { onToggle: "expansion:toggle" },
|
||||
"md-stepper": { onChange: "stepper:change" },
|
||||
// ── Pickers (fork) ─────────────────────────────────────────────────────
|
||||
"md-date-picker": { onChange: "date-picker:change" },
|
||||
"md-time-picker": { onChange: "time-picker:change" },
|
||||
// ── Data (fork) ────────────────────────────────────────────────────────
|
||||
"md-table": {
|
||||
onSort: "table:sort",
|
||||
onSelectionChange: "table:selection-change",
|
||||
},
|
||||
"md-paginator": { onPage: "paginator:page" },
|
||||
"md-tree": { onSelect: "tree:select" },
|
||||
"md-tree-item": {
|
||||
onToggle: "tree-item:toggle",
|
||||
onActivate: "tree-item:activate",
|
||||
},
|
||||
"md-virtual-scroller": { onRange: "virtual-scroll:range" },
|
||||
// ── Misc (fork) ────────────────────────────────────────────────────────
|
||||
"md-tooltip": { onShow: "tooltip-show", onHide: "tooltip-hide" },
|
||||
};
|
||||
|
||||
// ── 5. Resolve every requested tag ─────────────────────────────────────────
|
||||
const tags = (await Bun.file(ELEMENTS_TXT).text())
|
||||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const resolved: Resolved[] = [];
|
||||
const unresolved: Unresolved[] = [];
|
||||
|
||||
for (const tag of tags) {
|
||||
const candidates = defs.get(tag);
|
||||
if (!candidates || candidates.length === 0) {
|
||||
unresolved.push({
|
||||
tag,
|
||||
reason: "no @customElement entry file found in material-web/",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const def = pickDef(tag, candidates);
|
||||
resolved.push({
|
||||
tag,
|
||||
name: pascal(tag),
|
||||
file: def.file,
|
||||
className: def.className,
|
||||
events: EVENTS_BY_TAG[tag] ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
// ── 6. Group by top-level category dir, emit one wrapper file per group ─────
|
||||
const groupOf = (file: string): string => file.split("/")[0].replace(/\..*$/, "");
|
||||
const groups: Map<string, Resolved[]> = new Map();
|
||||
for (const r of resolved) {
|
||||
const g = groupOf(r.file);
|
||||
if (!groups.has(g)) groups.set(g, []);
|
||||
groups.get(g)!.push(r);
|
||||
}
|
||||
|
||||
mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
const HEADER = `// AUTO-GENERATED by generate.ts — do not edit by hand. Run \`bun run generate\`.\n`;
|
||||
const fmtEvents = (events: EventMap): string => {
|
||||
const keys = Object.keys(events);
|
||||
if (keys.length === 0) return "{}";
|
||||
const body = keys.map((k) => `${k}: '${events[k]}'`).join(", ");
|
||||
return `{${body}}`;
|
||||
};
|
||||
|
||||
const indexLines = [
|
||||
HEADER,
|
||||
`import * as React from 'react';`,
|
||||
`import {createComponent} from '@lit/react';`,
|
||||
"",
|
||||
];
|
||||
// Group elements that share the same import file (e.g. several tags per module is rare; one file = one element here).
|
||||
const writes: Promise<number>[] = [];
|
||||
for (const [group, items] of [...groups.entries()].toSorted()) {
|
||||
const fileName = `${group}.ts`;
|
||||
const lines = [
|
||||
HEADER,
|
||||
`import * as React from 'react';`,
|
||||
`import {createComponent} from '@lit/react';`,
|
||||
"",
|
||||
];
|
||||
// imports — alias element class to <Name>Element to avoid clashing with wrapper const.
|
||||
for (const r of items) {
|
||||
lines.push(
|
||||
`import {${r.className} as ${r.name}Element} from '@aphrody/material-web/${r.file}';`,
|
||||
);
|
||||
}
|
||||
lines.push("");
|
||||
for (const r of items) {
|
||||
lines.push(`export const ${r.name} = createComponent({`);
|
||||
lines.push(` react: React,`);
|
||||
lines.push(` tagName: '${r.tag}',`);
|
||||
lines.push(` elementClass: ${r.name}Element,`);
|
||||
lines.push(` events: ${fmtEvents(r.events)},`);
|
||||
lines.push(`});`);
|
||||
lines.push("");
|
||||
}
|
||||
writes.push(Bun.write(join(OUT_DIR, fileName), lines.join("\n")));
|
||||
indexLines.push(`export * from './wrappers/${group}.js';`.replace("./wrappers", "./"));
|
||||
}
|
||||
await Promise.all(writes);
|
||||
|
||||
// index.ts at package root re-exports all groups.
|
||||
const idx = [HEADER];
|
||||
for (const [group] of [...groups.entries()].toSorted()) {
|
||||
idx.push(`export * from './wrappers/${group}.js';`);
|
||||
}
|
||||
idx.push("");
|
||||
await Bun.write(join(outputRoot, "index.ts"), idx.join("\n"));
|
||||
|
||||
// ── 7. Report ──────────────────────────────────────────────────────────────
|
||||
console.log(`Tags requested: ${tags.length}`);
|
||||
console.log(`Wrappers generated: ${resolved.length} across ${groups.size} files`);
|
||||
if (unresolved.length) {
|
||||
console.log(`Unresolved (${unresolved.length}):`);
|
||||
for (const u of unresolved) console.log(` - ${u.tag}: ${u.reason}`);
|
||||
}
|
||||
// Emit a machine-readable manifest for the README.
|
||||
await Bun.write(
|
||||
join(outputRoot, ".manifest.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
resolved: resolved.map(({ tag, name, file, className, events }) => ({
|
||||
tag,
|
||||
name,
|
||||
file,
|
||||
className,
|
||||
events,
|
||||
})),
|
||||
unresolved,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
// Keep the README's component list in sync (it is the last section). This makes
|
||||
// the wrapper count + table self-maintaining — never stale after a `generate`.
|
||||
const README = join(outputRoot, "README.md");
|
||||
const rows = resolved
|
||||
.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
.map((r) => {
|
||||
const ev = Object.keys(r.events ?? {}).length || "—";
|
||||
return `| \`${r.name}\` | \`${r.tag}\` | \`@aphrody/material-web/${r.file}\` | ${ev} |`;
|
||||
})
|
||||
.join("\n");
|
||||
const listSection =
|
||||
`## Liste complète des wrappers (${resolved.length})\n\n` +
|
||||
`> Régénérée par \`generate.ts\` (\`.manifest.json\`). ${unresolved.length} non résolu(s).\n\n` +
|
||||
`| Wrapper | Tag | Import élément | # events |\n| --- | --- | --- | --- |\n${rows}\n`;
|
||||
try {
|
||||
const readme = await Bun.file(README).text();
|
||||
const updated = readme.replace(/## Liste complète des wrappers \(\d+\)[\s\S]*$/, listSection);
|
||||
if (updated !== readme) await Bun.write(README, updated);
|
||||
} catch {
|
||||
/* README optional */
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
export * from './wrappers/alert.js';
|
||||
export * from './wrappers/appbar.js';
|
||||
export * from './wrappers/autocomplete.js';
|
||||
export * from './wrappers/avatar.js';
|
||||
export * from './wrappers/backdrop.js';
|
||||
export * from './wrappers/badge.js';
|
||||
export * from './wrappers/banner.js';
|
||||
export * from './wrappers/breadcrumbs.js';
|
||||
export * from './wrappers/button.js';
|
||||
export * from './wrappers/buttongroup.js';
|
||||
export * from './wrappers/canvas3d.js';
|
||||
export * from './wrappers/card.js';
|
||||
export * from './wrappers/card3d.js';
|
||||
export * from './wrappers/carousel.js';
|
||||
export * from './wrappers/charts.js';
|
||||
export * from './wrappers/checkbox.js';
|
||||
export * from './wrappers/chips.js';
|
||||
export * from './wrappers/datepicker.js';
|
||||
export * from './wrappers/dialog.js';
|
||||
export * from './wrappers/divider.js';
|
||||
export * from './wrappers/effects.js';
|
||||
export * from './wrappers/elevation.js';
|
||||
export * from './wrappers/expansion.js';
|
||||
export * from './wrappers/fab.js';
|
||||
export * from './wrappers/fabmenu.js';
|
||||
export * from './wrappers/field.js';
|
||||
export * from './wrappers/focus.js';
|
||||
export * from './wrappers/globe3d.js';
|
||||
export * from './wrappers/gridlist.js';
|
||||
export * from './wrappers/icon.js';
|
||||
export * from './wrappers/iconbutton.js';
|
||||
export * from './wrappers/labs.js';
|
||||
export * from './wrappers/layout.js';
|
||||
export * from './wrappers/link.js';
|
||||
export * from './wrappers/list.js';
|
||||
export * from './wrappers/loadingindicator.js';
|
||||
export * from './wrappers/menu.js';
|
||||
export * from './wrappers/mobilestepper.js';
|
||||
export * from './wrappers/navigationrail.js';
|
||||
export * from './wrappers/paginator.js';
|
||||
export * from './wrappers/popover.js';
|
||||
export * from './wrappers/progress.js';
|
||||
export * from './wrappers/radio.js';
|
||||
export * from './wrappers/rating.js';
|
||||
export * from './wrappers/ripple.js';
|
||||
export * from './wrappers/scheduler.js';
|
||||
export * from './wrappers/search.js';
|
||||
export * from './wrappers/select.js';
|
||||
export * from './wrappers/sheet.js';
|
||||
export * from './wrappers/skeleton.js';
|
||||
export * from './wrappers/slider.js';
|
||||
export * from './wrappers/snackbar.js';
|
||||
export * from './wrappers/stepper.js';
|
||||
export * from './wrappers/surface.js';
|
||||
export * from './wrappers/switch.js';
|
||||
export * from './wrappers/table.js';
|
||||
export * from './wrappers/tabs.js';
|
||||
export * from './wrappers/textfield.js';
|
||||
export * from './wrappers/timepicker.js';
|
||||
export * from './wrappers/toolbar.js';
|
||||
export * from './wrappers/tooltip.js';
|
||||
export * from './wrappers/tree.js';
|
||||
export * from './wrappers/typography.js';
|
||||
export * from './wrappers/virtualscroll.js';
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
// Ambient shims for @aphrody/material-web build artifacts that are NOT present in this
|
||||
// source checkout of the fork.
|
||||
//
|
||||
// material-web's build pipeline generates two kinds of style modules that the
|
||||
// component sources import but that only exist after `npm run build`
|
||||
// (build:sass -> build:css-to-ts):
|
||||
// - `*-styles.cssresult.js` (upstream + aphrody-labs SASS components)
|
||||
// - `*.css` (labs/gb Google-Sans components import raw CSS as a CSSResult)
|
||||
//
|
||||
// We only need the *type* of these modules so the wrappers typecheck; the
|
||||
// actual CSS is irrelevant to the React wrapper layer (it ships with the
|
||||
// element at runtime). Both export `styles: CSSResult`.
|
||||
// NOTE: typed as `any`, not `CSSResult`. material-web bundles its own nested
|
||||
// `lit@2` whose `CSSResult` is a nominally different class from the wrappers'
|
||||
// `lit@3` `CSSResult`; importing either side here would make these style
|
||||
// modules non-assignable to the elements' `static styles: CSSResultOrNative[]`.
|
||||
// The wrapper layer never touches these values, so `any` is correct and safe.
|
||||
declare module "*.cssresult.js" {
|
||||
export const styles: any;
|
||||
}
|
||||
|
||||
declare module "*.css" {
|
||||
const styles: any;
|
||||
export default styles;
|
||||
export { styles };
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
md-accordion
|
||||
md-assist-chip
|
||||
md-autocomplete
|
||||
md-badge
|
||||
md-banner
|
||||
md-bottom-app-bar
|
||||
md-bottom-sheet
|
||||
md-branded-fab
|
||||
md-button
|
||||
md-button-group
|
||||
md-card
|
||||
md-carousel
|
||||
md-carousel-item
|
||||
md-checkbox
|
||||
md-chip-set
|
||||
md-circular-progress
|
||||
md-date-picker
|
||||
md-dialog
|
||||
md-divider
|
||||
md-elevated-button
|
||||
md-elevated-card
|
||||
md-elevation
|
||||
md-expansion-panel
|
||||
md-fab
|
||||
md-fab-menu
|
||||
md-fab-menu-item
|
||||
md-filled-button
|
||||
md-filled-card
|
||||
md-filled-field
|
||||
md-filled-icon-button
|
||||
md-filled-select
|
||||
md-filled-text-field
|
||||
md-filled-tonal-button
|
||||
md-filled-tonal-icon-button
|
||||
md-filter-chip
|
||||
md-focus-ring
|
||||
md-grid-list
|
||||
md-grid-tile
|
||||
md-icon
|
||||
md-icon-button
|
||||
md-input-chip
|
||||
md-item
|
||||
md-linear-progress
|
||||
md-list
|
||||
md-list-detail
|
||||
md-list-item
|
||||
md-loading-indicator
|
||||
md-menu
|
||||
md-menu-group
|
||||
md-menu-item
|
||||
md-navigation-bar
|
||||
md-navigation-drawer
|
||||
md-navigation-drawer-modal
|
||||
md-navigation-rail
|
||||
md-navigation-rail-item
|
||||
md-navigation-tab
|
||||
md-outlined-button
|
||||
md-outlined-card
|
||||
md-outlined-field
|
||||
md-outlined-icon-button
|
||||
md-outlined-segmented-button
|
||||
md-outlined-segmented-button-set
|
||||
md-outlined-select
|
||||
md-outlined-text-field
|
||||
md-paginator
|
||||
md-pane
|
||||
md-primary-tab
|
||||
md-radio
|
||||
md-ripple
|
||||
md-scaffold
|
||||
md-search-bar
|
||||
md-secondary-tab
|
||||
md-select-option
|
||||
md-side-sheet
|
||||
md-slider
|
||||
md-snackbar
|
||||
md-split-button
|
||||
md-step
|
||||
md-stepper
|
||||
md-sub-menu
|
||||
md-suggestion-chip
|
||||
md-supporting-pane
|
||||
md-switch
|
||||
md-table
|
||||
md-tabs
|
||||
md-text-button
|
||||
md-time-picker
|
||||
md-toolbar
|
||||
md-tooltip
|
||||
md-top-app-bar
|
||||
md-tree
|
||||
md-tree-item
|
||||
md-virtual-scroller
|
||||
md-webgpu-canvas
|
||||
md-avatar
|
||||
md-avatar-group
|
||||
md-alert
|
||||
md-skeleton
|
||||
md-breadcrumbs
|
||||
md-link
|
||||
md-surface
|
||||
md-backdrop
|
||||
md-popover
|
||||
md-rating
|
||||
md-line-chart
|
||||
md-bar-chart
|
||||
md-pie-chart
|
||||
md-area-chart
|
||||
md-sparkline
|
||||
md-gauge
|
||||
md-date-range-picker
|
||||
md-mobile-stepper
|
||||
md-scatter-chart
|
||||
md-radar-chart
|
||||
md-date-time-picker
|
||||
md-scheduler
|
||||
md-type
|
||||
md-3d-canvas
|
||||
md-3d-globe
|
||||
md-3d-card
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"name": "@aphrody/m3-react",
|
||||
"version": "1.4.4",
|
||||
"description": "React wrappers (@lit/react createComponent) for every <md-*> element of the @aphrody/material-web fork.",
|
||||
"homepage": "https://github.com/aphrody-code/material-web/tree/main/packages/react#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/aphrody-code/material-web/issues"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "aphrody-code",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/aphrody-code/material-web.git",
|
||||
"directory": "packages/m3-react"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"index.ts",
|
||||
"transitions",
|
||||
"adaptive",
|
||||
"state",
|
||||
"adapters",
|
||||
"wrappers",
|
||||
"data"
|
||||
],
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
"./dist/index.js",
|
||||
"./dist/**/*.js"
|
||||
],
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"bun": "./index.ts",
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./index.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./transitions": {
|
||||
"bun": "./transitions/index.ts",
|
||||
"types": "./dist/transitions/index.d.ts",
|
||||
"import": "./transitions/index.ts",
|
||||
"default": "./dist/transitions/index.js"
|
||||
},
|
||||
"./adaptive": {
|
||||
"bun": "./adaptive/index.ts",
|
||||
"types": "./dist/adaptive/index.d.ts",
|
||||
"import": "./adaptive/index.ts",
|
||||
"default": "./dist/adaptive/index.js"
|
||||
},
|
||||
"./state": {
|
||||
"bun": "./state/index.ts",
|
||||
"types": "./dist/state/index.d.ts",
|
||||
"import": "./state/index.ts",
|
||||
"default": "./dist/state/index.js"
|
||||
},
|
||||
"./adapters/tanstack": {
|
||||
"bun": "./adapters/tanstack.ts",
|
||||
"types": "./dist/adapters/tanstack.d.ts",
|
||||
"import": "./adapters/tanstack.ts",
|
||||
"default": "./dist/adapters/tanstack.js"
|
||||
},
|
||||
"./data": {
|
||||
"bun": "./data/index.ts",
|
||||
"types": "./dist/data/index.d.ts",
|
||||
"import": "./data/index.ts",
|
||||
"default": "./dist/data/index.js"
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
"./*": {
|
||||
"bun": "./wrappers/*.ts",
|
||||
"types": "./dist/wrappers/*.d.ts",
|
||||
"import": "./wrappers/*.ts",
|
||||
"default": "./dist/wrappers/*.js"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "bun run generate.ts",
|
||||
"build": "bun ../../scripts/build.ts build --only $npm_package_name",
|
||||
"typecheck": "bun check --noEmit",
|
||||
"typecheck:fast": "bun check --noEmit",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aphrody/m3-tokens": "workspace:*",
|
||||
"@aphrody/material-web": "catalog:",
|
||||
"@lit/react": "catalog:",
|
||||
"@tanstack/react-table": "catalog:",
|
||||
"@tanstack/react-virtual": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aphrody/m3-config": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.4.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Hoisted state primitives for Material Design 3 React components.
|
||||
*
|
||||
* Currently exposes {@link useTextFieldState} / {@link WebTextFieldState}: the
|
||||
* Web counterpart of Jetpack Compose `TextFieldState` / `rememberTextFieldState`,
|
||||
* an observable, transactional text + selection container for driving
|
||||
* `<md-outlined-text-field>` / `<md-filled-text-field>` without spurious
|
||||
* re-renders.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export {
|
||||
WebTextFieldState,
|
||||
useTextFieldState,
|
||||
bindTextFieldState,
|
||||
syncFromElement,
|
||||
} from "./use-text-field-state.js";
|
||||
export type {
|
||||
TextFieldSelection,
|
||||
TextFieldEditBuffer,
|
||||
TextFieldStateListener,
|
||||
SelectionCapableElement,
|
||||
} from "./use-text-field-state.js";
|
||||
@@ -0,0 +1,280 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Hoisted, observable text-field state for Material Design 3 React text fields.
|
||||
*
|
||||
* This is the Web counterpart of Jetpack Compose's `TextFieldState` /
|
||||
* `rememberTextFieldState()` paradigm: a persistent state container holding the
|
||||
* current text plus the cursor/selection, mutated through atomic, transactional
|
||||
* edits (`state.edit(...)`). It is designed to drive `<md-outlined-text-field>`
|
||||
* / `<md-filled-text-field>` (wrappers `MdOutlinedTextField` /
|
||||
* `MdFilledTextField`) without spurious re-renders while preserving the caret
|
||||
* and selection inside the Lit Shadow DOM.
|
||||
*
|
||||
* SSR-safe: the module never touches `window`/`document` at import time, and the
|
||||
* hook subscribes lazily through `useSyncExternalStore`.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { useState, useSyncExternalStore } from "react";
|
||||
|
||||
/** A caret position (`start === end`) or a directed/undirected selection range. */
|
||||
export interface TextFieldSelection {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/** The mutable buffer exposed inside an {@link WebTextFieldState.edit} block. */
|
||||
export interface TextFieldEditBuffer {
|
||||
text: string;
|
||||
selection: TextFieldSelection;
|
||||
}
|
||||
|
||||
/** A listener notified after every committed mutation. */
|
||||
export type TextFieldStateListener = () => void;
|
||||
|
||||
function clampSelection(selection: TextFieldSelection, length: number): TextFieldSelection {
|
||||
const clamp = (n: number): number => {
|
||||
if (!Number.isFinite(n) || n < 0) {
|
||||
return 0;
|
||||
}
|
||||
return n > length ? length : Math.floor(n);
|
||||
};
|
||||
const start = clamp(selection.start);
|
||||
const end = clamp(selection.end);
|
||||
// Keep start <= end for an undirected range; callers that need direction can
|
||||
// read it back as-is when start <= end already holds.
|
||||
return start <= end ? { start, end } : { start: end, end: start };
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable, transactional text-field state container.
|
||||
*
|
||||
* Mirrors Compose `TextFieldState`: the only way to mutate text + selection
|
||||
* together is the atomic {@link edit} transaction, so a single notification is
|
||||
* dispatched per logical change (text and caret never observed mid-update).
|
||||
*/
|
||||
export class WebTextFieldState {
|
||||
private _text: string;
|
||||
private _selection: TextFieldSelection;
|
||||
private readonly _listeners = new Set<TextFieldStateListener>();
|
||||
|
||||
constructor(initialText = "", initialSelection: TextFieldSelection = { start: 0, end: 0 }) {
|
||||
this._text = initialText;
|
||||
this._selection = clampSelection(initialSelection, initialText.length);
|
||||
// Bind so the methods can be passed directly to useSyncExternalStore /
|
||||
// addEventListener without losing `this`.
|
||||
this.subscribe = this.subscribe.bind(this);
|
||||
this.getSnapshot = this.getSnapshot.bind(this);
|
||||
}
|
||||
|
||||
/** Current text value. */
|
||||
get text(): string {
|
||||
return this._text;
|
||||
}
|
||||
|
||||
/** Current selection/caret, always clamped to `[0, text.length]`. */
|
||||
get selection(): TextFieldSelection {
|
||||
return this._selection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stable identity snapshot for `useSyncExternalStore`. Returns the raw text;
|
||||
* because edits are transactional, text identity changes iff a commit
|
||||
* happened, which is the correct "did the store change" signal for the value.
|
||||
*/
|
||||
getSnapshot(): string {
|
||||
return this._text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the caret / set the selection without changing text. Out-of-range
|
||||
* indices are clamped. Notifies subscribers only when the range changes.
|
||||
*/
|
||||
setSelection(start: number, end: number = start): void {
|
||||
const next = clampSelection({ start, end }, this._text.length);
|
||||
if (next.start === this._selection.start && next.end === this._selection.end) {
|
||||
return;
|
||||
}
|
||||
this._selection = next;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the whole text value, re-clamping the existing selection against
|
||||
* the new length. Prefer {@link edit} when you also need to move the caret.
|
||||
*/
|
||||
setText(text: string): void {
|
||||
if (text === this._text) {
|
||||
return;
|
||||
}
|
||||
this._text = text;
|
||||
this._selection = clampSelection(this._selection, text.length);
|
||||
this.notify();
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomic transaction over text + selection. The block receives a mutable
|
||||
* buffer initialised from the current state; mutations are validated and
|
||||
* committed together, dispatching a single notification. If the block throws,
|
||||
* nothing is committed (the state is left untouched).
|
||||
*
|
||||
* @example
|
||||
* state.edit((buffer) => {
|
||||
* buffer.text = buffer.text.toUpperCase();
|
||||
* buffer.selection = { start: buffer.text.length, end: buffer.text.length };
|
||||
* });
|
||||
*/
|
||||
edit(block: (buffer: TextFieldEditBuffer) => void): void {
|
||||
const buffer: TextFieldEditBuffer = {
|
||||
text: this._text,
|
||||
selection: { start: this._selection.start, end: this._selection.end },
|
||||
};
|
||||
block(buffer);
|
||||
const nextText = buffer.text;
|
||||
const nextSelection = clampSelection(buffer.selection, nextText.length);
|
||||
const changed =
|
||||
nextText !== this._text ||
|
||||
nextSelection.start !== this._selection.start ||
|
||||
nextSelection.end !== this._selection.end;
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
this._text = nextText;
|
||||
this._selection = nextSelection;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience editor: replace the current selection (or insert at the caret)
|
||||
* with `replacement`, then place the caret right after the inserted text.
|
||||
*/
|
||||
replaceSelection(replacement: string): void {
|
||||
this.edit((buffer) => {
|
||||
const { start, end } = buffer.selection;
|
||||
buffer.text = buffer.text.slice(0, start) + replacement + buffer.text.slice(end);
|
||||
const caret = start + replacement.length;
|
||||
buffer.selection = { start: caret, end: caret };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to commits. Returns an unsubscribe function. Shaped for direct
|
||||
* use as the first argument of `useSyncExternalStore`.
|
||||
*/
|
||||
subscribe(listener: TextFieldStateListener): () => void {
|
||||
this._listeners.add(listener);
|
||||
return () => {
|
||||
this._listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
private notify(): void {
|
||||
for (const listener of this._listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook recreating Compose's `rememberTextFieldState()`.
|
||||
*
|
||||
* The {@link WebTextFieldState} instance is created exactly once (lazy
|
||||
* `useState` initialiser) and stays stable across re-renders. The component
|
||||
* re-renders only when the committed text changes, via `useSyncExternalStore` —
|
||||
* chosen over a manual `useEffect` + `forceUpdate` because it is the React 19
|
||||
* tearing-safe primitive: it reads the snapshot synchronously during render
|
||||
* (so concurrent renders never observe a stale value) and avoids the
|
||||
* double-render that a "subscribe in effect then setState" approach incurs on
|
||||
* mount. The `getServerSnapshot` argument makes it SSR/hydration-safe.
|
||||
*
|
||||
* Because selection lives on the same instance, callers that need caret-driven
|
||||
* re-renders can subscribe to it themselves; the default snapshot intentionally
|
||||
* tracks text only, so moving the caret does not re-render the whole subtree.
|
||||
*
|
||||
* @param initialValue Initial text; caret is placed at the end of it.
|
||||
* @returns The stable, observable state container.
|
||||
*/
|
||||
export function useTextFieldState(initialValue = ""): WebTextFieldState {
|
||||
const [state] = useState(
|
||||
() =>
|
||||
new WebTextFieldState(initialValue, {
|
||||
start: initialValue.length,
|
||||
end: initialValue.length,
|
||||
}),
|
||||
);
|
||||
// Subscribe to text commits. The snapshot is the raw string, so React bails
|
||||
// out of re-rendering when an edit leaves the text unchanged.
|
||||
useSyncExternalStore(state.subscribe, state.getSnapshot, state.getSnapshot);
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* A structural view of a focusable text-entry element exposing the standard
|
||||
* selection API. Both a raw `<input>`/`<textarea>` and an `<md-*-text-field>`
|
||||
* host (which delegates these members to its inner control) satisfy it, so the
|
||||
* binding helper does not need to import the React wrapper or the Lit element.
|
||||
*/
|
||||
export interface SelectionCapableElement {
|
||||
value: string;
|
||||
selectionStart: number | null;
|
||||
selectionEnd: number | null;
|
||||
setSelectionRange?(start: number, end: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push the current {@link WebTextFieldState} value + selection onto a live
|
||||
* `<md-*-text-field>` (or raw input) element, preserving the caret/selection
|
||||
* inside the Lit Shadow DOM. Safe to call from a React effect after each
|
||||
* commit; it writes `value` only when it differs to avoid clobbering IME
|
||||
* composition, then restores the selection range.
|
||||
*
|
||||
* @returns `true` if anything was written, `false` if already in sync.
|
||||
*/
|
||||
export function bindTextFieldState(
|
||||
state: WebTextFieldState,
|
||||
element: SelectionCapableElement | null | undefined,
|
||||
): boolean {
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
let wrote = false;
|
||||
if (element.value !== state.text) {
|
||||
element.value = state.text;
|
||||
wrote = true;
|
||||
}
|
||||
const { start, end } = state.selection;
|
||||
if (element.selectionStart !== start || element.selectionEnd !== end) {
|
||||
if (typeof element.setSelectionRange === "function") {
|
||||
element.setSelectionRange(start, end);
|
||||
} else {
|
||||
element.selectionStart = start;
|
||||
element.selectionEnd = end;
|
||||
}
|
||||
wrote = true;
|
||||
}
|
||||
return wrote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a `<md-*-text-field>` (or raw input) element's current value + selection
|
||||
* back into a {@link WebTextFieldState} as a single atomic edit. Use this as the
|
||||
* `onInput`/`onSelect` handler bridge so user typing stays the source of truth.
|
||||
*/
|
||||
export function syncFromElement(
|
||||
state: WebTextFieldState,
|
||||
element: SelectionCapableElement | null | undefined,
|
||||
): void {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const caretStart = element.selectionStart;
|
||||
const caretEnd = element.selectionEnd;
|
||||
state.edit((buffer) => {
|
||||
buffer.text = element.value;
|
||||
const start = caretStart ?? buffer.text.length;
|
||||
const end = caretEnd ?? start;
|
||||
buffer.selection = { start, end };
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Adaptive layout hooks: M3 window size class boundaries + the React hook
|
||||
// rendering through the happy-dom harness.
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import * as React from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { flushSync } from "react-dom";
|
||||
import {
|
||||
BREAKPOINTS,
|
||||
classifyWidth,
|
||||
minWidthQuery,
|
||||
useWindowSizeClass,
|
||||
} from "../adaptive/index.js";
|
||||
|
||||
describe("classifyWidth — canonical M3 boundaries (600/840/1200/1600)", () => {
|
||||
test.each([
|
||||
[0, "compact"],
|
||||
[599, "compact"],
|
||||
[600, "medium"],
|
||||
[839, "medium"],
|
||||
[840, "expanded"],
|
||||
[1199, "expanded"],
|
||||
[1200, "large"],
|
||||
[1599, "large"],
|
||||
[1600, "extra-large"],
|
||||
[2560, "extra-large"],
|
||||
] as const)("%i → %s", (w, cls) => {
|
||||
expect(classifyWidth(w)).toBe(cls);
|
||||
});
|
||||
|
||||
test("BREAKPOINTS lower-bounds match the spec", () => {
|
||||
expect(BREAKPOINTS).toMatchObject({
|
||||
compact: 0,
|
||||
medium: 600,
|
||||
expanded: 840,
|
||||
large: 1200,
|
||||
"extra-large": 1600,
|
||||
});
|
||||
});
|
||||
|
||||
test("minWidthQuery builds a CSS media query", () => {
|
||||
expect(minWidthQuery("expanded")).toBe("(min-width: 840px)");
|
||||
});
|
||||
});
|
||||
|
||||
function Probe() {
|
||||
return React.createElement("span", { "data-testid": "size-class" }, useWindowSizeClass());
|
||||
}
|
||||
|
||||
describe("useWindowSizeClass", () => {
|
||||
let root: Root | undefined;
|
||||
let host: HTMLElement | undefined;
|
||||
afterEach(() => {
|
||||
flushSync(() => root?.unmount());
|
||||
host?.remove();
|
||||
root = undefined;
|
||||
host = undefined;
|
||||
});
|
||||
|
||||
test("renders a window size class and reflects the current viewport", () => {
|
||||
host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
root = createRoot(host);
|
||||
flushSync(() => root!.render(React.createElement(Probe)));
|
||||
// After mount the effect ran: the class is one of the five valid values.
|
||||
expect(["compact", "medium", "expanded", "large", "extra-large"]).toContain(
|
||||
host.querySelector("span")?.textContent ?? "",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
// Size budgets that lock in the tree-shaking guarantee: a deep family import
|
||||
// must stay tiny, and the barrel must stay light. Built from source with React
|
||||
// and @aphrody/material-web kept external (they resolve in the consumer / load the
|
||||
// element code on demand), minified + gzipped — the figures a real consumer
|
||||
// pays for the wrapper layer. Guards against accidental bloat or a regression
|
||||
// of the per-family subpath split.
|
||||
|
||||
const EXTERNAL = [
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"@lit/react",
|
||||
"@aphrody/material-web",
|
||||
"@aphrody/material-web/*",
|
||||
];
|
||||
|
||||
import { resolve } from "node:path";
|
||||
|
||||
async function gzipBytes(entry: string): Promise<number> {
|
||||
const result = await Bun.build({
|
||||
entrypoints: [resolve(import.meta.dir, "..", entry)],
|
||||
external: EXTERNAL,
|
||||
minify: true,
|
||||
target: "browser",
|
||||
});
|
||||
if (!result.success) {
|
||||
throw new Error(`build failed for ${entry}: ${result.logs.map((l) => l.message).join("; ")}`);
|
||||
}
|
||||
const sizes = await Promise.all(
|
||||
result.outputs.map(async (output) => Bun.gzipSync(Buffer.from(await output.text())).length),
|
||||
);
|
||||
return sizes.reduce((total, size) => total + size, 0);
|
||||
}
|
||||
|
||||
describe("bundle-size budgets (gzip)", () => {
|
||||
test("a single family deep-import stays under 1 KB", async () => {
|
||||
const size = await gzipBytes("wrappers/button.ts");
|
||||
expect(size, `wrappers/button.ts = ${size} B gzip`).toBeLessThan(1024);
|
||||
});
|
||||
|
||||
test("a chart family deep-import stays under 1.5 KB", async () => {
|
||||
const size = await gzipBytes("wrappers/charts.ts");
|
||||
expect(size, `wrappers/charts.ts = ${size} B gzip`).toBeLessThan(1536);
|
||||
});
|
||||
|
||||
test("the full barrel (all 115 wrappers) stays under 7 KB", async () => {
|
||||
const size = await gzipBytes("index.ts");
|
||||
expect(size, `index.ts = ${size} B gzip`).toBeLessThan(7168);
|
||||
});
|
||||
|
||||
test("the transitions entry stays under 2.5 KB", async () => {
|
||||
const size = await gzipBytes("transitions/index.ts");
|
||||
expect(size, `transitions/index.ts = ${size} B gzip`).toBeLessThan(2560);
|
||||
});
|
||||
|
||||
test("a deep family import is at least 5x lighter than the barrel", async () => {
|
||||
const family = await gzipBytes("wrappers/button.ts");
|
||||
const barrel = await gzipBytes("index.ts");
|
||||
expect(barrel / family).toBeGreaterThan(5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { DataTable, dataColumns, VirtualList } from "../data/index.ts";
|
||||
|
||||
interface Person {
|
||||
id: string;
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
const people: Person[] = Array.from({ length: 60 }, (_, i) => ({
|
||||
id: `p${i}`,
|
||||
name: `Person ${String(i).padStart(2, "0")}`,
|
||||
age: 20 + (i % 40),
|
||||
}));
|
||||
const col = dataColumns<Person>();
|
||||
const columns = [col.accessor("name", { header: "Name" }), col.accessor("age", { header: "Age" })];
|
||||
|
||||
const cellTexts = (html: string) => [...html.matchAll(/<td[^>]*>([^<]*)<\/td>/g)].map((m) => m[1]);
|
||||
|
||||
describe("DataTable", () => {
|
||||
test("renders a semantic table with headers, a caption and the first page", () => {
|
||||
const html = renderToString(
|
||||
<DataTable
|
||||
data={people}
|
||||
columns={columns}
|
||||
caption="People"
|
||||
pageSize={10}
|
||||
getRowId={(p) => p.id}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain("<caption");
|
||||
expect(html).toContain("People");
|
||||
expect(html).toContain('scope="col"');
|
||||
expect(html.match(/data-row-id=/g)?.length).toBe(10);
|
||||
expect(cellTexts(html)[0]).toBe("Person 00");
|
||||
expect(html).toMatch(/1(<!-- -->)? \/ (<!-- -->)?6/);
|
||||
});
|
||||
|
||||
test("initial sorting orders rows and marks the header", () => {
|
||||
const html = renderToString(
|
||||
<DataTable
|
||||
data={people}
|
||||
columns={columns}
|
||||
pageSize={5}
|
||||
initialSorting={[{ id: "name", desc: true }]}
|
||||
/>,
|
||||
);
|
||||
expect(cellTexts(html)[0]).toBe("Person 59");
|
||||
expect(html).toContain('aria-sort="descending"');
|
||||
});
|
||||
|
||||
test("virtual mode renders a window of rows with spacers and no pager", () => {
|
||||
const html = renderToString(
|
||||
<DataTable
|
||||
data={people}
|
||||
columns={columns}
|
||||
virtual={{ height: 260, rowHeight: 52, overscan: 0 }}
|
||||
/>,
|
||||
);
|
||||
const rendered = html.match(/data-row-id=/g)?.length ?? 0;
|
||||
expect(rendered).toBeGreaterThan(0);
|
||||
expect(rendered).toBeLessThan(people.length);
|
||||
expect(html).toContain('aria-rowcount="61"');
|
||||
expect(html).not.toContain('aria-label="Pagination"');
|
||||
});
|
||||
|
||||
test("an empty table shows the empty label", () => {
|
||||
expect(renderToString(<DataTable data={[]} columns={columns} emptyLabel="Nobody" />)).toContain(
|
||||
"Nobody",
|
||||
);
|
||||
});
|
||||
|
||||
test("clicking a header sorts, the search field filters", async () => {
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
const root = createRoot(container);
|
||||
await act(async () =>
|
||||
root.render(<DataTable data={people} columns={columns} searchable pageSize={100} />),
|
||||
);
|
||||
const firstName = () => container.querySelector("tbody td")?.textContent;
|
||||
expect(firstName()).toBe("Person 00");
|
||||
const sortName = container.querySelector<HTMLButtonElement>("th button")!;
|
||||
await act(async () => sortName.click());
|
||||
await act(async () => sortName.click());
|
||||
expect(firstName()).toBe("Person 59");
|
||||
const search = container.querySelector<HTMLInputElement>('input[type="search"]')!;
|
||||
await act(async () => {
|
||||
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!;
|
||||
setter.call(search, "Person 42");
|
||||
search.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
expect(container.querySelectorAll("tbody tr[data-row-id]").length).toBe(1);
|
||||
expect(firstName()).toBe("Person 42");
|
||||
await act(async () => root.unmount());
|
||||
container.remove();
|
||||
});
|
||||
});
|
||||
|
||||
describe("VirtualList", () => {
|
||||
test("renders only the visible window of a long list as listitems", () => {
|
||||
const items = Array.from({ length: 1000 }, (_, i) => `Item ${i}`);
|
||||
const html = renderToString(
|
||||
<VirtualList
|
||||
items={items}
|
||||
height={280}
|
||||
estimateSize={56}
|
||||
overscan={2}
|
||||
renderItem={(item) => <span>{item}</span>}
|
||||
/>,
|
||||
);
|
||||
const rendered = html.match(/<li[\s>]/g)?.length ?? 0;
|
||||
expect(rendered).toBeGreaterThan(0);
|
||||
expect(rendered).toBeLessThanOrEqual(10);
|
||||
expect(html.match(/<ul[\s>]/g)?.length).toBe(1);
|
||||
expect(html).toContain('aria-setsize="1000"');
|
||||
expect(html).toContain("Item 0");
|
||||
expect(html).not.toContain("Item 999");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test";
|
||||
import * as React from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { flushSync } from "react-dom";
|
||||
import { Fade, Collapse } from "../transitions/index.ts";
|
||||
import { useTransitionState } from "../transitions/use-transition-state.ts";
|
||||
|
||||
let root: Root | undefined;
|
||||
let host: HTMLElement | undefined;
|
||||
const originalMatchMedia = window.matchMedia;
|
||||
|
||||
afterEach(() => {
|
||||
if (root) flushSync(() => root!.unmount());
|
||||
host?.remove();
|
||||
root = undefined;
|
||||
host = undefined;
|
||||
window.matchMedia = originalMatchMedia;
|
||||
});
|
||||
|
||||
function render(element: React.ReactNode) {
|
||||
host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
root = createRoot(host);
|
||||
flushSync(() => root!.render(element));
|
||||
return host;
|
||||
}
|
||||
|
||||
describe("production transition lifecycle", () => {
|
||||
test("does not start an enter lifecycle on an already visible mount without appear", async () => {
|
||||
const entered = mock(() => {});
|
||||
function Probe() {
|
||||
const { phase } = useTransitionState(true, {
|
||||
enterTimeout: 1,
|
||||
exitTimeout: 1,
|
||||
onEntered: entered,
|
||||
});
|
||||
return <output>{phase}</output>;
|
||||
}
|
||||
const el = render(<Probe />);
|
||||
await Bun.sleep(40);
|
||||
expect(el.textContent).toBe("entered");
|
||||
expect(entered).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("cancels an interrupted enter without a stale completion callback", async () => {
|
||||
const entered = mock(() => {});
|
||||
const exited = mock(() => {});
|
||||
function Probe({ shown }: { shown: boolean }) {
|
||||
const { phase } = useTransitionState(shown, {
|
||||
enterTimeout: 80,
|
||||
exitTimeout: 5,
|
||||
onEntered: entered,
|
||||
onExited: exited,
|
||||
});
|
||||
return <output>{phase}</output>;
|
||||
}
|
||||
render(<Probe shown={false} />);
|
||||
flushSync(() => root!.render(<Probe shown />));
|
||||
await Bun.sleep(25);
|
||||
flushSync(() => root!.render(<Probe shown={false} />));
|
||||
await Bun.sleep(100);
|
||||
expect(entered).not.toHaveBeenCalled();
|
||||
expect(exited).toHaveBeenCalledTimes(1);
|
||||
expect(host!.textContent).toBe("exited");
|
||||
});
|
||||
|
||||
test("hidden preserved children cannot receive keyboard focus", () => {
|
||||
const el = render(
|
||||
<>
|
||||
<Fade in={false}>
|
||||
<button type="button" data-testid="fade">
|
||||
Hidden
|
||||
</button>
|
||||
</Fade>
|
||||
<Collapse in={false}>
|
||||
<button type="button" data-testid="collapse">
|
||||
Hidden
|
||||
</button>
|
||||
</Collapse>
|
||||
</>,
|
||||
);
|
||||
expect(el.querySelector('[data-testid="fade"]')!.hasAttribute("inert")).toBe(true);
|
||||
expect(el.querySelector('[data-testid="fade"]')!.getAttribute("aria-hidden")).toBe("true");
|
||||
expect(el.querySelector('[data-testid="collapse"]')!.closest("[inert]")).not.toBeNull();
|
||||
});
|
||||
|
||||
test("reduced motion removes CSS duration and unmounts exits immediately", () => {
|
||||
const media = new EventTarget() as MediaQueryList;
|
||||
Object.defineProperty(media, "matches", { value: true });
|
||||
window.matchMedia = () => media;
|
||||
const el = render(
|
||||
<Fade in unmountOnExit>
|
||||
<button type="button">Visible</button>
|
||||
</Fade>,
|
||||
);
|
||||
expect(el.querySelector("button")!.style.transition).toContain("0ms");
|
||||
flushSync(() =>
|
||||
root!.render(
|
||||
<Fade in={false} unmountOnExit>
|
||||
<button type="button">Visible</button>
|
||||
</Fade>,
|
||||
),
|
||||
);
|
||||
expect(el.querySelector("button")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
mapTanStackColumnsToM3,
|
||||
mapM3SortToTanStack,
|
||||
mapTanStackSortToM3,
|
||||
} from '../adapters/tanstack';
|
||||
|
||||
describe('TanStack Table Adapter for M3', () => {
|
||||
it('maps TanStack column definitions to M3 table columns', () => {
|
||||
const tanstackCols = [
|
||||
{ id: 'name', header: 'Dessert Name', enableSorting: true },
|
||||
{ id: 'calories', header: 'Calories', enableSorting: false, meta: { numeric: true } },
|
||||
{ accessorKey: 'fat', header: 'Fat (g)', meta: { filter: 'number' as const, width: '120px' } },
|
||||
];
|
||||
|
||||
const m3Cols = mapTanStackColumnsToM3(tanstackCols);
|
||||
expect(m3Cols).toHaveLength(3);
|
||||
expect(m3Cols[0]).toEqual({
|
||||
key: 'name',
|
||||
label: 'Dessert Name',
|
||||
sortable: true,
|
||||
numeric: false,
|
||||
filter: '',
|
||||
resizable: false,
|
||||
width: undefined,
|
||||
});
|
||||
expect(m3Cols[1].numeric).toBe(true);
|
||||
expect(m3Cols[1].sortable).toBe(false);
|
||||
expect(m3Cols[2].filter).toBe('number');
|
||||
expect(m3Cols[2].width).toBe('120px');
|
||||
});
|
||||
|
||||
it('bi-directionally converts sorting state', () => {
|
||||
const tanstackSort = [{ id: 'calories', desc: true }, { id: 'name', desc: false }];
|
||||
const m3Sort = mapTanStackSortToM3(tanstackSort);
|
||||
|
||||
expect(m3Sort).toEqual([
|
||||
{ key: 'calories', direction: 'desc' },
|
||||
{ key: 'name', direction: 'asc' },
|
||||
]);
|
||||
|
||||
const backToTanstack = mapM3SortToTanStack(m3Sort);
|
||||
expect(backToTanstack).toEqual(tanstackSort);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import * as React from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { flushSync } from "react-dom";
|
||||
import { Collapse, Fade, Grow, Slide, Zoom } from "../transitions/index.js";
|
||||
import { isVisible, resolveTimeout } from "../transitions/use-transition-state.js";
|
||||
|
||||
// --- Pure helpers (no DOM) -------------------------------------------------
|
||||
|
||||
describe("resolveTimeout", () => {
|
||||
test("falls back when undefined", () => {
|
||||
expect(resolveTimeout(undefined, "enter", 250)).toBe(250);
|
||||
});
|
||||
test("a single number applies to every direction", () => {
|
||||
expect(resolveTimeout(300, "enter", 250)).toBe(300);
|
||||
expect(resolveTimeout(300, "exit", 200)).toBe(300);
|
||||
expect(resolveTimeout(300, "appear", 250)).toBe(300);
|
||||
});
|
||||
test("per-direction object resolves each direction", () => {
|
||||
const t = { appear: 10, enter: 20, exit: 30 };
|
||||
expect(resolveTimeout(t, "appear", 0)).toBe(10);
|
||||
expect(resolveTimeout(t, "enter", 0)).toBe(20);
|
||||
expect(resolveTimeout(t, "exit", 0)).toBe(30);
|
||||
});
|
||||
test("appear falls back to enter, then to the fallback", () => {
|
||||
expect(resolveTimeout({ enter: 20 }, "appear", 99)).toBe(20);
|
||||
expect(resolveTimeout({ exit: 30 }, "appear", 99)).toBe(99);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isVisible", () => {
|
||||
test("entering/entered are visible; exiting/exited are not", () => {
|
||||
expect(isVisible("entering")).toBe(true);
|
||||
expect(isVisible("entered")).toBe(true);
|
||||
expect(isVisible("exiting")).toBe(false);
|
||||
expect(isVisible("exited")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Component rendering (happy-dom + flushSync) ---------------------------
|
||||
|
||||
let root: Root | null = null;
|
||||
let host: HTMLElement | null = null;
|
||||
|
||||
function render(element: React.ReactElement): HTMLElement {
|
||||
host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
root = createRoot(host);
|
||||
flushSync(() => root!.render(element));
|
||||
return host;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
if (root) flushSync(() => root!.unmount());
|
||||
host?.remove();
|
||||
root = null;
|
||||
host = null;
|
||||
});
|
||||
|
||||
describe("Fade", () => {
|
||||
test("shown: renders the child with opacity 1 and an opacity transition", () => {
|
||||
const el = render(
|
||||
<Fade in>
|
||||
<div data-testid="box">hi</div>
|
||||
</Fade>,
|
||||
);
|
||||
const box = el.querySelector<HTMLElement>('[data-testid="box"]');
|
||||
expect(box).not.toBeNull();
|
||||
expect(box!.style.opacity).toBe("1");
|
||||
expect(box!.style.transition).toContain("opacity");
|
||||
});
|
||||
|
||||
test("hidden (kept mounted): renders the child with opacity 0", () => {
|
||||
const el = render(
|
||||
<Fade in={false}>
|
||||
<div data-testid="box">hi</div>
|
||||
</Fade>,
|
||||
);
|
||||
const box = el.querySelector<HTMLElement>('[data-testid="box"]');
|
||||
expect(box).not.toBeNull();
|
||||
expect(box!.style.opacity).toBe("0");
|
||||
});
|
||||
|
||||
test("mountOnEnter + not in: renders nothing", () => {
|
||||
const el = render(
|
||||
<Fade in={false} mountOnEnter>
|
||||
<div data-testid="box">hi</div>
|
||||
</Fade>,
|
||||
);
|
||||
expect(el.querySelector('[data-testid="box"]')).toBeNull();
|
||||
});
|
||||
|
||||
test("preserves the child's own inline style", () => {
|
||||
const el = render(
|
||||
<Fade in>
|
||||
<div data-testid="box" style={{ color: "red" }}>
|
||||
hi
|
||||
</div>
|
||||
</Fade>,
|
||||
);
|
||||
const box = el.querySelector<HTMLElement>('[data-testid="box"]');
|
||||
expect(box!.style.color).toBe("red");
|
||||
expect(box!.style.opacity).toBe("1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Grow / Zoom / Slide / Collapse", () => {
|
||||
test("each renders its child when shown and applies a transition", () => {
|
||||
for (const T of [Grow, Zoom, Slide, Collapse]) {
|
||||
const el = render(
|
||||
<T in>
|
||||
<div data-testid="box">x</div>
|
||||
</T>,
|
||||
);
|
||||
const box = el.querySelector<HTMLElement>('[data-testid="box"]');
|
||||
expect(box).not.toBeNull();
|
||||
// Either the child is cloned with a transition (Fade/Grow/Zoom/Slide)
|
||||
// or it sits inside an animated wrapper (Collapse) — assert some
|
||||
// rendered element carries a transition.
|
||||
const animated = [...el.querySelectorAll<HTMLElement>("*")].some(
|
||||
(n) => n.style.transition.length > 0,
|
||||
);
|
||||
expect(animated).toBe(true);
|
||||
// reset between iterations
|
||||
if (root) flushSync(() => root!.unmount());
|
||||
host?.remove();
|
||||
root = null;
|
||||
host = null;
|
||||
}
|
||||
});
|
||||
|
||||
test("Slide/Collapse unmount nothing when shown but render null when mountOnEnter and not in", () => {
|
||||
const el = render(
|
||||
<Collapse in={false} mountOnEnter>
|
||||
<div data-testid="box">x</div>
|
||||
</Collapse>,
|
||||
);
|
||||
expect(el.querySelector('[data-testid="box"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,260 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { afterEach, describe, expect, it } from "bun:test";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { flushSync } from "react-dom";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { createElement, type ReactElement } from "react";
|
||||
import {
|
||||
bindTextFieldState,
|
||||
syncFromElement,
|
||||
useTextFieldState,
|
||||
WebTextFieldState,
|
||||
type SelectionCapableElement,
|
||||
} from "../state/use-text-field-state.js";
|
||||
|
||||
describe("WebTextFieldState (pure logic)", () => {
|
||||
it("constructs with defaults", () => {
|
||||
const state = new WebTextFieldState();
|
||||
expect(state.text).toBe("");
|
||||
expect(state.selection).toEqual({ start: 0, end: 0 });
|
||||
});
|
||||
|
||||
it("clamps the initial selection to the text length", () => {
|
||||
const state = new WebTextFieldState("abc", { start: 10, end: 99 });
|
||||
expect(state.selection).toEqual({ start: 3, end: 3 });
|
||||
});
|
||||
|
||||
it("edit() updates text and selection atomically", () => {
|
||||
const state = new WebTextFieldState("ab");
|
||||
let observed: { text: string; selStart: number } | null = null;
|
||||
state.subscribe(() => {
|
||||
observed = { text: state.text, selStart: state.selection.start };
|
||||
});
|
||||
state.edit((buffer) => {
|
||||
buffer.text = "abXYZ";
|
||||
buffer.selection = { start: 5, end: 5 };
|
||||
});
|
||||
expect(state.text).toBe("abXYZ");
|
||||
expect(state.selection).toEqual({ start: 5, end: 5 });
|
||||
// The listener must never observe a half-applied state.
|
||||
expect(observed).toEqual({ text: "abXYZ", selStart: 5 });
|
||||
});
|
||||
|
||||
it("edit() commits nothing when the block makes no change", () => {
|
||||
const state = new WebTextFieldState("hi", { start: 1, end: 1 });
|
||||
let calls = 0;
|
||||
state.subscribe(() => {
|
||||
calls++;
|
||||
});
|
||||
state.edit(() => {
|
||||
/* no-op */
|
||||
});
|
||||
expect(calls).toBe(0);
|
||||
expect(state.text).toBe("hi");
|
||||
});
|
||||
|
||||
it("edit() leaves state untouched if the block throws", () => {
|
||||
const state = new WebTextFieldState("safe", { start: 2, end: 2 });
|
||||
expect(() => {
|
||||
state.edit((buffer) => {
|
||||
buffer.text = "corrupt";
|
||||
throw new Error("boom");
|
||||
});
|
||||
}).toThrow("boom");
|
||||
expect(state.text).toBe("safe");
|
||||
expect(state.selection).toEqual({ start: 2, end: 2 });
|
||||
});
|
||||
|
||||
it("setSelection() clamps and notifies only on change", () => {
|
||||
const state = new WebTextFieldState("hello");
|
||||
let calls = 0;
|
||||
state.subscribe(() => {
|
||||
calls++;
|
||||
});
|
||||
state.setSelection(1, 3);
|
||||
expect(state.selection).toEqual({ start: 1, end: 3 });
|
||||
expect(calls).toBe(1);
|
||||
// Same range again -> no notification.
|
||||
state.setSelection(1, 3);
|
||||
expect(calls).toBe(1);
|
||||
// Out of range -> clamped to text length.
|
||||
state.setSelection(99, 100);
|
||||
expect(state.selection).toEqual({ start: 5, end: 5 });
|
||||
expect(calls).toBe(2);
|
||||
});
|
||||
|
||||
it("setSelection() defaults end to start (caret)", () => {
|
||||
const state = new WebTextFieldState("hello");
|
||||
state.setSelection(2);
|
||||
expect(state.selection).toEqual({ start: 2, end: 2 });
|
||||
});
|
||||
|
||||
it("replaceSelection() inserts and moves the caret", () => {
|
||||
const state = new WebTextFieldState("foobar", { start: 3, end: 6 });
|
||||
state.replaceSelection("X");
|
||||
expect(state.text).toBe("fooX");
|
||||
expect(state.selection).toEqual({ start: 4, end: 4 });
|
||||
});
|
||||
|
||||
it("subscribe() returns a working unsubscribe", () => {
|
||||
const state = new WebTextFieldState();
|
||||
let calls = 0;
|
||||
const off = state.subscribe(() => {
|
||||
calls++;
|
||||
});
|
||||
state.setText("a");
|
||||
off();
|
||||
state.setText("ab");
|
||||
expect(calls).toBe(1);
|
||||
});
|
||||
|
||||
it("getSnapshot() returns the current text for useSyncExternalStore", () => {
|
||||
const state = new WebTextFieldState("snap");
|
||||
expect(state.getSnapshot()).toBe("snap");
|
||||
state.setText("snap2");
|
||||
expect(state.getSnapshot()).toBe("snap2");
|
||||
});
|
||||
});
|
||||
|
||||
function makeElement(
|
||||
value = "",
|
||||
): SelectionCapableElement & { setSelectionRange(s: number, e: number): void } {
|
||||
return {
|
||||
value,
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
setSelectionRange(start: number, end: number) {
|
||||
this.selectionStart = start;
|
||||
this.selectionEnd = end;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("bindTextFieldState / syncFromElement", () => {
|
||||
it("writes value + selection onto the element", () => {
|
||||
const state = new WebTextFieldState("hello", { start: 5, end: 5 });
|
||||
const el = makeElement();
|
||||
const wrote = bindTextFieldState(state, el);
|
||||
expect(wrote).toBe(true);
|
||||
expect(el.value).toBe("hello");
|
||||
expect(el.selectionStart).toBe(5);
|
||||
expect(el.selectionEnd).toBe(5);
|
||||
});
|
||||
|
||||
it("is a no-op when element already matches", () => {
|
||||
const state = new WebTextFieldState("hi", { start: 2, end: 2 });
|
||||
const el = makeElement("hi");
|
||||
el.selectionStart = 2;
|
||||
el.selectionEnd = 2;
|
||||
expect(bindTextFieldState(state, el)).toBe(false);
|
||||
});
|
||||
|
||||
it("tolerates a null element", () => {
|
||||
const state = new WebTextFieldState();
|
||||
expect(bindTextFieldState(state, null)).toBe(false);
|
||||
expect(() => syncFromElement(state, undefined)).not.toThrow();
|
||||
});
|
||||
|
||||
it("syncFromElement() reads value + caret back atomically", () => {
|
||||
const state = new WebTextFieldState();
|
||||
const el = makeElement("typed");
|
||||
el.selectionStart = 5;
|
||||
el.selectionEnd = 5;
|
||||
syncFromElement(state, el);
|
||||
expect(state.text).toBe("typed");
|
||||
expect(state.selection).toEqual({ start: 5, end: 5 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("useTextFieldState (SSR render path)", () => {
|
||||
it("is SSR-safe and seeds initial value + end caret", () => {
|
||||
const captured: { state?: WebTextFieldState } = {};
|
||||
function Probe() {
|
||||
const state = useTextFieldState("init");
|
||||
// oxlint-disable-next-line react/immutability -- harnais de test : le composant publie l'état rendu pour les assertions
|
||||
captured.state = state;
|
||||
return createElement("output", null, state.text);
|
||||
}
|
||||
// renderToStaticMarkup exercises the getServerSnapshot path (no DOM).
|
||||
const markup = renderToStaticMarkup(createElement(Probe));
|
||||
expect(markup).toContain("init");
|
||||
expect(captured.state?.text).toBe("init");
|
||||
expect(captured.state?.selection).toEqual({ start: 4, end: 4 });
|
||||
});
|
||||
});
|
||||
|
||||
// Client render path: drive React with flushSync + happy-dom, matching the
|
||||
// package's existing harness (adaptive/transitions tests). The setup.ts preload
|
||||
// sets IS_REACT_ACT_ENVIRONMENT = false, so we do NOT use act().
|
||||
describe("useTextFieldState (client render path)", () => {
|
||||
let root: Root | null = null;
|
||||
let container: HTMLElement | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
if (root) {
|
||||
flushSync(() => root?.unmount());
|
||||
root = null;
|
||||
}
|
||||
container?.remove();
|
||||
container = null;
|
||||
});
|
||||
|
||||
function mount(element: ReactElement): void {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
flushSync(() => root?.render(element));
|
||||
}
|
||||
|
||||
it("returns a stable instance and re-renders on committed text changes", () => {
|
||||
let renders = 0;
|
||||
const instances: WebTextFieldState[] = [];
|
||||
const captured: { state?: WebTextFieldState } = {};
|
||||
|
||||
function Probe() {
|
||||
renders++;
|
||||
const state = useTextFieldState("init");
|
||||
instances.push(state);
|
||||
// oxlint-disable-next-line react/immutability -- harnais de test : le composant publie l'état rendu pour les assertions
|
||||
captured.state = state;
|
||||
return createElement("output", null, state.text);
|
||||
}
|
||||
|
||||
mount(createElement(Probe));
|
||||
expect(captured.state?.text).toBe("init");
|
||||
expect(captured.state?.selection).toEqual({ start: 4, end: 4 });
|
||||
expect(container?.querySelector("output")?.textContent).toBe("init");
|
||||
|
||||
const rendersAfterMount = renders;
|
||||
flushSync(() => {
|
||||
captured.state?.setText("changed");
|
||||
});
|
||||
expect(container?.querySelector("output")?.textContent).toBe("changed");
|
||||
expect(renders).toBeGreaterThan(rendersAfterMount);
|
||||
// Same instance across renders.
|
||||
expect(new Set(instances).size).toBe(1);
|
||||
});
|
||||
|
||||
it("does not re-render when an edit leaves the text unchanged", () => {
|
||||
let renders = 0;
|
||||
const captured: { state?: WebTextFieldState } = {};
|
||||
|
||||
function Probe() {
|
||||
renders++;
|
||||
const state = useTextFieldState("abc");
|
||||
// oxlint-disable-next-line react/immutability -- harnais de test : le composant publie l'état rendu pour les assertions
|
||||
captured.state = state;
|
||||
return createElement("output", null, state.text);
|
||||
}
|
||||
|
||||
mount(createElement(Probe));
|
||||
const baseline = renders;
|
||||
// Moving only the caret does not change the text snapshot -> no re-render.
|
||||
flushSync(() => {
|
||||
captured.state?.setSelection(1, 2);
|
||||
});
|
||||
expect(renders).toBe(baseline);
|
||||
expect(captured.state?.selection).toEqual({ start: 1, end: 2 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
// Verifies the generated @lit/react wrappers actually render their `md-*`
|
||||
// custom element through React — the core surface of the package. Uses the same
|
||||
// happy-dom harness as the transitions tests (no React Testing Library). We
|
||||
// assert what is deterministic headless: the wrapper mounts the correct,
|
||||
// upgraded custom element with a populated shadow root, and forwards children
|
||||
// to its slot. Property/event binding is exercised end-to-end by the aphrody-web gate
|
||||
// (real Chromium) since @lit/react property timing is browser-specific.
|
||||
// The ElementInternals polyfill (test/setup) lets form-associated elements upgrade.
|
||||
// oxlint-disable-next-line import/no-unassigned-import -- side effect: installs the happy-dom DOM globals this test renders into
|
||||
import "@aphrody/m3-config/test/dom";
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import * as React from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { flushSync } from "react-dom";
|
||||
|
||||
import { MdFilledButton } from "../wrappers/button.js";
|
||||
import { MdCheckbox } from "../wrappers/checkbox.js";
|
||||
import { MdIcon } from "../wrappers/icon.js";
|
||||
import { MdCard } from "../wrappers/card.js";
|
||||
import { MdOutlinedTextField } from "../wrappers/textfield.js";
|
||||
|
||||
let root: Root | undefined;
|
||||
let host: HTMLElement | undefined;
|
||||
|
||||
const tick = () => new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
async function mount(node: React.ReactNode, tag: string): Promise<HTMLElement> {
|
||||
host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
root = createRoot(host);
|
||||
flushSync(() => root!.render(node));
|
||||
await tick();
|
||||
const el = host.querySelector(tag) as HTMLElement | null;
|
||||
if (!el) throw new Error(`<${tag}> not rendered`);
|
||||
await (el as unknown as { updateComplete?: Promise<unknown> }).updateComplete;
|
||||
return el;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
flushSync(() => root?.unmount());
|
||||
host?.remove();
|
||||
root = undefined;
|
||||
host = undefined;
|
||||
});
|
||||
|
||||
describe("generated wrappers render their custom element", () => {
|
||||
test("MdFilledButton mounts an upgraded <md-filled-button> with a shadow root", async () => {
|
||||
const el = await mount(<MdFilledButton>Click</MdFilledButton>, "md-filled-button");
|
||||
expect(customElements.get("md-filled-button")).toBeDefined();
|
||||
expect(el.shadowRoot).toBeTruthy();
|
||||
expect(el.shadowRoot!.childNodes.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("MdCheckbox (form-associated, ElementInternals) upgrades and renders", async () => {
|
||||
const el = await mount(<MdCheckbox />, "md-checkbox");
|
||||
expect(el.shadowRoot).toBeTruthy();
|
||||
// attachInternals() succeeded (form association) — proves the polyfill path.
|
||||
expect(el.shadowRoot!.childNodes.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("MdIcon forwards children to its slot", async () => {
|
||||
const el = await mount(<MdIcon>home</MdIcon>, "md-icon");
|
||||
expect(el.textContent).toContain("home");
|
||||
});
|
||||
|
||||
test("MdCard and MdOutlinedTextField mount their elements", async () => {
|
||||
expect((await mount(<MdCard />, "md-card")).shadowRoot).toBeTruthy();
|
||||
expect(
|
||||
(await mount(<MdOutlinedTextField />, "md-outlined-text-field")).shadowRoot,
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { EASING } from "./create-transition.js";
|
||||
import { BaseTransitionProps, useTransitionProps } from "./use-transition-state.js";
|
||||
|
||||
export interface CollapseProps extends BaseTransitionProps {
|
||||
/** The axis along which to collapse. Defaults to `vertical` (height). */
|
||||
orientation?: "vertical" | "horizontal";
|
||||
/**
|
||||
* The size to keep visible while collapsed (e.g. `'0px'` or `'40px'` for a
|
||||
* peeking header). Defaults to `'0px'`.
|
||||
*/
|
||||
collapsedSize?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapses its child along one axis by animating its measured size between
|
||||
* `collapsedSize` and its natural extent. The MUI `Collapse` equivalent.
|
||||
*
|
||||
* The child is wrapped in a clipping container so that overflow is hidden
|
||||
* during the animation. The natural size is measured from a live ref each
|
||||
* commit, so dynamic content is handled.
|
||||
*/
|
||||
export function Collapse(props: CollapseProps): React.ReactElement | null {
|
||||
const { orientation = "vertical", collapsedSize = "0px", children } = props;
|
||||
const { mounted, phase, visible, durationMs } = useTransitionProps(props, 300, 250);
|
||||
|
||||
const innerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [naturalSize, setNaturalSize] = React.useState<number>(0);
|
||||
const isHorizontal = orientation === "horizontal";
|
||||
|
||||
// Measure the child's natural extent before paint whenever it should be
|
||||
// shown, so the enter animation has a concrete target size.
|
||||
React.useLayoutEffect(() => {
|
||||
const node = innerRef.current;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const measured = isHorizontal ? node.scrollWidth : node.scrollHeight;
|
||||
setNaturalSize(measured);
|
||||
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- phase and children are re-measurement triggers: the natural size changes with them although the body does not read them
|
||||
}, [isHorizontal, phase, children]);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sizeProp = isHorizontal ? "width" : "height";
|
||||
// `entered` lets the content size itself naturally (responsive); the other
|
||||
// phases use an explicit pixel size so the transition can interpolate.
|
||||
const targetSize = phase === "entered" ? "auto" : visible ? `${naturalSize}px` : collapsedSize;
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
[sizeProp]: targetSize,
|
||||
overflow: "hidden",
|
||||
transition: `${sizeProp} ${durationMs}ms ${EASING.standard}`,
|
||||
willChange: sizeProp,
|
||||
};
|
||||
|
||||
const innerStyle: React.CSSProperties = isHorizontal
|
||||
? { display: "inline-block", height: "100%" }
|
||||
: {};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={containerStyle}
|
||||
inert={!visible && collapsedSize === "0px"}
|
||||
aria-hidden={!visible && collapsedSize === "0px"}
|
||||
>
|
||||
<div ref={innerRef} style={innerStyle}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { createTransition, EASING } from "./create-transition.js";
|
||||
|
||||
/**
|
||||
* Fades its child in and out by animating `opacity`, using M3 motion tokens.
|
||||
* The MUI `Fade` equivalent.
|
||||
*/
|
||||
export const Fade = createTransition("Fade", {
|
||||
enter: { easing: EASING.standardDecelerate, fallbackMs: 250 },
|
||||
exit: { easing: EASING.standardAccelerate, fallbackMs: 200 },
|
||||
properties: ["opacity"],
|
||||
style: ({ visible }) => ({ opacity: visible ? 1 : 0 }),
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { createTransition, EASING } from "./create-transition.js";
|
||||
|
||||
/**
|
||||
* Scales its child up from a small, faded state and fades/scales it back out.
|
||||
* Combines opacity with a `transform: scale()`, like the MUI `Grow`.
|
||||
*/
|
||||
export const Grow = createTransition("Grow", {
|
||||
enter: { easing: EASING.emphasizedDecelerate, fallbackMs: 300 },
|
||||
exit: { easing: EASING.emphasizedAccelerate, fallbackMs: 200 },
|
||||
properties: ["opacity", "transform"],
|
||||
style: ({ visible, childStyle }) => ({
|
||||
opacity: visible ? 1 : 0,
|
||||
transform: visible ? "scale(1)" : "scale(0.75)",
|
||||
transformOrigin: childStyle?.transformOrigin ?? "center center",
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { createTransition, EASING } from "./create-transition.js";
|
||||
import type { BaseTransitionProps } from "./use-transition-state.js";
|
||||
|
||||
/** The edge the child slides in from. */
|
||||
export type SlideDirection = "left" | "right" | "up" | "down";
|
||||
|
||||
export interface SlideProps extends BaseTransitionProps {
|
||||
/**
|
||||
* The edge the child enters from (and exits toward). Defaults to `down`,
|
||||
* i.e. the child starts below its resting position and slides up into view.
|
||||
*/
|
||||
direction?: SlideDirection;
|
||||
/**
|
||||
* The off-screen offset used while hidden. Accepts any CSS length; defaults
|
||||
* to `100%` of the element's own size on the relevant axis.
|
||||
*/
|
||||
offset?: string;
|
||||
}
|
||||
|
||||
function hiddenTransform(direction: SlideDirection, offset: string): string {
|
||||
switch (direction) {
|
||||
case "left":
|
||||
return `translateX(${offset})`;
|
||||
case "right":
|
||||
return `translateX(-${offset})`;
|
||||
case "up":
|
||||
return `translateY(${offset})`;
|
||||
case "down":
|
||||
default:
|
||||
return `translateY(-${offset})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Slides its child in from a screen edge and back out, animating `transform`.
|
||||
* The MUI `Slide` equivalent.
|
||||
*/
|
||||
export const Slide = createTransition<SlideProps>("Slide", {
|
||||
enter: { easing: EASING.emphasizedDecelerate, fallbackMs: 300 },
|
||||
exit: { easing: EASING.emphasizedAccelerate, fallbackMs: 200 },
|
||||
properties: ["transform"],
|
||||
style: ({ visible, props }) => ({
|
||||
transform: visible
|
||||
? "translate(0, 0)"
|
||||
: hiddenTransform(props.direction ?? "down", props.offset ?? "100%"),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { createTransition, EASING } from "./create-transition.js";
|
||||
|
||||
/**
|
||||
* Zooms its child in from scale 0 to its natural size and back out, animating
|
||||
* only `transform`. The MUI `Zoom` equivalent.
|
||||
*/
|
||||
export const Zoom = createTransition("Zoom", {
|
||||
enter: { easing: EASING.emphasizedDecelerate, fallbackMs: 250 },
|
||||
exit: { easing: EASING.emphasizedAccelerate, fallbackMs: 200 },
|
||||
properties: ["transform"],
|
||||
style: ({ visible, childStyle }) => ({
|
||||
transform: visible ? "scale(1)" : "scale(0)",
|
||||
transformOrigin: childStyle?.transformOrigin ?? "center center",
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { BaseTransitionProps, useTransitionProps } from "./use-transition-state.js";
|
||||
|
||||
/** M3 easing tokens with their spec values as fallbacks. */
|
||||
export const EASING = {
|
||||
standard: "var(--md-sys-motion-easing-standard, cubic-bezier(0.2, 0, 0, 1))",
|
||||
standardDecelerate: "var(--md-sys-motion-easing-standard-decelerate, cubic-bezier(0, 0, 0, 1))",
|
||||
standardAccelerate: "var(--md-sys-motion-easing-standard-accelerate, cubic-bezier(0.3, 0, 1, 1))",
|
||||
emphasizedDecelerate:
|
||||
"var(--md-sys-motion-easing-emphasized-decelerate, cubic-bezier(0.05, 0.7, 0.1, 1))",
|
||||
emphasizedAccelerate:
|
||||
"var(--md-sys-motion-easing-emphasized-accelerate, cubic-bezier(0.3, 0, 0.8, 0.15))",
|
||||
} as const;
|
||||
|
||||
/** How one style transition looks; everything else (state machine, cloning, timing) is shared. */
|
||||
export interface TransitionSpec<P extends BaseTransitionProps> {
|
||||
enter: { easing: string; fallbackMs: number };
|
||||
exit: { easing: string; fallbackMs: number };
|
||||
/** CSS properties that animate, in `transition` / `will-change` order. */
|
||||
properties: readonly string[];
|
||||
/** The values of those properties for the current visual state. */
|
||||
style(state: {
|
||||
visible: boolean;
|
||||
childStyle: React.CSSProperties | undefined;
|
||||
props: P;
|
||||
}): React.CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a transition component that clones its single child with an inline style: the visual
|
||||
* state comes from `spec.style`, the `transition` and `will-change` declarations from the spec.
|
||||
*/
|
||||
export function createTransition<P extends BaseTransitionProps = BaseTransitionProps>(
|
||||
name: string,
|
||||
spec: TransitionSpec<P>,
|
||||
): (props: P) => React.ReactElement | null {
|
||||
const willChange = spec.properties.join(", ");
|
||||
function Transition(props: P): React.ReactElement | null {
|
||||
const { mounted, visible, durationMs } = useTransitionProps(
|
||||
props,
|
||||
spec.enter.fallbackMs,
|
||||
spec.exit.fallbackMs,
|
||||
);
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
const easing = visible ? spec.enter.easing : spec.exit.easing;
|
||||
const childStyle = (props.children.props as { style?: React.CSSProperties }).style;
|
||||
const style: React.CSSProperties = {
|
||||
...childStyle,
|
||||
...spec.style({ visible, childStyle, props }),
|
||||
transition: spec.properties.map((p) => `${p} ${durationMs}ms ${easing}`).join(", "),
|
||||
willChange,
|
||||
};
|
||||
const childProps = props.children.props as React.HTMLAttributes<HTMLElement>;
|
||||
return React.cloneElement(props.children, {
|
||||
style,
|
||||
inert: !visible || childProps.inert,
|
||||
"aria-hidden": !visible || childProps["aria-hidden"],
|
||||
} as React.HTMLAttributes<HTMLElement>);
|
||||
}
|
||||
Transition.displayName = name;
|
||||
return Transition;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export { Collapse } from "./Collapse.js";
|
||||
export type { CollapseProps } from "./Collapse.js";
|
||||
export { Fade } from "./Fade.js";
|
||||
export { Grow } from "./Grow.js";
|
||||
export { Slide } from "./Slide.js";
|
||||
export type { SlideDirection, SlideProps } from "./Slide.js";
|
||||
export { Zoom } from "./Zoom.js";
|
||||
export type {
|
||||
BaseTransitionProps,
|
||||
TransitionPhase,
|
||||
TransitionTimeout,
|
||||
} from "./use-transition-state.js";
|
||||
@@ -0,0 +1,233 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
/**
|
||||
* Resolves an MUI-style `timeout` (a single duration or per-direction
|
||||
* durations) to the millisecond value for the given direction.
|
||||
*/
|
||||
export type TransitionTimeout = number | { appear?: number; enter?: number; exit?: number };
|
||||
|
||||
export function resolveTimeout(
|
||||
timeout: TransitionTimeout | undefined,
|
||||
direction: "appear" | "enter" | "exit",
|
||||
fallback: number,
|
||||
): number {
|
||||
if (timeout == null) {
|
||||
return fallback;
|
||||
}
|
||||
if (typeof timeout === "number") {
|
||||
return timeout;
|
||||
}
|
||||
if (direction === "appear") {
|
||||
return timeout.appear ?? timeout.enter ?? fallback;
|
||||
}
|
||||
return timeout[direction] ?? fallback;
|
||||
}
|
||||
|
||||
/** The discrete lifecycle phases of a transition. */
|
||||
export type TransitionPhase = "entering" | "entered" | "exiting" | "exited";
|
||||
|
||||
/** Props shared by every transition component. */
|
||||
export interface BaseTransitionProps {
|
||||
/** Whether the child should be shown. */
|
||||
in: boolean;
|
||||
/**
|
||||
* Duration in ms, or per-direction durations. When omitted, the component's
|
||||
* M3 motion-token default is used.
|
||||
*/
|
||||
timeout?: TransitionTimeout;
|
||||
/** Animate on the initial mount when `in` is already true. */
|
||||
appear?: boolean;
|
||||
/** Keep the child mounted while hidden instead of unmounting it. */
|
||||
mountOnEnter?: boolean;
|
||||
/** Unmount the child once it has finished exiting. */
|
||||
unmountOnExit?: boolean;
|
||||
/** The single React element to animate. */
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
export interface UseTransitionStateResult {
|
||||
/** Whether the child should currently be rendered in the tree. */
|
||||
mounted: boolean;
|
||||
/** The current lifecycle phase. */
|
||||
phase: TransitionPhase;
|
||||
/** Whether this is the very first commit (used to gate `appear`). */
|
||||
isAppearing: boolean;
|
||||
/** Call from the element's `onTransitionEnd` to settle the phase. */
|
||||
handleTransitionEnd: () => void;
|
||||
}
|
||||
|
||||
const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
||||
|
||||
function subscribeReducedMotion(onChange: () => void): () => void {
|
||||
if (typeof window === "undefined" || !window.matchMedia) return () => {};
|
||||
const media = window.matchMedia(REDUCED_MOTION_QUERY);
|
||||
media.addEventListener("change", onChange);
|
||||
return () => media.removeEventListener("change", onChange);
|
||||
}
|
||||
|
||||
function useReducedMotion(): boolean {
|
||||
return React.useSyncExternalStore(
|
||||
subscribeReducedMotion,
|
||||
() => typeof window !== "undefined" && !!window.matchMedia?.(REDUCED_MOTION_QUERY).matches,
|
||||
() => false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drives mount/unmount and entering/entered/exiting/exited phases from the
|
||||
* boolean `in` prop, using a small internal state machine. No external
|
||||
* dependency. SSR-safe (effects run only on the client).
|
||||
*/
|
||||
export function useTransitionState(
|
||||
inProp: boolean,
|
||||
{
|
||||
appear = false,
|
||||
mountOnEnter = false,
|
||||
unmountOnExit = false,
|
||||
enterTimeout,
|
||||
exitTimeout,
|
||||
appearTimeout = enterTimeout,
|
||||
onEntered,
|
||||
onExited,
|
||||
}: {
|
||||
appear?: boolean;
|
||||
mountOnEnter?: boolean;
|
||||
unmountOnExit?: boolean;
|
||||
enterTimeout: number;
|
||||
exitTimeout: number;
|
||||
appearTimeout?: number;
|
||||
onEntered?: () => void;
|
||||
onExited?: () => void;
|
||||
},
|
||||
): UseTransitionStateResult {
|
||||
const firstRender = React.useRef(true);
|
||||
const initialSteady = React.useRef(!inProp || !appear);
|
||||
const initialIn = React.useRef(inProp);
|
||||
const appearing = React.useRef(inProp && appear);
|
||||
const callbacks = React.useRef({ onEntered, onExited });
|
||||
callbacks.current = { onEntered, onExited };
|
||||
|
||||
const [phase, setPhase] = React.useState<TransitionPhase>(() => {
|
||||
if (inProp) {
|
||||
return appear ? "exited" : "entered";
|
||||
}
|
||||
return "exited";
|
||||
});
|
||||
|
||||
// Whether the element is in the DOM at all.
|
||||
const [mounted, setMounted] = React.useState(() => inProp || (!mountOnEnter && !unmountOnExit));
|
||||
|
||||
const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
const settlement = React.useRef<(() => void) | undefined>(undefined);
|
||||
const clear = () => {
|
||||
if (timer.current !== undefined) {
|
||||
clearTimeout(timer.current);
|
||||
timer.current = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const initial = firstRender.current;
|
||||
firstRender.current = false;
|
||||
// A steady initial state must not start a transition or fire its callback.
|
||||
if (initialIn.current !== inProp) initialSteady.current = false;
|
||||
if ((initial && (!inProp || !appear)) || initialSteady.current) return clear;
|
||||
let cancelled = false;
|
||||
let settled = false;
|
||||
let raf: number | undefined;
|
||||
const duration = inProp ? (appearing.current ? appearTimeout : enterTimeout) : exitTimeout;
|
||||
const settle = () => {
|
||||
if (cancelled || settled) return;
|
||||
settled = true;
|
||||
clear();
|
||||
appearing.current = false;
|
||||
setPhase(inProp ? "entered" : "exited");
|
||||
if (!inProp && unmountOnExit) setMounted(false);
|
||||
(inProp ? callbacks.current.onEntered : callbacks.current.onExited)?.();
|
||||
};
|
||||
settlement.current = settle;
|
||||
clear();
|
||||
if (inProp) {
|
||||
setMounted(true);
|
||||
if (duration <= 0) settle();
|
||||
else {
|
||||
// Commit the hidden state before entering so browsers can interpolate.
|
||||
setPhase("exited");
|
||||
raf = requestAnimationFrame(() => {
|
||||
setPhase("entering");
|
||||
timer.current = setTimeout(settle, duration);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
appearing.current = false;
|
||||
setPhase("exiting");
|
||||
if (duration <= 0) settle();
|
||||
else timer.current = setTimeout(settle, duration);
|
||||
}
|
||||
return () => {
|
||||
cancelled = true;
|
||||
settlement.current = undefined;
|
||||
if (raf !== undefined) cancelAnimationFrame(raf);
|
||||
clear();
|
||||
};
|
||||
// Callbacks are kept current without restarting an animation on every render.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inProp, enterTimeout, exitTimeout, appearTimeout, unmountOnExit]);
|
||||
|
||||
const handleTransitionEnd = React.useCallback(() => {
|
||||
if (phase === "entering" || phase === "exiting") settlement.current?.();
|
||||
}, [phase]);
|
||||
|
||||
return { mounted, phase, isAppearing: appearing.current, handleTransitionEnd };
|
||||
}
|
||||
|
||||
/** True while the element should be in its "shown" visual state. */
|
||||
export function isVisible(phase: TransitionPhase): boolean {
|
||||
return phase === "entering" || phase === "entered";
|
||||
}
|
||||
|
||||
/**
|
||||
* What every transition component derives from its props: the resolved enter/exit durations,
|
||||
* the state machine and the current visual state. `durationMs` is the one of the running direction.
|
||||
*/
|
||||
export function useTransitionProps(
|
||||
props: BaseTransitionProps,
|
||||
enterFallbackMs: number,
|
||||
exitFallbackMs: number,
|
||||
) {
|
||||
const {
|
||||
in: inProp,
|
||||
timeout,
|
||||
appear = false,
|
||||
mountOnEnter = false,
|
||||
unmountOnExit = false,
|
||||
} = props;
|
||||
const reduced = useReducedMotion();
|
||||
const enterMs = reduced ? 0 : resolveTimeout(timeout, "enter", enterFallbackMs);
|
||||
const exitMs = reduced ? 0 : resolveTimeout(timeout, "exit", exitFallbackMs);
|
||||
const appearMs = reduced ? 0 : resolveTimeout(timeout, "appear", enterFallbackMs);
|
||||
const { mounted, phase, isAppearing } = useTransitionState(inProp, {
|
||||
appear,
|
||||
mountOnEnter,
|
||||
unmountOnExit,
|
||||
enterTimeout: enterMs,
|
||||
exitTimeout: exitMs,
|
||||
appearTimeout: appearMs,
|
||||
});
|
||||
const resolvedPhase = reduced ? (inProp ? "entered" : "exited") : phase;
|
||||
const visible = isVisible(resolvedPhase);
|
||||
return {
|
||||
mounted: reduced ? inProp || (!mountOnEnter && !unmountOnExit) : mounted,
|
||||
phase: resolvedPhase,
|
||||
visible,
|
||||
durationMs: isAppearing ? appearMs : visible ? enterMs : exitMs,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": [
|
||||
"./tsconfig.json",
|
||||
"@aphrody/m3-config/tsconfig.build.json"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"wrappers/**/*.ts",
|
||||
"transitions/**/*.ts",
|
||||
"transitions/**/*.tsx",
|
||||
"adaptive/**/*.ts",
|
||||
"state/**/*.ts",
|
||||
"adapters/**/*.ts",
|
||||
"data/**/*.ts",
|
||||
"data/**/*.tsx",
|
||||
"material-web-shims.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"generate.ts",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"extends": "@aphrody/m3-config/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext.Disposable"
|
||||
],
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"verbatimModuleSyntax": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"types": [
|
||||
"react",
|
||||
"react-dom"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"wrappers/**/*.ts",
|
||||
"transitions/**/*.ts",
|
||||
"transitions/**/*.tsx",
|
||||
"adaptive/**/*.ts",
|
||||
"state/**/*.ts",
|
||||
"data/**/*.ts",
|
||||
"data/**/*.tsx",
|
||||
"*.ts",
|
||||
"*.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"generate.ts",
|
||||
"tsup.config.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdAlert as MdAlertElement} from '@aphrody/material-web/alert/alert.js';
|
||||
|
||||
export const MdAlert = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-alert',
|
||||
elementClass: MdAlertElement,
|
||||
events: {onClose: 'close'},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdBottomAppBar as MdBottomAppBarElement} from '@aphrody/material-web/appbar/bottom-app-bar.js';
|
||||
import {MdTopAppBar as MdTopAppBarElement} from '@aphrody/material-web/appbar/top-app-bar.js';
|
||||
|
||||
export const MdBottomAppBar = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-bottom-app-bar',
|
||||
elementClass: MdBottomAppBarElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdTopAppBar = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-top-app-bar',
|
||||
elementClass: MdTopAppBarElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdAutocomplete as MdAutocompleteElement} from '@aphrody/material-web/autocomplete/autocomplete.js';
|
||||
|
||||
export const MdAutocomplete = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-autocomplete',
|
||||
elementClass: MdAutocompleteElement,
|
||||
events: {onInput: 'input', onSelect: 'autocomplete:select'},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdAvatar as MdAvatarElement} from '@aphrody/material-web/avatar/avatar.js';
|
||||
import {MdAvatarGroup as MdAvatarGroupElement} from '@aphrody/material-web/avatar/avatar-group.js';
|
||||
|
||||
export const MdAvatar = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-avatar',
|
||||
elementClass: MdAvatarElement,
|
||||
events: {onError: 'error'},
|
||||
});
|
||||
|
||||
export const MdAvatarGroup = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-avatar-group',
|
||||
elementClass: MdAvatarGroupElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdBackdrop as MdBackdropElement} from '@aphrody/material-web/backdrop/backdrop.js';
|
||||
|
||||
export const MdBackdrop = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-backdrop',
|
||||
elementClass: MdBackdropElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdBadge as MdBadgeElement} from '@aphrody/material-web/badge/badge.js';
|
||||
|
||||
export const MdBadge = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-badge',
|
||||
elementClass: MdBadgeElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdBanner as MdBannerElement} from '@aphrody/material-web/banner/banner.js';
|
||||
|
||||
export const MdBanner = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-banner',
|
||||
elementClass: MdBannerElement,
|
||||
events: {onClose: 'close'},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdBreadcrumbs as MdBreadcrumbsElement} from '@aphrody/material-web/breadcrumbs/breadcrumbs.js';
|
||||
|
||||
export const MdBreadcrumbs = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-breadcrumbs',
|
||||
elementClass: MdBreadcrumbsElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdElevatedButton as MdElevatedButtonElement} from '@aphrody/material-web/button/elevated-button.js';
|
||||
import {MdFilledButton as MdFilledButtonElement} from '@aphrody/material-web/button/filled-button.js';
|
||||
import {MdFilledTonalButton as MdFilledTonalButtonElement} from '@aphrody/material-web/button/filled-tonal-button.js';
|
||||
import {MdOutlinedButton as MdOutlinedButtonElement} from '@aphrody/material-web/button/outlined-button.js';
|
||||
import {MdTextButton as MdTextButtonElement} from '@aphrody/material-web/button/text-button.js';
|
||||
|
||||
export const MdElevatedButton = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-elevated-button',
|
||||
elementClass: MdElevatedButtonElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdFilledButton = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-filled-button',
|
||||
elementClass: MdFilledButtonElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdFilledTonalButton = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-filled-tonal-button',
|
||||
elementClass: MdFilledTonalButtonElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdOutlinedButton = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-outlined-button',
|
||||
elementClass: MdOutlinedButtonElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdTextButton = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-text-button',
|
||||
elementClass: MdTextButtonElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdButtonGroup as MdButtonGroupElement} from '@aphrody/material-web/buttongroup/button-group.js';
|
||||
|
||||
export const MdButtonGroup = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-button-group',
|
||||
elementClass: MdButtonGroupElement,
|
||||
events: {onChange: 'button-group:change'},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {M3Canvas3D as Md3dCanvasElement} from '@aphrody/material-web/canvas3d/canvas-3d.js';
|
||||
|
||||
export const Md3dCanvas = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-3d-canvas',
|
||||
elementClass: Md3dCanvasElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdCard as MdCardElement} from '@aphrody/material-web/card/card.js';
|
||||
|
||||
export const MdCard = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-card',
|
||||
elementClass: MdCardElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {M3Card3D as Md3dCardElement} from '@aphrody/material-web/card3d/card-3d.js';
|
||||
|
||||
export const Md3dCard = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-3d-card',
|
||||
elementClass: Md3dCardElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdCarousel as MdCarouselElement} from '@aphrody/material-web/carousel/carousel.js';
|
||||
import {MdCarouselItem as MdCarouselItemElement} from '@aphrody/material-web/carousel/carousel-item.js';
|
||||
|
||||
export const MdCarousel = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-carousel',
|
||||
elementClass: MdCarouselElement,
|
||||
events: {onChange: 'carousel-change'},
|
||||
});
|
||||
|
||||
export const MdCarouselItem = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-carousel-item',
|
||||
elementClass: MdCarouselItemElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdLineChart as MdLineChartElement} from '@aphrody/material-web/charts/line-chart.js';
|
||||
import {MdBarChart as MdBarChartElement} from '@aphrody/material-web/charts/bar-chart.js';
|
||||
import {MdPieChart as MdPieChartElement} from '@aphrody/material-web/charts/pie-chart.js';
|
||||
import {MdAreaChart as MdAreaChartElement} from '@aphrody/material-web/charts/area-chart.js';
|
||||
import {MdSparkline as MdSparklineElement} from '@aphrody/material-web/charts/sparkline.js';
|
||||
import {MdGauge as MdGaugeElement} from '@aphrody/material-web/charts/gauge.js';
|
||||
import {MdScatterChart as MdScatterChartElement} from '@aphrody/material-web/charts/scatter-chart.js';
|
||||
import {MdRadarChart as MdRadarChartElement} from '@aphrody/material-web/charts/radar-chart.js';
|
||||
|
||||
export const MdLineChart = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-line-chart',
|
||||
elementClass: MdLineChartElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdBarChart = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-bar-chart',
|
||||
elementClass: MdBarChartElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdPieChart = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-pie-chart',
|
||||
elementClass: MdPieChartElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdAreaChart = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-area-chart',
|
||||
elementClass: MdAreaChartElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdSparkline = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-sparkline',
|
||||
elementClass: MdSparklineElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdGauge = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-gauge',
|
||||
elementClass: MdGaugeElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdScatterChart = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-scatter-chart',
|
||||
elementClass: MdScatterChartElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdRadarChart = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-radar-chart',
|
||||
elementClass: MdRadarChartElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdCheckbox as MdCheckboxElement} from '@aphrody/material-web/checkbox/checkbox.js';
|
||||
|
||||
export const MdCheckbox = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-checkbox',
|
||||
elementClass: MdCheckboxElement,
|
||||
events: {onChange: 'change', onInput: 'input'},
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdAssistChip as MdAssistChipElement} from '@aphrody/material-web/chips/assist-chip.js';
|
||||
import {MdChipSet as MdChipSetElement} from '@aphrody/material-web/chips/chip-set.js';
|
||||
import {MdFilterChip as MdFilterChipElement} from '@aphrody/material-web/chips/filter-chip.js';
|
||||
import {MdInputChip as MdInputChipElement} from '@aphrody/material-web/chips/input-chip.js';
|
||||
import {MdSuggestionChip as MdSuggestionChipElement} from '@aphrody/material-web/chips/suggestion-chip.js';
|
||||
|
||||
export const MdAssistChip = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-assist-chip',
|
||||
elementClass: MdAssistChipElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdChipSet = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-chip-set',
|
||||
elementClass: MdChipSetElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdFilterChip = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-filter-chip',
|
||||
elementClass: MdFilterChipElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdInputChip = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-input-chip',
|
||||
elementClass: MdInputChipElement,
|
||||
events: {onRemove: 'remove', onUpdateFocus: 'update-focus'},
|
||||
});
|
||||
|
||||
export const MdSuggestionChip = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-suggestion-chip',
|
||||
elementClass: MdSuggestionChipElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdDatePicker as MdDatePickerElement} from '@aphrody/material-web/datepicker/date-picker.js';
|
||||
import {MdDateRangePicker as MdDateRangePickerElement} from '@aphrody/material-web/datepicker/date-range-picker.js';
|
||||
import {MdDateTimePicker as MdDateTimePickerElement} from '@aphrody/material-web/datepicker/date-time-picker.js';
|
||||
|
||||
export const MdDatePicker = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-date-picker',
|
||||
elementClass: MdDatePickerElement,
|
||||
events: {onChange: 'date-picker:change'},
|
||||
});
|
||||
|
||||
export const MdDateRangePicker = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-date-range-picker',
|
||||
elementClass: MdDateRangePickerElement,
|
||||
events: {onInput: 'input', onChange: 'change', onDateRangeChange: 'date-range-picker:change'},
|
||||
});
|
||||
|
||||
export const MdDateTimePicker = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-date-time-picker',
|
||||
elementClass: MdDateTimePickerElement,
|
||||
events: {onInput: 'input', onChange: 'change', onDateTimeChange: 'date-time-picker:change'},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdDialog as MdDialogElement} from '@aphrody/material-web/dialog/dialog.js';
|
||||
|
||||
export const MdDialog = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-dialog',
|
||||
elementClass: MdDialogElement,
|
||||
events: {onOpen: 'open', onOpened: 'opened', onClose: 'close', onClosed: 'closed', onCancel: 'cancel'},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdDivider as MdDividerElement} from '@aphrody/material-web/divider/divider.js';
|
||||
|
||||
export const MdDivider = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-divider',
|
||||
elementClass: MdDividerElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdWebgpuCanvas as MdWebgpuCanvasElement} from '@aphrody/material-web/effects/webgpu-canvas.js';
|
||||
|
||||
export const MdWebgpuCanvas = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-webgpu-canvas',
|
||||
elementClass: MdWebgpuCanvasElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdElevation as MdElevationElement} from '@aphrody/material-web/elevation/elevation.js';
|
||||
|
||||
export const MdElevation = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-elevation',
|
||||
elementClass: MdElevationElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdAccordion as MdAccordionElement} from '@aphrody/material-web/expansion/accordion.js';
|
||||
import {MdExpansionPanel as MdExpansionPanelElement} from '@aphrody/material-web/expansion/expansion-panel.js';
|
||||
|
||||
export const MdAccordion = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-accordion',
|
||||
elementClass: MdAccordionElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdExpansionPanel = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-expansion-panel',
|
||||
elementClass: MdExpansionPanelElement,
|
||||
events: {onToggle: 'expansion:toggle'},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdBrandedFab as MdBrandedFabElement} from '@aphrody/material-web/fab/branded-fab.js';
|
||||
import {MdFab as MdFabElement} from '@aphrody/material-web/fab/fab.js';
|
||||
|
||||
export const MdBrandedFab = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-branded-fab',
|
||||
elementClass: MdBrandedFabElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdFab = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-fab',
|
||||
elementClass: MdFabElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdFabMenu as MdFabMenuElement} from '@aphrody/material-web/fabmenu/fab-menu.js';
|
||||
import {MdFabMenuItem as MdFabMenuItemElement} from '@aphrody/material-web/fabmenu/fab-menu-item.js';
|
||||
|
||||
export const MdFabMenu = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-fab-menu',
|
||||
elementClass: MdFabMenuElement,
|
||||
events: {onOpen: 'fab-menu:open', onClose: 'fab-menu:close'},
|
||||
});
|
||||
|
||||
export const MdFabMenuItem = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-fab-menu-item',
|
||||
elementClass: MdFabMenuItemElement,
|
||||
events: {onClick: 'fab-menu-item:click'},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdFilledField as MdFilledFieldElement} from '@aphrody/material-web/field/filled-field.js';
|
||||
import {MdOutlinedField as MdOutlinedFieldElement} from '@aphrody/material-web/field/outlined-field.js';
|
||||
|
||||
export const MdFilledField = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-filled-field',
|
||||
elementClass: MdFilledFieldElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdOutlinedField = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-outlined-field',
|
||||
elementClass: MdOutlinedFieldElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdFocusRing as MdFocusRingElement} from '@aphrody/material-web/focus/md-focus-ring.js';
|
||||
|
||||
export const MdFocusRing = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-focus-ring',
|
||||
elementClass: MdFocusRingElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {M3Globe3D as Md3dGlobeElement} from '@aphrody/material-web/globe3d/globe-3d.js';
|
||||
|
||||
export const Md3dGlobe = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-3d-globe',
|
||||
elementClass: Md3dGlobeElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdGridList as MdGridListElement} from '@aphrody/material-web/gridlist/grid-list.js';
|
||||
import {MdGridTile as MdGridTileElement} from '@aphrody/material-web/gridlist/grid-tile.js';
|
||||
|
||||
export const MdGridList = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-grid-list',
|
||||
elementClass: MdGridListElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdGridTile = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-grid-tile',
|
||||
elementClass: MdGridTileElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdIcon as MdIconElement} from '@aphrody/material-web/icon/icon.js';
|
||||
|
||||
export const MdIcon = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-icon',
|
||||
elementClass: MdIconElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdFilledIconButton as MdFilledIconButtonElement} from '@aphrody/material-web/iconbutton/filled-icon-button.js';
|
||||
import {MdFilledTonalIconButton as MdFilledTonalIconButtonElement} from '@aphrody/material-web/iconbutton/filled-tonal-icon-button.js';
|
||||
import {MdIconButton as MdIconButtonElement} from '@aphrody/material-web/iconbutton/icon-button.js';
|
||||
import {MdOutlinedIconButton as MdOutlinedIconButtonElement} from '@aphrody/material-web/iconbutton/outlined-icon-button.js';
|
||||
|
||||
export const MdFilledIconButton = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-filled-icon-button',
|
||||
elementClass: MdFilledIconButtonElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdFilledTonalIconButton = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-filled-tonal-icon-button',
|
||||
elementClass: MdFilledTonalIconButtonElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdIconButton = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-icon-button',
|
||||
elementClass: MdIconButtonElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdOutlinedIconButton = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-outlined-icon-button',
|
||||
elementClass: MdOutlinedIconButtonElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {Button as MdButtonElement} from '@aphrody/material-web/labs/gb/components/button/md-button.js';
|
||||
import {MdElevatedCard as MdElevatedCardElement} from '@aphrody/material-web/labs/card/elevated-card.js';
|
||||
import {MdFilledCard as MdFilledCardElement} from '@aphrody/material-web/labs/card/filled-card.js';
|
||||
import {MdItem as MdItemElement} from '@aphrody/material-web/labs/item/item.js';
|
||||
import {MenuGroup as MdMenuGroupElement} from '@aphrody/material-web/labs/gb/components/menu/md-menu-group.js';
|
||||
import {MdNavigationBar as MdNavigationBarElement} from '@aphrody/material-web/labs/navigationbar/navigation-bar.js';
|
||||
import {MdNavigationDrawer as MdNavigationDrawerElement} from '@aphrody/material-web/labs/navigationdrawer/navigation-drawer.js';
|
||||
import {MdNavigationDrawerModal as MdNavigationDrawerModalElement} from '@aphrody/material-web/labs/navigationdrawer/navigation-drawer-modal.js';
|
||||
import {MdNavigationTab as MdNavigationTabElement} from '@aphrody/material-web/labs/navigationtab/navigation-tab.js';
|
||||
import {MdOutlinedCard as MdOutlinedCardElement} from '@aphrody/material-web/labs/card/outlined-card.js';
|
||||
import {MdOutlinedSegmentedButton as MdOutlinedSegmentedButtonElement} from '@aphrody/material-web/labs/segmentedbutton/outlined-segmented-button.js';
|
||||
import {MdOutlinedSegmentedButtonSet as MdOutlinedSegmentedButtonSetElement} from '@aphrody/material-web/labs/segmentedbuttonset/outlined-segmented-button-set.js';
|
||||
import {SplitButton as MdSplitButtonElement} from '@aphrody/material-web/labs/gb/components/splitbutton/md-split-button.js';
|
||||
|
||||
export const MdButton = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-button',
|
||||
elementClass: MdButtonElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdElevatedCard = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-elevated-card',
|
||||
elementClass: MdElevatedCardElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdFilledCard = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-filled-card',
|
||||
elementClass: MdFilledCardElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdItem = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-item',
|
||||
elementClass: MdItemElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdMenuGroup = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-menu-group',
|
||||
elementClass: MdMenuGroupElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdNavigationBar = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-navigation-bar',
|
||||
elementClass: MdNavigationBarElement,
|
||||
events: {onActivated: 'navigation-bar-activated'},
|
||||
});
|
||||
|
||||
export const MdNavigationDrawer = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-navigation-drawer',
|
||||
elementClass: MdNavigationDrawerElement,
|
||||
events: {onNavigationDrawerChanged: 'navigation-drawer-changed'},
|
||||
});
|
||||
|
||||
export const MdNavigationDrawerModal = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-navigation-drawer-modal',
|
||||
elementClass: MdNavigationDrawerModalElement,
|
||||
events: {onNavigationDrawerChanged: 'navigation-drawer-changed'},
|
||||
});
|
||||
|
||||
export const MdNavigationTab = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-navigation-tab',
|
||||
elementClass: MdNavigationTabElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdOutlinedCard = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-outlined-card',
|
||||
elementClass: MdOutlinedCardElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdOutlinedSegmentedButton = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-outlined-segmented-button',
|
||||
elementClass: MdOutlinedSegmentedButtonElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdOutlinedSegmentedButtonSet = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-outlined-segmented-button-set',
|
||||
elementClass: MdOutlinedSegmentedButtonSetElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdSplitButton = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-split-button',
|
||||
elementClass: MdSplitButtonElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdListDetail as MdListDetailElement} from '@aphrody/material-web/layout/md-list-detail.js';
|
||||
import {MdPane as MdPaneElement} from '@aphrody/material-web/layout/md-pane.js';
|
||||
import {MdScaffold as MdScaffoldElement} from '@aphrody/material-web/layout/md-scaffold.js';
|
||||
import {MdSupportingPane as MdSupportingPaneElement} from '@aphrody/material-web/layout/md-supporting-pane.js';
|
||||
|
||||
export const MdListDetail = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-list-detail',
|
||||
elementClass: MdListDetailElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdPane = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-pane',
|
||||
elementClass: MdPaneElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdScaffold = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-scaffold',
|
||||
elementClass: MdScaffoldElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdSupportingPane = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-supporting-pane',
|
||||
elementClass: MdSupportingPaneElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdLink as MdLinkElement} from '@aphrody/material-web/link/link.js';
|
||||
|
||||
export const MdLink = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-link',
|
||||
elementClass: MdLinkElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdList as MdListElement} from '@aphrody/material-web/list/list.js';
|
||||
import {MdListItem as MdListItemElement} from '@aphrody/material-web/list/list-item.js';
|
||||
|
||||
export const MdList = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-list',
|
||||
elementClass: MdListElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdListItem = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-list-item',
|
||||
elementClass: MdListItemElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdLoadingIndicator as MdLoadingIndicatorElement} from '@aphrody/material-web/loadingindicator/loading-indicator.js';
|
||||
|
||||
export const MdLoadingIndicator = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-loading-indicator',
|
||||
elementClass: MdLoadingIndicatorElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdMenu as MdMenuElement} from '@aphrody/material-web/menu/menu.js';
|
||||
import {MdMenuItem as MdMenuItemElement} from '@aphrody/material-web/menu/menu-item.js';
|
||||
import {MdSubMenu as MdSubMenuElement} from '@aphrody/material-web/menu/sub-menu.js';
|
||||
|
||||
export const MdMenu = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-menu',
|
||||
elementClass: MdMenuElement,
|
||||
events: {onOpening: 'opening', onOpened: 'opened', onClosing: 'closing', onClosed: 'closed'},
|
||||
});
|
||||
|
||||
export const MdMenuItem = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-menu-item',
|
||||
elementClass: MdMenuItemElement,
|
||||
events: {onCloseMenu: 'close-menu'},
|
||||
});
|
||||
|
||||
export const MdSubMenu = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-sub-menu',
|
||||
elementClass: MdSubMenuElement,
|
||||
events: {onOpening: 'opening', onOpened: 'opened', onClosing: 'closing', onClosed: 'closed'},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdMobileStepper as MdMobileStepperElement} from '@aphrody/material-web/mobilestepper/mobile-stepper.js';
|
||||
|
||||
export const MdMobileStepper = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-mobile-stepper',
|
||||
elementClass: MdMobileStepperElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdNavigationRail as MdNavigationRailElement} from '@aphrody/material-web/navigationrail/navigation-rail.js';
|
||||
import {MdNavigationRailItem as MdNavigationRailItemElement} from '@aphrody/material-web/navigationrail/navigation-rail-item.js';
|
||||
|
||||
export const MdNavigationRail = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-navigation-rail',
|
||||
elementClass: MdNavigationRailElement,
|
||||
events: {onChange: 'navigation-rail:change', onItemActivate: 'navigation-rail-item:activate'},
|
||||
});
|
||||
|
||||
export const MdNavigationRailItem = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-navigation-rail-item',
|
||||
elementClass: MdNavigationRailItemElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdPaginator as MdPaginatorElement} from '@aphrody/material-web/paginator/paginator.js';
|
||||
|
||||
export const MdPaginator = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-paginator',
|
||||
elementClass: MdPaginatorElement,
|
||||
events: {onPage: 'paginator:page'},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdPopover as MdPopoverElement} from '@aphrody/material-web/popover/popover.js';
|
||||
|
||||
export const MdPopover = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-popover',
|
||||
elementClass: MdPopoverElement,
|
||||
events: {onOpened: 'opened', onClosed: 'closed'},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdCircularProgress as MdCircularProgressElement} from '@aphrody/material-web/progress/circular-progress.js';
|
||||
import {MdLinearProgress as MdLinearProgressElement} from '@aphrody/material-web/progress/linear-progress.js';
|
||||
|
||||
export const MdCircularProgress = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-circular-progress',
|
||||
elementClass: MdCircularProgressElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdLinearProgress = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-linear-progress',
|
||||
elementClass: MdLinearProgressElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdRadio as MdRadioElement} from '@aphrody/material-web/radio/radio.js';
|
||||
|
||||
export const MdRadio = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-radio',
|
||||
elementClass: MdRadioElement,
|
||||
events: {onChange: 'change', onInput: 'input'},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdRating as MdRatingElement} from '@aphrody/material-web/rating/rating.js';
|
||||
|
||||
export const MdRating = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-rating',
|
||||
elementClass: MdRatingElement,
|
||||
events: {onChange: 'change', onInput: 'input'},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdRipple as MdRippleElement} from '@aphrody/material-web/ripple/ripple.js';
|
||||
|
||||
export const MdRipple = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-ripple',
|
||||
elementClass: MdRippleElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdScheduler as MdSchedulerElement} from '@aphrody/material-web/scheduler/scheduler.js';
|
||||
|
||||
export const MdScheduler = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-scheduler',
|
||||
elementClass: MdSchedulerElement,
|
||||
events: {onNavigate: 'scheduler:navigate', onSelect: 'scheduler:select', onEventClick: 'scheduler:event-click'},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdSearchBar as MdSearchBarElement} from '@aphrody/material-web/search/search-bar.js';
|
||||
|
||||
export const MdSearchBar = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-search-bar',
|
||||
elementClass: MdSearchBarElement,
|
||||
events: {onInput: 'input', onSearch: 'search', onSearchOpen: 'search:open', onSearchClose: 'search:close'},
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdFilledSelect as MdFilledSelectElement} from '@aphrody/material-web/select/filled-select.js';
|
||||
import {MdOutlinedSelect as MdOutlinedSelectElement} from '@aphrody/material-web/select/outlined-select.js';
|
||||
import {MdSelectOption as MdSelectOptionElement} from '@aphrody/material-web/select/select-option.js';
|
||||
|
||||
export const MdFilledSelect = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-filled-select',
|
||||
elementClass: MdFilledSelectElement,
|
||||
events: {onChange: 'change', onInput: 'input', onOpening: 'opening', onOpened: 'opened', onClosing: 'closing', onClosed: 'closed'},
|
||||
});
|
||||
|
||||
export const MdOutlinedSelect = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-outlined-select',
|
||||
elementClass: MdOutlinedSelectElement,
|
||||
events: {onChange: 'change', onInput: 'input', onOpening: 'opening', onOpened: 'opened', onClosing: 'closing', onClosed: 'closed'},
|
||||
});
|
||||
|
||||
export const MdSelectOption = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-select-option',
|
||||
elementClass: MdSelectOptionElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdBottomSheet as MdBottomSheetElement} from '@aphrody/material-web/sheet/bottom-sheet.js';
|
||||
import {MdSideSheet as MdSideSheetElement} from '@aphrody/material-web/sheet/side-sheet.js';
|
||||
|
||||
export const MdBottomSheet = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-bottom-sheet',
|
||||
elementClass: MdBottomSheetElement,
|
||||
events: {onOpening: 'bottom-sheet:opening', onOpened: 'bottom-sheet:opened', onClosing: 'bottom-sheet:closing', onClosed: 'bottom-sheet:closed'},
|
||||
});
|
||||
|
||||
export const MdSideSheet = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-side-sheet',
|
||||
elementClass: MdSideSheetElement,
|
||||
events: {onOpening: 'side-sheet:opening', onOpened: 'side-sheet:opened', onClosing: 'side-sheet:closing', onClosed: 'side-sheet:closed'},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdSkeleton as MdSkeletonElement} from '@aphrody/material-web/skeleton/skeleton.js';
|
||||
|
||||
export const MdSkeleton = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-skeleton',
|
||||
elementClass: MdSkeletonElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdSlider as MdSliderElement} from '@aphrody/material-web/slider/slider.js';
|
||||
|
||||
export const MdSlider = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-slider',
|
||||
elementClass: MdSliderElement,
|
||||
events: {onChange: 'change', onInput: 'input'},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdSnackbar as MdSnackbarElement} from '@aphrody/material-web/snackbar/snackbar.js';
|
||||
|
||||
export const MdSnackbar = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-snackbar',
|
||||
elementClass: MdSnackbarElement,
|
||||
events: {onOpening: 'opening', onOpened: 'opened', onClosing: 'closing', onClosed: 'closed'},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdStep as MdStepElement} from '@aphrody/material-web/stepper/step.js';
|
||||
import {MdStepper as MdStepperElement} from '@aphrody/material-web/stepper/stepper.js';
|
||||
|
||||
export const MdStep = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-step',
|
||||
elementClass: MdStepElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdStepper = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-stepper',
|
||||
elementClass: MdStepperElement,
|
||||
events: {onChange: 'stepper:change'},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdSurface as MdSurfaceElement} from '@aphrody/material-web/surface/surface.js';
|
||||
|
||||
export const MdSurface = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-surface',
|
||||
elementClass: MdSurfaceElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdSwitch as MdSwitchElement} from '@aphrody/material-web/switch/switch.js';
|
||||
|
||||
export const MdSwitch = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-switch',
|
||||
elementClass: MdSwitchElement,
|
||||
events: {onChange: 'change', onInput: 'input'},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdTable as MdTableElement} from '@aphrody/material-web/table/table.js';
|
||||
|
||||
export const MdTable = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-table',
|
||||
elementClass: MdTableElement,
|
||||
events: {onSort: 'table:sort', onSelectionChange: 'table:selection-change'},
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdPrimaryTab as MdPrimaryTabElement} from '@aphrody/material-web/tabs/primary-tab.js';
|
||||
import {MdSecondaryTab as MdSecondaryTabElement} from '@aphrody/material-web/tabs/secondary-tab.js';
|
||||
import {MdTabs as MdTabsElement} from '@aphrody/material-web/tabs/tabs.js';
|
||||
|
||||
export const MdPrimaryTab = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-primary-tab',
|
||||
elementClass: MdPrimaryTabElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdSecondaryTab = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-secondary-tab',
|
||||
elementClass: MdSecondaryTabElement,
|
||||
events: {},
|
||||
});
|
||||
|
||||
export const MdTabs = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-tabs',
|
||||
elementClass: MdTabsElement,
|
||||
events: {onChange: 'change'},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdFilledTextField as MdFilledTextFieldElement} from '@aphrody/material-web/textfield/filled-text-field.js';
|
||||
import {MdOutlinedTextField as MdOutlinedTextFieldElement} from '@aphrody/material-web/textfield/outlined-text-field.js';
|
||||
|
||||
export const MdFilledTextField = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-filled-text-field',
|
||||
elementClass: MdFilledTextFieldElement,
|
||||
events: {onChange: 'change', onInput: 'input'},
|
||||
});
|
||||
|
||||
export const MdOutlinedTextField = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-outlined-text-field',
|
||||
elementClass: MdOutlinedTextFieldElement,
|
||||
events: {onChange: 'change', onInput: 'input'},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdTimePicker as MdTimePickerElement} from '@aphrody/material-web/timepicker/time-picker.js';
|
||||
|
||||
export const MdTimePicker = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-time-picker',
|
||||
elementClass: MdTimePickerElement,
|
||||
events: {onChange: 'time-picker:change'},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdToolbar as MdToolbarElement} from '@aphrody/material-web/toolbar/md-toolbar.js';
|
||||
|
||||
export const MdToolbar = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-toolbar',
|
||||
elementClass: MdToolbarElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdTooltip as MdTooltipElement} from '@aphrody/material-web/tooltip/tooltip.js';
|
||||
|
||||
export const MdTooltip = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-tooltip',
|
||||
elementClass: MdTooltipElement,
|
||||
events: {onShow: 'tooltip-show', onHide: 'tooltip-hide'},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdTree as MdTreeElement} from '@aphrody/material-web/tree/tree.js';
|
||||
import {MdTreeItem as MdTreeItemElement} from '@aphrody/material-web/tree/tree-item.js';
|
||||
|
||||
export const MdTree = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-tree',
|
||||
elementClass: MdTreeElement,
|
||||
events: {onSelect: 'tree:select'},
|
||||
});
|
||||
|
||||
export const MdTreeItem = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-tree-item',
|
||||
elementClass: MdTreeItemElement,
|
||||
events: {onToggle: 'tree-item:toggle', onActivate: 'tree-item:activate'},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdType as MdTypeElement} from '@aphrody/material-web/typography/md-type.js';
|
||||
|
||||
export const MdType = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-type',
|
||||
elementClass: MdTypeElement,
|
||||
events: {},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED by generate.ts — do not edit by hand. Run `bun run generate`.
|
||||
|
||||
import * as React from 'react';
|
||||
import {createComponent} from '@lit/react';
|
||||
|
||||
import {MdVirtualScroller as MdVirtualScrollerElement} from '@aphrody/material-web/virtualscroll/virtual-scroller.js';
|
||||
|
||||
export const MdVirtualScroller = createComponent({
|
||||
react: React,
|
||||
tagName: 'md-virtual-scroller',
|
||||
elementClass: MdVirtualScrollerElement,
|
||||
events: {onRange: 'virtual-scroll:range'},
|
||||
});
|
||||
Reference in new issue
Block a user