diff --git a/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js b/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js index 1f3c339608098..a3f7b0701ef17 100644 --- a/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js +++ b/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js @@ -40,6 +40,12 @@ export type IconType = | 'panel-right-open' | 'panel-bottom-open' | 'panel-bottom-close' + | 'filter-on' + | 'filter-off' + | 'play' + | 'pause' + | 'skip-previous' + | 'skip-next' | 'error' | 'suspend' | 'undo' @@ -153,6 +159,30 @@ export default function ButtonIcon({className = '', type}: Props): React.Node { pathData = PATH_MATERIAL_PANEL_BOTTOM_CLOSE; viewBox = panelIcons; break; + case 'filter-on': + pathData = PATH_MATERIAL_FILTER_ALT; + viewBox = panelIcons; + break; + case 'filter-off': + pathData = PATH_MATERIAL_FILTER_ALT_OFF; + viewBox = panelIcons; + break; + case 'play': + pathData = PATH_MATERIAL_PLAY_ARROW; + viewBox = panelIcons; + break; + case 'pause': + pathData = PATH_MATERIAL_PAUSE; + viewBox = panelIcons; + break; + case 'skip-previous': + pathData = PATH_MATERIAL_SKIP_PREVIOUS_ARROW; + viewBox = panelIcons; + break; + case 'skip-next': + pathData = PATH_MATERIAL_SKIP_NEXT_ARROW; + viewBox = panelIcons; + break; case 'suspend': pathData = PATH_SUSPEND; break; @@ -338,3 +368,33 @@ const PATH_MATERIAL_PANEL_BOTTOM_OPEN = ` const PATH_MATERIAL_PANEL_BOTTOM_CLOSE = ` m506-508 102-110q8-8.82 3.5-19.41T595-648H365q-12.25 0-16.62 10.5Q344-627 352-618l102 110q11.18 11 26.09 11T506-508Zm243-308q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538ZM216-336v120h528v-120H216Zm528-72v-336H216v336h528Zm-528 72v120-120Z `; + +// Source: Material Design Icons filter_alt +const PATH_MATERIAL_FILTER_ALT = ` + M440-160q-17 0-28.5-11.5T400-200v-240L168-736q-15-20-4.5-42t36.5-22h560q26 0 36.5 22t-4.5 42L560-440v240q0 17-11.5 28.5T520-160h-80Zm40-308 198-252H282l198 252Zm0 0Z +`; + +// Source: Material Design Icons filter_alt_off +const PATH_MATERIAL_FILTER_ALT_OFF = ` + m592-481-57-57 143-182H353l-80-80h487q25 0 36 22t-4 42L592-481ZM791-56 560-287v87q0 17-11.5 28.5T520-160h-80q-17 0-28.5-11.5T400-200v-247L56-791l56-57 736 736-57 56ZM535-538Z +`; + +// Source: Material Design Icons play_arrow +const PATH_MATERIAL_PLAY_ARROW = ` + M320-200v-560l440 280-440 280Zm80-280Zm0 134 210-134-210-134v268Z +`; + +// Source: Material Design Icons pause +const PATH_MATERIAL_PAUSE = ` + M520-200v-560h240v560H520Zm-320 0v-560h240v560H200Zm400-80h80v-400h-80v400Zm-320 0h80v-400h-80v400Zm0-400v400-400Zm320 0v400-400Z +`; + +// Source: Material Design Icons skip_previous +const PATH_MATERIAL_SKIP_PREVIOUS_ARROW = ` + M220-240v-480h80v480h-80Zm520 0L380-480l360-240v480Zm-80-240Zm0 90v-180l-136 90 136 90Z +`; + +// Source: Material Design Icons skip_next +const PATH_MATERIAL_SKIP_NEXT_ARROW = ` + M660-240v-480h80v480h-80Zm-440 0v-480l360 240-360 240Zm80-240Zm0 90 136-90-136-90v180Z +`; diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.css b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.css new file mode 100644 index 0000000000000..94e51ef63d330 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.css @@ -0,0 +1,57 @@ +.SuspenseScrubber { + position: relative; + width: 100%; + height: 1.5rem; + border-radius: 0.75rem; + padding: 0.25rem; + box-sizing: border-box; + display: flex; + align-items: center; +} + +.SuspenseScrubber:has(.SuspenseScrubberInput:focus-visible) { + outline: 2px solid var(--color-button-background-focus); +} + +.SuspenseScrubberInput { + position: absolute; + width: 100%; + opacity: 0; + height: 0px; + overflow: hidden; +} + +.SuspenseScrubberInput:focus { + outline: none; +} + +.SuspenseScrubberStep { + cursor: pointer; + flex: 1; + height: 100%; + padding-right: 1px; /* we use this instead of flex gap to make every pixel clickable */ + display: flex; + align-items: center; +} +.SuspenseScrubberStep:last-child { + padding-right: 0; +} + +.SuspenseScrubberBead, .SuspenseScrubberBeadSelected { + flex: 1; + height: 0.5rem; + background: var(--color-background-selected); + border-radius: 0.5rem; + background: var(--color-selected-tree-highlight-active); + transition: all 0.3s ease-in-out; +} + +.SuspenseScrubberBeadSelected { + height: 1rem; + background: var(--color-background-selected); +} + +.SuspenseScrubberStep:hover > .SuspenseScrubberBead, +.SuspenseScrubberStep:hover > .SuspenseScrubberBeadSelected { + height: 0.75rem; +} diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.js new file mode 100644 index 0000000000000..f1f96a33e00e2 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.js @@ -0,0 +1,86 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import typeof {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent'; + +import * as React from 'react'; +import {useRef} from 'react'; + +import styles from './SuspenseScrubber.css'; + +export default function SuspenseScrubber({ + min, + max, + value, + onBlur, + onChange, + onFocus, + onHoverSegment, + onHoverLeave, +}: { + min: number, + max: number, + value: number, + onBlur: () => void, + onChange: (index: number) => void, + onFocus: () => void, + onHoverSegment: (index: number) => void, + onHoverLeave: () => void, +}): React$Node { + const inputRef = useRef(); + function handleChange(event: SyntheticEvent) { + const newValue = +event.currentTarget.value; + onChange(newValue); + } + function handlePress(index: number, event: SyntheticEvent) { + event.preventDefault(); + if (inputRef.current == null) { + throw new Error( + 'The input should always be mounted while we can click things.', + ); + } + inputRef.current.focus(); + onChange(index); + } + const steps = []; + for (let index = min; index <= max; index++) { + steps.push( +
+
+
, + ); + } + + return ( +
+ + {steps} +
+ ); +} diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css index 3e042402a4c3a..c718be7ea9499 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css @@ -14,6 +14,14 @@ -webkit-font-smoothing: var(--font-smoothing); } +.VRule { + height: 20px; + width: 1px; + flex: 0 0 1px; + margin: 0 0.5rem; + background-color: var(--color-border); +} + .TreeWrapper { border-top: 1px solid var(--color-border); flex: 1 1 65%; @@ -107,13 +115,13 @@ .SuspenseTreeViewHeader { flex: 0 0 42px; padding: 0.5rem; - display: grid; - grid-template-columns: auto 1fr auto auto auto; - align-items: flex-start; + display: flex; + align-items: center; border-bottom: 1px solid var(--color-border); } .SuspenseBreadcrumbs { + flex: 1; /** * TODO: Switch to single item view on overflow like OwnerStack does. * OwnerStack has more constraints that make it easier so it won't be a 1:1 port. @@ -129,5 +137,9 @@ padding: 0.5rem; display: grid; grid-template-columns: 1fr auto; - align-items: flex-start; + align-items: center; } + +.SuspenseTreeViewFooterButtons { + padding: 0.25rem; +} \ No newline at end of file diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js index c7b53605b70a1..b4ed7ec1f93c8 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js @@ -33,14 +33,17 @@ import { SuspenseTreeDispatcherContext, SuspenseTreeStateContext, } from './SuspenseTreeContext'; -import {StoreContext} from '../context'; +import {StoreContext, OptionsContext} from '../context'; import {TreeDispatcherContext} from '../Components/TreeContext'; import Button from '../Button'; -import Tooltip from '../Components/reach-ui/tooltip'; +import Toggle from '../Toggle'; import typeof { SyntheticEvent, SyntheticPointerEvent, } from 'react-dom-bindings/src/events/SyntheticEvent'; +import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal'; +import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle'; +import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext'; type Orientation = 'horizontal' | 'vertical'; @@ -72,9 +75,8 @@ function ToggleUniqueSuspenders() { SuspenseTreeStateContext, ); - function handleToggleUniqueSuspenders(event: SyntheticEvent) { - const nextUniqueSuspendersOnly = (event.currentTarget as HTMLInputElement) - .checked; + function handleToggleUniqueSuspenders() { + const nextUniqueSuspendersOnly = !uniqueSuspendersOnly; const nextTimeline = rootID === null ? [] @@ -90,13 +92,12 @@ function ToggleUniqueSuspenders() { } return ( - - - + + + ); } @@ -212,6 +213,7 @@ function ToggleInspectedElement({ } function SuspenseTab(_: {}) { + const {hideSettings} = useContext(OptionsContext); const [state, dispatch] = useReducer( layoutReducer, null, @@ -394,75 +396,82 @@ function SuspenseTab(_: {}) { }; return ( -
-
- {treeListDisabled ? null : ( - - )} - {treeListDisabled ? null : ( -