enhance(mobile): add silkhq basic components

This commit is contained in:
charlie
2025-07-08 17:23:05 +08:00
parent 9c83237ba7
commit dff4aa64bb
16 changed files with 1310 additions and 5 deletions

View File

@@ -7,7 +7,8 @@
"watch:ui:examples": "parcel serve ./examples/index.html",
"build:ui:only": "parcel build --target ui",
"build:ionic:only": "parcel build --target ionic",
"build:ui": "rm -rf .parcel-cache && yarn build:ui:only && yarn build:ionic:only",
"build:silkhq:only": "parcel build --target silkhq",
"build:ui": "rm -rf .parcel-cache && yarn build:ui:only && yarn build:ionic:only && yarn build:silkhq:only",
"watch:storybook": "storybook dev -p 6006",
"postinstall": "yarn build:ui"
},
@@ -34,6 +35,7 @@
"@radix-ui/react-toggle-group": "^1.1.7",
"@radix-ui/react-tooltip": "^1.2.4",
"@ionic/react": "8.5.7",
"@silk-hq/components": "^0.9.10",
"class-variance-authority": "^0.7.1",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
@@ -107,6 +109,14 @@
"react": false,
"react-dom": false
}
},
"silkhq": {
"source": "src/silkhq/silkhq.ts",
"outputFormat": "global",
"includeNodeModules": {
"react": false,
"react-dom": false
}
}
},
"resolutions": {

View File

@@ -0,0 +1,34 @@
.BottomSheet-view {
/* SELF-LAYOUT */
z-index: 1;
/* Adding 60px to make it fully visible below iOS Safari's bottom UI */
height: calc(var(--silk-100-lvh-dvh-pct) + 60px);
}
.BottomSheet-content {
/* SELF-LAYOUT */
box-sizing: border-box;
height: auto;
min-height: 100px;
}
.BottomSheet-bleedingBackground {
/* APPEARANCE */
border-radius: 24px;
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);
}
.BottomSheet-handle {
/* SELF-LAYOUT */
width: 50px;
height: 6px;
/* APPEARANCE */
border: 0;
border-radius: 9999px;
background-color: rgb(209, 213, 219);
/* INTERACTIVITY */
cursor: pointer;
}

View File

@@ -0,0 +1,126 @@
import React from "react";
import { Sheet } from "@silk-hq/components";
import "./BottomSheet.css";
// ================================================================================================
// Root
// ================================================================================================
type SheetRootProps = React.ComponentPropsWithoutRef<typeof Sheet.Root>;
type BottomSheetRootProps = Omit<SheetRootProps, "license"> & {
license?: SheetRootProps["license"];
};
const BottomSheetRoot = React.forwardRef<React.ElementRef<typeof Sheet.Root>, BottomSheetRootProps>(
({ children, ...restProps }, ref) => {
return (
<Sheet.Root license="commercial" {...restProps} ref={ref}>
{children}
</Sheet.Root>
);
}
);
BottomSheetRoot.displayName = "BottomSheet.Root";
// ================================================================================================
// View
// ================================================================================================
const BottomSheetView = React.forwardRef<
React.ElementRef<typeof Sheet.View>,
React.ComponentPropsWithoutRef<typeof Sheet.View>
>(({ children, className, ...restProps }, ref) => {
return (
<Sheet.View
className={`BottomSheet-view ${className ?? ""}`.trim()}
nativeEdgeSwipePrevention={true}
{...restProps}
ref={ref}
>
{children}
</Sheet.View>
);
});
BottomSheetView.displayName = "BottomSheet.View";
// ================================================================================================
// Backdrop
// ================================================================================================
const BottomSheetBackdrop = React.forwardRef<
React.ElementRef<typeof Sheet.Backdrop>,
React.ComponentPropsWithoutRef<typeof Sheet.Backdrop>
>(({ className, ...restProps }, ref) => {
return (
<Sheet.Backdrop
className={`BottomSheet-backdrop ${className ?? ""}`.trim()}
themeColorDimming="auto"
{...restProps}
ref={ref}
/>
);
});
BottomSheetBackdrop.displayName = "BottomSheet.Backdrop";
// ================================================================================================
// Content
// ================================================================================================
const BottomSheetContent = React.forwardRef<
React.ElementRef<typeof Sheet.Content>,
React.ComponentPropsWithoutRef<typeof Sheet.Content>
>(({ children, className, ...restProps }, ref) => {
return (
<Sheet.Content
className={`BottomSheet-content ${className ?? ""}`.trim()}
{...restProps}
ref={ref}
>
<Sheet.BleedingBackground className="BottomSheet-bleedingBackground" />
{children}
</Sheet.Content>
);
});
BottomSheetContent.displayName = "BottomSheet.Content";
// ================================================================================================
// Handle
// ================================================================================================
const BottomSheetHandle = React.forwardRef<
React.ElementRef<typeof Sheet.Handle>,
React.ComponentPropsWithoutRef<typeof Sheet.Handle>
>(({ className, ...restProps }, ref) => {
return (
<Sheet.Handle
className={`BottomSheet-handle ${className ?? ""}`.trim()}
action="dismiss"
{...restProps}
ref={ref}
/>
);
});
BottomSheetHandle.displayName = "BottomSheet.Handle";
// ================================================================================================
// Unchanged Components
// ================================================================================================
const BottomSheetPortal = Sheet.Portal;
const BottomSheetTrigger = Sheet.Trigger;
const BottomSheetOutlet = Sheet.Outlet;
const BottomSheetTitle = Sheet.Title;
const BottomSheetDescription = Sheet.Description;
export const BottomSheet = {
Root: BottomSheetRoot,
Portal: BottomSheetPortal,
View: BottomSheetView,
Backdrop: BottomSheetBackdrop,
Content: BottomSheetContent,
Trigger: BottomSheetTrigger,
Handle: BottomSheetHandle,
Outlet: BottomSheetOutlet,
Title: BottomSheetTitle,
Description: BottomSheetDescription,
};

