diff --git a/pages/docs/scroll.mdx b/pages/docs/scroll.mdx index 61bcb1f6..5eb59223 100644 --- a/pages/docs/scroll.mdx +++ b/pages/docs/scroll.mdx @@ -28,9 +28,7 @@ Provide a [IIIF Presentation API](https://iiif.io/api/presentation/3.0/) Manifes - Renders the Canvases of a IIIF Manifest as HTML5 `article` elements - Outputs `Annotation` textual content along with `OpenSeadragon` for images -- Automatically creates a client-side search index for full-text search of Annotations - Supports vertical scrolling and textual discovery with a fixed **Search...** input -- Supports `commenting`, `transcribing`, and `translating` motivations ## Installation @@ -104,11 +102,12 @@ const MyCustomScroll = () => { `Scroll` can configured through an `options` prop, which will serve as a object for common options. -| Prop | Type | Required | Default | -| ---------------- | -------- | -------- | ------- | -| `iiifContent` | `string` | Yes | | -| `options` | `object` | No | | -| `options.offset` | `number` | No | `0` | +| Prop | Type | Required | Default | +| ---------------- | --------------------- | -------- | ------- | +| `iiifContent` | `string` | Yes | | +| `options` | `object` | No | | +| `options.offset` | `number` | No | `0` | +| `options.figure` | [See Figure](#figure) | No | | ### Offset @@ -120,3 +119,26 @@ The `options.offset` refers to the number of pixels to offset the fixed **Search options={{ offset: 90 }} /> ``` + +### Figure + +The Scroll component renders a `figure` element for each Canvas. The `options.figure` object allows for customization of the `figure` width, aspect ratio, and display. This can be useful for customizing the Scroll component to fit within a specific layout alongside other image viewers. + +| Prop | Type | Required | Default | +| ---------------------------- | ---------------------------------------------------------------------- | -------- | ------------------------------------------------------------------ | +| `options.figure.aspectRatio` | `number` | No | `1.618` [Golden Ratio](https://en.wikipedia.org/wiki/Golden_ratio) | +| `options.figure.display` | `thumbnail`, `image-viewer` | No | `image-viewer` | +| `options.figure.width` | `string` [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/width) | No | `38.2%` | + +```jsx + +``` diff --git a/src/components/Scroll/Figure/Figure.styled.tsx b/src/components/Scroll/Figure/Figure.styled.tsx index 8a3830c7..c49f2fdc 100644 --- a/src/components/Scroll/Figure/Figure.styled.tsx +++ b/src/components/Scroll/Figure/Figure.styled.tsx @@ -1,5 +1,8 @@ +import { AspectRatio } from "@radix-ui/react-aspect-ratio"; import { styled } from "src/styles/stitches.config"; +const StyledFigurePlaceholder = styled(AspectRatio, {}); + const StyledFigure = styled("figure", { figcaption: { display: "flex", @@ -16,4 +19,4 @@ const StyledFigure = styled("figure", { }, }); -export { StyledFigure }; +export { StyledFigure, StyledFigurePlaceholder }; diff --git a/src/components/Scroll/Figure/Figure.tsx b/src/components/Scroll/Figure/Figure.tsx index 3e84fac3..b1d3f5e7 100644 --- a/src/components/Scroll/Figure/Figure.tsx +++ b/src/components/Scroll/Figure/Figure.tsx @@ -1,10 +1,14 @@ import React, { useContext } from "react"; +import { ScrollContext, initialState } from "src/context/scroll-context"; +import { + StyledFigure, + StyledFigurePlaceholder, +} from "src/components/Scroll/Figure/Figure.styled"; import { CanvasNormalized } from "@iiif/presentation-3"; import FigureCaption from "src/components/Scroll/Figure/Caption"; -import ImageViewer from "src/components/Scroll/Figure/ImageViewer"; -import { ScrollContext } from "src/context/scroll-context"; -import { StyledFigure } from "src/components/Scroll/Figure/Figure.styled"; +import FigureImageViewer from "src/components/Scroll/Figure/ImageViewer"; +import FigureThumbnail from "./Thumbnail"; import { getPaintingResource } from "src/hooks/use-iiif"; interface CanvasProps { @@ -17,7 +21,15 @@ interface CanvasProps { const ScrollCanvasFigure: React.FC = ({ canvas, canvasInfo }) => { const { state } = useContext(ScrollContext); - const { vault } = state; + const { vault, options } = state; + const { figure } = options; + + const display = figure.display + ? figure.display + : initialState.options.figure.display; + const aspectRatio = figure.aspectRatio + ? figure.aspectRatio + : initialState.options.figure.aspectRatio; const painting = getPaintingResource(vault, canvas.id); @@ -25,9 +37,26 @@ const ScrollCanvasFigure: React.FC = ({ canvas, canvasInfo }) => { return ( - {painting?.map((body) => ( - - ))} + {painting?.map((body) => { + return ( + + {display === "thumbnail" && ( + + )} + {display === "image-viewer" && ( + + )} + + ); + })} ); diff --git a/src/components/Scroll/Figure/ImageViewer.styled.tsx b/src/components/Scroll/Figure/ImageViewer.styled.tsx index bf1697f2..78534bf4 100644 --- a/src/components/Scroll/Figure/ImageViewer.styled.tsx +++ b/src/components/Scroll/Figure/ImageViewer.styled.tsx @@ -2,7 +2,7 @@ import { styled } from "src/styles/stitches.config"; const StyledImageViewer = styled("div", { width: "100%", - height: "360px", + height: "100%", background: "#6662", backgroundSize: "contain", color: "white", diff --git a/src/components/Scroll/Figure/Thumbnail.test.tsx b/src/components/Scroll/Figure/Thumbnail.test.tsx new file mode 100644 index 00000000..8bca6223 --- /dev/null +++ b/src/components/Scroll/Figure/Thumbnail.test.tsx @@ -0,0 +1,55 @@ +import { + IIIFExternalWebResource, + InternationalString, +} from "@iiif/presentation-3"; +import { render, screen } from "@testing-library/react"; + +import FigureThumbnail from "./Thumbnail"; +import React from "react"; + +const body: IIIFExternalWebResource = { + id: "https://example.com/iiif/2/image/example.jpg", + type: "Image", + format: "image/jpeg", + width: 500, + height: 500, + service: [ + { + id: "https://example.com/iiif/2/image", + type: "ImageService2", + profile: "level2", + }, + ], +}; + +const label: InternationalString = { + en: ["Test label"], +}; + +describe("", () => { + it("renders the thumbnail with label as alt", () => { + render(); + + const thumbnail = screen.getByTestId("scroll-figure-thumbnail"); + expect(thumbnail).toBeInTheDocument(); + + const img = thumbnail.querySelector("img"); + expect(img).toBeInTheDocument(); + expect(img).toHaveAttribute( + "src", + "https://example.com/iiif/2/image/full/500,500/0/default.jpg", + ); + expect(img).toHaveAttribute("alt", "Test label"); + }); + + it("renders the thumbnail without a service", () => { + render(); + + const img = screen.getByRole("img"); + expect(img).toBeInTheDocument(); + expect(img).toHaveAttribute( + "src", + "https://example.com/iiif/2/image/example.jpg", + ); + }); +}); diff --git a/src/components/Scroll/Figure/Thumbnail.tsx b/src/components/Scroll/Figure/Thumbnail.tsx new file mode 100644 index 00000000..3f2a4c3d --- /dev/null +++ b/src/components/Scroll/Figure/Thumbnail.tsx @@ -0,0 +1,32 @@ +import { + IIIFExternalWebResource, + InternationalString, +} from "@iiif/presentation-3"; + +import { Thumbnail as CloverPrimitivesThumbnail } from "src/components/Primitives"; +import React from "react"; +import { StyledImageViewer } from "src/components/Scroll/Figure/ImageViewer.styled"; + +interface FigureThumbnailProps { + body: IIIFExternalWebResource; + label?: InternationalString | null; +} + +const FigureThumbnail: React.FC = ({ body, label }) => { + return ( + + + + ); +}; + +export default React.memo(FigureThumbnail); diff --git a/src/components/Scroll/Items/Item.tsx b/src/components/Scroll/Items/Item.tsx index d46d20cd..64f361ae 100644 --- a/src/components/Scroll/Items/Item.tsx +++ b/src/components/Scroll/Items/Item.tsx @@ -9,9 +9,9 @@ import { StyledItemFigure, StyledItemTextualBodies, } from "src/components/Scroll/Items/Items.styled"; +import { ScrollContext, initialState } from "src/context/scroll-context"; import React from "react"; -import { ScrollContext } from "src/context/scroll-context"; import ScrollFigure from "src/components/Scroll/Figure/Figure"; import ScrollItemBody from "src/components/Scroll/Annotation/Body"; @@ -33,7 +33,8 @@ const ScrollItem: React.FC = ({ itemNumber, }) => { const { state } = React.useContext(ScrollContext); - const { annotations, vault } = state; + const { annotations, vault, options } = state; + const { figure } = options; const canvas = vault?.get(item) as CanvasNormalized; @@ -67,7 +68,14 @@ const ScrollItem: React.FC = ({ data-page-number={itemNumber} data-last-item={isLastItem} > - + {canvas && } diff --git a/src/components/Scroll/Items/Items.styled.tsx b/src/components/Scroll/Items/Items.styled.tsx index 5a10e327..46ab37b2 100644 --- a/src/components/Scroll/Items/Items.styled.tsx +++ b/src/components/Scroll/Items/Items.styled.tsx @@ -10,12 +10,12 @@ const StyledItem = styled("article", { const StyledItemFigure = styled("div", { transition: "$all", - width: "38.2%", + flexShrink: 0, }); const StyledItemTextualBodies = styled("div", { - width: "61.8%", display: "flex", + flexGrow: 1, flexDirection: "column", justifyContent: "flex-start", diff --git a/src/components/Scroll/Items/Items.tsx b/src/components/Scroll/Items/Items.tsx index 61967826..532125ec 100644 --- a/src/components/Scroll/Items/Items.tsx +++ b/src/components/Scroll/Items/Items.tsx @@ -5,25 +5,23 @@ import { StyledScrollItems } from "src/components/Scroll/Items/Items.styled"; const ScrollItems = ({ items }: { items: Reference<"Canvas">[] }) => { return ( - <> - - {items.map((item, index) => { - const itemNumber = index + 1; - const isLastItem = itemNumber === items.length; + + {items.map((item, index) => { + const itemNumber = index + 1; + const isLastItem = itemNumber === items.length; - return ( - - ); - })} - - + return ( + + ); + })} + ); }; diff --git a/src/context/scroll-context.tsx b/src/context/scroll-context.tsx index c5e7948a..11075b2a 100644 --- a/src/context/scroll-context.tsx +++ b/src/context/scroll-context.tsx @@ -8,6 +8,11 @@ interface StateType { manifest?: ManifestNormalized; options: { offset: number; + figure: { + display: "thumbnail" | "image-viewer"; + aspectRatio?: number; + width?: CSSStyleDeclaration["width"]; + }; }; searchActiveMatch?: string; searchMatches?: { @@ -25,11 +30,16 @@ interface ActionType { type: string; } -const initialState: StateType = { +export const initialState: StateType = { annotations: [], manifest: undefined, options: { offset: 0, + figure: { + display: "image-viewer", + aspectRatio: 100 / 61.8, // golden ratio + width: "38.2%", + }, }, searchActiveMatch: undefined, searchMatches: undefined,