Explorar o código

enhance(ui): refactor mobile UI components for improved readability and maintainability

charlie hai 4 meses
pai
achega
1de207a0ca

+ 115 - 115
packages/ui/src/silkhq/SheetWithDepth.tsx

@@ -6,7 +6,7 @@ import React, {
   useMemo,
   useRef,
   useState,
-} from "react";
+} from 'react'
 import {
   Sheet,
   SheetStack,
@@ -15,14 +15,14 @@ import {
   usePageScrollData,
   SheetViewProps,
   createComponentId,
-} from "@silk-hq/components";
-import "./SheetWithDepth.css";
+} from '@silk-hq/components'
+import './SheetWithDepth.css'
 
 // ================================================================================================
 // Stack Id
 // ================================================================================================
 
-const sheetWithDepthStackId = createComponentId();
+const sheetWithDepthStackId = createComponentId()
 
 // ================================================================================================
 // StackRoot Context
@@ -37,16 +37,16 @@ type SheetWithDepthStackRootContextValue = {
 
 const SheetWithDepthStackRootContext = createContext<SheetWithDepthStackRootContextValue | null>(
   null
-);
+)
 const useSheetWithDepthStackRootContext = () => {
-  const context = useContext(SheetWithDepthStackRootContext);
+  const context = useContext(SheetWithDepthStackRootContext)
   if (!context) {
     throw new Error(
-      "useSheetWithDepthStackRootContext must be used within a SheetWithDepthStackRootContext"
-    );
+      'useSheetWithDepthStackRootContext must be used within a SheetWithDepthStackRootContext'
+    )
   }
-  return context;
-};
+  return context
+}
 
 // ================================================================================================
 // View Context
@@ -54,14 +54,14 @@ const useSheetWithDepthStackRootContext = () => {
 
 const SheetWithDepthViewContext = createContext<{
   indexInStack: number;
-} | null>(null);
+} | null>(null)
 const useSheetWithDepthViewContext = () => {
-  const context = useContext(SheetWithDepthViewContext);
+  const context = useContext(SheetWithDepthViewContext)
   if (!context) {
-    throw new Error("useSheetWithDepthViewContext must be used within a SheetWithDepthViewContext");
+    throw new Error('useSheetWithDepthViewContext must be used within a SheetWithDepthViewContext')
   }
-  return context;
-};
+  return context
+}
 
 // ================================================================================================
 // StackRoot
@@ -71,10 +71,10 @@ 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 stackBackgroundRef = useRef<HTMLDivElement | null>(null)
+  const stackFirstSheetBackdropRef = useRef<HTMLDivElement | null>(null)
 
-  const [stackingCount, setStackingCount] = useState(0);
+  const [stackingCount, setStackingCount] = useState(0)
 
   const contextValue = useMemo(
     () => ({
@@ -84,7 +84,7 @@ const SheetWithDepthStackRoot = React.forwardRef<
       setStackingCount,
     }),
     [stackingCount]
-  );
+  )
 
   return (
     <SheetWithDepthStackRootContext.Provider value={contextValue}>
@@ -92,9 +92,9 @@ const SheetWithDepthStackRoot = React.forwardRef<
         {children}
       </SheetStack.Root>
     </SheetWithDepthStackRootContext.Provider>
-  );
-});
-SheetWithDepthStackRoot.displayName = "SheetWithDepthStack.Root";
+  )
+})
+SheetWithDepthStackRoot.displayName = 'SheetWithDepthStack.Root'
 
 // ================================================================================================
 // StackSceneryOutlets
@@ -103,27 +103,27 @@ SheetWithDepthStackRoot.displayName = "SheetWithDepthStack.Root";
 // 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 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">
