diff --git a/package.json b/package.json index 7d9dc634..e1d2c992 100644 --- a/package.json +++ b/package.json @@ -93,5 +93,8 @@ "react-native@^0.66": "patch:react-native@npm%3A0.66.5#./.yarn/patches/react-native-npm-0.66.5-22e5dd8ec5.patch", "react-material-symbols@^4.1.1": "patch:react-material-symbols@npm%3A4.1.1#./.yarn/patches/react-material-symbols-npm-4.1.1-d06ca7bff7.patch", "jsdom@^21.1.2": "patch:jsdom@npm%3A21.1.2#./.yarn/patches/jsdom-npm-21.1.2-4916b89bf1.patch" + }, + "dependencies": { + "@react-md/alert": "^2" } } diff --git a/premiser-ui/package.json b/premiser-ui/package.json index bfe9fdd8..27bf8374 100644 --- a/premiser-ui/package.json +++ b/premiser-ui/package.json @@ -118,6 +118,7 @@ "dependencies": { "@babel/runtime-corejs3": "^7.22.6", "@grrr/cookie-consent": "^1.0.4", + "@react-md/app-bar": "^2", "@react-md/autocomplete": "Howdju/react-md#workspace=@react-md/autocomplete&commit=739fcf8b359727d958d6befe46014fe8b3dfe11c", "@react-md/avatar": "^2", "@react-md/button": "^2", @@ -130,7 +131,9 @@ "@react-md/list": "^2", "@react-md/menu": "^2", "@react-md/progress": "^2", + "@react-md/sheet": "^2", "@react-md/states": "^2", + "@react-md/tabs": "^2", "@react-md/theme": "^2", "@react-md/typography": "^2", "@react-md/utils": "^2", diff --git a/premiser-ui/src/AddMessageCapturer.tsx b/premiser-ui/src/AddMessageCapturer.tsx new file mode 100644 index 00000000..3e009bb6 --- /dev/null +++ b/premiser-ui/src/AddMessageCapturer.tsx @@ -0,0 +1,23 @@ +import { useAddMessage } from "@react-md/alert"; +import { useEffect } from "react"; +import app from "./app/appSlice"; +import { useAppDispatch } from "./hooks"; + +/** + * This component is a hack to work around react-md's Toast/Snackbar/Alert design + * where it is only possible to add messages from a functional component. Since + * we want to add messages from sagas, we need to capture the addMessage function. + * + * TODO(#605) figure out a better approach to app toasts. + */ +export function AddMessageCapturer() { + const addMessage = useAddMessage(); + const dispatch = useAppDispatch(); + // Only capture addMessage once. + useEffect(() => { + if (addMessage) { + dispatch(app.captureAddMessage(addMessage)); + } + }, [addMessage, dispatch]); + return null; +} diff --git a/premiser-ui/src/App.scss b/premiser-ui/src/App.scss index d376d0c1..fd1514aa 100644 --- a/premiser-ui/src/App.scss +++ b/premiser-ui/src/App.scss @@ -113,6 +113,7 @@ strong { // Tabs that appear to be contiguous with the toolbar. But to avoid handling the navigate ourselves, they are actually not in the toolbar .toolbarTabs { + background-color: $primary-color; * { color: $light-text-color; } diff --git a/premiser-ui/src/App.tsx b/premiser-ui/src/App.tsx index b305188b..0f74769b 100644 --- a/premiser-ui/src/App.tsx +++ b/premiser-ui/src/App.tsx @@ -1,26 +1,19 @@ -import cn from "classnames"; import { ConnectedRouter } from "connected-react-router"; import { Action, Location, UnregisterCallback } from "history"; import forEach from "lodash/forEach"; import isFinite from "lodash/isFinite"; import map from "lodash/map"; import throttle from "lodash/throttle"; -import React, { Component, ComponentClass, MouseEvent } from "react"; +import React, { Component, MouseEvent } from "react"; import { hot } from "react-hot-loader/root"; -import { - Drawer, - ListItem, - Snackbar, - Tab, - Tabs, - TabsProps, - Toolbar, -} from "react-md"; -import { IdPropType } from "react-md/lib"; +import { TabsList, Tab } from "@react-md/tabs"; import { connect, ConnectedProps } from "react-redux"; import { Switch } from "react-router"; import { Link } from "react-router-dom"; import { FontIcon } from "@react-md/icon"; +import { Sheet } from "@react-md/sheet"; +import { List } from "@react-md/list"; +import { MessageQueue } from "@react-md/alert"; import { actions, inIframe } from "howdju-client-common"; import { isTruthy } from "howdju-common"; @@ -73,6 +66,8 @@ import t, { } from "./texts"; import { isDevice, isScrollPastBottom, isScrollPastTop } from "./util"; import WindowMessageHandler from "./WindowMessageHandler"; +import { MenuItem, MenuItemLink } from "@/components/menu/Menu"; +import { AddMessageCapturer } from "./AddMessageCapturer"; import "./App.scss"; import "./fonts.js"; @@ -301,22 +296,12 @@ class App extends Component { this.props.app.hideNavDrawer(); }; - onNavDrawerVisibilityChange = (visible: boolean) => { - this.props.app.setNavDrawerVisibility(visible); - }; - - dismissSnackbar = () => { - this.props.app.dismissToast(); + onNavSheetRequestClose = () => { + this.props.app.setNavDrawerVisibility(false); }; - onTabChange = ( - activeTabIndex: number, - _tabId: IdPropType, - _tabControlsId: IdPropType, - _tabChildren: React.ReactNode, - _event: Event - ) => { - this.setState({ activeTabIndex }); + onTabChange = (activeIndexNumber: number) => { + this.setState({ activeTabIndex: activeIndexNumber }); }; onHistoryListen = (location: Location, _action: Action) => { @@ -345,45 +330,67 @@ class App extends Component { authEmail, hasAuthToken, isNavDrawerVisible, - toasts, isMobileSiteDisabled, } = this.props; const { activeTabIndex } = this.state; + const authEmailDiv = ( +
+ {authEmail} + {hasAuthToken || ( +
+ login expired +
+ )} +
+ ); const navItems = [ - Not logged in} + leftAddon={ + + close + + } + />, + home} + leftAddon={home} component={Link} to={paths.home()} />, - add} + leftAddon={add} component={Link} to="/create-proposition" />, - format_quote} + leftAddon={format_quote} component={Link} to="/media-excerpts/new" />, - build} + leftAddon={build} component="a" href="https://chrome.google.com/webstore/detail/howdju-extension/gijlmlebhfiglpgdlgphbmaamhkchoei/" target="_blank" />, - gavel} + leftAddon={gavel} component="a" href={paths.policies()} target="_blank" @@ -393,10 +400,10 @@ class App extends Component { if (authEmail || hasAuthToken) { // Authenticated users can access their settings navItems.push( - settings} + leftAddon={settings} component={Link} to={paths.settings()} /> @@ -404,10 +411,10 @@ class App extends Component { } else { // Anonymous users still need access to privacy settings navItems.push( - speaker_phone} + leftAddon={speaker_phone} onClick={() => showPrivacyConsentDialog()} /> ); @@ -416,19 +423,19 @@ class App extends Component { if (isDevice()) { if (isMobileSiteDisabled) { navItems.push( - smartphone} + leftAddon={smartphone} onClick={this.enableMobileSite} /> ); } else { navItems.push( - desktop_windows} + leftAddon={desktop_windows} onClick={this.disableMobileSite} /> ); @@ -436,94 +443,61 @@ class App extends Component { } if (authEmail || hasAuthToken) { navItems.push( - exit_to_app} + leftAddon={exit_to_app} onClick={this.logout} /> ); } else { if (config.isRegistrationEnabled) { navItems.push( - person_add} + leftAddon={person_add} component={Link} to={paths.requestRegistration()} /> ); } navItems.push( - https} + leftAddon={https} component={Link} to={paths.login()} /> ); } - - const authEmailDiv = ( -
- {authEmail} - {hasAuthToken || ( -
- login expired -
- )} -
- ); const navDrawer = ( - - close - - } - className="md-divider-border md-divider-border--bottom" - > -
- {authEmail ? authEmailDiv : Not logged in} -
- - } - navItems={navItems} visible={isNavDrawerVisible} - onVisibilityChange={this.onNavDrawerVisibilityChange} - style={{ zIndex: 100 }} - /> + onRequestClose={this.onNavSheetRequestClose} + > + {navItems} + ); - const pageTabs = ( - - {map(tabInfos, (ti) => ( - {ti.text}} - id={ti.id} - key={ti.id} - /> + {map(tabInfos, (ti, i) => ( + + {ti.text} + ))} - - ) as unknown as ComponentClass; + + ); const title = isFinite(activeTabIndex) && activeTabIndex >= 0 @@ -537,38 +511,34 @@ class App extends Component { return ( -
- - {title} - - - -
- - {navDrawer} - -
- {routes} + +
+ + {title} + + + +
+ {newTabs} + + {navDrawer} + +
+ {routes} +
+ + + + + + +
- - - - - - - - -
+ ); @@ -580,13 +550,12 @@ const mapStateToProps = (state: RootState) => { const authEmail = selectAuthEmail(state); const hasAuthToken = isTruthy(selectAuthToken(state)); const privacyConsentState = selectPrivacyConsent(state); - const { isMobileSiteDisabled, isNavDrawerVisible, toasts } = app; + const { isMobileSiteDisabled, isNavDrawerVisible } = app; return { authEmail, hasAuthToken, isNavDrawerVisible, - toasts, isMobileSiteDisabled, privacyConsentState, }; diff --git a/premiser-ui/src/Header.scss b/premiser-ui/src/Header.scss index 1f49b6ea..37e684b1 100644 --- a/premiser-ui/src/Header.scss +++ b/premiser-ui/src/Header.scss @@ -29,6 +29,10 @@ } } +#main-search-container { + flex-grow: 1; +} + a.md-title--toolbar { text-decoration: none; } @@ -37,7 +41,3 @@ a.md-title--toolbar { font-family: "Orbitron", sans-serif; font-weight: 400; } - -.toggleNavDrawerVisibility { - margin-right: 1em; -} diff --git a/premiser-ui/src/Header.tsx b/premiser-ui/src/Header.tsx index 14aed7bb..15af65b2 100644 --- a/premiser-ui/src/Header.tsx +++ b/premiser-ui/src/Header.tsx @@ -1,45 +1,33 @@ import React from "react"; import { Link } from "react-router-dom"; import { useDispatch } from "react-redux"; -import { TabsProps, Toolbar } from "react-md"; +import { FontIcon } from "@react-md/icon"; +import { AppBar, AppBarTitle, AppBarAction } from "@react-md/app-bar"; import app from "./app/appSlice"; import MainSearch from "@/components/mainSearchBox/MainSearchBox"; -import IconButton from "@/components/button/IconButton"; import "./Header.scss"; -import { FontIcon } from "@react-md/icon"; - -interface Props { - tabs: React.ComponentClass; -} -export default function Header(props: Props) { +export default function Header() { const dispatch = useDispatch(); - const { tabs } = props; - const hasTabs = !!tabs; return ( - + howdju? - } - prominent={hasTabs} - actions={ - dispatch(app.toggleNavDrawerVisibility())} - > - menu - - } - > - - {tabs} - + + + + + dispatch(app.toggleNavDrawerVisibility())} + > + menu + + ); } diff --git a/premiser-ui/src/JustificationBranch.scss b/premiser-ui/src/JustificationBranch.scss index 9c0ef6c0..363d0e76 100644 --- a/premiser-ui/src/JustificationBranch.scss +++ b/premiser-ui/src/JustificationBranch.scss @@ -62,8 +62,11 @@ $slide-size: $md-btn-icon-size; } } - .otherSelected .md-icon { - color: $concrete; + .otherSelected { + .rmd-icon, + .material-symbols { + color: $concrete; + } } .md-list--menu { @@ -82,7 +85,8 @@ $slide-size: $md-btn-icon-size; // Override App.scss .md-dialog-footer--card justify-content: flex-start; - .rmd-icon { + .rmd-icon, + .material-symbols { // When positivey, the icons make more sense flipped @include flip-icon-horizontal; } @@ -98,7 +102,8 @@ $slide-size: $md-btn-icon-size; justify-content: flex-start; direction: rtl; - .md-icon { + .rmd-icon, + .material-symbols { transform: none; } diff --git a/premiser-ui/src/LandingPage.scss b/premiser-ui/src/LandingPage.scss index 416e4e73..9d831c4b 100644 --- a/premiser-ui/src/LandingPage.scss +++ b/premiser-ui/src/LandingPage.scss @@ -49,7 +49,8 @@ $banner-padding: 3em; width: 66%; &, - .md-icon, + .rmd-icon, + .material-symbols, a { color: $clouds; } @@ -58,7 +59,8 @@ $banner-padding: 3em; text-align: center; margin-bottom: 16px; - .md-icon { + .rmd-icon, + .material-symbols { font-size: 64px; text-shadow: none; } diff --git a/premiser-ui/src/WritQuoteEditorFields.scss b/premiser-ui/src/WritQuoteEditorFields.scss index 7b3c30e0..dc9ee011 100644 --- a/premiser-ui/src/WritQuoteEditorFields.scss +++ b/premiser-ui/src/WritQuoteEditorFields.scss @@ -5,7 +5,7 @@ // TODO(282) this should appear disabled when the inputs are actually disabled via the passdown prop .md-text-field-icon--disabled { &, - .md-icon { + .rmd-icon { color: $icon-color; } } diff --git a/premiser-ui/src/WritQuoteViewer.scss b/premiser-ui/src/WritQuoteViewer.scss index 1e6f27be..aa770b61 100644 --- a/premiser-ui/src/WritQuoteViewer.scss +++ b/premiser-ui/src/WritQuoteViewer.scss @@ -46,7 +46,7 @@ @include material-icon-list-item("link"); > a { - .md-icon { + .rmd-icon { vertical-align: middle; font-size: 12px; margin-left: 2px; diff --git a/premiser-ui/src/_react-md.scss b/premiser-ui/src/_react-md.scss index 00748913..443863b3 100644 --- a/premiser-ui/src/_react-md.scss +++ b/premiser-ui/src/_react-md.scss @@ -9,6 +9,8 @@ $md-typography-extended: false; @import "./react-md-variable-overrides"; @import "./react-md-everything"; +@import "~@react-md/alert/dist/mixins"; +@import "~@react-md/app-bar/dist/mixins"; @import "~@react-md/avatar/dist/mixins"; @import "~@react-md/button/dist/mixins"; @import "~@react-md/card/dist/mixins"; @@ -20,7 +22,9 @@ $md-typography-extended: false; @import "~@react-md/list/dist/mixins"; @import "~@react-md/menu/dist/mixins"; @import "~@react-md/progress/dist/mixins"; +@import "~@react-md/sheet/dist/mixins"; @import "~@react-md/states/dist/mixins"; +@import "~@react-md/tabs/dist/mixins"; @import "~@react-md/theme/dist/mixins"; @import "~@react-md/typography/dist/mixins"; @import "~@react-md/utils/dist/mixins"; diff --git a/premiser-ui/src/_rmd-variable-overrides.scss b/premiser-ui/src/_rmd-variable-overrides.scss index a53138b3..001ee771 100644 --- a/premiser-ui/src/_rmd-variable-overrides.scss +++ b/premiser-ui/src/_rmd-variable-overrides.scss @@ -24,6 +24,8 @@ $rmd-typography-font-family: "Lato", sans-serif; $rmd-typography-desktop-max-line-length: 60em; $rmd-typography-mobile-max-line-length: 40em; +$rmd-tab-indicator-color: $secondary-color; + // TODO(17) don't apply a CSS override; figure out why active input labels on top of cards have a bad color. // _colors.scss: `.md-background { &--card { background: get-color('card', $light-theme); } }` // vs. diff --git a/premiser-ui/src/_util.scss b/premiser-ui/src/_util.scss index 2abae4ba..fb1ca085 100644 --- a/premiser-ui/src/_util.scss +++ b/premiser-ui/src/_util.scss @@ -7,7 +7,8 @@ &.md-card-title, &.context-menu--floating, - & .md-icon { + & .rmd-icon, + & .material-symbols { display: none; } } diff --git a/premiser-ui/src/app/appSagas.ts b/premiser-ui/src/app/appSagas.ts index ca814d5e..50dc9484 100644 --- a/premiser-ui/src/app/appSagas.ts +++ b/premiser-ui/src/app/appSagas.ts @@ -24,6 +24,7 @@ import { callApiResponse } from "@/apiActions"; import app from "@/app/appSlice"; import { logger } from "../logger"; import { uiErrorTypes } from "../uiErrors"; +import { ToastMessage } from "@react-md/alert"; export function* showAlertForLogin() { yield takeEvery( @@ -38,6 +39,42 @@ export function* showAlertForLogin() { ); } +let reactMdAddMessage: (message: ToastMessage) => void; +const earlyToastMessages: ToastMessage[] = []; + +/** + * Translate our addToast action into a react-md alert message action. + * + * We designed our toast system around react-md@1, which had a Snackbar Component + * accepting a toast prop. react-md@2's version only accepts messages from hooks, + * which is incompatible with us creating toasts from sagas. + */ +export function* toastToReactMdAlertMessageConverter() { + yield takeEvery( + app.captureAddMessage, + function* captureAddMessageWorker(action) { + reactMdAddMessage = action.payload.reactMdAddMessage; + if (earlyToastMessages.length) { + for (const message of earlyToastMessages) { + reactMdAddMessage(message); + } + earlyToastMessages.length = 0; + } + } + ); + yield takeEvery( + app.addToast, + function* toastToReactMdAlertMessageConverterWorker(action) { + const { message } = action.payload; + if (reactMdAddMessage) { + reactMdAddMessage(message); + } else { + earlyToastMessages.push(message); + } + } + ); +} + export function* showAlertForLogout() { yield takeEvery( api.logout.response, diff --git a/premiser-ui/src/app/appSlice.ts b/premiser-ui/src/app/appSlice.ts index aabd1d28..27dd4b7f 100644 --- a/premiser-ui/src/app/appSlice.ts +++ b/premiser-ui/src/app/appSlice.ts @@ -1,5 +1,6 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { LocationChangeAction, LOCATION_CHANGE } from "connected-react-router"; +import { ToastMessage } from "@react-md/alert"; import { ui } from "@/actions"; import paths from "../paths"; @@ -7,10 +8,6 @@ import { goto } from "../actions"; import { isWindowNarrow } from "../util"; import { Location, LocationState } from "history"; -export interface ToastData { - text: string; -} - export const appSlice = createSlice({ name: "app", initialState: { @@ -18,7 +15,6 @@ export const appSlice = createSlice({ isMobileSiteDisabled: false, isWindowNarrow: isWindowNarrow(), loginRedirectLocation: null as Location | null, - toasts: [] as ToastData[], }, reducers: { showNavDrawer: (state) => { @@ -33,14 +29,22 @@ export const appSlice = createSlice({ setNavDrawerVisibility: (state, action: PayloadAction) => { state.isNavDrawerVisible = action.payload; }, + captureAddMessage: { + prepare: (reactMdAddMessage: (message: ToastMessage) => void) => ({ + payload: { reactMdAddMessage }, + }), + // This action exists just to forward the payload to the saga + // eslint-disable-next-line @typescript-eslint/no-empty-function + reducer: () => {}, + }, addToast: { - prepare: (text: string) => ({ payload: { text } }), - reducer: (state, action: PayloadAction) => { - state.toasts.push(action.payload); + prepare: (text: string) => { + const message: ToastMessage = { children: text }; + return { payload: { message } }; }, - }, - dismissToast: (state) => { - state.toasts = state.toasts.slice(1); + // This action exists just to forward the payload to the saga + // eslint-disable-next-line @typescript-eslint/no-empty-function + reducer: () => {}, }, disableMobileSite: (state) => { state.isMobileSiteDisabled = true; diff --git a/premiser-ui/src/components/mediaExcerpts/MediaExcerptViewer.scss b/premiser-ui/src/components/mediaExcerpts/MediaExcerptViewer.scss index 37e65e5b..878eb18f 100644 --- a/premiser-ui/src/components/mediaExcerpts/MediaExcerptViewer.scss +++ b/premiser-ui/src/components/mediaExcerpts/MediaExcerptViewer.scss @@ -35,7 +35,7 @@ ul.url-locators { @include material-icon-list-item("link"); > a { - .md-icon, + .rmd-icon, .material-symbols { vertical-align: middle; font-size: 12px; diff --git a/premiser-ui/src/hooks.ts b/premiser-ui/src/hooks.ts index d18feaa5..c81baf56 100644 --- a/premiser-ui/src/hooks.ts +++ b/premiser-ui/src/hooks.ts @@ -1,6 +1,7 @@ import { useEffect, useRef } from "react"; import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; import { Schema } from "normalizr"; +import { ToastMessage, useAddMessage } from "@react-md/alert"; import type { RootState, AppDispatch } from "./setupStore"; import { Denormalized, denormalizedEntity, InferResult } from "./selectors"; @@ -9,6 +10,14 @@ import { Denormalized, denormalizedEntity, InferResult } from "./selectors"; export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppSelector: TypedUseSelectorHook = useSelector; +export function useAddToast() { + const addMessage = useAddMessage(); + return function (text: string) { + const message: ToastMessage = { children: text }; + addMessage(message); + }; +} + export function usePreviousValue(value: T) { const ref = useRef(); useEffect(() => { diff --git a/premiser-ui/src/setupStore.ts b/premiser-ui/src/setupStore.ts index 591549a7..9f15ff85 100644 --- a/premiser-ui/src/setupStore.ts +++ b/premiser-ui/src/setupStore.ts @@ -77,6 +77,7 @@ export const setupStore = ( /payload\.mediaExcerpt\.locators\.urlLocators\.\d+\.autoConfirmationStatus\.(earliest|latest)(Not)?FoundAt/, "payload.urlLocator.created", /payload\.urlLocator\.autoConfirmationStatus\.(earliest|latest)(Not)?FoundAt/, + "payload.reactMdAddMessage", ], ignoredPaths: [ // TODO(484) figure out how to handle timestamps in a way that is acceptable to redux. diff --git a/yarn.lock b/yarn.lock index 587df19e..56d614c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6519,7 +6519,30 @@ __metadata: languageName: node linkType: hard -"@react-md/app-bar@npm:^2.9.1": +"@react-md/alert@npm:^2": + version: 2.9.1 + resolution: "@react-md/alert@npm:2.9.1" + dependencies: + "@react-md/button": ^2.9.1 + "@react-md/elevation": ^2.9.1 + "@react-md/portal": ^2.9.1 + "@react-md/theme": ^2.9.1 + "@react-md/transition": ^2.9.1 + "@react-md/typography": ^2.9.1 + "@react-md/utils": ^2.9.1 + classnames: ^2.3.1 + prop-types: ">= 15.6" + react-transition-group: ^4.4.2 + peerDependencies: + react: ">= 16.8" + dependenciesMeta: + prop-types: + optional: true + checksum: 8b997cd6a963005a7db5206dc0f4e4cd0a652d35d6f520b89ad56eb37f34a5ecf7da8b2d3d9b6b5056d9682870b324224fbc76eb4ea656c1493b1febf1c5b43c + languageName: node + linkType: hard + +"@react-md/app-bar@npm:^2, @react-md/app-bar@npm:^2.9.1": version: 2.9.1 resolution: "@react-md/app-bar@npm:2.9.1" dependencies: @@ -6919,7 +6942,7 @@ __metadata: languageName: node linkType: hard -"@react-md/sheet@npm:^2.9.1": +"@react-md/sheet@npm:^2, @react-md/sheet@npm:^2.9.1": version: 2.9.1 resolution: "@react-md/sheet@npm:2.9.1" dependencies: @@ -6959,6 +6982,28 @@ __metadata: languageName: node linkType: hard +"@react-md/tabs@npm:^2": + version: 2.9.1 + resolution: "@react-md/tabs@npm:2.9.1" + dependencies: + "@react-md/icon": ^2.9.1 + "@react-md/states": ^2.9.1 + "@react-md/theme": ^2.9.1 + "@react-md/transition": ^2.9.1 + "@react-md/typography": ^2.9.1 + "@react-md/utils": ^2.9.1 + classnames: ^2.3.1 + prop-types: ">= 15.6" + react-transition-group: ^4.4.2 + peerDependencies: + react: ">= 16.8" + dependenciesMeta: + prop-types: + optional: true + checksum: 4b30fd896809073170b2d453d1652afb927894a2dfe35d3c7e3d62cbad381235a5a49a907cf99e0a5f6706f802a99860060919ef973847798aa237cbaed4cbd1 + languageName: node + linkType: hard + "@react-md/theme@npm:^2, @react-md/theme@npm:^2.9.1": version: 2.9.1 resolution: "@react-md/theme@npm:2.9.1" @@ -19990,6 +20035,7 @@ __metadata: "@babel/plugin-syntax-import-meta": ^7.10.4 "@babel/preset-env": ^7.20.2 "@babel/preset-typescript": ^7.18.6 + "@react-md/alert": ^2 babel-jest: ^29.3.1 browserslist: ^4.17.0 debug: ^4.3.4 @@ -28476,6 +28522,7 @@ __metadata: "@babel/preset-typescript": ^7.18.6 "@babel/runtime-corejs3": ^7.22.6 "@grrr/cookie-consent": ^1.0.4 + "@react-md/app-bar": ^2 "@react-md/autocomplete": "Howdju/react-md#workspace=@react-md/autocomplete&commit=739fcf8b359727d958d6befe46014fe8b3dfe11c" "@react-md/avatar": ^2 "@react-md/button": ^2 @@ -28488,7 +28535,9 @@ __metadata: "@react-md/list": ^2 "@react-md/menu": ^2 "@react-md/progress": ^2 + "@react-md/sheet": ^2 "@react-md/states": ^2 + "@react-md/tabs": ^2 "@react-md/theme": ^2 "@react-md/typography": ^2 "@react-md/utils": ^2