- {item.title}
+ {item._type !== "folder" ? (
+
+ ) : (
+ item.title
+ )}
- {path}
+ {item._type !== "folder" ? (
+
+ ) : (
+ path
+ )}
@@ -411,21 +419,19 @@ const ListItem = ({
);
};
-const ItemIcon = ({ type }: { type: string }) => {
- const schema = useSchema();
+const ItemIcon = ({ item }: { item: TreeNode }) => {
const iconProps = {
fontSize: "calc(21 / 16 * 1em)",
color: "var(--card-icon-color)",
};
- if (type === "folder") {
+ if (item._type === "folder") {
return
;
}
- const fullSchema = schema.get(type);
- const Icon = fullSchema?.icon ?? DocumentIcon;
-
- return
;
+ return (
+
} type="media" item={item} />
+ );
};
const SkeletonListItems = ({ items }: { items: number }) => {
diff --git a/packages/sanity-studio/src/plugins/navigator/components/Preview.tsx b/packages/sanity-studio/src/plugins/navigator/components/Preview.tsx
new file mode 100644
index 0000000..49b9116
--- /dev/null
+++ b/packages/sanity-studio/src/plugins/navigator/components/Preview.tsx
@@ -0,0 +1,174 @@
+import { isImageSource, SanityImageSource } from "@sanity/asset-utils";
+import { DocumentIcon } from "@sanity/icons";
+import imageUrlBuilder from "@sanity/image-url";
+import React from "react";
+import { isValidElementType } from "react-is";
+import { useMemoObservable } from "react-rx";
+import {
+ getPreviewStateObservable,
+ getPreviewValueWithFallback,
+ ImageUrlFitMode,
+ isString,
+ SanityDefaultPreviewProps,
+ useClient,
+ useDocumentPreviewStore,
+ useSchema,
+} from "sanity";
+
+import { FolderTreeNode, TreeNode } from "../../../types";
+
+const PreviewElement = ({
+ item,
+ type,
+ fallback,
+}: {
+ item: Exclude
; // Only accepts a PageTreeNode, FolderTreeNode is forbidden
+ type: "media" | "title" | "subtitle";
+ fallback?: React.ReactNode | string;
+}): React.ReactElement => {
+ const schema = useSchema();
+ const { _id, _type } = item;
+
+ const documentPreviewStore = useDocumentPreviewStore();
+ const schemaType = schema.get(_type);
+
+ const { draft, published, isLoading } = useMemoObservable(
+ () => getPreviewStateObservable(documentPreviewStore, schemaType, _id, ""),
+ [_id, documentPreviewStore, schemaType]
+ )!;
+
+ const previewValues = getPreviewValueWithFallback({
+ draft,
+ published,
+ value: { ...item },
+ });
+
+ const showPreview =
+ typeof schemaType.preview?.prepare === "function" && !isLoading;
+
+ if (type === "media") {
+ return showPreview ? (
+
+ ) : (
+ <>{!isLoading ? fallback : null}>
+ );
+ }
+
+ if (type === "title") {
+ return showPreview && previewValues?.title ? (
+ <>{previewValues?.title}>
+ ) : (
+ <>{fallback}>
+ );
+ }
+
+ if (type === "subtitle") {
+ return showPreview && previewValues?.subtitle ? (
+ <>{previewValues?.subtitle}>
+ ) : (
+ <>{fallback}>
+ );
+ }
+
+ return null;
+};
+
+PreviewElement.displayName = "PreviewElement";
+
+const PreviewMedia = (props: SanityDefaultPreviewProps): React.ReactElement => {
+ const { icon, media: mediaProp, imageUrl, title } = props;
+
+ const client = useClient({
+ apiVersion: "2024-03-12",
+ });
+ const imageBuilder = React.useMemo(() => imageUrlBuilder(client), [client]);
+
+ // NOTE: This function exists because the previews provides options
+ // for the rendering of the media (dimensions)
+ const renderMedia = React.useCallback(
+ (options: {
+ dimensions: {
+ width?: number;
+ height?: number;
+ fit: ImageUrlFitMode;
+ dpr?: number;
+ };
+ }) => {
+ const { dimensions } = options;
+
+ // Handle sanity image
+ return (
+
+ );
+ },
+ [imageBuilder, mediaProp, title]
+ );
+
+ const renderIcon = React.useCallback(() => {
+ return React.createElement(icon || DocumentIcon);
+ }, [icon]);
+
+ const media = React.useMemo(() => {
+ if (icon === false) {
+ // Explicitly disabled
+ return false;
+ }
+
+ if (isValidElementType(mediaProp)) {
+ return mediaProp;
+ }
+
+ if (React.isValidElement(mediaProp)) {
+ return mediaProp;
+ }
+
+ if (isImageSource(mediaProp)) {
+ return renderMedia;
+ }
+
+ // Handle image urls
+ if (isString(imageUrl)) {
+ return (
+
+ );
+ }
+
+ // Render fallback icon
+ return renderIcon;
+ }, [icon, imageUrl, mediaProp, renderIcon, renderMedia, title]);
+
+ if (typeof media === "number" || typeof media === "string") {
+ return <>{media}>;
+ }
+
+ const Media = media as React.ComponentType;
+
+ return ;
+};
+
+PreviewMedia.displayName = "PreviewMedia";
+
+export { PreviewElement };
diff --git a/packages/sanity-studio/src/plugins/navigator/index.ts b/packages/sanity-studio/src/plugins/navigator/index.ts
index 44f3085..f357997 100644
--- a/packages/sanity-studio/src/plugins/navigator/index.ts
+++ b/packages/sanity-studio/src/plugins/navigator/index.ts
@@ -1,6 +1,6 @@
import { definePlugin } from "sanity";
-
import { presentationTool } from "sanity/presentation";
+
import { PagesNavigatorPluginOptions } from "../../types";
import { createPagesNavigator } from "./components/DefaultPagesNavigator";
import { createPageTemplates, normalizeCreatablePages } from "./utils";
diff --git a/packages/sanity-studio/src/types.ts b/packages/sanity-studio/src/types.ts
index 5149bc0..4bd18ce 100644
--- a/packages/sanity-studio/src/types.ts
+++ b/packages/sanity-studio/src/types.ts
@@ -7,7 +7,6 @@ import {
SlugDefinition,
SlugOptions,
} from "sanity";
-
import {
NavigatorOptions as PresentationNavigatorOptions,
PresentationPluginOptions,
@@ -37,9 +36,10 @@ export type PagesNavigatorPluginOptions = PresentationPluginOptions & {
};
export type Page = {
+ _rev: string;
_id: string;
_originalId: string;
- _type: string;
+ _type: Exclude<"string", "folder">;
_updatedAt: string;
_createdAt: string;
pathname: string | null;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 75b5eef..13761ff 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -48,6 +48,9 @@ importers:
classnames:
specifier: 2.5.1
version: 2.5.1
+ lucide-react:
+ specifier: ^0.360.0
+ version: 0.360.0(react@18.2.0)
next:
specifier: 14.1.0
version: 14.1.0(@babel/core@7.24.1)(react-dom@18.2.0)(react@18.2.0)
@@ -212,9 +215,15 @@ importers:
packages/sanity-studio:
dependencies:
+ '@sanity/asset-utils':
+ specifier: ^1.3.0
+ version: 1.3.0
'@sanity/icons':
- specifier: ^2.10.2
+ specifier: ^2.11.2
version: 2.11.2(react@18.2.0)
+ '@sanity/image-url':
+ specifier: ^1.0.2
+ version: 1.0.2
'@sanity/incompatible-plugin':
specifier: ^1.0.4
version: 1.0.4(react-dom@18.2.0)(react@18.2.0)
@@ -8190,6 +8199,14 @@ packages:
dependencies:
yallist: 4.0.0
+ /lucide-react@0.360.0(react@18.2.0):
+ resolution: {integrity: sha512-MskvbEsAhD2zxgx/I05vXq1cjFQXrmhL97YFIi4wSaKH793ZMvU/Com4d+DE7OB3QMmZig1fY1q94aTX5skozw==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0
+ dependencies:
+ react: 18.2.0
+ dev: false
+
/magic-string@0.30.8:
resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
engines: {node: '>=12'}