enhance(mobile): adapt silkhq page components

This commit is contained in:
charlie
2025-07-10 11:49:12 +08:00
parent c1a78095af
commit e1db8e958d
10 changed files with 992 additions and 5 deletions

View File

@@ -82,4 +82,31 @@
(def stacking-sheet-outlet (silkhq-wrap "SheetWithStacking.Outlet"))
(def stacking-sheet-backdrop (silkhq-wrap "SheetWithStacking.Backdrop"))
(def stacking-sheet-view (silkhq-wrap "SheetWithStacking.View"))
(def stacking-sheet-stack (silkhq-wrap "SheetWithStackingStack.Root"))
(def stacking-sheet-stack (silkhq-wrap "SheetWithStackingStack.Root"))
(def parallax-page (silkhq-wrap "ParallaxPage.Root"))
(def parallax-page-portal (silkhq-wrap "ParallaxPage.Portal"))
(def parallax-page-handle (silkhq-wrap "ParallaxPage.Handle"))
(def parallax-page-content (silkhq-wrap "ParallaxPage.Content"))
(def parallax-page-description (silkhq-wrap "ParallaxPage.Description"))
(def parallax-page-title (silkhq-wrap "ParallaxPage.Title"))
(def parallax-page-trigger (silkhq-wrap "ParallaxPage.Trigger"))
(def parallax-page-outlet (silkhq-wrap "ParallaxPage.Outlet"))
(def parallax-page-backdrop (silkhq-wrap "ParallaxPage.Backdrop"))
(def parallax-page-view (silkhq-wrap "ParallaxPage.View"))
(def parallax-page-view-portal (silkhq-wrap "ParallaxPage.ViewPortal"))
(def parallax-page-topbar-title (silkhq-wrap "ParallaxPage.TopBarTitle"))
(def parallax-page-topbar-portal (silkhq-wrap "ParallaxPage.TopBarTitlePortal"))
(def parallax-page-topbar-dismiss-trigger (silkhq-wrap "ParallaxPage.TopBarDismissTrigger"))
(def parallax-page-topbar-dismiss-trigger-portal (silkhq-wrap "ParallaxPage.TopBarDismissTriggerPortal"))
(def card-sheet (silkhq-wrap "CardSheet.Root"))
(def card-sheet-portal (silkhq-wrap "CardSheet.Portal"))
(def card-sheet-handle (silkhq-wrap "CardSheet.Handle"))
(def card-sheet-content (silkhq-wrap "CardSheet.Content"))
(def card-sheet-title (silkhq-wrap "CardSheet.Title"))
(def card-sheet-description (silkhq-wrap "CardSheet.Description"))
(def card-sheet-trigger (silkhq-wrap "CardSheet.Trigger"))
(def card-sheet-outlet (silkhq-wrap "CardSheet.Outlet"))
(def card-sheet-backdrop (silkhq-wrap "CardSheet.Backdrop"))
(def card-sheet-view (silkhq-wrap "CardSheet.View"))

View File

@@ -0,0 +1,18 @@
.Card-view {
--card-radius: 36px;
/* SELF-LAYOUT */
z-index: 1;
}
.Card-content {
/* SELF-LAYOUT */
box-sizing: border-box;
width: min(600px, calc(100% - 2 * 1rem));
height: auto;
/* APPEARANCE */
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
border-radius: var(--card-radius);
background-color: white;
}

View File

