diff --git a/examples/hello-world/package.json b/examples/hello-world/package.json index 363dfdf..0a3d4b6 100644 --- a/examples/hello-world/package.json +++ b/examples/hello-world/package.json @@ -21,6 +21,7 @@ "@tailwindcss/typography": "0.5.10", "@tinloof/sanity-studio": "workspace:*", "classnames": "2.5.1", + "lucide-react": "^0.360.0", "next": "14.1.0", "next-sanity": "^8.0.0", "react": "18.2.0", diff --git a/examples/hello-world/sanity/schemas/page.ts b/examples/hello-world/sanity/schemas/page.ts deleted file mode 100644 index 84e4aa1..0000000 --- a/examples/hello-world/sanity/schemas/page.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { definePathname } from '@tinloof/sanity-studio' -import { defineType } from 'sanity' - -export default defineType({ - type: 'document', - name: 'page', - fields: [ - { - type: 'string', - name: 'title', - }, - definePathname({ name: 'pathname' }), - ], -}) diff --git a/examples/hello-world/sanity/schemas/page.tsx b/examples/hello-world/sanity/schemas/page.tsx new file mode 100644 index 0000000..9044f06 --- /dev/null +++ b/examples/hello-world/sanity/schemas/page.tsx @@ -0,0 +1,33 @@ +import { definePathname } from '@tinloof/sanity-studio' +import { defineType } from 'sanity' +import { StickyNote } from 'lucide-react' + +export default defineType({ + type: 'document', + name: 'page', + fields: [ + { + type: 'string', + name: 'title', + }, + { + type: 'image', + name: 'image', + }, + definePathname({ name: 'pathname' }), + ], + preview: { + select: { + title: 'title', + image: 'image.asset.url', + }, + prepare({ title, image }) { + const Image = () => + const Icon = () => + return { + title, + media: image ? Image : Icon, + } + }, + }, +}) diff --git a/packages/sanity-studio/package.json b/packages/sanity-studio/package.json index 9b18029..3357918 100644 --- a/packages/sanity-studio/package.json +++ b/packages/sanity-studio/package.json @@ -45,7 +45,9 @@ } }, "dependencies": { - "@sanity/icons": "^2.10.2", + "@sanity/asset-utils": "^1.3.0", + "@sanity/icons": "^2.11.2", + "@sanity/image-url": "^1.0.2", "@sanity/incompatible-plugin": "^1.0.4", "@sanity/ui": "^2.0.1", "@tanstack/react-virtual": "^3.0.4", diff --git a/packages/sanity-studio/src/plugins/navigator/components/DefaultPagesNavigator.tsx b/packages/sanity-studio/src/plugins/navigator/components/DefaultPagesNavigator.tsx index 62b36e1..4526335 100644 --- a/packages/sanity-studio/src/plugins/navigator/components/DefaultPagesNavigator.tsx +++ b/packages/sanity-studio/src/plugins/navigator/components/DefaultPagesNavigator.tsx @@ -1,4 +1,5 @@ import React from "react"; + import { PagesNavigatorOptions } from "../../../types"; import { NavigatorProvider } from "../context"; import { useSanityFetch } from "../utils"; @@ -17,6 +18,7 @@ export function createPagesNavigator(props: PagesNavigatorOptions) { function DefaultPagesNavigator(props: PagesNavigatorOptions) { const pagesRoutesQuery = ` *[pathname.current != null]{ + _rev, _id, _originalId, _type, diff --git a/packages/sanity-studio/src/plugins/navigator/components/List.tsx b/packages/sanity-studio/src/plugins/navigator/components/List.tsx index 0158255..034e0e3 100644 --- a/packages/sanity-studio/src/plugins/navigator/components/List.tsx +++ b/packages/sanity-studio/src/plugins/navigator/components/List.tsx @@ -10,15 +10,16 @@ import { Badge, Box, Card, Flex, Stack, Text, Tooltip } from "@sanity/ui"; import { useVirtualizer } from "@tanstack/react-virtual"; import { localizePathname } from "@tinloof/sanity-web"; import React, { useRef } from "react"; -import { useColorSchemeValue, useSchema } from "sanity"; +import { useColorSchemeValue } from "sanity"; import { usePresentationNavigate, usePresentationParams, } from "sanity/presentation"; import styled from "styled-components"; -import { ListItemProps, PageTreeNode } from "../../../types"; +import { ListItemProps, PageTreeNode, TreeNode } from "../../../types"; import { useNavigator } from "../context"; +import { PreviewElement } from "./Preview"; type PreviewStyleProps = { isPreviewed?: boolean; @@ -218,7 +219,6 @@ const ListItem = ({ const scheme = useColorSchemeValue(); const { preview } = usePresentationParams(); const navigate = usePresentationNavigate(); - const previewed = preview === path; const handleClick = (e: React.MouseEvent) => { @@ -293,7 +293,7 @@ const ListItem = ({ justify="center" style={{ position: "relative", width: 33, height: 33, flexShrink: 0 }} > - +
- {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 ( + {isString(title) + ); + }, + [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 ( + {isString(title) + ); + } + + // 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'}