Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/components/AppNavigation/AppNavigation.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
.app-navigation {
--bleed: 100dvh;
--indicator-thickness: 0.25rem;
--vertical-padding: 0.75rem;

@include shadows.surface;

Expand All @@ -17,8 +18,8 @@

box-sizing: border-box;
width: 100vw;
height: calc(var(--app-bar-height) + var(--bleed));
padding-top: var(--bleed);
height: calc(var(--app-bar-height) + var(--bleed) + env(safe-area-inset-top));
padding-top: calc(var(--bleed) + env(safe-area-inset-top));

&-content {
display: grid;
Expand All @@ -27,7 +28,10 @@

box-sizing: border-box;
margin: 0 auto;
padding: 0.75rem var(--page-padding) calc(0.75rem - var(--border-width));
padding-top: calc(var(--vertical-padding) - var(--border-width));
padding-right: calc(var(--page-padding) + env(safe-area-inset-right));
padding-bottom: var(--vertical-padding);
padding-left: calc(var(--page-padding) + env(safe-area-inset-left));

&[data-layout="mobile"] {
grid-template-areas: "navigation logo secondary";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
display: grid;
grid-template-areas: "header" "routes";
grid-template-columns: 1fr;
grid-template-rows: var(--app-bar-height) 1fr;
grid-template-rows: calc(var(--app-bar-height) + env(safe-area-inset-top)) 1fr;

width: 20rem;
max-width: calc(100vw - 2rem);
Expand All @@ -55,7 +55,7 @@
grid-area: header;
flex-shrink: 0;
justify-content: space-between;
padding: 0 var(--padding-x);
padding: env(safe-area-inset-top) var(--padding-x) 0 calc(var(--padding-x) + env(safe-area-inset-left));
}

&-scroll-area {
Expand All @@ -66,7 +66,11 @@
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem var(--padding-x);

padding-top: 1rem;
padding-right: var(--padding-x);
padding-bottom: calc(1rem + env(safe-area-inset-bottom));
padding-left: calc(var(--padding-x) + env(safe-area-inset-left));
}
}
}
14 changes: 9 additions & 5 deletions src/components/DialogManager/Dialog.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,20 @@
}