+  Omit<React.ComponentProps<typeof SheetStack.Outlet>, 'asChild'>
 >(({ children, className, stackingAnimation: stackingAnimationFromProps, ...restProps }, ref) => {
-  const { stackBackgroundRef, stackFirstSheetBackdropRef } = useSheetWithDepthStackRootContext();
+  const { stackBackgroundRef, stackFirstSheetBackdropRef } = useSheetWithDepthStackRootContext()
 
-  const { nativePageScrollReplaced } = usePageScrollData();
+  const { nativePageScrollReplaced } = usePageScrollData()
 
-  const [iOSStandalone, setiOSStandalone] = useState(false);
+  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"] = {
+  >['stackingAnimation'] = {
     // Clipping & border-radius. We have a different animation
     // when the native page scroll is replaced, and in iOS
     // standalone mode.
@@ -133,39 +133,39 @@ const SheetWithDepthStackSceneryOutlets = React.forwardRef<
           // 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",
-          }
+        {
+          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",
-          }
+        {
+          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",
-        }),
+      {
+        clipBoundary: 'layout-viewport',
+        clipBorderRadius: '24px',
+        clipTransformOrigin: '50% 0',
+      }),
 
     // Translate & scale
     translateY: ({ progress }) =>
       progress <= 1
-        ? "calc(" + progress + " * " + initialTopOffset + ")"
+        ? 'calc(' + progress + ' * ' + initialTopOffset + ')'
         : // prettier-ignore
-          "calc(" + initialTopOffset + " + 0.65vh * " + (progress - 1) + ")",
+        'calc(' + initialTopOffset + ' + 0.65vh * ' + (progress - 1) + ')',
     scale: [1, 0.91],
 
     // We merge animations coming from the props
     ...stackingAnimationFromProps,
-  };
+  }
 
   return (
     <>
@@ -176,7 +176,7 @@ const SheetWithDepthStackSceneryOutlets = React.forwardRef<
       />
       {/* Element used as a container for the content under the stack. */}
       <SheetStack.Outlet
-        className={`SheetWithDepth-stackSceneryContainer ${className ?? ""}`.trim()}
+        className={`SheetWithDepth-stackSceneryContainer ${className ?? ''}`.trim()}
         forComponent={sheetWithDepthStackId}
         stackingAnimation={stackingAnimation}
         {...restProps}
@@ -190,17 +190,17 @@ const SheetWithDepthStackSceneryOutlets = React.forwardRef<
         />
       </SheetStack.Outlet>
     </>
-  );
-});
-SheetWithDepthStackSceneryOutlets.displayName = "SheetWithDepthStack.SceneryOutlets";
+  )
+})
+SheetWithDepthStackSceneryOutlets.displayName = 'SheetWithDepthStack.SceneryOutlets'
 
 // ================================================================================================
 // Root
 // ================================================================================================
 
 type SheetRootProps = React.ComponentPropsWithoutRef<typeof Sheet.Root>;
-type SheetWithDepthRootProps = Omit<SheetRootProps, "license"> & {
-  license?: SheetRootProps["license"];
+type SheetWithDepthRootProps = Omit<SheetRootProps, 'license'> & {
+  license?: SheetRootProps['license'];
 };
 
 const SheetWithDepthRoot = React.forwardRef<
@@ -208,10 +208,10 @@ const SheetWithDepthRoot = React.forwardRef<
   SheetWithDepthRootProps
 >((props, ref) => {
   return (
-    <Sheet.Root license="commercial" forComponent={sheetWithDepthStackId} {...props} ref={ref} />
-  );
-});
-SheetWithDepthRoot.displayName = "SheetWithDepth.Root";
+    <Sheet.Root license="commercial" forComponent={sheetWithDepthStackId} {...props} ref={ref}/>
+  )
+})
+SheetWithDepthRoot.displayName = 'SheetWithDepth.Root'
 
 // ================================================================================================
 // View
@@ -237,58 +237,58 @@ const SheetWithDepthView = React.forwardRef<
 
       stackBackgroundRef,
       stackFirstSheetBackdropRef,
-    } = useSheetWithDepthStackRootContext();
+    } = useSheetWithDepthStackRootContext()
 
-    const [indexInStack, setIndexInStack] = useState(0);
-    const [travelStatus, setTravelStatus] = useState("idleOutside");
+    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)",
-    });
+      dimmingColor: 'rgb(0, 0, 0)',
+    })
 
     //
     // travelStatusChangeHandler
 
     const travelStatusChangeHandler = useCallback<