@@ -0,0 +1,119 @@
import React from 'react'
import { Sheet } from '@silk-hq/components'
import './Card.css'
// ================================================================================================
// Root
// ================================================================================================
type SheetRootProps = React.ComponentPropsWithoutRef<typeof Sheet.Root>;
type CardRootProps = Omit<SheetRootProps, 'license'> & {
license?: SheetRootProps['license'];
};
const CardRoot = React.forwardRef<React.ElementRef<typeof Sheet.Root>, CardRootProps>(
({ children, ...restProps }, ref) => {
return (
<Sheet.Root license="commercial" {...restProps} ref={ref}>
{children}
</Sheet.Root>
)
}
)
CardRoot.displayName = 'Card.Root'
// ================================================================================================
// View
// ================================================================================================
const CardView = React.forwardRef<
React.ElementRef<typeof Sheet.View>,
React.ComponentPropsWithoutRef<typeof Sheet.View>
>(({ children, className, ...restProps }, ref) => {
return (
<Sheet.View
className={`Card-view ${className ?? ''}`.trim()}
contentPlacement="center"
tracks="top"
enteringAnimationSettings={{
easing: 'spring',
stiffness: 260,
damping: 20,
mass: 1,
}}
nativeEdgeSwipePrevention={true}
{...restProps}
ref={ref}
>
{children}
</Sheet.View>
)
})
CardView.displayName = 'Card.View'
// ================================================================================================
// Backdrop
// ================================================================================================
const CardBackdrop = React.forwardRef<
React.ElementRef<typeof Sheet.Backdrop>,
React.ComponentPropsWithoutRef<typeof Sheet.Backdrop>
>(({ className, ...restProps }, ref) => {
return (
<Sheet.Backdrop
className={`Card-backdrop ${className ?? ''}`.trim()}
travelAnimation={{
opacity: ({ progress }) => Math.min(0.4 * progress, 0.4),
}}
themeColorDimming="auto"
{...restProps}
ref={ref}
/>
)
})
CardBackdrop.displayName = 'Card.Backdrop'
// ================================================================================================
// Content
// ================================================================================================
const CardContent = React.forwardRef<
React.ElementRef<typeof Sheet.Content>,
React.ComponentPropsWithoutRef<typeof Sheet.Content>
>(({ children, className, ...restProps }, ref) => {
return (
<Sheet.Content
className={`Card-content ${className ?? ''}`.trim()}
travelAnimation={{ scale: [0.8, 1] }}
{...restProps}
ref={ref}
>
{children}
</Sheet.Content>
)
})
CardContent.displayName = 'Card.Content'
// ================================================================================================
// Unchanged Components
// ================================================================================================
const CardPortal = Sheet.Portal
const CardTrigger = Sheet.Trigger
const CardHandle = Sheet.Handle
const CardOutlet = Sheet.Outlet
const CardTitle = Sheet.Title
const CardDescription = Sheet.Description
export const Card = {
Root: CardRoot,
Portal: CardPortal,
View: CardView,
Backdrop: CardBackdrop,
Content: CardContent,
Trigger: CardTrigger,
Handle: CardHandle,
Outlet: CardOutlet,
Title: CardTitle,
Description: CardDescription,
}

View File