&-popup {
--dialog-usable-height: calc(100dvh - (2 * var(--page-padding)));
--dialog-usable-width: calc(100dvw - (2 * var(--page-padding)));
--dialog-offset-top: calc(var(--page-padding) + env(safe-area-inset-top));
--dialog-offset-bottom: calc(var(--page-padding) + env(safe-area-inset-bottom));
--dialog-offset-left: calc(var(--page-padding) + env(safe-area-inset-left));
--dialog-offset-right: calc(var(--page-padding) + env(safe-area-inset-right));
--dialog-usable-height: calc(100dvh - (var(--dialog-offset-top) + var(--dialog-offset-bottom)));
--dialog-usable-width: calc(100dvw - (var(--dialog-offset-left) + var(--dialog-offset-right)));
--dialog-max-height: min(var(--dialog-user-max-height, 100dvh), var(--dialog-usable-height));
--dialog-max-width: min(var(--dialog-user-max-width, 100dvw), var(--dialog-usable-width));

@include shadows.elevated;

position: fixed;
top: 50%;
left: 50%;
top: calc((var(--dialog-usable-height) / 2) + var(--dialog-offset-top));
left: calc((var(--dialog-usable-width) / 2) + var(--dialog-offset-left));
transform: translate(-50%, -50%) scale(calc(1 - 0.1 * var(--nested-dialogs)));
translate: 0 calc(0px + 1.25rem * var(--nested-dialogs));

Expand All @@ -52,7 +56,7 @@

box-sizing: border-box;
width: max-content;
min-width: min(calc(100dvw - (2 * var(--page-padding))), 24rem);
min-width: min(var(--dialog-usable-width), 24rem);
max-width: var(--dialog-max-width);
max-height: var(--dialog-max-height);
padding: var(--modal-padding) 0;
Expand Down
50 changes: 50 additions & 0 deletions src/components/Drawer/Drawer.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@
&-popup {
--bleed: 3rem;
--stack-scale: calc(1 - 0.05 * var(--nested-drawers, 0));
--inset-top: calc(var(--modal-padding) + env(safe-area-inset-top));
--inset-right: calc(var(--modal-padding) + env(safe-area-inset-right));
--inset-bottom: calc(var(--modal-padding) + env(safe-area-inset-bottom));
--inset-left: calc(var(--modal-padding) + env(safe-area-inset-left));

@include shadows.elevated;

Expand Down Expand Up @@ -247,6 +251,29 @@
justify-content: space-between;
padding: var(--modal-padding) var(--modal-padding) 1rem;

// Only inset for the safe area on edges where this drawer's popup actually touches the
// screen edge for its anchor direction.
.drawer-popup[data-swipe-direction="up"] & {
padding-top: var(--inset-top);
padding-right: var(--inset-right);
padding-left: var(--inset-left);
}

.drawer-popup[data-swipe-direction="down"] & {
padding-right: var(--inset-right);
padding-left: var(--inset-left);
}

.drawer-popup[data-swipe-direction="left"] & {
padding-top: var(--inset-top);
padding-left: var(--inset-left);
}

.drawer-popup[data-swipe-direction="right"] & {
padding-top: var(--inset-top);
padding-right: var(--inset-right);
}

&-close {
margin: -0.25rem;
}
Expand All @@ -266,6 +293,29 @@
min-height: 0;
padding: 0 var(--modal-padding) var(--modal-padding);

// Only inset for the safe area on edges where this drawer's popup actually touches the
// screen edge for its anchor direction.
.drawer-popup[data-swipe-direction="up"] & {
padding-right: var(--inset-right);
padding-left: var(--inset-left);
}

.drawer-popup[data-swipe-direction="down"] & {
padding-right: var(--inset-right);
padding-bottom: var(--inset-bottom);
padding-left: var(--inset-left);
}

.drawer-popup[data-swipe-direction="left"] & {
padding-bottom: var(--inset-bottom);
padding-left: var(--inset-left);
}

.drawer-popup[data-swipe-direction="right"] & {
padding-right: var(--inset-right);
padding-bottom: var(--inset-bottom);
}

&[data-padding="false"] {
padding: 0;
}
Expand Down
41 changes: 41 additions & 0 deletions src/components/FooterBar/FooterBar.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@use "../../style/layers";
@use "../../style/flex";
@use "../../style/shadows";

@layer components {
.footer-bar {
--bleed: 100dvh;
--vertical-padding: 1rem;

@include shadows.surface;

position: fixed;
bottom: calc(-1 * var(--bleed));
left: 0;

overflow: hidden;

box-sizing: border-box;
width: 100vw;
padding-bottom: calc(var(--bleed) + env(safe-area-inset-bottom));

&-content {
@include flex.row($gap: 1.5rem, $yAlign: center);

box-sizing: border-box;
margin: 0 auto;
padding-top: calc(var(--vertical-padding) - var(--border-width));
padding-right: calc(var(--page-padding) + env(safe-area-inset-right));
padding-bottom: var(--vertical-padding);
padding-left: calc(var(--page-padding) + env(safe-area-inset-left));

&[data-mobile] {
--mobile-offset: 0.5rem;

padding-right: calc(var(--vertical-padding) + var(--mobile-offset) + env(safe-area-inset-right));
padding-bottom: calc(var(--vertical-padding) + var(--mobile-offset));
padding-left: calc(var(--vertical-padding) + var(--mobile-offset) + env(safe-area-inset-left));
}
}
}
}
2 changes: 2 additions & 0 deletions src/components/FooterBar/FooterBar.module.scss.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export declare const footerBar: string;
export declare const footerBarContent: string;
120 changes: 120 additions & 0 deletions src/components/FooterBar/FooterBar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import {
ArrowLeft,
Save,
X,
} from 'lucide-react';

import { ButtonProps } from '../Button';
import { Spinner } from '../Spinner';
import { FooterBar, FooterBarProps } from './FooterBar';
import { FooterBarActions } from './FooterBarActions';

interface StoryArgs extends FooterBarProps {
mobile?: boolean;
}

const meta: Meta<StoryArgs> = {
title: 'Components/FooterBar',
component: FooterBar,
parameters: {
layout: 'centered',
bodyBackground: 'var(--color-page-bg)',
docs: {
story: { inline: false, height: '12rem' },
},
},
argTypes: {
maxWidth: {
control: 'number',
description: 'Constrains the inner content width.',
},
mobile: {
control: 'boolean',
description: 'Switches between desktop and mobile layouts.',
},
portalTarget: {
table: { disable: true },
},
children: {
table: { disable: true },
},
},
decorators: [
(Story, { args }) => {
const [portalTarget, setPortalTarget] = useState<Element | null>(null);
return (
<div ref={setPortalTarget} style={{ width: '100%', height: '100%' }}>
{portalTarget && <Story args={{ ...args, portalTarget }} />}
</div>
);
},
],
};

export default meta;
type Story = StoryObj<typeof meta>;

const actions: {
left: ButtonProps[];
right: ButtonProps[];
} = {
left: [{ icon: <ArrowLeft />, text: 'Cancel', variant: 'shaded' }],
right: [{ icon: <Save />, text: 'Save', variant: 'solid', intent: 'primary' }],
};

export const Default: Story = {
args: {
mobile: false,
},
render: (args) => (
<FooterBar {...args}>
<FooterBarActions
mobile={args.mobile}
{...actions}
/>
</FooterBar>
),
};

export const MaxWidth: Story = {
args: {
mobile: false,
maxWidth: 256,
},
render: (args) => (
<FooterBar {...args}>
<FooterBarActions
mobile={args.mobile}
left={[
{ icon: <X />, text: 'Cancel', variant: 'ghost' },
]}
right={[
{ icon: <Save />, text: 'Save', variant: 'solid', intent: 'primary' },
]}
/>
</FooterBar>
),
};

export const CustomElement: Story = {
args: {
mobile: false,
},
render: (args) => (
<FooterBar {...args}>
<div style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'start',
gap: '0.75rem',
flex: 1,
}}>
<Spinner size={20} />
<span>Syncing changes…</span>
</div>
</FooterBar>
),
};
59 changes: 59 additions & 0 deletions src/components/FooterBar/FooterBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
ReactElement,
ReactNode,
useEffect,
useRef,
} from 'react';
import { createPortal } from 'react-dom';
import clsx from 'clsx';

import { getStyleClassNames } from '../../utils/getStyleClassNames';

import styles from './FooterBar.module.scss';

export interface FooterBarProps {
children?: ReactNode;
className?: string;
maxWidth?: number | string;
mobile?: boolean;
portalTarget?: Element;
}

export const FooterBar = ({
children,
className,
maxWidth = '100vw',
mobile = false,
portalTarget = document.body,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}: FooterBarProps): ReactElement => {
const contentRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const content = contentRef.current;
if (!content) {
return;
}
const observer = new ResizeObserver(([entry]) => {
document.documentElement.style.setProperty('--footer-bar-height', `${entry.contentRect.height}px`);
});
observer.observe(content);
return () => {
observer.disconnect();
document.documentElement.style.removeProperty('--footer-bar-height');
};
}, []);
Comment thread
ianpaschal marked this conversation as resolved.

return createPortal((
<div
className={clsx(styles.footerBar, ...getStyleClassNames({
variant: 'surface',
border: 'top',
}), className)}

>
<div ref={contentRef} className={styles.footerBarContent} style={{ maxWidth }} data-mobile={mobile || undefined}>
{children}
</div>
</div>
), portalTarget);
};
Loading
Loading