-      NonNullable<SheetViewProps["onTravelStatusChange"]>
+      NonNullable<SheetViewProps['onTravelStatusChange']>
     >(
       (newTravelStatus) => {
         // Set indexInStack & stackingCount
-        if (travelStatus !== "stepping" && newTravelStatus === "idleInside") {
-          setStackingCount((prevStackingCount: number) => prevStackingCount + 1);
+        if (travelStatus !== 'stepping' && newTravelStatus === 'idleInside') {
+          setStackingCount((prevStackingCount: number) => prevStackingCount + 1)
           if (indexInStack === 0) {
-            setIndexInStack(stackingCount + 1);
+            setIndexInStack(stackingCount + 1)
           }
         }
         //
-        else if (newTravelStatus === "idleOutside") {
-          setStackingCount((prevStackingCount: number) => prevStackingCount - 1);
-          setIndexInStack(0);
+        else if (newTravelStatus === 'idleOutside') {
+          setStackingCount((prevStackingCount: number) => prevStackingCount - 1)
+          setIndexInStack(0)
         }
 
         // Animate on entering
-        if (newTravelStatus === "entering" && stackingCount === 0) {
-          animateDimmingOverlayOpacity({ keyframes: [0, 1] });
+        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] });
+        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);
+        onTravelStatusChange?.(newTravelStatus)
+        setTravelStatus(newTravelStatus)
       },
       [
         travelStatus,
@@ -299,26 +299,26 @@ const SheetWithDepthView = React.forwardRef<
         animateDimmingOverlayOpacity,
         onTravelStatusChange,
       ]
-    );
+    )
 
     //
     // travelHandler
 
     const travelHandler = useMemo(() => {
-      if (indexInStack === 1 && travelStatus !== "entering" && travelStatus !== "exiting") {
-        const handler: NonNullable<SheetViewProps["onTravel"]> = ({ progress, ...rest }) => {
-          setDimmingOverlayOpacity(progress);
+      if (indexInStack === 1 && travelStatus !== 'entering' && travelStatus !== 'exiting') {
+        const handler: NonNullable<SheetViewProps['onTravel']> = ({ progress, ...rest }) => {
+          setDimmingOverlayOpacity(progress)
           stackFirstSheetBackdropRef.current?.style.setProperty(
-            "opacity",
+            'opacity',
             (progress * 0.33) as unknown as string
-          );
-          travelHandlerFromProps?.({ progress, ...rest });
-        };
-        return handler;
+          )
+          travelHandlerFromProps?.({ progress, ...rest })
+        }
+        return handler
       } else {
-        return travelHandlerFromProps;
+        return travelHandlerFromProps
       }
-    }, [indexInStack, travelStatus, stackFirstSheetBackdropRef, setDimmingOverlayOpacity]);
+    }, [indexInStack, travelStatus, stackFirstSheetBackdropRef, setDimmingOverlayOpacity])
 
     //
     // Return
@@ -326,7 +326,7 @@ const SheetWithDepthView = React.forwardRef<
     return (
       <SheetWithDepthViewContext.Provider value={{ indexInStack }}>
         <Sheet.View
-          className={`SheetWithDepth-view ${className ?? ""}`.trim()}
+          className={`SheetWithDepth-view ${className ?? ''}`.trim()}
           contentPlacement="bottom"
           onTravelStatusChange={travelStatusChangeHandler}
           onTravel={travelHandler}
@@ -337,10 +337,10 @@ const SheetWithDepthView = React.forwardRef<
           {children}
         </Sheet.View>
       </SheetWithDepthViewContext.Provider>
-    );
+    )
   }
-);
-SheetWithDepthView.displayName = "SheetWithDepth.View";
+)
+SheetWithDepthView.displayName = 'SheetWithDepth.View'
 
 // ================================================================================================
 // Backdrop
@@ -351,8 +351,8 @@ const SheetWithDepthBackdrop = React.forwardRef<
   React.ComponentPropsWithoutRef<typeof Sheet.Backdrop>
 // @ts-ignore
 >(({ className, ...restProps }, ref) => {
-  const { stackingCount } = useSheetWithDepthStackRootContext();
-  const { indexInStack } = useSheetWithDepthViewContext();
+  const { stackingCount } = useSheetWithDepthStackRootContext()
+  const { indexInStack } = useSheetWithDepthViewContext()
 
   return (
     // We don't render the Backdrop for the first sheet in the
@@ -361,15 +361,15 @@ const SheetWithDepthBackdrop = React.forwardRef<
     stackingCount > 0 &&
     indexInStack !== 1 && (
       <Sheet.Backdrop
-        className={`SheetWithDepth-backdrop ${className ?? ""}`.trim()}
+        className={`SheetWithDepth-backdrop ${className ?? ''}`.trim()}
         travelAnimation={{ opacity: [0, 0.33] }}
         {...restProps}
         ref={ref}
       />
     )
-  );
-});
-SheetWithDepthBackdrop.displayName = "SheetWithDepth.Backdrop";
+  )
+})
+SheetWithDepthBackdrop.displayName = 'SheetWithDepth.Backdrop'
 
 // ================================================================================================
 // Content