View File

@@ -0,0 +1,63 @@
/* Stack Scenery */
.SheetWithDepth-stackSceneryBackground {
/* SELF-LAYOUT */
position: fixed;
inset: 0;
/* APPEARANCE */
background-color: black;
opacity: 0;
will-change: opacity;
}
.SheetWithDepth-stackSceneryBackground.nativePageScrollReplaced-true {
/* APPEARANCE */
opacity: 1;
}
.SheetWithDepth-stackSceneryContainer {
/* INNER-LAYOUT */
position: relative;
}
.SheetWithDepth-stackSceneryFirstSheetBackdrop {
/* SELF-LAYOUT */
position: absolute;
z-index: 1;
inset: 0;
/* APPEARANCE */
background-color: rgb(0, 0, 0);
opacity: 0;
/* INTERACTIVITY */
pointer-events: none;
/* MISCELLANEOUS */
will-change: opacity;
}
/* Sheet */
.SheetWithDepth-view {
/* SELF-LAYOUT */
top: 0;
bottom: initial;
/* Adding 60px to make it fully visible below iOS Safari's
bottom UI */
height: calc(var(--silk-100-lvh-dvh-pct) + 60px);
}
.SheetWithDepth-content {
/* SELF-LAYOUT */
box-sizing: border-box;
height: calc(100% - max(calc(env(safe-area-inset-top) + 1.3vh), 2.6vh));
}
.SheetWithDepth-bleedingBackground {
/* APPEARANCE */
border-radius: 24px 24px 0 0;
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);
}

View File

