فهرست منبع

enhance(mobile): add silkhq basic components

charlie 3 ماه پیش
والد
کامیت
dff4aa64bb

+ 28 - 0
deps/shui/src/logseq/shui/silkhq.cljs

@@ -0,0 +1,28 @@
+(ns logseq.shui.silkhq
+  (:require [logseq.shui.util :refer [component-wrap] :as util]
+            [goog.object :refer [getValueByKeys] :as gobj]))
+
+(goog-define NODETEST false)
+
+(def silkhq-wrap
+  (partial component-wrap js/window.LSSilkhq))
+
+(defn silkhq-get
+  [name]
+  (if NODETEST
+    #js {}
+    (let [path (util/get-path name)]
+      (some-> js/window.LSSilkhq (gobj/getValueByKeys (clj->js path))))))
+
+
+(def sheet (silkhq-wrap "Sheet"))
+(def bottom-sheet (silkhq-wrap "BottomSheet.Root"))
+(def bottom-sheet-portal (silkhq-wrap "BottomSheet.Portal"))
+(def bottom-sheet-handle (silkhq-wrap "BottomSheet.Handle"))
+(def bottom-sheet-content (silkhq-wrap "BottomSheet.Content"))
+(def bottom-sheet-title (silkhq-wrap "BottomSheet.Title"))
+(def bottom-sheet-description (silkhq-wrap "BottomSheet.Description"))
+(def bottom-sheet-trigger (silkhq-wrap "BottomSheet.Trigger"))
+(def bottom-sheet-outlet (silkhq-wrap "BottomSheet.Outlet"))
+(def bottom-sheet-backdrop (silkhq-wrap "BottomSheet.Backdrop"))
+(def bottom-sheet-view (silkhq-wrap "BottomSheet.View"))

+ 1 - 0
gulpfile.js

@@ -133,6 +133,7 @@ const common = {
       ]).pipe(gulp.dest(path.join(outputPath, 'mobile', 'js'))),
       () => gulp.src([
         'packages/ui/dist/ionic/*.js',
+        'packages/ui/dist/silkhq/*'
       ]).pipe(gulp.dest(path.join(outputPath, 'mobile'))),
     )(...params)
   },

+ 11 - 1
packages/ui/package.json

@@ -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": {

+ 34 - 0
packages/ui/src/silkhq/BottomSheet.css

@@ -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;
+}

+ 126 - 0
packages/ui/src/silkhq/BottomSheet.tsx

@@ -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,
+};

+ 63 - 0
packages/ui/src/silkhq/SheetWithDepth.css

@@ -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);
+}

+ 432 - 0
packages/ui/src/silkhq/SheetWithDepth.tsx

@@ -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,
+};

+ 45 - 0
packages/ui/src/silkhq/SheetWithDetent.css

@@ -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;
+}

+ 275 - 0
packages/ui/src/silkhq/SheetWithDetent.tsx

@@ -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,
+};

+ 46 - 0
packages/ui/src/silkhq/SheetWithStacking.css

@@ -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;
+}

+ 199 - 0
packages/ui/src/silkhq/SheetWithStacking.tsx

@@ -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,
+};

+ 21 - 0
packages/ui/src/silkhq/silkhq.ts

@@ -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

+ 5 - 0
packages/ui/yarn.lock

@@ -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"

+ 2 - 0
resources/mobile/index.html

@@ -4,6 +4,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
     <link href="./ionic.bundle.css" rel="stylesheet">
+    <link href="./silkhq.css" rel="stylesheet">
     <link href="./style.css" rel="stylesheet" type="text/css">
     <title>Logseq: A privacy-first platform for knowledge management and collaboration</title>
 </head>
@@ -36,6 +37,7 @@ const portal = new MagicPortal(worker);
 <script defer src="./js/ui.js"></script>
 <script defer src="./js/amplify.js"></script>
 <script defer src="./ionic.js"></script>
+<script defer src="./silkhq.js"></script>
 <script defer src="./js/shared.js"></script>
 <script defer src="./js/main.js"></script>
 <script>

+ 21 - 4
src/main/mobile/components/settings.cljs

@@ -5,6 +5,7 @@
             [frontend.handler.user :as user-handler]
             [frontend.state :as state]
             [logseq.shui.ui :as shui]
+            [logseq.shui.silkhq :as silkhq]
             [mobile.components.ui :as ui-component]
             [mobile.ionic :as ion]
             [rum.core :as rum]))
@@ -25,6 +26,21 @@
       [:strong.text-4xl.font-semibold (user-handler/username)]]
      [:div.text-sm.text-muted-foreground.px-1 (user-handler/email)]]))
 
+(rum/defc silk-bottom-sheet
+  []
+  (silkhq/bottom-sheet
+    (silkhq/bottom-sheet-trigger
+      (shui/button "open bottom sheet / trigger"))
+    (silkhq/bottom-sheet-portal
+      (silkhq/bottom-sheet-view
+        (silkhq/bottom-sheet-backdrop)
+        (silkhq/bottom-sheet-content
+          {:class "flex flex-col justify-center items-center p-2"}
+          (silkhq/bottom-sheet-handle)
+          [:div.py-24.flex
+           [:h1.my-4.text-2xl "hello silkhq"]])))
+    ))
+
 (rum/defc page < rum/reactive
   []
   (let [login? (and (state/sub :auth/id-token) (user-handler/logged-in?))]
@@ -56,7 +72,8 @@
                                                                :modal-props {:class "graph-switcher"}}))}
                        [:span.text-muted-foreground {:slot "icon-only"}
                         (ion/tabler-icon "dots" {:size 20})])))))
-     (ion/content {:class "ion-padding"}
-      (user-profile login?)
-      [:div.mt-8
-       (repo/repos-cp)]))))
+      (ion/content {:class "ion-padding"}
+        (user-profile login?)
+        [:div.mt-8
+         (repo/repos-cp)]
+        (silk-bottom-sheet)))))

+ 1 - 0
src/main/mobile/components/ui.cljs

@@ -1,6 +1,7 @@
 (ns mobile.components.ui
   "Mobile ui"
   (:require [cljs-bean.core :as bean]
+            [logseq.shui.silkhq]
             [frontend.handler.notification :as notification]
             [frontend.rum :as r]
             [frontend.state :as state]