@@ -381,42 +381,42 @@ const SheetWithDepthContent = React.forwardRef<
 >(({ children, className, stackingAnimation, ...restProps }, ref) => {
   return (
     <Sheet.Content
-      className={`SheetWithDepth-content ${className ?? ""}`.trim()}
+      className={`SheetWithDepth-content ${className ?? ''}`.trim()}
       stackingAnimation={{
         translateY: ({ progress }) =>
           progress <= 1
-            ? progress * -1.3 + "vh"
+            ? progress * -1.3 + 'vh'
             : // prettier-ignore
-              "calc(-1.3vh + 0.65vh * " + (progress - 1) + ")",
+            'calc(-1.3vh + 0.65vh * ' + (progress - 1) + ')',
         scale: [1, 0.91],
-        transformOrigin: "50% 0",
+        transformOrigin: '50% 0',
         ...stackingAnimation,
       }}
       {...restProps}
       ref={ref}
     >
-      <Sheet.BleedingBackground className="SheetWithDepth-bleedingBackground" />
+      <Sheet.BleedingBackground className="SheetWithDepth-bleedingBackground"/>
       {children}
     </Sheet.Content>
-  );
-});
-SheetWithDepthContent.displayName = "SheetWithDepth.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;
+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,
@@ -429,4 +429,4 @@ export const SheetWithDepth = {
   Outlet: SheetWithDepthOutlet,
   Title: SheetWithDepthTitle,
   Description: SheetWithDepthDescription,
-};
+}

+ 23 - 12
src/main/mobile/components/app.css

@@ -5,7 +5,7 @@
 
 :root {
   --ls-page-title-size: 26px;
-  --silk-topbar-height: 48px;
+  --silk-topbar-inner-height: 32px;
   --silk-tabbar-bottom-paddding: 12px;
 }
 
@@ -14,11 +14,20 @@ html.is-native-ios {
 }
 
 html.is-native-android {
+  --silk-topbar-inner-padding-top: 10px;
+  --silk-topbar-inner-height: 40px;
+  --silk-topbar-inner-padding-bottom: 6px;
+
+  .app-silk-topbar {}
 }
 
 #mobile-editor-toolbar {
 }
 