@@ -0,0 +1,432 @@
import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Sheet,
SheetStack,
animate,
useThemeColorDimmingOverlay,
usePageScrollData,
SheetViewProps,
createComponentId,
} from "@silk-hq/components";
import "./SheetWithDepth.css";
// ================================================================================================
// Stack Id
// ================================================================================================
const sheetWithDepthStackId = createComponentId();
// ================================================================================================
// StackRoot Context
// ================================================================================================
type SheetWithDepthStackRootContextValue = {
stackBackgroundRef: React.RefObject<HTMLDivElement>;
stackFirstSheetBackdropRef: React.RefObject<HTMLDivElement>;
stackingCount: number;
setStackingCount: React.Dispatch<React.SetStateAction<number>>;
};
const SheetWithDepthStackRootContext = createContext<SheetWithDepthStackRootContextValue | null>(
null
);
const useSheetWithDepthStackRootContext = () => {
const context = useContext(SheetWithDepthStackRootContext);
if (!context) {
throw new Error(
"useSheetWithDepthStackRootContext must be used within a SheetWithDepthStackRootContext"
);
}
return context;
};
// ================================================================================================
// View Context
// ================================================================================================
const SheetWithDepthViewContext = createContext<{
indexInStack: number;
} | null>(null);
const useSheetWithDepthViewContext = () => {
const context = useContext(SheetWithDepthViewContext);
if (!context) {
throw new Error("useSheetWithDepthViewContext must be used within a SheetWithDepthViewContext");
}
return context;
};
// ================================================================================================
// StackRoot
// ================================================================================================
const SheetWithDepthStackRoot = React.forwardRef<
React.ElementRef<typeof SheetStack.Root>,
React.ComponentProps<typeof SheetStack.Root>
>(({ children, ...restProps }, ref) => {
const stackBackgroundRef = useRef<HTMLDivElement | null>(null);
const stackFirstSheetBackdropRef = useRef<HTMLDivElement | null>(null);
const [stackingCount, setStackingCount] = useState(0);
const contextValue = useMemo(
() => ({
stackBackgroundRef,
stackFirstSheetBackdropRef,
stackingCount,
setStackingCount,
}),
[stackingCount]
);
return (
<SheetWithDepthStackRootContext.Provider value={contextValue}>
<SheetStack.Root componentId={sheetWithDepthStackId} {...restProps} ref={ref}>
{children}
</SheetStack.Root>
</SheetWithDepthStackRootContext.Provider>
);
});
SheetWithDepthStackRoot.displayName = "SheetWithDepthStack.Root";
// ================================================================================================
// StackSceneryOutlets
// ================================================================================================
// The SheetStack outlets that define the scenery of the stack
// (i.e. the content underneath) for the depth effect.
const initialTopOffset = "max(env(safe-area-inset-top), 1.3vh)";
const SheetWithDepthStackSceneryOutlets = React.forwardRef<
React.ElementRef<typeof SheetStack.Outlet>,
Omit<React.ComponentProps<typeof SheetStack.Outlet>, "asChild">
>(({ children, className, stackingAnimation: stackingAnimationFromProps, ...restProps }, ref) => {
const { stackBackgroundRef, stackFirstSheetBackdropRef } = useSheetWithDepthStackRootContext();
const { nativePageScrollReplaced } = usePageScrollData();
const [iOSStandalone, setiOSStandalone] = useState(false);
useEffect(() => {
setiOSStandalone(
// @ts-ignore
window.navigator.standalone && window.navigator.userAgent?.match(/iPhone|iPad/i)
);
}, []);
const stackingAnimation: React.ComponentPropsWithoutRef<
typeof Sheet.Outlet
>["stackingAnimation"] = {
// Clipping & border-radius. We have a different animation
// when the native page scroll is replaced, and in iOS
// standalone mode.
...(nativePageScrollReplaced
? iOSStandalone
? // In iOS standalone mode we don't need to animate the
// border-radius because the corners are hidden by the
// screen corners. So we just set the border-radius to
// the needed value.
{
overflow: "clip",
borderRadius: "24px",
transformOrigin: "50% 0",
}
: // Outside of iOS standalone mode we do animate
// the border-radius because the scenery is a visible
// rectangle.
{
overflow: "clip",
borderRadius: ({ progress }: any) => Math.min(progress * 24, 24) + "px",
transformOrigin: "50% 0",
}
: // When the native page scroll is not replaced we
// need to use the Silk's special clip properties to cut
// off the rest of the page.
{
clipBoundary: "layout-viewport",
clipBorderRadius: "24px",
clipTransformOrigin: "50% 0",
}),
// Translate & scale
translateY: ({ progress }) =>
progress <= 1
? "calc(" + progress + " * " + initialTopOffset + ")"
: // prettier-ignore
"calc(" + initialTopOffset + " + 0.65vh * " + (progress - 1) + ")",
scale: [1, 0.91],
// We merge animations coming from the props
...stackingAnimationFromProps,
};
return (
<>
{/* Element used as a black background representing the void under the stack. */}
<div
className={`SheetWithDepth-stackSceneryBackground nativePageScrollReplaced-${nativePageScrollReplaced}`}
ref={stackBackgroundRef}
/>
{/* Element used as a container for the content under the stack. */}
<SheetStack.Outlet
className={`SheetWithDepth-stackSceneryContainer ${className ?? ""}`.trim()}
forComponent={sheetWithDepthStackId}
stackingAnimation={stackingAnimation}
{...restProps}
ref={ref}
>
{children}
{/* Element used as the first sheet's backdrop, which only covers the stackSceneryContainer, not the entire viewport. */}
<div
className="SheetWithDepth-stackSceneryFirstSheetBackdrop"
ref={stackFirstSheetBackdropRef}
/>
</SheetStack.Outlet>
</>
);
});
SheetWithDepthStackSceneryOutlets.displayName = "SheetWithDepthStack.SceneryOutlets";
// ================================================================================================
// Root
// ================================================================================================
type SheetRootProps = React.ComponentPropsWithoutRef<typeof Sheet.Root>;
type SheetWithDepthRootProps = Omit<SheetRootProps, "license"> & {
license?: SheetRootProps["license"];
};
const SheetWithDepthRoot = React.forwardRef<
React.ElementRef<typeof Sheet.Root>,
SheetWithDepthRootProps
>((props, ref) => {
return (
<Sheet.Root license="commercial" forComponent={sheetWithDepthStackId} {...props} ref={ref} />
);
});
SheetWithDepthRoot.displayName = "SheetWithDepth.Root";
// ================================================================================================
// View
// ================================================================================================
// We use animate(), animateDimmingOverlayOpacity() and the
// travelHandler instead of relying on stackingAnimation for the
// stackSceneryBackground and stackSceneryFirstSheetBackdrop
// elements in order to have a different (the default CSS
// "ease"), less abrupt animation easing for them.
const SheetWithDepthView = React.forwardRef<
React.ElementRef<typeof Sheet.View>,
React.ComponentPropsWithoutRef<typeof Sheet.View>
>(
(
{ children, className, onTravelStatusChange, onTravel: travelHandlerFromProps, ...restProps },
ref
) => {
const {
stackingCount,
setStackingCount,
stackBackgroundRef,
stackFirstSheetBackdropRef,
} = useSheetWithDepthStackRootContext();
const [indexInStack, setIndexInStack] = useState(0);
const [travelStatus, setTravelStatus] = useState("idleOutside");
//
// Define a dimming overlay
const { setDimmingOverlayOpacity, animateDimmingOverlayOpacity } = useThemeColorDimmingOverlay({
elementRef: stackBackgroundRef,
dimmingColor: "rgb(0, 0, 0)",
});
//
// travelStatusChangeHandler
const travelStatusChangeHandler = useCallback<
NonNullable<SheetViewProps["onTravelStatusChange"]>
>(
(newTravelStatus) => {
// Set indexInStack & stackingCount
if (travelStatus !== "stepping" && newTravelStatus === "idleInside") {
setStackingCount((prevStackingCount: number) => prevStackingCount + 1);
if (indexInStack === 0) {
setIndexInStack(stackingCount + 1);
}
}
//
else if (newTravelStatus === "idleOutside") {
setStackingCount((prevStackingCount: number) => prevStackingCount - 1);
setIndexInStack(0);
}
// Animate on entering
if (newTravelStatus === "entering" && stackingCount === 0) {
animateDimmingOverlayOpacity({ keyframes: [0, 1] });
animate(stackFirstSheetBackdropRef.current as HTMLElement, {
opacity: [0, 0.33],
});
}
// Animate on exiting
if (newTravelStatus === "exiting" && stackingCount === 1) {
animateDimmingOverlayOpacity({ keyframes: [1, 0] });
animate(stackFirstSheetBackdropRef.current as HTMLElement, {
opacity: [0.33, 0],
});
}
// Set the state
onTravelStatusChange?.(newTravelStatus);
setTravelStatus(newTravelStatus);
},
[
travelStatus,
indexInStack,
stackingCount,
setStackingCount,
stackFirstSheetBackdropRef,
animateDimmingOverlayOpacity,
onTravelStatusChange,
]
);
//
// travelHandler
const travelHandler = useMemo(() => {
if (indexInStack === 1 && travelStatus !== "entering" && travelStatus !== "exiting") {
const handler: NonNullable<SheetViewProps["onTravel"]> = ({ progress, ...rest }) => {
setDimmingOverlayOpacity(progress);
stackFirstSheetBackdropRef.current?.style.setProperty(
"opacity",
(progress * 0.33) as unknown as string
);
travelHandlerFromProps?.({ progress, ...rest });
};
return handler;
} else {
return travelHandlerFromProps;
}
}, [indexInStack, travelStatus, stackFirstSheetBackdropRef, setDimmingOverlayOpacity]);
//
// Return
return (
<SheetWithDepthViewContext.Provider value={{ indexInStack }}>
<Sheet.View
className={`SheetWithDepth-view ${className ?? ""}`.trim()}
contentPlacement="bottom"
onTravelStatusChange={travelStatusChangeHandler}
onTravel={travelHandler}
nativeEdgeSwipePrevention={true}
{...restProps}
ref={ref}
>
{children}
</Sheet.View>
</SheetWithDepthViewContext.Provider>
);
}
);
SheetWithDepthView.displayName = "SheetWithDepth.View";
// ================================================================================================
// Backdrop
// ================================================================================================
const SheetWithDepthBackdrop = React.forwardRef<
React.ElementRef<typeof Sheet.Backdrop>,
React.ComponentPropsWithoutRef<typeof Sheet.Backdrop>
// @ts-ignore
>(({ className, ...restProps }, ref) => {
const { stackingCount } = useSheetWithDepthStackRootContext();
const { indexInStack } = useSheetWithDepthViewContext();
return (
// We don't render the Backdrop for the first sheet in the
// stack, instead we use the stackSceneryFirstSheetBackdrop
// element.
stackingCount > 0 &&
indexInStack !== 1 && (
<Sheet.Backdrop
className={`SheetWithDepth-backdrop ${className ?? ""}`.trim()}
travelAnimation={{ opacity: [0, 0.33] }}
{...restProps}
ref={ref}
/>
)
);
});
SheetWithDepthBackdrop.displayName = "SheetWithDepth.Backdrop";
// ================================================================================================
// Content
// ================================================================================================
const SheetWithDepthContent = React.forwardRef<
React.ElementRef<typeof Sheet.Content>,
React.ComponentProps<typeof Sheet.Content>
>(({ children, className, stackingAnimation, ...restProps }, ref) => {
return (
<Sheet.Content
className={`SheetWithDepth-content ${className ?? ""}`.trim()}
stackingAnimation={{
translateY: ({ progress }) =>
progress <= 1
? progress * -1.3 + "vh"
: // prettier-ignore
"calc(-1.3vh + 0.65vh * " + (progress - 1) + ")",
scale: [1, 0.91],
transformOrigin: "50% 0",
...stackingAnimation,
}}
{...restProps}
ref={ref}
>
<Sheet.BleedingBackground className="SheetWithDepth-bleedingBackground" />
{children}
</Sheet.Content>
);
});
SheetWithDepthContent.displayName = "SheetWithDepth.Content";
// ================================================================================================
// Unchanged components
// ================================================================================================
const SheetWithDepthPortal = Sheet.Portal;
const SheetWithDepthTrigger = Sheet.Trigger;
const SheetWithDepthHandle = Sheet.Handle;
const SheetWithDepthOutlet = Sheet.Outlet;
const SheetWithDepthTitle = Sheet.Title;
const SheetWithDepthDescription = Sheet.Description;
export const SheetWithDepthStack = {
Root: SheetWithDepthStackRoot,
SceneryOutlets: SheetWithDepthStackSceneryOutlets,
};
export const SheetWithDepth = {
Root: SheetWithDepthRoot,
Portal: SheetWithDepthPortal,
View: SheetWithDepthView,
Content: SheetWithDepthContent,
Backdrop: SheetWithDepthBackdrop,
Trigger: SheetWithDepthTrigger,
Handle: SheetWithDepthHandle,
Outlet: SheetWithDepthOutlet,
Title: SheetWithDepthTitle,
Description: SheetWithDepthDescription,
};

