diff --git a/.changeset/calm-pillows-mix.md b/.changeset/calm-pillows-mix.md new file mode 100644 index 00000000..b9bc889e --- /dev/null +++ b/.changeset/calm-pillows-mix.md @@ -0,0 +1,5 @@ +--- +'@solidmaterial/landing-page': minor +--- + +Add action button to EmptyState component diff --git a/.changeset/dirty-icons-flow.md b/.changeset/dirty-icons-flow.md new file mode 100644 index 00000000..a7071031 --- /dev/null +++ b/.changeset/dirty-icons-flow.md @@ -0,0 +1,5 @@ +--- +'@solidmaterial/material': minor +--- + +Add supporting-pane canonical layout diff --git a/apps/landing-page/src/components/empty-state/EmptyState.module.css b/apps/landing-page/src/components/empty-state/EmptyState.module.css index b32279eb..c00f3251 100644 --- a/apps/landing-page/src/components/empty-state/EmptyState.module.css +++ b/apps/landing-page/src/components/empty-state/EmptyState.module.css @@ -4,4 +4,16 @@ place-content: center; justify-items: center; text-align: center; + + & > md-icon { + padding-bottom: var(--md-user-spacing-xs); + } + + & > span { + padding-top: var(--md-user-spacing-xs); + } + + & > sm-button { + padding-top: var(--md-user-spacing-lg); + } } diff --git a/apps/landing-page/src/components/empty-state/EmptyState.tsx b/apps/landing-page/src/components/empty-state/EmptyState.tsx index 6cb5561e..54ac3cc2 100644 --- a/apps/landing-page/src/components/empty-state/EmptyState.tsx +++ b/apps/landing-page/src/components/empty-state/EmptyState.tsx @@ -1,5 +1,7 @@ -import type { JSX, VoidComponent } from 'solid-js'; +import type { MaterialButtonProps } from '@solidmaterial/material/components/button'; +import type { FlowProps, JSX, VoidComponent } from 'solid-js'; +import { MaterialButton } from '@solidmaterial/material/components/button'; import { MaterialIcon } from '@solidmaterial/material/components/icon'; import { H1, Span } from '@solidmaterial/material/components/typography'; import { Show } from 'solid-js'; @@ -10,6 +12,7 @@ export interface EmptyStateProps { label: string; supportingText?: string; icon?: JSX.Element; + action?: FlowProps; } export const EmptyState: VoidComponent = props => { @@ -26,6 +29,7 @@ export const EmptyState: VoidComponent = props => { {props.supportingText} + {action => } ); }; diff --git a/packages/material/src/components/side-sheet/MaterialSideSheet.module.css b/packages/material/src/components/side-sheet/MaterialSideSheet.module.css index dcef8835..869014f0 100644 --- a/packages/material/src/components/side-sheet/MaterialSideSheet.module.css +++ b/packages/material/src/components/side-sheet/MaterialSideSheet.module.css @@ -188,7 +188,10 @@ display: flex; flex-direction: row; gap: 12px; - padding-bottom: var(--md-user-spacing-md); + + &:not(:empty) { + padding-bottom: var(--md-user-spacing-md); + } & > h1 { color: var(--md-sys-color-on-surface-variant); diff --git a/packages/material/src/layouts/body-layout/MaterialBodyLayout.tsx b/packages/material/src/layouts/body-layout/MaterialBodyLayout.tsx index a32ef6f1..d2dbf09a 100644 --- a/packages/material/src/layouts/body-layout/MaterialBodyLayout.tsx +++ b/packages/material/src/layouts/body-layout/MaterialBodyLayout.tsx @@ -13,8 +13,9 @@ import type { TwoPaneLayoutProps } from './pane-layouts/pane-layout.types'; import { SplitPaneLayout } from './layouts/split-pane-layout/SplitPaneLayout'; import { TwoPaneFixedFlexibleLayout } from './layouts/two-pane-fixed-flexible-layout/TwoPaneFixedFlexibleLayout'; import { TwoPaneFlexibleFixedLayout } from './layouts/two-pane-flexible-fixed-layout/TwoPaneFlexibleFixedLayout'; +import { TwoFlexiblePanesLayout } from './pane-layouts/two-flexible-panes-layout/TwoFlexiblePanesLayout'; -export type MaterialBodyLayoutVariant = 'split' | 'flexible-fixed' | 'fixed-flexible'; +export type MaterialBodyLayoutVariant = 'split' | 'flexible-fixed' | 'fixed-flexible' | 'flexible-flexible'; export interface MaterialBodyLayoutProps extends MaterialBodyLayoutWithDragHandleProps { variant: MaterialBodyLayoutVariant; @@ -24,7 +25,8 @@ export interface MaterialBodyLayoutProps extends MaterialBodyLayoutWithDragHandl const LAYOUTS: Record> = { split: SplitPaneLayout, 'flexible-fixed': TwoPaneFlexibleFixedLayout, - 'fixed-flexible': TwoPaneFixedFlexibleLayout + 'fixed-flexible': TwoPaneFixedFlexibleLayout, + 'flexible-flexible': TwoFlexiblePanesLayout }; export const MaterialBodyLayout: FlowComponent = props => { @@ -37,8 +39,8 @@ export const MaterialBodyLayout: FlowComponent = props const railWidth = () => navigationRailWidth?.() ?? 0; const margin = () => (Breakpoints.isCompactWidth() ? 16 : 24); - const marginLeft = () => (railWidth() === 0 && visiblePanes().length > 1 ? margin() : 0); - const marginRight = () => (railWidth() > 0 || visiblePanes().length > 1 ? margin() : 0); + const marginLeft = () => (railWidth() === 0 && visiblePanes().length > 1 && props.rounded !== false ? margin() : 0); + const marginRight = () => (visiblePanes().length > 1 && props.rounded !== false ? margin() : 0); const windowSize = createWindowSize(); const maximumWidth = () => windowSize.width - marginLeft() - marginRight() - railWidth(); diff --git a/packages/material/src/layouts/body-layout/MaterialBodyLayout.types.ts b/packages/material/src/layouts/body-layout/MaterialBodyLayout.types.ts index a4d997b8..5a52f4a3 100644 --- a/packages/material/src/layouts/body-layout/MaterialBodyLayout.types.ts +++ b/packages/material/src/layouts/body-layout/MaterialBodyLayout.types.ts @@ -6,6 +6,7 @@ export type DragHandlePosition = { }; export interface MaterialBodyLayoutWithDragHandleProps { + rounded?: boolean; showDragHandle?: boolean; dragHandleAriaLabel?: string; dragHandleAriaValue?: (position: DragHandlePosition) => string; diff --git a/packages/material/src/layouts/body-layout/layouts/split-pane-layout/SplitPaneLayout.tsx b/packages/material/src/layouts/body-layout/layouts/split-pane-layout/SplitPaneLayout.tsx index 7f71545a..4fae6464 100644 --- a/packages/material/src/layouts/body-layout/layouts/split-pane-layout/SplitPaneLayout.tsx +++ b/packages/material/src/layouts/body-layout/layouts/split-pane-layout/SplitPaneLayout.tsx @@ -49,6 +49,7 @@ export const SplitPaneLayout: FlowComponent = props => { width={fixedPaneWidth()} snapWidths={snapWidths()} preferredWidth={visuallyCentered()} + rounded={props.rounded} showDragHandle={showDragHandle()} dragHandleAriaLabel={props.dragHandleAriaLabel} dragHandleAriaValue={props.dragHandleAriaValue} diff --git a/packages/material/src/layouts/body-layout/layouts/two-pane-fixed-flexible-layout/TwoPaneFixedFlexibleLayout.tsx b/packages/material/src/layouts/body-layout/layouts/two-pane-fixed-flexible-layout/TwoPaneFixedFlexibleLayout.tsx index e3e1a72e..96f9095e 100644 --- a/packages/material/src/layouts/body-layout/layouts/two-pane-fixed-flexible-layout/TwoPaneFixedFlexibleLayout.tsx +++ b/packages/material/src/layouts/body-layout/layouts/two-pane-fixed-flexible-layout/TwoPaneFixedFlexibleLayout.tsx @@ -41,6 +41,7 @@ export const TwoPaneFixedFlexibleLayout: FlowComponent = pro width={fixedPaneWidth()} snapWidths={snapWidths()} preferredWidth={spacerVisuallyCentered()} + rounded={props.rounded} showDragHandle={showDragHandle()} dragHandleAriaLabel={props.dragHandleAriaLabel} dragHandleAriaValue={props.dragHandleAriaValue} diff --git a/packages/material/src/layouts/body-layout/layouts/two-pane-flexible-fixed-layout/TwoPaneFlexibleFixedLayout.tsx b/packages/material/src/layouts/body-layout/layouts/two-pane-flexible-fixed-layout/TwoPaneFlexibleFixedLayout.tsx index 2cac1f46..5b7535ed 100644 --- a/packages/material/src/layouts/body-layout/layouts/two-pane-flexible-fixed-layout/TwoPaneFlexibleFixedLayout.tsx +++ b/packages/material/src/layouts/body-layout/layouts/two-pane-flexible-fixed-layout/TwoPaneFlexibleFixedLayout.tsx @@ -40,6 +40,7 @@ export const TwoPaneFlexibleFixedLayout: FlowComponent = pro width={fixedPaneWidth()} snapWidths={snapWidths()} preferredWidth={spacerVisuallyCentered()} + rounded={props.rounded} showDragHandle={showDragHandle()} dragHandleAriaLabel={props.dragHandleAriaLabel} dragHandleAriaValue={props.dragHandleAriaValue} diff --git a/packages/material/src/layouts/body-layout/pane-layouts/two-flexible-panes-layout/TwoFlexiblePanesLayout.module.css b/packages/material/src/layouts/body-layout/pane-layouts/two-flexible-panes-layout/TwoFlexiblePanesLayout.module.css new file mode 100644 index 00000000..45f2f363 --- /dev/null +++ b/packages/material/src/layouts/body-layout/pane-layouts/two-flexible-panes-layout/TwoFlexiblePanesLayout.module.css @@ -0,0 +1,25 @@ +@layer material.components { + .layout { + display: flex; + contain: layout size; + height: 100%; + + view-transition-name: sm-body-layout; + + /* Do not apply transition to nested layout */ + .layout { + view-transition-name: none; + } + } + + .pane { + flex-grow: 1; + height: 100%; + contain: style; + overflow-y: auto; + } + + .layout:not([data-rounded='false']) > .pane { + padding-block: var(--md-comp-pane-container-margin-block); + } +} diff --git a/packages/material/src/layouts/body-layout/pane-layouts/two-flexible-panes-layout/TwoFlexiblePanesLayout.tsx b/packages/material/src/layouts/body-layout/pane-layouts/two-flexible-panes-layout/TwoFlexiblePanesLayout.tsx new file mode 100644 index 00000000..e9fce3f1 --- /dev/null +++ b/packages/material/src/layouts/body-layout/pane-layouts/two-flexible-panes-layout/TwoFlexiblePanesLayout.tsx @@ -0,0 +1,35 @@ +import type { FlowComponent } from 'solid-js'; + +import { Show, children, createMemo } from 'solid-js'; + +import styles from './TwoFlexiblePanesLayout.module.css'; + +export interface TwoFlexiblePanesLayoutProps { + rounded?: boolean; + margin: [number, number]; + class?: string | undefined; +} + +export const TwoFlexiblePanesLayout: FlowComponent = props => { + const panes = children(() => props.children); + const visiblePanesCount = createMemo(() => panes.toArray().filter(item => item !== undefined).length); + + return ( + +
{panes.toArray()[0]}
+ 1}> +
{panes.toArray()[1]}
+
+
+ ); +}; diff --git a/packages/material/src/layouts/body-layout/pane-layouts/two-panes-with-spacer-layout/TwoPanesWithSpacerLayout.module.css b/packages/material/src/layouts/body-layout/pane-layouts/two-panes-with-spacer-layout/TwoPanesWithSpacerLayout.module.css index 1f42d816..2fd39d4a 100644 --- a/packages/material/src/layouts/body-layout/pane-layouts/two-panes-with-spacer-layout/TwoPanesWithSpacerLayout.module.css +++ b/packages/material/src/layouts/body-layout/pane-layouts/two-panes-with-spacer-layout/TwoPanesWithSpacerLayout.module.css @@ -20,6 +20,11 @@ } view-transition-name: sm-body-layout; + + /* Do not apply transition to nested layout */ + .layout { + view-transition-name: none; + } } .spacer { @@ -32,4 +37,8 @@ height: 100%; contain: inline-size style; } + + .layout:is(:has(.spacer):not([data-rounded='false']), [data-rounded='true']) > .pane { + padding-block: var(--md-comp-pane-container-margin-block); + } } diff --git a/packages/material/src/layouts/body-layout/pane-layouts/two-panes-with-spacer-layout/TwoPanesWithSpacerLayout.tsx b/packages/material/src/layouts/body-layout/pane-layouts/two-panes-with-spacer-layout/TwoPanesWithSpacerLayout.tsx index 5bd4a276..4a42d491 100644 --- a/packages/material/src/layouts/body-layout/pane-layouts/two-panes-with-spacer-layout/TwoPanesWithSpacerLayout.tsx +++ b/packages/material/src/layouts/body-layout/pane-layouts/two-panes-with-spacer-layout/TwoPanesWithSpacerLayout.tsx @@ -68,6 +68,7 @@ export const TwoPanesWithSpacerLayout: FlowComponent :last-child sm-pane > * { + & > :last-child > sm-pane > :first-child { view-transition-name: sm-detail-pane; height: 100%; diff --git a/packages/material/src/layouts/canonical-layouts/supporting-pane-layout/MaterialSupportingPane.tsx b/packages/material/src/layouts/canonical-layouts/supporting-pane-layout/MaterialSupportingPane.tsx new file mode 100644 index 00000000..c8a5ccc8 --- /dev/null +++ b/packages/material/src/layouts/canonical-layouts/supporting-pane-layout/MaterialSupportingPane.tsx @@ -0,0 +1,60 @@ +import type { FlowComponent } from 'solid-js'; + +import { Match, Show, Switch, createEffect, createSignal } from 'solid-js'; + +import { MaterialBottomSheet } from '../../../components/bottom-sheet'; +import { MaterialSideSheet } from '../../../components/side-sheet/MaterialSideSheet'; +import { Breakpoints } from '../../../utils'; +import { MaterialPane } from '../../pane/MaterialPane'; + +export type MaterialSupportingPaneContainerVariant = 'standard' | 'modal'; + +export interface MaterialSupportingPaneProps { + variant: MaterialSupportingPaneContainerVariant; + title?: string; + dragHandle?: boolean; + dragHandleAriaLabel?: string; + open?: boolean; + onClose?: () => void; +} + +export const MaterialSupportingPane: FlowComponent = props => { + const isMobile = () => Breakpoints.isCompactWidth() || Breakpoints.isMediumWidth(); + + const [isOpen, setOpen] = createSignal(props.open ?? true); + + createEffect(() => { + setOpen(props.open ?? true); + }); + + return ( + + + + {props.children}}> + props.onClose?.()} + > + {props.children} + + + + + setOpen(false) : undefined} + onClose={() => props.onClose?.()} + > + {props.children} + + + + + ); +}; diff --git a/packages/material/src/layouts/canonical-layouts/supporting-pane-layout/MaterialSupportingPaneLayout.module.css b/packages/material/src/layouts/canonical-layouts/supporting-pane-layout/MaterialSupportingPaneLayout.module.css new file mode 100644 index 00000000..6f72edd8 --- /dev/null +++ b/packages/material/src/layouts/canonical-layouts/supporting-pane-layout/MaterialSupportingPaneLayout.module.css @@ -0,0 +1,61 @@ +@layer material.components { + :root { + --md-comp-layouts-supporting-pane-width: 360px; + } + + .layout { + display: grid; + grid-template-columns: auto max-content; + + --md-comp-side-sheet-width: var(--md-comp-layouts-supporting-pane-width); + transition-property: gap; + + &:has(> :last-child > sm-pane > :is(dialog, aside)[data-open]:not(:modal)) { + gap: var(--md-user-spacing-lg); + + /* Transition when entering the screen */ + transition-duration: var(--md-sys-motion-duration-emphasized-decelerate); + transition-timing-function: var(--md-sys-motion-easing-emphasized-decelerate); + } + + &:has(> :last-child > sm-pane > :is(dialog, aside):not([data-open])) { + gap: 0; + + /* Transition when exiting the screen */ + transition-duration: var(--md-sys-motion-duration-emphasized-accelerate); + transition-timing-function: var(--md-sys-motion-easing-emphasized-accelerate); + } + + &:not(:has(> :last-child > sm-pane > dialog)) { + grid-template-columns: 1fr; + grid-template-rows: auto max-content; + + & > *:not(:first-child) { + padding-block-start: unset; + } + & > *:not(:last-child) { + padding-block-end: unset; + } + + & > :last-child > sm-pane { + transition-property: max-height; + interpolate-size: allow-keywords; + + max-height: 0; + overflow-y: hidden; + + /* Transition when exiting the screen */ + transition-duration: var(--md-sys-motion-duration-emphasized-accelerate); + transition-timing-function: var(--md-sys-motion-easing-emphasized-accelerate); + + &:has(> aside[data-open]) { + max-height: fit-content; + + /* Transition when entering the screen */ + transition-duration: var(--md-sys-motion-duration-emphasized-decelerate); + transition-timing-function: var(--md-sys-motion-easing-emphasized-decelerate); + } + } + } + } +} diff --git a/packages/material/src/layouts/canonical-layouts/supporting-pane-layout/MaterialSupportingPaneLayout.stories.tsx b/packages/material/src/layouts/canonical-layouts/supporting-pane-layout/MaterialSupportingPaneLayout.stories.tsx new file mode 100644 index 00000000..2885cd3f --- /dev/null +++ b/packages/material/src/layouts/canonical-layouts/supporting-pane-layout/MaterialSupportingPaneLayout.stories.tsx @@ -0,0 +1,119 @@ +import type { Component } from 'solid-js'; + +import { Show, createSignal } from 'solid-js'; +import { createJSXDecorator } from 'storybook-solidjs-vite'; + +import preview from '../../../../.storybook/preview'; +import { MaterialButton } from '../../../components/button/MaterialButton'; +import { MaterialPane } from '../../pane/MaterialPane'; + +import type { MaterialSupportingPaneProps } from './MaterialSupportingPane'; + +import { MaterialSupportingPane } from './MaterialSupportingPane'; +import { MaterialSupportingPaneLayout } from './MaterialSupportingPaneLayout'; + +const meta = preview.meta({ + title: 'Layouts/MaterialSupportingPaneLayout', + component: MaterialSupportingPane, + decorators: [ + createJSXDecorator(Story => ( +
+ +
+ )) + ], + globals: { + backgrounds: { + value: 'surface-container' + } + }, + parameters: { + docs: { + story: { + iframeHeight: '500px', + inline: false + } + }, + layout: 'fullscreen' + } +}); + +const MaterialSupportingPaneLayoutRenderer: Component> = args => { + const [isOpen, setOpen] = createSignal(args.open); + + const onClose = () => setOpen(false); + const onClickButton = () => setOpen(value => (value !== undefined ? !value : undefined)); + + return ( + + +
+ Main content + + + + Toggle supporting pane + + +
+
+ +
Supporting content
+
+
+ ); +}; + +export const Example = meta.story({ + args: { + variant: 'standard', + dragHandleAriaLabel: 'Drag handle for bottom sheet' + }, + render: () => +}); + +export const StandardSide = meta.story({ + args: { + variant: 'standard', + title: 'Title', + open: false + }, + globals: { + viewport: { value: 'ipad', isRotated: true } + }, + render: () => +}); + +export const StandardBelow = meta.story({ + args: { + variant: 'standard', + open: false + }, + globals: { + viewport: { value: 'pixel', isRotated: false } + }, + render: () => +}); + +export const ModalSide = meta.story({ + args: { + variant: 'modal', + title: 'Title', + open: false + }, + globals: { + viewport: { value: 'ipad', isRotated: true } + }, + render: () => +}); + +export const ModalBelow = meta.story({ + args: { + variant: 'modal', + open: false + }, + globals: { + viewport: { value: 'pixel', isRotated: false } + }, + render: () => +}); diff --git a/packages/material/src/layouts/canonical-layouts/supporting-pane-layout/MaterialSupportingPaneLayout.tsx b/packages/material/src/layouts/canonical-layouts/supporting-pane-layout/MaterialSupportingPaneLayout.tsx new file mode 100644 index 00000000..d25daf28 --- /dev/null +++ b/packages/material/src/layouts/canonical-layouts/supporting-pane-layout/MaterialSupportingPaneLayout.tsx @@ -0,0 +1,22 @@ +import type { FlowComponent } from 'solid-js'; + +import { MaterialBodyLayout } from '../../body-layout/MaterialBodyLayout'; + +import styles from './MaterialSupportingPaneLayout.module.css'; + +export interface MaterialSupportingPaneLayoutProps { + rounded?: boolean; +} + +export const MaterialSupportingPaneLayout: FlowComponent = props => { + return ( + + {props.children} + + ); +}; diff --git a/packages/material/src/layouts/index.ts b/packages/material/src/layouts/index.ts index ea971fd5..ac42dd5d 100644 --- a/packages/material/src/layouts/index.ts +++ b/packages/material/src/layouts/index.ts @@ -8,6 +8,13 @@ export type { export { MaterialBodyLayout } from './body-layout/MaterialBodyLayout'; export { MaterialListDetailLayout } from './canonical-layouts/list-detail-layout/MaterialListDetailLayout'; +export { MaterialSupportingPane } from './canonical-layouts/supporting-pane-layout/MaterialSupportingPane'; +export type { + MaterialSupportingPaneProps, + MaterialSupportingPaneContainerVariant +} from './canonical-layouts/supporting-pane-layout/MaterialSupportingPane'; +export { MaterialSupportingPaneLayout } from './canonical-layouts/supporting-pane-layout/MaterialSupportingPaneLayout'; +export type { MaterialSupportingPaneLayoutProps } from './canonical-layouts/supporting-pane-layout/MaterialSupportingPaneLayout'; export { MaterialNavigationBarLayout } from './navigation-bar-layout/MaterialNavigationBarLayout'; export { MaterialNavigationLayout } from './navigation-layout/MaterialNavigationLayout'; export { MaterialNavigationRailLayout } from './navigation-rail-layout/MaterialNavigationRailLayout'; diff --git a/packages/material/src/layouts/pane/MaterialPane.module.css b/packages/material/src/layouts/pane/MaterialPane.module.css index ee4c9b23..597e4b43 100644 --- a/packages/material/src/layouts/pane/MaterialPane.module.css +++ b/packages/material/src/layouts/pane/MaterialPane.module.css @@ -22,11 +22,7 @@ } } - :has(sm-spacer) *:has(> .pane) { - padding-block: var(--md-comp-pane-container-margin-block); - - .pane { - border-radius: var(--md-comp-pane-container-shape); - } + sm-body-layout:is(:has(sm-spacer):not([data-rounded='false']), [data-rounded='true']) .pane { + border-radius: var(--md-comp-pane-container-shape); } }