+#main-container {
+  overflow-y: visible;
+}
+
 html.has-mobile-keyboard {
   body {
     @apply overflow-hidden
@@ -140,9 +149,7 @@ a, button {
     }
   }
 
-  .app-silk-sheet-scroll-content {
-    height: 92vh;
-  }
+  .app-silk-depth-sheet-content {}
 }
 
 .block-content-or-editor-inner {
@@ -293,8 +300,8 @@ a, button {
   @apply bg-gray-01 min-h-[100svh] overflow-x-hidden;
 }
 
-.app-silk-sheet-scroll-content {
-  @apply flex flex-col items-center;
+.app-silk-depth-sheet-content {
+  display: grid;
 }
 
 .BottomSheet-bleedingBackground,
@@ -315,7 +322,6 @@ a, button {
   overflow: hidden;
   overflow: clip;
   position: relative;
-  display: grid;
 }
 
 html[data-silk-native-page-scroll-replaced=false] .app-silk-index-scroll-view {
@@ -329,11 +335,15 @@ html[data-silk-native-page-scroll-replaced=false] .app-silk-index-scroll-view {
   @apply p-4 flex flex-col gap-3 bg-gray-01;
 
   &[data-tab=search] {
-    --silk-topbar-height: 2px;
+    --silk-topbar-inner-height: 2px;
   }
 
-  padding-top: calc(env(safe-area-inset-top, 0px) + var(--silk-topbar-height) + 10px);
+  padding-top: calc(env(safe-area-inset-top, 0px) + var(--silk-topbar-inner-height) + 22px);
   padding-bottom: 120px;
+
+  #journals {
+    @apply -mt-4 px-1;
+  }
 }
 
 .app-silk-topbar {
@@ -348,9 +358,10 @@ html[data-silk-native-page-scroll-replaced=false] .app-silk-index-scroll-view {
     }
   }
 
-  padding-top: calc(env(safe-area-inset-top, 0px) + 2px);
-  padding-bottom: 8px;
-  height: 32px;
+  padding-top: calc(env(safe-area-inset-top, 0px) + var(--silk-topbar-inner-padding-top, 2px));
+  height: var(--silk-topbar-inner-height, 32px);
+
+  padding-bottom: var(--silk-topbar-inner-padding-bottom, 8px);
   box-sizing: content-box;
 
   &.search {

+ 78 - 72
src/main/mobile/components/modal.cljs

@@ -30,79 +30,85 @@
         (set-favorited! (page-handler/favorited? (str (:block/uuid block)))))
       [block])
 
-    (when open?
-      (state/clear-edit!)
-      (init/keyboard-hide))
+    (hooks/use-effect!
+      (fn []
+        (when open?
+          (state/clear-edit!)
+          (init/keyboard-hide)))
+      [open?])
+
+    (silkhq/depth-sheet
+      {:presented (boolean open?)
+       :onPresentedChange (fn [v?]
+                            (when (false? v?)
+                              (mobile-state/set-singleton-modal! nil)
+                              (state/clear-edit!)
+                              (state/pub-event! [:mobile/keyboard-will-hide])))}
+      (silkhq/depth-sheet-portal
+        (silkhq/depth-sheet-view
+          {:class "block-modal-page"
+           :inertOutside false}
+          (silkhq/depth-sheet-backdrop)
+          (silkhq/depth-sheet-content
+            {:class "app-silk-depth-sheet-content"}
+            (silkhq/scroll {:as-child true}
+              (silkhq/scroll-view
+                {:class "app-silk-scroll-view"
+                 :scrollGestureTrap {:yEnd true}}
+                (silkhq/scroll-content
+                  {:class "app-silk-scroll-content"}
 
-    (silkhq/bottom-sheet
-     {:presented (boolean open?)
-      :onPresentedChange (fn [v?]
-                           (when (false? v?)
-                             (mobile-state/set-singleton-modal! nil)
-                             (state/clear-edit!)
-                             (state/pub-event! [:mobile/keyboard-will-hide])))}
-     (silkhq/bottom-sheet-portal
-      (silkhq/bottom-sheet-view
-       {:class "block-modal-page"
-        :inertOutside false}
-       (silkhq/bottom-sheet-backdrop)
-       (silkhq/bottom-sheet-content
-        {:class "app-silk-sheet-scroll-content"}
-         (silkhq/scroll {:as-child true}
-           (silkhq/scroll-view
-             {:class "app-silk-scroll-view"}
-             (silkhq/scroll-content
-               {:class "app-silk-scroll-content"}
-               [:div.app-silk-scroll-content-inner
-                [:div.flex.justify-between.items-center.block-modal-page-header
-                 [:a.opacity-40.active:opacity-60.px-2
-                  {:on-pointer-down close!}
-                  (shui/tabler-icon "chevron-down" {:size 18 :stroke 3})]
+                  [:div.app-silk-scroll-content-inner
+                   [:div.flex.justify-between.items-center.block-modal-page-header
+                    [:a.opacity-40.active:opacity-60.px-2
+                     {:on-pointer-down close!}
+                     (shui/tabler-icon "chevron-down" {:size 18 :stroke 3})]
 
-                 [:span.flex.items-center.gap-2
-                  (when-let [block-id-str (str (:block/uuid block))]
-                    [:a.active:opacity-80.pr-1
-                     {:class (if favorited? "opacity-80 !text-yellow-800" "opacity-40")
-                      :on-click #(-> (if favorited?
-                                       (page-handler/<unfavorite-page! block-id-str)
-                                       (page-handler/<favorite-page! block-id-str))
-                                   (p/then (fn [] (set-favorited! (not favorited?)))))}
-                     (shui/tabler-icon (if favorited? "star-filled" "star") {:size 18 :stroke 2})])
-                  [:a.opacity-40.active:opacity-60.pr-1
-                   {:on-pointer-down (fn []
-                                       (mobile-ui/open-popup!
-                                         (fn []
-                                           [:div.-mx-2
-                                            (ui/menu-link
-                                              {:on-click #(mobile-ui/close-popup!)}
-                                              [:span.text-lg.flex.gap-2.items-center
-                                               (shui/tabler-icon "copy" {:class "opacity-80" :size 22})
-                                               "Copy"])
+                    [:span.flex.items-center.gap-2
+                     (when-let [block-id-str (str (:block/uuid block))]
+                       [:a.active:opacity-80.pr-1
+                        {:class (if favorited? "opacity-80 !text-yellow-800" "opacity-40")
+                         :on-click #(-> (if favorited?
+                                          (page-handler/<unfavorite-page! block-id-str)
+                                          (page-handler/<favorite-page! block-id-str))
+                                      (p/then (fn [] (set-favorited! (not favorited?)))))}
+                        (shui/tabler-icon (if favorited? "star-filled" "star") {:size 18 :stroke 2})])
+                     [:a.opacity-40.active:opacity-60.pr-1
+                      {:on-pointer-down (fn []
+                                          (mobile-ui/open-popup!
+                                            (fn []
+                                              [:div.-mx-2
+                                               (ui/menu-link
+                                                 {:on-click #(mobile-ui/close-popup!)}
+                                                 [:span.text-lg.flex.gap-2.items-center
+                                                  (shui/tabler-icon "copy" {:class "opacity-80" :size 22})
+                                                  "Copy"])
 
-                                            (ui/menu-link
-                                              {:on-click #(-> (shui/dialog-confirm!
-                                                                (str "⚠️ Are you sure you want to delete this "
-                                                                  (if (entity-util/page? block) "page" "block")
-                                                                  "?"))
-                                                            (p/then
-                                                              (fn []
-                                                                (mobile-ui/close-popup!)
-                                                                (some->
-                                                                  (:block/uuid block)
-                                                                  (page-handler/<delete!
-                                                                    (fn [] (close!))
-                                                                    {:error-handler
-                                                                     (fn [{:keys [msg]}]
-                                                                       (notification/show! msg :warning))})))))}
-                                              [:span.text-lg.flex.gap-2.items-center.text-red-700
-                                               (shui/tabler-icon "trash" {:class "opacity-80" :size 22})
-                                               "Delete"])])
-                                         {:title "Actions"
-                                          :type :action-sheet}))}
-                   (shui/tabler-icon "dots-vertical" {:size 18 :stroke 2})]]]
+                                               (ui/menu-link
+                                                 {:on-click #(-> (shui/dialog-confirm!
+                                                                   (str "⚠️ Are you sure you want to delete this "
+                                                                     (if (entity-util/page? block) "page" "block")
+                                                                     "?"))
+                                                               (p/then
+                                                                 (fn []
+                                                                   (mobile-ui/close-popup!)
+                                                                   (some->
+                                                                     (:block/uuid block)
+                                                                     (page-handler/<delete!
+                                                                       (fn [] (close!))
+                                                                       {:error-handler
+                                                                        (fn [{:keys [msg]}]
+                                                                          (notification/show! msg :warning))})))))}
+                                                 [:span.text-lg.flex.gap-2.items-center.text-red-700
+                                                  (shui/tabler-icon "trash" {:class "opacity-80" :size 22})
+                                                  "Delete"])])
+                                            {:title "Actions"
+                                             :type :action-sheet}))}
+                      (shui/tabler-icon "dots-vertical" {:size 18 :stroke 2})]]]
 
-                ;; block page content
-                [:div.block-modal-page-content
-                 (when open?
-                   (mobile-ui/classic-app-container-wrap
-                     (page/page-cp (db/entity [:block/uuid (:block/uuid block)]))))]])))))))))
+                   ;; block page content
+                   [:div.block-modal-page-content
+                    (when open?
+                      (mobile-ui/classic-app-container-wrap
+                        (page/page-cp (db/entity [:block/uuid (:block/uuid block)]))))]])))
+            ))))))