View File

@@ -0,0 +1,45 @@
.SheetWithDetent-view {
/* SELF-LAYOUT */
z-index: 1;
top: 0;
bottom: initial;
/* Adding 60px to make it fully visible below iOS Safari's
bottom UI */
height: calc(var(--silk-100-lvh-dvh-pct) + 60px);
}
.SheetWithDetent-content {
/* SELF-LAYOUT */
box-sizing: border-box;
height: calc(100% - max(env(safe-area-inset-top), 6px));
max-width: 800px;
/* APPEARANCE */
border-radius: 24px 24px 0 0;
overflow: hidden;
background-color: white;
}
@media (min-width: 800px) {
.SheetWithDetent-content {
/* SELF-LAYOUT */
height: calc(100% - max(env(safe-area-inset-top), 5vh));
/* 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;
}
}
.SheetWithDetent-handle {
/* SELF-LAYOUT */
width: 50px;
height: 6px;
/* APPEARANCE */
border: 0;
border-radius: 9999px;
background-color: rgb(209, 213, 219);
/* INTERACTIVITY */
cursor: pointer;
}

View File

@@ -0,0 +1,275 @@
import React, { createContext, useContext, useMemo, useRef, useState } from "react";
import { Sheet, Scroll, type SheetViewProps } from "@silk-hq/components";
import "./SheetWithDetent.css";
// ================================================================================================
// Context
// ================================================================================================
type SheetWithDetentContextValue = {
reachedLastDetent: boolean;
setReachedLastDetent: React.Dispatch<React.SetStateAction<boolean>>;
viewRef: React.RefObject<HTMLElement>;
};
const SheetWithDetentContext = createContext<SheetWithDetentContextValue | null>(null);
const useSheetWithDetentContext = () => {
const context = useContext(SheetWithDetentContext);
if (!context) {
throw new Error(
"useSheetWithDetentContext must be used within a SheetWithDetentContextProvider"
);
}
return context;
};
// ================================================================================================
// Root
// ================================================================================================
type SheetRootProps = React.ComponentPropsWithoutRef<typeof Sheet.Root>;
type SheetWithDetentRootProps = Omit<SheetRootProps, "license"> & {
license?: SheetRootProps["license"];
};
const SheetWithDetentRoot = React.forwardRef<
React.ElementRef<typeof Sheet.Root>,
SheetWithDetentRootProps
>(({ children, ...restProps }, ref) => {
const [reachedLastDetent, setReachedLastDetent] = useState(false);
const viewRef = useRef<HTMLElement>(null);
return (
<SheetWithDetentContext.Provider
value={{
reachedLastDetent,
setReachedLastDetent,
viewRef,
}}
>
<Sheet.Root license="commercial" {...restProps} ref={ref}>
{children}
</Sheet.Root>
</SheetWithDetentContext.Provider>
);
});
SheetWithDetentRoot.displayName = "SheetWithDetent.Root";
// ================================================================================================
// View
// ================================================================================================
const SheetWithDetentView = React.forwardRef<
React.ElementRef<typeof Sheet.View>,
React.ComponentPropsWithoutRef<typeof Sheet.View>
>(
(
{ children, className, onTravelStatusChange, onTravelRangeChange, onTravel, ...restProps },
ref
) => {
const { reachedLastDetent, setReachedLastDetent, viewRef } = useSheetWithDetentContext();
//
const travelHandler = useMemo(() => {
if (!reachedLastDetent) return onTravel;
const handler: SheetViewProps["onTravel"] = ({ progress, ...rest }) => {
if (!viewRef.current) return onTravel?.({ progress, ...rest });
// Dismiss the on-screen keyboard.
if (progress < 0.999) {
viewRef.current.focus();
}
onTravel?.({ progress, ...rest });
};
return handler;
}, [reachedLastDetent, onTravel, viewRef]);
//
const setRefs = React.useCallback((node: HTMLElement | null) => {
// @ts-ignore - intentionally breaking the readonly nature for compatibility
viewRef.current = node;
if (typeof ref === "function") {
ref(node);
} else if (ref) {
ref.current = node;
}
}, []);
return (
<Sheet.View
className={`SheetWithDetent-view ${className ?? ""}`.trim()}
detents={!reachedLastDetent ? "66vh" : undefined}
swipeOvershoot={false}
nativeEdgeSwipePrevention={true}
onTravelStatusChange={(travelStatus) => {
if (travelStatus === "idleOutside") setReachedLastDetent(false);
onTravelStatusChange?.(travelStatus);
}}
onTravelRangeChange={(range) => {
if (range.start === 2) setReachedLastDetent(true);
onTravelRangeChange?.(range);
}}
onTravel={travelHandler}
ref={setRefs}
{...restProps}
>
{children}
</Sheet.View>
);
}
);
SheetWithDetentView.displayName = "SheetWithDetent.View";
// ================================================================================================
// Backdrop
// ================================================================================================
const SheetWithDetentBackdrop = React.forwardRef<
React.ElementRef<typeof Sheet.Backdrop>,
React.ComponentPropsWithoutRef<typeof Sheet.Backdrop>
>(({ className, ...restProps }, ref) => {
return (
<Sheet.Backdrop
className={`SheetWithDetent-backdrop ${className ?? ""}`.trim()}
themeColorDimming="auto"
{...restProps}
ref={ref}
/>
);
});
SheetWithDetentBackdrop.displayName = "SheetWithDetent.Backdrop";
// ================================================================================================
// Content
// ================================================================================================
const SheetWithDetentContent = React.forwardRef<
React.ElementRef<typeof Sheet.Content>,
React.ComponentPropsWithoutRef<typeof Sheet.Content>
>(({ children, className, ...restProps }, ref) => {
return (
<Sheet.Content
className={`SheetWithDetent-content ${className ?? ""}`.trim()}
{...restProps}
ref={ref}
>
{children}
</Sheet.Content>
);
});
SheetWithDetentContent.displayName = "SheetWithDetent.Content";
// ================================================================================================
// Handle
// ================================================================================================
const SheetWithDetentHandle = React.forwardRef<
React.ElementRef<typeof Sheet.Handle>,
React.ComponentPropsWithoutRef<typeof Sheet.Handle>
>(({ className, ...restProps }, ref) => {
const { reachedLastDetent } = useSheetWithDetentContext();
return (
<Sheet.Handle
className={`SheetWithDetent-handle ${className ?? ""}`.trim()}
action={reachedLastDetent ? "dismiss" : "step"}
{...restProps}
ref={ref}
/>
);
});
SheetWithDetentHandle.displayName = "SheetWithDetent.Handle";
// ================================================================================================
// Scroll Root
// ================================================================================================
const SheetWithDetentScrollRoot = React.forwardRef<
React.ElementRef<typeof Scroll.Root>,
React.ComponentPropsWithoutRef<typeof Scroll.Root>
>(({ children, ...restProps }, ref) => {
return (
<Scroll.Root {...restProps} ref={ref}>
{children}
</Scroll.Root>
);
});
SheetWithDetentScrollRoot.displayName = "SheetWithDetent.ScrollRoot";
// ================================================================================================
// Scroll View
// ================================================================================================
const SheetWithDetentScrollView = React.forwardRef<
React.ElementRef<typeof Scroll.View>,
React.ComponentPropsWithoutRef<typeof Scroll.View>
>(({ children, className, ...restProps }, ref) => {
const { reachedLastDetent } = useSheetWithDetentContext();
return (
<Scroll.View
className={`SheetWithDetent-scrollView ${className ?? ""}`.trim()}
scrollGestureTrap={{ yEnd: true }}
scrollGesture={!reachedLastDetent ? false : "auto"}
safeArea="layout-viewport"
onScrollStart={{ dismissKeyboard: true }}
{...restProps}
ref={ref}
>
{children}
</Scroll.View>
);
});
SheetWithDetentScrollView.displayName = "SheetWithDetent.ScrollView";
// ================================================================================================
// Scroll Content
// ================================================================================================
const SheetWithDetentScrollContent = React.forwardRef<
React.ElementRef<typeof Scroll.Content>,
React.ComponentPropsWithoutRef<typeof Scroll.Content>
>(({ children, className, ...restProps }, ref) => {
return (
<Scroll.Content
className={`SheetWithDetent-scrollContent ${className ?? ""}`.trim()}
{...restProps}
ref={ref}
>
{children}
</Scroll.Content>
);
});
SheetWithDetentScrollContent.displayName = "SheetWithDetent.ScrollContent";
// ================================================================================================
// Unchanged Components
// ================================================================================================
const SheetWithDetentPortal = Sheet.Portal;
const SheetWithDetentTrigger = Sheet.Trigger;
const SheetWithDetentOutlet = Sheet.Outlet;
const SheetWithDetentTitle = Sheet.Title;
const SheetWithDetentDescription = Sheet.Description;
export const SheetWithDetent = {
Root: SheetWithDetentRoot,
Portal: SheetWithDetentPortal,
View: SheetWithDetentView,
Backdrop: SheetWithDetentBackdrop,
Content: SheetWithDetentContent,
Trigger: SheetWithDetentTrigger,
Handle: SheetWithDetentHandle,
Outlet: SheetWithDetentOutlet,
Title: SheetWithDetentTitle,
Description: SheetWithDetentDescription,
//
ScrollRoot: SheetWithDetentScrollRoot,
ScrollView: SheetWithDetentScrollView,
ScrollContent: SheetWithDetentScrollContent,
};

View File

@@ -0,0 +1,46 @@
.SheetWithStacking-view {
/* SELF-LAYOUT */
z-index: 1;
/* Adding 60px to make it fully visible below iOS Safari's
bottom UI */
height: calc(var(--silk-100-lvh-dvh-pct) + 60px);
}
.SheetWithStacking-view.contentPlacement-right {
/* SELF-LAYOUT */
height: var(--silk-100-lvh-dvh-pct);
}
.SheetWithStacking-content {
/* SELF-LAYOUT */
box-sizing: border-box;
height: calc(min(500px, 90svh) + env(safe-area-inset-bottom, 0px));
/* APPEARANCE */
background-color: transparent;
/* INNER-LAYOUT */
padding-inline: 0.5rem;
padding-block: 0.5rem max(env(safe-area-inset-bottom, 0px), 0.5rem);
display: grid;
}
.SheetWithStacking-content.contentPlacement-right {
/* SELF-LAYOUT */
height: 100%;
width: min(80%, 700px);
/* INNER-LAYOUT */
padding: 0.75rem;
}
.SheetWithStacking-innerContent {
/* SELF-LAYOUT */
height: 100%;
min-height: 0;
/* APPEARANCE */
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
overflow: hidden;
overflow: clip;
border-radius: 24px;
background-color: white;
}

View File

@@ -0,0 +1,199 @@
import React, { createContext, useContext } from "react";
import {
Sheet,
SheetStack,
useClientMediaQuery,
type SheetContentProps,
} from "@silk-hq/components";
import "./SheetWithStacking.css";
// ================================================================================================
// Context
// ================================================================================================
type SheetWithStackingContextValue = {
travelStatus: string;
setTravelStatus: (status: string) => void;
contentPlacement: "right" | "bottom";
};
const SheetWithStackingContext = createContext<SheetWithStackingContextValue | null>(null);
// ================================================================================================
// Stack Root
// ================================================================================================
const SheetWithStackingStackRoot = React.forwardRef<
React.ElementRef<typeof SheetStack.Root>,
React.ComponentPropsWithoutRef<typeof SheetStack.Root>
>(({ children, ...restProps }, ref) => {
return (
<SheetStack.Root {...restProps} ref={ref}>
{children}
</SheetStack.Root>
);
});
SheetWithStackingStackRoot.displayName = "SheetWithStackingStack.Root";
// ================================================================================================
// Root
// ================================================================================================
type SheetRootProps = React.ComponentPropsWithoutRef<typeof Sheet.Root>;
type SheetWithStackingRootProps = Omit<SheetRootProps, "license"> & {
license?: SheetRootProps["license"];
};
const SheetWithStackingRoot = React.forwardRef<
React.ElementRef<typeof Sheet.Root>,
SheetWithStackingRootProps
>(({ children, ...restProps }, ref) => {
const [travelStatus, setTravelStatus] = React.useState("idleOutside");
const largeViewport = useClientMediaQuery("(min-width: 700px)");
const contentPlacement = largeViewport ? "right" : "bottom";
return (
<SheetWithStackingContext.Provider
value={{
travelStatus,
setTravelStatus,
contentPlacement,
}}
>
<Sheet.Root license="commercial" forComponent="closest" {...restProps} ref={ref}>
{children}
</Sheet.Root>
</SheetWithStackingContext.Provider>
);
});
SheetWithStackingRoot.displayName = "SheetWithStacking.Root";
// ================================================================================================
// View
// ================================================================================================
const SheetWithStackingView = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof Sheet.View>
>(({ children, className, ...restProps }, ref) => {
const context = useContext(SheetWithStackingContext);
if (!context)
throw new Error(
"SheetWithStackingView must be used within a SheetWithStackingContext.Provider"
);
const { setTravelStatus, contentPlacement } = context;
return (
<Sheet.View
className={`SheetWithStacking-view contentPlacement-${contentPlacement} ${className ?? ""}`}
contentPlacement={contentPlacement}
nativeEdgeSwipePrevention={true}
onTravelStatusChange={setTravelStatus}
{...restProps}
ref={ref}
>
{children}
</Sheet.View>
);
});
SheetWithStackingView.displayName = "SheetWithStacking.View";
// ================================================================================================
// Backdrop
// ================================================================================================
const SheetWithStackingBackdrop = React.forwardRef<
React.ElementRef<typeof Sheet.Backdrop>,
React.ComponentPropsWithoutRef<typeof Sheet.Backdrop>
>((props, ref) => {
return (
<Sheet.Backdrop
travelAnimation={{ opacity: [0, 0.2] }}
themeColorDimming="auto"
{...props}
ref={ref}
/>
);
});
SheetWithStackingBackdrop.displayName = "SheetWithStacking.Backdrop";
// ================================================================================================
// Content
// ================================================================================================
const SheetWithStackingContent = React.forwardRef<
React.ElementRef<typeof Sheet.Content>,
React.ComponentPropsWithoutRef<typeof Sheet.Content>
>(({ children, className, stackingAnimation: stackingAnimationFromProps, ...restProps }, ref) => {
const context = useContext(SheetWithStackingContext);
if (!context)
throw new Error(
"SheetWithStackingContent must be used within a SheetWithStackingContext.Provider"
);
const { contentPlacement } = context;
const stackingAnimation: SheetContentProps["stackingAnimation"] =
contentPlacement === "right"
? {
translateX: ({ progress }: { progress: number }) =>
progress <= 1
? progress * -10 + "px"
: // prettier-ignore
"calc(-12.5px + 2.5px *" + progress + ")",
scale: [1, 0.933],
transformOrigin: "0 50%",
...stackingAnimationFromProps,
}
: {
translateY: ({ progress }: { progress: number }) =>
progress <= 1
? progress * -10 + "px"
: // prettier-ignore
"calc(-12.5px + 2.5px *" + progress + ")",
scale: [1, 0.933],
transformOrigin: "50% 0",
...stackingAnimationFromProps,
};
return (
<Sheet.Content
className={`SheetWithStacking-content contentPlacement-${contentPlacement} ${
className ?? ""
}`}
stackingAnimation={stackingAnimation}
{...restProps}
ref={ref}
>
<div className="SheetWithStacking-innerContent">{children}</div>
</Sheet.Content>
);
});
SheetWithStackingContent.displayName = "SheetWithStacking.Content";
// ================================================================================================
// Unchanged Components
// ================================================================================================
const SheetWithStackingPortal = Sheet.Portal;
const SheetWithStackingTrigger = Sheet.Trigger;
const SheetWithStackingHandle = Sheet.Handle;
const SheetWithStackingOutlet = Sheet.Outlet;
const SheetWithStackingTitle = Sheet.Title;
const SheetWithStackingDescription = Sheet.Description;
export const SheetWithStackingStack = {
Root: SheetWithStackingStackRoot,
};
export const SheetWithStacking = {
Root: SheetWithStackingRoot,
View: SheetWithStackingView,
Portal: SheetWithStackingPortal,
Backdrop: SheetWithStackingBackdrop,
Content: SheetWithStackingContent,
Trigger: SheetWithStackingTrigger,
Handle: SheetWithStackingHandle,
Outlet: SheetWithStackingOutlet,
Title: SheetWithStackingTitle,
Description: SheetWithStackingDescription,
};

View File

@@ -0,0 +1,21 @@
import "@silk-hq/components/dist/main-unlayered.css"
import { Sheet } from '@silk-hq/components'
import { BottomSheet } from './BottomSheet'
import { SheetWithDepth, SheetWithDepthStack } from './SheetWithDepth'
import { SheetWithDetent } from './SheetWithDetent'
import { SheetWithStacking, SheetWithStackingStack } from './SheetWithStacking'
declare global {
var LSSilkhq: any
}
const silkhq = {
Sheet, BottomSheet,
SheetWithDepth, SheetWithDepthStack,
SheetWithStacking, SheetWithDetent,
SheetWithStackingStack,
}
window.LSSilkhq = silkhq
export default silkhq

View File

@@ -3157,6 +3157,11 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz#1973871850856ae72bc678aeb066ab952330e923"
integrity sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==
"@silk-hq/components@^0.9.10":
version "0.9.10"
resolved "https://registry.yarnpkg.com/@silk-hq/components/-/components-0.9.10.tgz#ed6baa898b4f36ce0e5ecadabfecef748546db74"
integrity sha512-dr6NRdGR2vovh4Uv27IhnkvpcUwHR9D7YZLCxTE6fyl4Zb6K6cGUlWVo3b3tgfCHVyirvrRvqWOF2nxMVlmVXg==
"@sinclair/typebox@^0.27.8":
version "0.27.8"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e"