@@ -0,0 +1,99 @@
/* Stack top bar elements */
.ParallaxPage-stackTopBarDismissTriggerContainer {
/* INNER-LAYOUT */
display: grid;
place-items: center;
}
.ParallaxPage-stackTopBarTitleContainer {
/* INNER-LAYOUT */
display: grid;
place-items: center;
}
/* Page */
.ParallaxPage-view {
--header-height: 49px;
/* SELF-LAYOUT */
/* Adding 60px to make it fully visible below iOS Safari's bottom UI */
/* Subtracting 1px to avoid sub-pixel gap in Safari */
height: calc(
var(--silk-100-lvh-dvh-pct) + 60px -
(env(safe-area-inset-top, 0px) + var(--header-height) + 1px)
);
top: calc(env(safe-area-inset-top, 0px) + var(--header-height) - 1px);
}
@media (min-width: 1000px) {
.ParallaxPage-view {
height: calc(var(--silk-100-lvh-dvh-pct) - var(--header-height));
}
}
.ParallaxPage-content {
/* SELF-LAYOUT */
box-sizing: border-box;
width: 100%;
/* APPEARANCE */
background-color: white;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
/* Page top bar elements */
.ParallaxPage-topBarDismissTrigger {
/* SELF-LAYOUT */
grid-area: 1 / 1;
align-self: center;
margin-left: -14px;
width: 36px;
height: 44px;
/* APPEARANCE */
border-radius: 12px;
outline-offset: -5px;
appearance: none;
border: none;
background-color: transparent;
/* INTERACTIVITY */
cursor: pointer;
/* TRANSFORMATION */
opacity: 0;
visibility: hidden;
/* INNER-LAYOUT */
padding: 0;
display: grid;
align-items: center;
}
.ParallaxPage-topBarDismissIcon {
/* SELF-LAYOUT */
width: 36px;
height: 36px;
/* APPEARANCE */
color: rgb(75, 85, 99);
}
.ParallaxPage-topBarTitle {
/* SELF-LAYOUT */
grid-area: 1 / 1;
max-width: 100%;
/* APPEARANCE */
opacity: 0;
/* TEXT */
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
font-size: 17.25px;
font-weight: 620;
color: rgb(31, 41, 55);
}

View File

@@ -0,0 +1,464 @@
import React, { createContext, useContext, useRef, useState } from 'react'
import { createComponentId, Island, Sheet, SheetStack } from '@silk-hq/components'
import './ParallaxPage.css'
// ================================================================================================
// Utils
// ================================================================================================
const setRefs = <T, >(...refs: (React.Ref<T> | undefined)[]): ((node: T) => void) => {
return (node: T) => {
refs.forEach((ref) => {
if (typeof ref === 'function') {
ref(node)
} else if (ref) {
// @ts-ignore - intentionally breaking the readonly nature for compatibility
ref.current = node
}
})
}
}
// ================================================================================================
// Stack Id
// ================================================================================================
const ParallaxPageExampleStackId = createComponentId()
// ================================================================================================
// StackRoot Context
// ================================================================================================
type ParallaxPageStackRootContextValue = {
pageContainer: HTMLElement | null;
setPageContainer: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
dismissTriggerContainerRef: React.RefObject<HTMLDivElement>;
topBarTitleContainerRef: React.RefObject<HTMLDivElement>;
};
const ParallaxPageStackRootContext = createContext<ParallaxPageStackRootContextValue | null>(null)
const useParallaxPageStackRootContext = () => {
const context = useContext(ParallaxPageStackRootContext)
if (!context) {
throw new Error(
'useParallaxPageStackRootContext must be used within a ParallaxPageStackRootContext'
)
}
return context
}
// ================================================================================================
// Stack Root
// ================================================================================================
// In this component, the stack root is designed to receive the
// parallax page Views.
const ParallaxPageStackRoot = React.forwardRef<
React.ElementRef<typeof SheetStack.Root>,
React.ComponentPropsWithoutRef<typeof SheetStack.Root>
>(({ componentId, ...restProps }, ref) => {
const [pageContainer, setPageContainer] = useState<HTMLElement | null>(null)
const dismissTriggerContainerRef = useRef<HTMLDivElement>(null)
const topBarTitleContainerRef = useRef<HTMLDivElement>(null)
return (
<ParallaxPageStackRootContext.Provider
value={{
pageContainer,
setPageContainer,
dismissTriggerContainerRef,
topBarTitleContainerRef,
}}
>
<SheetStack.Root
// Using a componentId to associate the SheetStack
// with the ParallaxPageStackIslandRoot
componentId={componentId ?? ParallaxPageExampleStackId}
{...restProps}
ref={setRefs(ref, setPageContainer)}
/>
</ParallaxPageStackRootContext.Provider>
)
})
ParallaxPageStackRoot.displayName = 'ParallaxPageStack.Root'
// ================================================================================================
// Stack Scenery Outlet
// ================================================================================================
// An outlet meant to wrap the content below the stack for the
// initial parallax effect.
const ParallaxPageStackSceneryOutlet = React.forwardRef<
React.ElementRef<typeof SheetStack.Outlet>,
React.ComponentPropsWithoutRef<typeof SheetStack.Outlet>
>((props, ref) => {
return (
<SheetStack.Outlet
stackingAnimation={{
transformOrigin: '50% 0px',
translateX: ({ progress }) => (progress <= 1 ? progress * -80 + 'px' : '-80px'),
}}
{...props}
ref={ref}
/>
)
})
ParallaxPageStackSceneryOutlet.displayName = 'ParallaxPageStack.SceneryOutlet'
// ================================================================================================
// Stack Island Root
// ================================================================================================
// An Island meant to wrap the top bar, but which can be used
// elsewhere as well.
const ParallaxPageStackIslandRoot = React.forwardRef<
React.ElementRef<typeof Island.Root>,
React.ComponentPropsWithoutRef<typeof Island.Root>
>(({ forComponent, ...restProps }, ref) => {
return (
<Island.Root
forComponent={forComponent ?? ParallaxPageExampleStackId}
{...restProps}
ref={ref}
/>
)
})
ParallaxPageStackIslandRoot.displayName = 'ParallaxPageStack.IslandRoot'
// ================================================================================================
// Stack Island Content
// ================================================================================================
const ParallaxPageStackIslandContent = Island.Content
// ================================================================================================
// Stack Top Bar Dismiss Trigger Container
// ================================================================================================
// A container meant to receive the parallax pages dismiss
// triggers in the top bar.
const ParallaxPageStackTopBarDismissTriggerContainer = ({
className,
...restProps
}: React.ComponentPropsWithoutRef<'div'>) => {
const { dismissTriggerContainerRef } = useParallaxPageStackRootContext()
return (
<div
className={`ParallaxPage-stackTopBarDismissTriggerContainer ${className ?? ''}`.trim()}
{...restProps}
ref={dismissTriggerContainerRef}
/>
)
}
ParallaxPageStackTopBarDismissTriggerContainer.displayName =
'ParallaxPageStack.TopBarDismissTriggerContainer'
// ================================================================================================
// Stack Top Bar Title Outlet
// ================================================================================================
// An outlet meant to wrap the initial title of the top bar.
const ParallaxPageStackTopBarTitleOutlet = React.forwardRef<
React.ElementRef<typeof SheetStack.Outlet>,
React.ComponentPropsWithoutRef<typeof SheetStack.Outlet>
>(({ stackingAnimation, ...restProps }, ref) => {
return (
<SheetStack.Outlet
stackingAnimation={{
opacity: ({ progress }) => 0.75 - (1 / 0.75) * (progress - 0.25),
...stackingAnimation,
}}
{...restProps}
ref={ref}
/>
)
})
ParallaxPageStackTopBarTitleOutlet.displayName = 'ParallaxPageStack.TopBarTitleOutlet'
// ================================================================================================
// Stack Top Bar Title Container
// ================================================================================================
// A container meant to receive the parallax pages titles in the
// top bar.
const ParallaxPageStackTopBarTitleContainer = ({
className,
...restProps
}: React.ComponentPropsWithoutRef<'div'>) => {
const { topBarTitleContainerRef } = useParallaxPageStackRootContext()
return (
<div
className={`ParallaxPage-stackTopBarTitleContainer ${className ?? ''}`.trim()}
{...restProps}
ref={topBarTitleContainerRef}
/>
)
}
ParallaxPageStackTopBarTitleContainer.displayName = 'ParallaxPageStack.TopBarTitleContainer'
// ================================================================================================
// Root
// ================================================================================================
type SheetRootProps = React.ComponentPropsWithoutRef<typeof Sheet.Root>;
type ParallaxPageRootProps = Omit<SheetRootProps, 'license'> & {
license?: SheetRootProps['license'];
};
const ParallaxPageRoot = React.forwardRef<
React.ElementRef<typeof Sheet.Root>,
ParallaxPageRootProps
>((props, ref) => {
return (
<Sheet.Root
license="commercial"
// By default, the Sheet will be associated with the
// closest SheetStack
forComponent="closest"
{...props}
ref={ref}
/>
)
})
ParallaxPageRoot.displayName = 'ParallaxPage.Root'
// ================================================================================================
// View Portal
// ================================================================================================
// A portal that will render the View in the stack root by
// default.
const ParallaxPageViewPortal = (props: React.ComponentPropsWithoutRef<typeof Sheet.Portal>) => {
const { pageContainer } = useParallaxPageStackRootContext()
return <Sheet.Portal container={pageContainer as HTMLElement} {...props} />
}
ParallaxPageViewPortal.displayName = 'ParallaxPage.ViewPortal'
// ================================================================================================
// View
// ================================================================================================
const ParallaxPageView = React.forwardRef<
React.ElementRef<typeof Sheet.View>,
React.ComponentPropsWithoutRef<typeof Sheet.View>
>(
(
{ className, contentPlacement, swipeOvershoot, nativeEdgeSwipePrevention, ...restProps },
ref
) => {
return (
<Sheet.View
className={`ParallaxPage-view ${className ?? ''}`.trim()}
contentPlacement={contentPlacement ?? 'right'}
swipeOvershoot={swipeOvershoot ?? false}
nativeEdgeSwipePrevention={nativeEdgeSwipePrevention ?? true}
{...restProps}
ref={ref}
/>
)
}
)
ParallaxPageView.displayName = 'ParallaxPage.View'
// ================================================================================================
// Backdrop
// ================================================================================================
const ParallaxPageBackdrop = React.forwardRef<
React.ElementRef<typeof Sheet.Backdrop>,
React.ComponentPropsWithoutRef<typeof Sheet.Backdrop>
>(({ travelAnimation, ...restProps }, ref) => {
return (
<Sheet.Backdrop
travelAnimation={{ opacity: [0, 0.25], ...travelAnimation }}
{...restProps}
ref={ref}
/>
)
})
ParallaxPageBackdrop.displayName = 'ParallaxPage.Backdrop'
// ================================================================================================
// Content
// ================================================================================================
const ParallaxPageContent = React.forwardRef<
React.ElementRef<typeof Sheet.Content>,
React.ComponentPropsWithoutRef<typeof Sheet.Content>
>(({ stackingAnimation, ...restProps }, ref) => {
return (
<Sheet.Content
className="ParallaxPage-content"
stackingAnimation={{
translateX: ({ progress }) => (progress <= 1 ? progress * -80 + 'px' : '-80px'),
...stackingAnimation,
}}
{...restProps}
ref={ref}
/>
)
})
ParallaxPageContent.displayName = 'ParallaxPage.Content'
// ================================================================================================
// Top Bar Dismiss Trigger Portal
// ================================================================================================
// A portal that will render the dismiss trigger in the top bar
// dismiss trigger container by default.
const ParallaxPageTopBarDismissTriggerPortal = ({
children,
...props
}: React.ComponentPropsWithoutRef<typeof Sheet.Portal>) => {
const { dismissTriggerContainerRef } = useParallaxPageStackRootContext()
return (
<Sheet.Portal container={dismissTriggerContainerRef.current as HTMLElement} {...props}>
{children}
</Sheet.Portal>
)
}
ParallaxPageTopBarDismissTriggerPortal.displayName = 'ParallaxPage.TopBarDismissTriggerPortal'
// ================================================================================================
// Top Bar Dismiss Trigger
// ================================================================================================
// The top bar dismiss trigger associated with the parallax page.
const ParallaxPageTopBarDismissTrigger = React.forwardRef<
React.ElementRef<typeof Sheet.Trigger>,
React.ComponentPropsWithoutRef<typeof Sheet.Trigger>
>(({ className, action, travelAnimation, ...restProps }, ref) => {
return (
<Sheet.Trigger
className={`ParallaxPage-topBarDismissTrigger ${className ?? ''}`.trim()}
action={action ?? 'dismiss'}
travelAnimation={{
visibility: 'visible',
opacity: ({ progress }) => (1 / 0.75) * (progress - 0.25),
...travelAnimation,
}}
{...restProps}
ref={ref}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="ParallaxPage-topBarDismissIcon"
>
<path
fillRule="evenodd"
d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z"
clipRule="evenodd"
/>
</svg>
</Sheet.Trigger>
)
})
ParallaxPageTopBarDismissTrigger.displayName = 'ParallaxPage.TopBarDismissTrigger'
// ================================================================================================
// Top Bar Title Portal
// ================================================================================================
// A portal that will render the top bar title in the top bar
// title container by default.
const ParallaxPageTopBarTitlePortal = ({
children,
...props
}: React.ComponentPropsWithoutRef<typeof Sheet.Outlet>) => {
const { topBarTitleContainerRef } = useParallaxPageStackRootContext()
return (
<Sheet.Portal container={topBarTitleContainerRef.current as HTMLElement} {...props}>
{children}
</Sheet.Portal>
)
}
ParallaxPageTopBarTitlePortal.displayName = 'ParallaxPage.TopBarTitlePortal'
// ================================================================================================
// Top Bar Title
// ================================================================================================
// The top bar title associated with the parallax page.
const ParallaxPageTopBarTitle = React.forwardRef<
React.ElementRef<typeof Sheet.Outlet>,
React.ComponentPropsWithoutRef<typeof Sheet.Outlet>
>(({ className, travelAnimation, stackingAnimation, ...restProps }, ref) => {
return (
<Sheet.Outlet
className={`ParallaxPage-topBarTitle ${className ?? ''}`.trim()}
travelAnimation={{
opacity: ({ progress }) => (1 / 0.75) * (progress - 0.25),
...travelAnimation,
}}
stackingAnimation={{
opacity: ({ progress }) => 0.75 - (1 / 0.75) * (progress - 0.25),
...stackingAnimation,
}}
{...restProps}
ref={ref}
/>
)
})
ParallaxPageTopBarTitle.displayName = 'ParallaxPage.TopBarTitle'
// ================================================================================================
// Unchanged components
// ================================================================================================
const ParallaxPagePortal = Sheet.Portal
const ParallaxPageTrigger = Sheet.Trigger
const ParallaxPageHandle = Sheet.Handle
const ParallaxPageOutlet = Sheet.Outlet
const ParallaxPageTitle = Sheet.Title
const ParallaxPageDescription = Sheet.Description
export const ParallaxPageStack = {
// Stack
Root: ParallaxPageStackRoot,
SceneryOutlet: ParallaxPageStackSceneryOutlet,
// Stack top bar components
IslandRoot: ParallaxPageStackIslandRoot,
IslandContent: ParallaxPageStackIslandContent,
TopBarDismissTriggerContainer: ParallaxPageStackTopBarDismissTriggerContainer,
TopBarTitleOutlet: ParallaxPageStackTopBarTitleOutlet,
TopBarTitleContainer: ParallaxPageStackTopBarTitleContainer,
}
export const ParallaxPage = {
// Page
Root: ParallaxPageRoot,
ViewPortal: ParallaxPageViewPortal,
View: ParallaxPageView,
Backdrop: ParallaxPageBackdrop,
Content: ParallaxPageContent,
Trigger: ParallaxPageTrigger,
Handle: ParallaxPageHandle,
Outlet: ParallaxPageOutlet,
Portal: ParallaxPagePortal,
Title: ParallaxPageTitle,
Description: ParallaxPageDescription,
// Top Bar page components
TopBarDismissTriggerPortal: ParallaxPageTopBarDismissTriggerPortal,
TopBarDismissTrigger: ParallaxPageTopBarDismissTrigger,
TopBarTitlePortal: ParallaxPageTopBarTitlePortal,
TopBarTitle: ParallaxPageTopBarTitle,
}

View File

@@ -0,0 +1,63 @@
.Toast-container {
/* SELF-LAYOUT */
position: fixed;
z-index: 2;
}
.Toast-view {
--padding-x: 0.75rem;
--padding-top: 0.75rem;
--padding-bottom: 2.75rem;
--max-height: 90px;
--contentHeight: calc(
max(env(safe-area-inset-top, 0px), var(--padding-top)) + var(--max-height) +
var(--padding-bottom)
);
}
@media (min-width: 1000px) {
.Toast-view {
--padding-x: 0.875rem;
--padding-top: 0.875rem;
/* SELF-LAYOUT */
height: var(--contentHeight);
}
}
.Toast-content {
/* SELF-LAYOUT */
box-sizing: border-box;
width: 100%;
max-width: 560px;
height: var(--contentHeight);
/* APPEARANCE */
background-color: transparent;
/* INNER-LAYOUT */
padding: var(--padding-x);
padding-top: max(env(safe-area-inset-top), var(--padding-top));
padding-bottom: var(--padding-bottom);
display: grid;
}
@media (min-width: 1000px) {
.Toast-content {
/* SELF-LAYOUT */
max-width: 400px;
/* INNER-LAYOUT */
padding-top: var(--padding-top);
}
}
.Toast-innerContent {
/* SELF-LAYOUT */
max-height: var(--max-height);
/* APPEARANCE */
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
border-radius: 24px;
border: 1px solid rgb(240, 240, 240);
background-color: rgba(255, 255, 255, 0.95);
backdrop-filter: saturate(5) blur(20px);
}

View File

@@ -0,0 +1,181 @@
import React, { useEffect, useState, useRef, createContext, useContext } from 'react'
import { Sheet, useClientMediaQuery } from '@silk-hq/components'
import './Toast.css'
// ================================================================================================
// Context
// ================================================================================================
type ToastContextValue = {
presented: boolean;
setPresented: (presented: boolean) => void;
pointerOver: boolean;
setPointerOver: (pointerOver: boolean) => void;
travelStatus: string;
setTravelStatus: (status: string) => void;
};
const ToastContext = createContext<ToastContextValue | null>(null)
// ================================================================================================
// Root
// ================================================================================================
type SheetRootProps = React.ComponentPropsWithoutRef<typeof Sheet.Root>;
type ToastRootProps = Omit<SheetRootProps, 'license'> & {
license?: SheetRootProps['license'];
};
const ToastRoot = React.forwardRef<React.ElementRef<typeof Sheet.Root>, ToastRootProps>(
({ children, ...restProps }, ref) => {
const [presented, setPresented] = useState(false)
const [pointerOver, setPointerOver] = useState(false)
const [travelStatus, setTravelStatus] = useState('idleOutside')
const autoCloseTimeout = useRef<ReturnType<typeof setTimeout> | undefined>()
useEffect(() => {
const startAutoCloseTimeout = () => {
autoCloseTimeout.current = setTimeout(() => setPresented(false), 5000)
}
const clearAutoCloseTimeout = () => {
clearTimeout(autoCloseTimeout.current)
}
if (presented) {
if (travelStatus === 'idleInside' && !pointerOver) {
startAutoCloseTimeout()
} else {
clearAutoCloseTimeout()
}
}
return clearAutoCloseTimeout
}, [presented, travelStatus, pointerOver])
return (
<ToastContext.Provider
value={{
presented,
setPresented,
pointerOver,
setPointerOver,
travelStatus,
setTravelStatus,
}}
>
<Sheet.Root
license="commercial"
presented={presented}
onPresentedChange={setPresented}
sheetRole=""
{...restProps}
ref={ref}
>
{children}
</Sheet.Root>
</ToastContext.Provider>
)
}
)
ToastRoot.displayName = 'Toast.Root'
// ================================================================================================
// View
// ================================================================================================
const ToastView = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof Sheet.View>
>(({ children, className, ...restProps }, ref) => {
const largeViewport = useClientMediaQuery('(min-width: 1000px)')
const contentPlacement = largeViewport ? 'right' : 'top'
const context = useContext(ToastContext)
if (!context) throw new Error('ToastView must be used within a ToastContext.Provider')
const { setTravelStatus } = context
return (
<div
className={`Toast-container ${className ?? ''}`.trim()}
role="status"
aria-live="polite"
{...restProps}
ref={ref}
>
<Sheet.View
className={`Toast-view ${className ?? ''}`.trim()}
contentPlacement={contentPlacement}
inertOutside={false}
onPresentAutoFocus={{ focus: false }}
onDismissAutoFocus={{ focus: false }}
onClickOutside={{
dismiss: false,
stopOverlayPropagation: false,
}}
onEscapeKeyDown={{
dismiss: false,
stopOverlayPropagation: false,
}}
onTravelStatusChange={setTravelStatus}
>
{children}
</Sheet.View>
</div>
)
})
ToastView.displayName = 'Toast.View'
// ================================================================================================
// Content
// ================================================================================================
const ToastContent = React.forwardRef<
React.ElementRef<typeof Sheet.Content>,
React.ComponentPropsWithoutRef<typeof Sheet.Content>
>(({ children, className, ...restProps }, ref) => {
const context = useContext(ToastContext)
if (!context) throw new Error('ToastContent must be used within ToastRoot')
return (
<Sheet.Content
className={`Toast-content ${className ?? ''}`.trim()}
asChild
{...restProps}
ref={ref}
>
<Sheet.SpecialWrapper.Root>
<Sheet.SpecialWrapper.Content
className="Toast-innerContent"
onPointerEnter={() => context.setPointerOver(true)}
onPointerLeave={() => context.setPointerOver(false)}
>
{children}
</Sheet.SpecialWrapper.Content>
</Sheet.SpecialWrapper.Root>
</Sheet.Content>
)
})
ToastContent.displayName = 'Toast.Content'
// ================================================================================================
// Unchanged Components
// ================================================================================================
const ToastPortal = Sheet.Portal
const ToastTrigger = Sheet.Trigger
const ToastHandle = Sheet.Handle
const ToastOutlet = Sheet.Outlet
const ToastTitle = Sheet.Title
const ToastDescription = Sheet.Description
export const Toast = {
Root: ToastRoot,
Portal: ToastPortal,
View: ToastView,
Content: ToastContent,
Trigger: ToastTrigger,
Title: ToastTitle,
Description: ToastDescription,
Handle: ToastHandle,
Outlet: ToastOutlet,
}

View File

@@ -1,9 +1,12 @@
import "@silk-hq/components/dist/main-unlayered.css"
import '@silk-hq/components/dist/main-unlayered.css'
import { Fixed, Scroll, Sheet } from '@silk-hq/components'
import { BottomSheet } from './BottomSheet'
import { SheetWithDepth, SheetWithDepthStack } from './SheetWithDepth'
import { SheetWithDetent } from './SheetWithDetent'
import { SheetWithStacking, SheetWithStackingStack } from './SheetWithStacking'
import { ParallaxPage } from './ParallaxPage'
import { Toast } from './Toast'
import { Card } from './Card'
declare global {
var LSSilkhq: any
@@ -13,7 +16,8 @@ const silkhq = {
Sheet, Fixed, Scroll, BottomSheet,
SheetWithDepth, SheetWithDepthStack,
SheetWithStacking, SheetWithDetent,
SheetWithStackingStack,
SheetWithStackingStack, ParallaxPage,
Toast, CardSheet: Card,
}
window.LSSilkhq = silkhq

View File

@@ -2,6 +2,16 @@
--ls-page-title-size: 26px;
}
html.is-native-ios {
--safe-area-inset-top: 59px;
--safe-area-inset-bottom: 16px;
}
html.is-native-android {
--safe-area-inset-top: 46px;
--safe-area-inset-bottom: 16px;
}
html.plt-capacitor.plt-android {
--ion-safe-area-top: 42px;
--ion-safe-area-bottom: 16px;
@@ -345,5 +355,7 @@ html[data-color=logseq] {
.app-silk-index-container {
@apply p-4 flex flex-col gap-3;
padding-top: var(--safe-area-inset-top);
}

View File

@@ -41,7 +41,7 @@
(silkhq/stacking-sheet-trigger
(shui/button "open: nested stacking sheet"))
(silkhq/stacking-sheet-portal
(stacking-view-example {:nested? true}))))])))
(stacking-view-example {:nested? false}))))])))
(rum/defc silkhq-demos-page
[]
@@ -54,7 +54,7 @@
:nativePageScrollReplacement true}
(silkhq/scroll-content {:class "app-silk-index-scroll-content"}
[:div.app-silk-index-container
[:h2.text-lg.font-semibold "Silk sheets demos"]
[:h2.text-xl.font-semibold.pt-4 "Silk sheets demos"]
;; Bottom Sheet case
(silkhq/bottom-sheet