├── global.d.ts ├── .npmignore ├── .prettierrc.ts ├── public ├── netlify.toml ├── index.html ├── dom.html └── fixtures │ └── iiif │ └── manifests │ └── tenncities.json ├── .gitignore ├── src ├── screens │ ├── index.tsx │ ├── Progression.tsx │ ├── Projection.tsx │ ├── Presentation.tsx │ ├── Presentation.styled.tsx │ └── Projection.styled.tsx ├── components │ ├── Canvas.tsx │ ├── Annotation.tsx │ ├── Collection.tsx │ ├── Descriptive │ │ ├── MetadataItem.styled.tsx │ │ ├── Label.tsx │ │ └── MetadataItem.tsx │ ├── ByClass.tsx │ ├── Viewer │ │ ├── Note.tsx │ │ ├── Note.styled.tsx │ │ ├── Mirador.tsx │ │ ├── Viewer.styled.tsx │ │ └── Viewer.tsx │ ├── Previews │ │ ├── Figure.tsx │ │ ├── Interstitial.tsx │ │ ├── Figure.styled.tsx │ │ └── Interstitial.styled.tsx │ ├── UI │ │ ├── Modal.styled.tsx │ │ └── Modal.tsx │ └── Manifest.tsx ├── hooks │ ├── getSequence.ts │ ├── getResourceURI.ts │ ├── getLabel.ts │ ├── getNote.ts │ ├── viewer │ │ └── getMiradorConfig.ts │ └── getStepData.ts ├── services │ └── uuid.ts ├── dom.tsx ├── theme.tsx ├── dev.tsx ├── context │ └── yith-context.tsx └── index.tsx ├── deploy.ts ├── jest.config.ts ├── stiches.config.ts ├── README.md ├── .eslintrc.ts ├── dev.ts ├── tsconfig.json └── package.json /global.d.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | public -------------------------------------------------------------------------------- /.prettierrc.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "all", 4 | tabWidth: 2, 5 | }; 6 | -------------------------------------------------------------------------------- /public/netlify.toml: -------------------------------------------------------------------------------- 1 | [[headers]] 2 | for = "/fixtures/*" 3 | [headers.values] 4 | Access-Control-Allow-Origin = "*" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | node_modules 3 | dist 4 | public/script.js 5 | public/script.js.map 6 | 7 | # 8 | .DS_Store 9 | 10 | # 11 | .idea 12 | -------------------------------------------------------------------------------- /src/screens/index.tsx: -------------------------------------------------------------------------------- 1 | import Presentation from "./Presentation"; 2 | import Progression from "./Progression"; 3 | import Projection from "./Projection"; 4 | 5 | export { Presentation, Progression, Projection }; 6 | -------------------------------------------------------------------------------- /src/components/Canvas.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export interface CanvasProps { 4 | id: string; 5 | } 6 | 7 | export const Canvas: React.FC = () => { 8 | return <>; 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/Annotation.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export interface AnnotationProps { 4 | id: string; 5 | } 6 | 7 | export const Annotation: React.FC = () => { 8 | return <>; 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/Collection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export interface CollectionProps { 4 | id: string; 5 | } 6 | 7 | export const Collection: React.FC = () => { 8 | return <>; 9 | }; 10 | -------------------------------------------------------------------------------- /src/screens/Progression.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface Props { 4 | children: React.ReactChildren; 5 | } 6 | 7 | const Progression: React.FC = ({ children }) => { 8 | return <>{children}; 9 | }; 10 | 11 | export default Progression; 12 | -------------------------------------------------------------------------------- /src/hooks/getSequence.ts: -------------------------------------------------------------------------------- 1 | export const getSequence = (sequences: any, instance: string) => { 2 | const filtered = sequences.filter((yith: any) => { 3 | if (yith.id === instance) return yith.items; 4 | }); 5 | if (!filtered[0]) return; 6 | return filtered[0].items; 7 | }; 8 | -------------------------------------------------------------------------------- /src/services/uuid.ts: -------------------------------------------------------------------------------- 1 | export const uuid = () => { 2 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace( 3 | /[xy]/g, 4 | function (c: string) { 5 | var r = (Math.random() * 16) | 0, 6 | v = c == "x" ? r : (r & 0x3) | 0x8; 7 | return v.toString(16); 8 | } 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/screens/Projection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ProjectionWrapper } from "./Projection.styled"; 3 | 4 | interface Props { 5 | children: React.ReactChildren; 6 | } 7 | 8 | const Projection: React.FC = ({ children }) => { 9 | return {children}; 10 | }; 11 | 12 | export default Projection; 13 | -------------------------------------------------------------------------------- /deploy.ts: -------------------------------------------------------------------------------- 1 | const { build } = require("esbuild"); 2 | 3 | build({ 4 | bundle: true, 5 | define: { 6 | "process.env.NODE_ENV": JSON.stringify( 7 | process.env.NODE_ENV || "development" 8 | ), 9 | }, 10 | logLevel: "info", 11 | platform: "node", 12 | entryPoints: ["src/dev.tsx"], 13 | minify: true, 14 | outfile: "public/script.js", 15 | }); 16 | -------------------------------------------------------------------------------- /src/screens/Presentation.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PresentationWrapper } from "./Presentation.styled"; 3 | 4 | interface Props { 5 | children: React.ReactChildren; 6 | } 7 | 8 | const Presentation: React.FC = ({ children }) => { 9 | return {children}; 10 | }; 11 | 12 | export default Presentation; 13 | -------------------------------------------------------------------------------- /src/screens/Presentation.styled.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@stitches/react"; 2 | 3 | const PresentationWrapper = styled("div", { 4 | display: "flex", 5 | justifyContent: "flex-start", 6 | alignItems: "flex-start", 7 | flexGrow: "1", 8 | 9 | "> button": { 10 | flexShrink: "1", 11 | margin: "0 0 2rem 0", 12 | }, 13 | }); 14 | 15 | export { PresentationWrapper }; 16 | -------------------------------------------------------------------------------- /src/screens/Projection.styled.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@stitches/react"; 2 | 3 | const ProjectionWrapper = styled("div", { 4 | display: "flex", 5 | justifyContent: "flex-start", 6 | alignItems: "flex-start", 7 | flexGrow: "1", 8 | 9 | "> button": { 10 | margin: "0 2rem 2rem 0", 11 | flexShrink: "1", 12 | 13 | "&:last-child": { 14 | marginRight: "0", 15 | }, 16 | }, 17 | }); 18 | 19 | export { ProjectionWrapper }; 20 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@jest/types"; 2 | 3 | // Sync object 4 | const config: Config.InitialOptions = { 5 | moduleDirectories: ["node_modules", "src"], 6 | roots: ["/src"], 7 | testEnvironment: "jsdom", 8 | testMatch: [ 9 | "**/__tests__/**/*.+(ts|tsx|js)", 10 | "**/?(*.)+(spec|test).+(ts|tsx|js)", 11 | ], 12 | transform: { 13 | "^.+\\.(ts|tsx)$": "ts-jest", 14 | }, 15 | }; 16 | module.exports = { config }; 17 | -------------------------------------------------------------------------------- /src/components/Descriptive/MetadataItem.styled.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@stitches/react"; 2 | 3 | const MetadataItemWrapper = styled("span", { 4 | margin: "0", 5 | fontStyle: "italic", 6 | justifyContent: "flex-start", 7 | display: "block", 8 | 9 | dt: { 10 | display: "inline", 11 | fontWeight: "700", 12 | ["&:after"]: { content: ": " }, 13 | }, 14 | 15 | dd: { display: "inline", margin: "0", padding: "0" }, 16 | }); 17 | 18 | export { MetadataItemWrapper }; 19 | -------------------------------------------------------------------------------- /src/hooks/getResourceURI.ts: -------------------------------------------------------------------------------- 1 | export const getResourceURI = (annotation: any, size: number) => { 2 | if (!annotation.body[0].service || annotation.body[0]?.service[0]?.profile == "level0") { 3 | const resource = annotation.body[0].id 4 | return { 5 | img: resource, 6 | lqip: undefined 7 | } 8 | } 9 | 10 | const resource = annotation.body[0].service[0].id; 11 | return { 12 | img: `${resource}/full/${size},/0/default.jpg`, 13 | lqip: `${resource}/full/20,/0/default.jpg` 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/ByClass.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Yith from "index"; 3 | 4 | interface DomShape { 5 | children: any; 6 | preview?: string; 7 | text?: string; 8 | type: string; 9 | } 10 | 11 | export const ByClass: React.FC = ({ 12 | children, 13 | preview, 14 | text, 15 | type, 16 | }) => { 17 | return ( 18 | 19 | {children.map((child: any) => { 20 | const { id } = child.dataset; 21 | return ; 22 | })} 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/dom.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { ByClass } from "components/ByClass"; 4 | 5 | /** 6 | * Render to DOM (non-JSX method) 7 | */ 8 | Array.prototype.forEach.call( 9 | document.getElementsByClassName("yith-iiif"), 10 | function (element) { 11 | const children = Array.from(element.children); 12 | const preview = element.dataset.preview; 13 | const text = element.dataset.text; 14 | const type = element.dataset.type; 15 | ReactDOM.render( 16 | , 17 | element 18 | ); 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /stiches.config.ts: -------------------------------------------------------------------------------- 1 | import { createStitches } from "@stitches/react"; 2 | import type * as Stitches from "@stitches/react"; 3 | 4 | export type { VariantProps } from "@stitches/react"; 5 | 6 | export const { 7 | styled, 8 | css, 9 | globalCss, 10 | keyframes, 11 | getCssText, 12 | theme, 13 | createTheme, 14 | config, 15 | } = createStitches({ 16 | theme: {}, 17 | media: { 18 | xs: "(min-width: 520px)", 19 | sm: "(min-width: 900px)", 20 | md: "(min-width: 1200px)", 21 | lg: "(min-width: 1800px)", 22 | }, 23 | }); 24 | 25 | export const globalStyles = globalCss({ 26 | html: {}, 27 | }); 28 | 29 | export type CSS = Stitches.CSS; 30 | -------------------------------------------------------------------------------- /src/components/Descriptive/Label.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { InternationalString } from "@hyperion-framework/types"; 4 | import { getLabel } from "hooks/getLabel"; 5 | import { styled } from "@stitches/react"; 6 | 7 | interface LabelProps { 8 | label: InternationalString; 9 | el?: any; 10 | } 11 | 12 | export const Label: React.FC = ({ label, el = "span" }) => { 13 | const value = getLabel(label, "en"); 14 | const LabelElement = styled(el, {}); 15 | 16 | if (!value) return null; 17 | 18 | /* 19 | * Return label value for InternationalString code `en` 20 | */ 21 | return {getLabel(label, "en")}; 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/Descriptive/MetadataItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MetadataItem as MetadataItemPair } from "@hyperion-framework/types"; 3 | import { Label } from "./Label"; 4 | import { MetadataItemWrapper } from "./MetadataItem.styled"; 5 | 6 | export interface MetadataItemProps { 7 | item: MetadataItemPair; 8 | } 9 | 10 | export const MetadataItem: React.FC = ({ item }) => { 11 | if (!item) return <>; 12 | 13 | return ( 14 | 15 |
16 |
18 |
19 |
21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/hooks/getLabel.ts: -------------------------------------------------------------------------------- 1 | import { InternationalString } from "@hyperion-framework/types"; 2 | 3 | // Get string from a IIIF pres 3 label by language code 4 | export const getLabel = ( 5 | label: InternationalString, 6 | language: string = "en" 7 | ) => { 8 | /* 9 | * If InternationalString code does not exist on label, then 10 | * return what may be there, ex: label.none[0] OR label.fr[0] 11 | */ 12 | if (!label) return; 13 | 14 | if (!label[language]) { 15 | const codes: Array = Object.getOwnPropertyNames(label); 16 | if (codes.length > 0) return label[codes[0]]; 17 | } 18 | 19 | /* 20 | * Return label value for InternationalString code `en` 21 | */ 22 | return label[language]; 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/Viewer/Note.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MetadataItem } from "components/Descriptive/MetadataItem"; 3 | import { ViewerNote } from "./Note.styled"; 4 | import { Label } from "components/Descriptive/Label"; 5 | 6 | interface NoteProps { 7 | data: any; 8 | } 9 | 10 | export const Note: React.FC = ({ data }) => { 11 | return ( 12 | 13 |
14 |
17 | 18 | {data.annotation && ( 19 |
{data.annotation}
20 | )} 21 | 22 |
23 | 24 |
25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/theme.tsx: -------------------------------------------------------------------------------- 1 | import { createTheme } from "@stitches/react"; 2 | 3 | export const theme = createTheme("light", { 4 | color: { 5 | /* 6 | * Black and dark grays in a light theme. 7 | * Must contrast to 4.5 or greater with `secondary`. 8 | */ 9 | primary: "#342F2E", 10 | primaryMuted: "#716C6B", 11 | primaryAlt: "#000000", 12 | 13 | /* 14 | * Key brand color(s). 15 | * Must contrast to 4.5 or greater with `secondary`. 16 | */ 17 | accent: "#4E2A84", 18 | accentMuted: "#B6ACD1", 19 | accentAlt: "#401F68", 20 | 21 | /* 22 | * White and light grays in a light theme. 23 | * Must contrast to 4.5 or greater with `primary` and `accent`. 24 | */ 25 | secondary: "#FFFFFF", 26 | secondaryMuted: "#F0F0F0", 27 | secondaryAlt: "#CCCCCC", 28 | }, 29 | font: { 30 | sans: "'Akkurat Pro Regular', Arial, sans-serif", 31 | display: "Campton, 'Akkurat Pro Regular', Arial, sans-serif", 32 | }, 33 | transition: { 34 | all: "all 200ms ease-in-out", 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /src/hooks/getNote.ts: -------------------------------------------------------------------------------- 1 | import { useYithState } from "context/yith-context"; 2 | import { 3 | CanvasNormalized, 4 | ManifestNormalized, 5 | } from "@hyperion-framework/types"; 6 | 7 | export const getManifestNote = (id: string) => { 8 | const state: any = useYithState(); 9 | const { vault } = state; 10 | 11 | const manifest: ManifestNormalized = vault.fromRef({ 12 | id, 13 | type: "Manifest", 14 | }); 15 | 16 | return { 17 | id: manifest.id, 18 | label: manifest.label, 19 | requiredStatement: manifest.requiredStatement, 20 | items: manifest.items, 21 | }; 22 | }; 23 | 24 | export const getCanvasNote = (id: string) => { 25 | const state: any = useYithState(); 26 | const { vault } = state; 27 | 28 | const canvas: CanvasNormalized = vault.fromRef({ 29 | id, 30 | type: "Canvas", 31 | }); 32 | 33 | return { 34 | id: canvas.id, 35 | label: canvas.label, 36 | }; 37 | }; 38 | 39 | export const getAnnotationNote = (annotation: any) => { 40 | if (!annotation.body.value) return null; 41 | 42 | return annotation.body.value; 43 | }; 44 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Yith 7 | 8 | 20 | 21 |

Inject

22 |
23 |

DOM

24 |
30 |
34 |
38 |
39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://user-images.githubusercontent.com/7376450/142130947-905bab5b-308a-4b05-a2c2-e2e8b3ca095c.png) 2 | 3 | # Yith IIIF 4 | 5 | A frontend tool that leverages IIIF manifests and interweaves them into flexible layouts. 6 | 7 | [**View Demo**](https://yith.dev/) 8 | 9 | ## Types 10 | 11 | ### Under Development 12 | 13 | - **Presentation** - Showcase individual manifests or compare multiple in separate deep-zoom windows 14 | - **Projection** - Provide an immersive guided tour driven by region targeted annotations 15 | 16 | ### Proposed 17 | 18 | - **Progression** - Tell a narrative story walking end users through multiple manifests 19 | 20 | ## Usage 21 | 22 | ```shell 23 | 24 | 25 | 26 | ``` 27 | 28 | ## Build 29 | 30 | - [Hyperion Framework](https://github.com/digirati-labs/hyperion) by [@stephenwf](https://github.com/stephenwf) of [Digirati](https://digirati.com/) 31 | - [Mirador](https://github.com/ProjectMirador/mirador) 32 | - [Radix Primitives](https://www.radix-ui.com/docs/primitives) 33 | - [Stitches](https://stitches.dev/) 34 | -------------------------------------------------------------------------------- /src/hooks/viewer/getMiradorConfig.ts: -------------------------------------------------------------------------------- 1 | export const getMiradorConfig = (type: string) => { 2 | let config = {}; 3 | 4 | switch (type) { 5 | case "projection": 6 | config = minimalConfig; 7 | break; 8 | default: 9 | config = defaultConfig; 10 | } 11 | 12 | return config; 13 | }; 14 | 15 | const defaultConfig = { 16 | workspace: { 17 | showZoomControls: true, 18 | }, 19 | workspaceControlPanel: { 20 | enabled: false, 21 | }, 22 | window: { 23 | allowClose: false, 24 | allowMaximize: false, 25 | allowTopMenuButton: true, 26 | allowWindowSideBar: true, 27 | defaultSidebarPanelWidth: 300, 28 | forceDrawAnnotations: false, 29 | hideWindowTitle: false, 30 | sideBarOpen: false, 31 | }, 32 | }; 33 | 34 | const minimalConfig = { 35 | workspace: { 36 | showZoomControls: false, 37 | }, 38 | workspaceControlPanel: { 39 | enabled: false, 40 | }, 41 | window: { 42 | allowClose: false, 43 | allowMaximize: false, 44 | allowTopMenuButton: false, 45 | allowWindowSideBar: false, 46 | defaultSidebarPanelWidth: 0, 47 | forceDrawAnnotations: false, 48 | hideWindowTitle: true, 49 | sideBarOpen: false, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/Viewer/Note.styled.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@stitches/react"; 2 | 3 | const ViewerNote = styled("div", { 4 | display: "flex", 5 | flexDirection: "column", 6 | position: "absolute", 7 | backgroundColor: "white", 8 | zIndex: "2", 9 | left: "2rem", 10 | bottom: "2rem", 11 | maxWidth: "30vw", 12 | borderRadius: "3px", 13 | boxShadow: "8px 8px 13px #00000033", 14 | 15 | "> div": { 16 | padding: "1rem", 17 | }, 18 | 19 | ".yith-note-header": { 20 | strong: { 21 | display: "flex", 22 | fontSize: "1.15em", 23 | fontWeight: "900", 24 | }, 25 | 26 | span: { 27 | display: "flex", 28 | fontSize: "0.8333rem", 29 | marginTop: "0.5rem", 30 | color: "#555555", 31 | }, 32 | }, 33 | 34 | ".yith-note-body": { 35 | paddingTop: "0", 36 | fontSize: "0.8333rem", 37 | fontWeight: "700", 38 | }, 39 | 40 | ".yith-note-footer": { 41 | margin: "0", 42 | padding: "1rem", 43 | justifyContent: "flex-start", 44 | fontSize: "0.7222rem", 45 | backgroundColor: "#e0e0e0", 46 | color: "#555555", 47 | borderBottomRightRadius: "3px", 48 | borderBottomLeftRadius: "3px", 49 | }, 50 | }); 51 | 52 | export { ViewerNote }; 53 | -------------------------------------------------------------------------------- /public/dom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Yith 7 | 8 | 9 | 13 | 14 | 15 | 27 | 28 |
29 |
33 |
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /.eslintrc.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", // Specifies the ESLint parser 3 | extends: [ 4 | "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react 5 | "plugin:@typescript-eslint/recommended", // Uses the recommended rules from @typescript-eslint/eslint-plugin 6 | "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 7 | "plugin:prettier/recommended", // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 8 | ], 9 | parserOptions: { 10 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 11 | sourceType: "module", // Allows for the use of imports 12 | ecmaFeatures: { 13 | jsx: true, // Allows for the parsing of JSX 14 | }, 15 | }, 16 | rules: { 17 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 18 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 19 | }, 20 | settings: { 21 | react: { 22 | version: "detect", // Tells eslint-plugin-react to automatically detect the version of React to use 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/Previews/Figure.tsx: -------------------------------------------------------------------------------- 1 | import { Annotation, InternationalString } from "@hyperion-framework/types"; 2 | import { Label } from "components/Descriptive/Label"; 3 | import { getResourceURI } from "hooks/getResourceURI"; 4 | import React from "react"; 5 | import { FigureStyled, LQIP } from "./Figure.styled"; 6 | import { MetadataItem } from "components/Descriptive/MetadataItem"; 7 | import { MetadataItem as MetadataItemPair } from "@hyperion-framework/types"; 8 | 9 | export interface FigureProps { 10 | label: InternationalString; 11 | painting: Annotation; 12 | size: number; 13 | requiredStatement: MetadataItemPair; 14 | } 15 | 16 | export const Figure: React.FC = ({ 17 | label, 18 | painting, 19 | size, 20 | requiredStatement, 21 | }) => { 22 | const { img, lqip } = getResourceURI(painting, size); 23 | return ( 24 | 25 |
26 |
27 | Expand in Viewer 28 | 29 | {lqip && ( 30 | 31 | )} 32 |
33 |
34 |
35 |
37 |
38 | 39 |
40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/dev.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Yith from "./index"; 4 | 5 | const App: React.FC = () => { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | ReactDOM.render(, document.getElementById("root")); 29 | -------------------------------------------------------------------------------- /dev.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Live Reloading MVP: 3 | * https://github.com/zaydek/esbuild-hot-reload/blob/master/serve.js 4 | */ 5 | 6 | const { build } = require("esbuild"); 7 | const chokidar = require("chokidar"); 8 | const liveServer = require("live-server"); 9 | 10 | (async () => { 11 | const builder = await build({ 12 | bundle: true, 13 | // Defines env variables for bundled JavaScript; here `process.env.NODE_ENV` 14 | // is propagated with a fallback. 15 | define: { 16 | "process.env.NODE_ENV": JSON.stringify( 17 | process.env.NODE_ENV || "development" 18 | ), 19 | }, 20 | platform: "node", 21 | entryPoints: ["src/dev.tsx"], 22 | // Uses incremental compilation (see `chokidar.on`). 23 | incremental: true, 24 | // Removes whitespace, etc. depending on `NODE_ENV=...`. 25 | minify: process.env.NODE_ENV === "production", 26 | outfile: "public/script.js", 27 | sourcemap: true, 28 | }); 29 | // `chokidar` watcher source changes. 30 | chokidar 31 | // Watches TypeScript and React TypeScript. 32 | .watch("src/**/*.{ts,tsx}", { 33 | interval: 0, // No delay 34 | }) 35 | // Rebuilds esbuild (incrementally -- see `build.incremental`). 36 | .on("all", () => { 37 | builder.rebuild(); 38 | }); 39 | // `liveServer` local server for hot reload. 40 | liveServer.start({ 41 | // Opens the local server on start. 42 | open: true, 43 | // Uses `PORT=...` or 8080 as a fallback. 44 | port: +process.env.PORT || 8080, 45 | // Uses `public` as the local server folder. 46 | root: "public", 47 | }); 48 | })(); 49 | -------------------------------------------------------------------------------- /src/components/Previews/Interstitial.tsx: -------------------------------------------------------------------------------- 1 | import { Annotation, InternationalString } from "@hyperion-framework/types"; 2 | import { Label } from "components/Descriptive/Label"; 3 | import { getResourceURI } from "hooks/getResourceURI"; 4 | import React from "react"; 5 | import { InterstitialStyled, LQIP } from "./Interstitial.styled"; 6 | import { MetadataItem } from "components/Descriptive/MetadataItem"; 7 | import { MetadataItem as MetadataItemPair } from "@hyperion-framework/types"; 8 | 9 | export interface InterstitialProps { 10 | label: InternationalString; 11 | requiredStatement: MetadataItemPair; 12 | painting: Annotation; 13 | size: number; 14 | text: string; 15 | } 16 | 17 | export const Interstitial: React.FC = ({ 18 | label, 19 | requiredStatement, 20 | painting, 21 | size, 22 | text, 23 | }) => { 24 | const { img, lqip } = getResourceURI(painting, size); 25 | 26 | return ( 27 | 28 |
29 |
30 | {text} 31 |
32 |
37 |
38 |
39 | 40 | {/* Expand in Viewer */} 41 | {lqip && ( 42 | 43 | )} 44 |
45 |
46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/UI/Modal.styled.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DialogTrigger, 3 | DialogContent, 4 | DialogOverlay, 5 | } from "@radix-ui/react-dialog"; 6 | import { styled } from "@stitches/react"; 7 | import { FigureStyled, LQIP } from "components/Previews/Figure.styled"; 8 | import { InterstitialStyled } from "components/Previews/Interstitial.styled"; 9 | 10 | const TriggerStyled = styled(DialogTrigger, { 11 | cursor: "pointer", 12 | border: "none", 13 | justifyContent: "flex-start", 14 | width: "100%", 15 | backgroundColor: "transparent", 16 | 17 | dl: { 18 | opacity: "0.7", 19 | fontSize: "0.7222rem", 20 | disply: "flex", 21 | }, 22 | 23 | "&:hover, &:focus": { 24 | [`${FigureStyled}`]: { 25 | img: { 26 | opacity: "0.75", 27 | }, 28 | 29 | span: { 30 | opacity: "1", 31 | }, 32 | 33 | [`${LQIP}`]: { 34 | opacity: "0.25", 35 | }, 36 | }, 37 | [`${InterstitialStyled}`]: { 38 | img: { 39 | opacity: "0.75", 40 | }, 41 | 42 | a: { 43 | opacity: "1", 44 | }, 45 | 46 | [`${LQIP}`]: { 47 | opacity: "0.25", 48 | }, 49 | }, 50 | }, 51 | }); 52 | 53 | const ContentStyled = styled(DialogContent, { 54 | position: "fixed", 55 | display: "flex", 56 | width: "100%", 57 | height: "100%", 58 | margin: "0", 59 | backgroundColor: "#DCDCDC", 60 | }); 61 | 62 | const OverlayStyled = styled(DialogOverlay, { 63 | position: "fixed", 64 | width: "100%", 65 | height: "100%", 66 | backgroundColor: "#000000DD", 67 | }); 68 | 69 | export { ContentStyled, OverlayStyled, TriggerStyled }; 70 | -------------------------------------------------------------------------------- /src/components/Viewer/Mirador.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import React from "react"; 3 | // @ts-ignore 4 | import mirador from "mirador"; 5 | 6 | interface MiradorProps { 7 | config: any; 8 | plugins: Array; 9 | step: any; 10 | region: Array; 11 | } 12 | 13 | export const Mirador: React.FC = ({ 14 | config, 15 | plugins, 16 | step, 17 | region, 18 | }) => { 19 | const [currentWindows, setCurrentWindows] = React.useState([{}]); 20 | const [miradorInstance, setMiradorInstance] = React.useState(); 21 | 22 | React.useEffect(() => { 23 | switch (step.type) { 24 | case "Annotation": 25 | if (currentWindows[0].manifestId !== config.windows[0].manifestId) { 26 | setCurrentWindows(config.windows); 27 | } 28 | panZoom(miradorInstance, region, 0); 29 | break; 30 | default: 31 | setCurrentWindows(config.windows); 32 | } 33 | }, [step]); 34 | 35 | React.useEffect(() => { 36 | setMiradorInstance(mirador.viewer(config, plugins)); 37 | }, [currentWindows]); 38 | 39 | return
; 40 | }; 41 | 42 | function panZoom( 43 | miradorInstance: any, 44 | xywh: Array, 45 | windowIndex: number 46 | ) { 47 | const windowId = Object.keys(miradorInstance.store.getState().windows)[ 48 | windowIndex 49 | ]; 50 | 51 | const boxToZoom = { 52 | x: parseInt(xywh[0]), 53 | y: parseInt(xywh[1]), 54 | width: parseInt(xywh[2]), 55 | height: parseInt(xywh[3]), 56 | }; 57 | 58 | const zoomCenter = { 59 | x: boxToZoom.x + boxToZoom.width / 2, 60 | y: boxToZoom.y + boxToZoom.height / 2, 61 | }; 62 | 63 | var action = mirador.actions.updateViewport(windowId, { 64 | x: zoomCenter.x, 65 | y: zoomCenter.y, 66 | zoom: 0.61888 / boxToZoom.width, 67 | }); 68 | 69 | miradorInstance.store.dispatch(action); 70 | } 71 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, // Allow JavaScript files to be compiled 4 | "allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export 5 | "baseUrl": "src", 6 | "declaration": true, // Generate corresponding .d.ts file 7 | "esModuleInterop": true, // Disables namespace imports (import * as fs from "fs") and enables CJS/AMD/UMD style imports (import fs from "fs") 8 | "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file. 9 | "incremental": true, // Enable incremental compilation by reading/writing information from prior compilations to a file on disk 10 | "isolatedModules": true, // Unconditionally emit imports for unresolved files 11 | "jsx": "react-jsx", // Support JSX in .tsx files 12 | "lib": ["dom", "dom.iterable", "es6"], // List of library files to be included in the compilation 13 | "module": "es6", // Specify module code generation 14 | "moduleResolution": "node", // Resolve modules using Node.js style 15 | "noEmit": false, // Do not emit output (meaning do not compile code, only perform type checking) 16 | "noFallthroughCasesInSwitch": true, // Report errors for fallthrough cases in switch statement 17 | "noUnusedLocals": true, // Report errors on unused locals 18 | "noUnusedParameters": true, // Report errors on unused parameters 19 | "resolveJsonModule": true, // Include modules imported with .json extension 20 | "outDir": "./dist", 21 | "skipLibCheck": true, // Skip type checking of all declaration files 22 | "sourceMap": true, // Generate corrresponding .map file 23 | "strict": true, // Enable all strict type checking options 24 | "target": "es6" // Specify ECMAScript target version 25 | }, 26 | "include": ["src/**/*.ts", "src/**/*.tsx"], 27 | "exclude": [ 28 | "node_modules", 29 | "**/*.test.ts", 30 | "**/*.test.tsx", 31 | "src/dom.tsx", 32 | "src/dev.tsx" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /src/hooks/getStepData.ts: -------------------------------------------------------------------------------- 1 | import { AnnotationNormalized } from "@hyperion-framework/types"; 2 | import { useYithState } from "context/yith-context"; 3 | import { 4 | getAnnotationNote, 5 | getCanvasNote, 6 | getManifestNote, 7 | } from "hooks/getNote"; 8 | 9 | export const getStepData = (step: any) => { 10 | const state: any = useYithState(); 11 | const { vault } = state; 12 | 13 | let data: any = { 14 | currentWindows: [], 15 | region: [], 16 | note: {}, 17 | }; 18 | 19 | switch (step.type) { 20 | case "Manifest": 21 | data.currentWindows = [{ manifestId: step.id }]; 22 | data.note.manifest = getManifestNote(step.id); 23 | data.note.canvas = getCanvasNote(data.note.manifest.items[0].id); 24 | break; 25 | case "Canvas": 26 | data.currentWindows = [ 27 | { manifestId: step.manifestId, canvasId: step.id }, 28 | ]; 29 | data.note.manifest = getManifestNote(step.manifestId); 30 | data.note.canvas = getCanvasNote(step.id); 31 | break; 32 | case "Annotation": 33 | const manifest: any = vault.fromRef({ 34 | id: step.manifestId, 35 | type: "Manifest", 36 | }); 37 | const annotation: any = vault 38 | .allFromRef(manifest.annotations[0].items) 39 | .filter((item: AnnotationNormalized) => { 40 | if (item.id === step.id) return item; 41 | })[0]; 42 | 43 | const target: string[] = annotation.target.split("#xywh="); 44 | 45 | data.region = target[1].split(","); 46 | 47 | data.currentWindows = [ 48 | { manifestId: step.manifestId, canvasId: target[0] }, 49 | ]; 50 | 51 | data.note.manifest = getManifestNote(step.manifestId); 52 | data.note.canvas = getCanvasNote(target[0]); 53 | data.note.annotation = getAnnotationNote(annotation); 54 | 55 | break; 56 | default: 57 | console.error( 58 | `Step ${step.type} is unknown. Must be of type Manifest, Canvas, or Annotation.` 59 | ); 60 | } 61 | return data; 62 | }; 63 | -------------------------------------------------------------------------------- /src/components/Previews/Figure.styled.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@stitches/react"; 2 | 3 | const FigureStyled = styled("figure", { 4 | margin: "0", 5 | 6 | "> div": { 7 | position: "relative", 8 | backgroundColor: "white", 9 | height: "0", 10 | width: "100%", 11 | overflow: "hidden", 12 | paddingTop: "77%", 13 | background: "white", 14 | 15 | "> div": { 16 | backgroundColor: "#0F111A", 17 | position: "absolute", 18 | top: "0", 19 | left: "0", 20 | width: "100%", 21 | height: "100%", 22 | zIndex: "0", 23 | display: "flex", 24 | justifyContent: "center", 25 | alignItems: "center", 26 | 27 | span: { 28 | position: "absolute", 29 | display: "inline", 30 | backgroundColor: "white", 31 | color: "#000000", 32 | padding: "0.5rem 1rem", 33 | alignSelf: "flex-end", 34 | opacity: "0", 35 | zIndex: "2", 36 | fontSize: "0.722rem", 37 | textTransform: "uppercase", 38 | fontWeight: "700", 39 | bottom: "1rem", 40 | boxShadow: "2px 2px 5px #00000011", 41 | transition: "all 200ms ease-in-out", 42 | }, 43 | 44 | img: { 45 | opacity: "1", 46 | width: "calc(100% - 2rem)", 47 | height: "calc(100% - 2rem)", 48 | objectFit: "contain", 49 | zIndex: "1", 50 | position: "relative", 51 | margin: "1rem", 52 | transition: "all 200ms ease-in-out", 53 | }, 54 | }, 55 | }, 56 | 57 | figcaption: { 58 | margin: "1rem 0", 59 | display: "flex-inline", 60 | fontSize: "1rem", 61 | fontWeight: "900", 62 | textAlign: "center", 63 | }, 64 | }); 65 | 66 | const LQIP = styled("div", { 67 | position: "absolute", 68 | display: "flex", 69 | left: "0", 70 | top: "0", 71 | width: "100%", 72 | height: "100%", 73 | zIndex: "0", 74 | opacity: "0.618", 75 | backgroundSize: "cover", 76 | filter: "blur(50px) contrast(1.15)", 77 | transform: "scale3d(1.15,1.15,1.15)", 78 | }); 79 | 80 | export { FigureStyled, LQIP }; 81 | -------------------------------------------------------------------------------- /src/components/UI/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ContentStyled, OverlayStyled, TriggerStyled } from "./Modal.styled"; 3 | import { 4 | Annotation, 5 | InternationalString, 6 | ManifestNormalized, 7 | } from "@hyperion-framework/types"; 8 | import { Figure } from "components/Previews/Figure"; 9 | import { Viewer } from "components/Viewer/Viewer"; 10 | import { Interstitial } from "components/Previews/Interstitial"; 11 | import { MetadataItem as MetadataItemPair } from "@hyperion-framework/types"; 12 | import { Dialog } from "@radix-ui/react-dialog"; 13 | 14 | export interface FigureProps { 15 | manifest: ManifestNormalized; 16 | painting: Annotation; 17 | sequence: Array; 18 | type: string; 19 | text: string; 20 | preview?: string; 21 | } 22 | 23 | export const size: number = 240; 24 | 25 | export const Modal: React.FC = ({ 26 | manifest, 27 | painting, 28 | sequence, 29 | type, 30 | text, 31 | preview = "figure", 32 | }) => { 33 | /* 34 | * todo: build a hook that gets the image from the image server 35 | */ 36 | 37 | return ( 38 | 39 | 40 | {preview === "figure" && ( 41 | <> 42 |
50 | 51 | )} 52 | {preview === "interstitial" && ( 53 | 62 | )} 63 | 64 | 65 | 66 | 67 | 68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/Viewer/Viewer.styled.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@stitches/react"; 2 | 3 | const ViewerControls = styled("div", { 4 | display: "flex", 5 | position: "relative", 6 | zIndex: "1", 7 | backgroundColor: "#fff", 8 | width: "100%", 9 | 10 | "> button": { 11 | display: "flex", 12 | flexGrow: "1", 13 | color: "#1f1f1f", 14 | padding: "0.75rem 1rem", 15 | alignSelf: "flex-end", 16 | zIndex: "2", 17 | fontSize: "0.8333rem", 18 | fontWeight: "700", 19 | border: "none", 20 | transition: "all 200ms ease-in-out", 21 | cursor: "pointer", 22 | justifyContent: "center", 23 | backgroundColor: "transparent", 24 | 25 | "&:hover": { 26 | color: "#000", 27 | }, 28 | 29 | "&[disabled='']": { 30 | color: "#666", 31 | fontWeight: "400", 32 | }, 33 | }, 34 | }); 35 | 36 | const ViewerWrapper = styled("div", { 37 | position: "relative", 38 | display: "flex", 39 | flexDirection: "column", 40 | width: "100%", 41 | height: "100%", 42 | margin: "0", 43 | backgroundColor: "#DCDCDC", 44 | 45 | "&[data-screen='projection']": { 46 | [`& ${ViewerControls}`]: { 47 | position: "absolute", 48 | backgroundColor: "#000e", 49 | 50 | "> button": { 51 | color: "#e0e0e0", 52 | 53 | "&:hover": { 54 | color: "#fff", 55 | }, 56 | 57 | "&[disabled='']": { 58 | color: "#666", 59 | }, 60 | }, 61 | }, 62 | 63 | ".mirador-viewer": { 64 | background: "none", 65 | backgroundColor: "black", 66 | 67 | ".mosaic-root": { 68 | top: "0", 69 | left: "0", 70 | right: "0", 71 | bottom: "0", 72 | 73 | ".mosaic-window": { 74 | backgroundColor: "transparent", 75 | }, 76 | 77 | ".mirador-window": { 78 | backgroundColor: "black", 79 | 80 | "> div > header": { 81 | display: "none", 82 | }, 83 | }, 84 | 85 | ".mirador-osd-info, .mirador-osd-navigation": { 86 | margin: "0", 87 | display: "none", 88 | }, 89 | }, 90 | }, 91 | }, 92 | 93 | "> [id^='mirador-']": { 94 | position: "relative", 95 | flexGrow: "1", 96 | zIndex: "0", 97 | }, 98 | }); 99 | 100 | export { ViewerControls, ViewerWrapper }; 101 | -------------------------------------------------------------------------------- /src/components/Manifest.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Annotation, 4 | AnnotationPageNormalized, 5 | CanvasNormalized, 6 | ContentResource, 7 | ManifestNormalized, 8 | } from "@hyperion-framework/types"; 9 | import { useYithState } from "context/yith-context"; 10 | import { getSequence } from "hooks/getSequence"; 11 | import { Modal } from "./UI/Modal"; 12 | 13 | export interface ManifestProps { 14 | id: string; 15 | instance?: string | undefined; 16 | type?: string; 17 | text?: string; 18 | preview?: string; 19 | children?: React.ReactChild | React.ReactChild[]; 20 | } 21 | 22 | export const Manifest: React.FC = ({ 23 | id, 24 | instance, 25 | type, 26 | text, 27 | preview, 28 | }) => { 29 | const [manifest, setManifest] = React.useState(); 30 | const state: any = useYithState(); 31 | const { vault, sequences } = state; 32 | 33 | const sequence = getSequence(sequences, instance as string); 34 | 35 | React.useEffect(() => { 36 | vault 37 | .loadManifest(id) 38 | .then((data: ManifestNormalized) => { 39 | setManifest(data); 40 | }) 41 | .catch((error: any) => { 42 | console.error(`Manifest failed to load: ${error}`); 43 | }); 44 | }, [id]); 45 | 46 | if (!manifest) return <>Loading...; 47 | 48 | /* 49 | * a hook here maybe should handle the below logic 50 | * retriving the content resource. 51 | */ 52 | 53 | if (manifest) { 54 | const canvas: CanvasNormalized = vault.fromRef(manifest.items[0]); 55 | const annotationPage: AnnotationPageNormalized = vault.fromRef( 56 | canvas.items[0] 57 | ); 58 | const annotations: Annotation[] = vault.allFromRef(annotationPage.items); 59 | 60 | const painting: Annotation[] = annotations.filter((annotation: any) => { 61 | if (annotation.motivation) 62 | if (annotation.motivation[0] === "painting") { 63 | const resource: ContentResource = vault.fromRef(annotation.body[0]); 64 | annotation.body[0] = resource; 65 | return annotation; 66 | } 67 | }); 68 | 69 | // if (sequence[0].id !== id) return null; 70 | 71 | return ( 72 | 80 | ); 81 | } 82 | 83 | return <>The manifest {id} failed to load.; 84 | }; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yith/yith", 3 | "version": "1.1.1", 4 | "description": "A component library that interweaves IIIF manifests and their content into flexible layouts in a usable, responsive, and accessible way.", 5 | "main": "dist/index.cjs.js", 6 | "types": "dist/index.d.ts", 7 | "type": "commonjs", 8 | "scripts": { 9 | "build": "rm -rf dist && node build.ts && tsc --outDir dist", 10 | "deploy": "node deploy.ts", 11 | "dev": "node dev.ts", 12 | "test": "jest --watch", 13 | "clean": "rimraf dist" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/mathewjordan/yith.git" 18 | }, 19 | "keywords": [ 20 | "IIIF", 21 | "mirador" 22 | ], 23 | "author": "Mathew Jordan", 24 | "license": "Apache-2.0", 25 | "bugs": { 26 | "url": "https://github.com/mathewjordan/yith/issues" 27 | }, 28 | "homepage": "https://github.com/mathewjordan/yith#readme", 29 | "devDependencies": { 30 | "@testing-library/react": "^12.1.0", 31 | "@types/jest": "^27.0.2", 32 | "@types/testing-library__react": "^10.2.0", 33 | "@typescript-eslint/eslint-plugin": "^4.31.2", 34 | "@typescript-eslint/parser": "^4.31.2", 35 | "chokidar": "^3.5.2", 36 | "esbuild": "^0.12.28", 37 | "esbuild-css-modules-plugin": "^2.0.9", 38 | "eslint": "^7.32.0", 39 | "eslint-config-prettier": "^8.3.0", 40 | "eslint-plugin-prettier": "^4.0.0", 41 | "eslint-plugin-react": "^7.26.0", 42 | "jest": "^27.2.1", 43 | "live-server": "^1.2.1", 44 | "prettier": "^2.4.1", 45 | "rimraf": "^3.0.2", 46 | "ts-jest": "^27.0.5", 47 | "ts-node": "^10.2.1" 48 | }, 49 | "dependencies": { 50 | "@hyperion-framework/parser": "^1.0.0", 51 | "@hyperion-framework/types": "^1.0.0", 52 | "@hyperion-framework/validator": "^1.0.0", 53 | "@hyperion-framework/vault": "^1.0.2", 54 | "@radix-ui/react-collapsible": "^0.1.1", 55 | "@radix-ui/react-dialog": "^0.1.0", 56 | "@stitches/react": "^1.2.0", 57 | "@types/jest": "^27.0.2", 58 | "@types/react": "^17.0.21", 59 | "@types/react-dom": "^17.0.9", 60 | "lodash.findkey": "^4.6.0", 61 | "mirador": "^3.2.0", 62 | "prismjs": "^1.25.0", 63 | "react": "^17.0.2", 64 | "react-dom": "^17.0.2", 65 | "react-error-boundary": "^3.1.4", 66 | "typescript": "^4.4.3" 67 | }, 68 | "peerDependencies": { 69 | "mirador": "^3.2.0", 70 | "react": "^17.0.2", 71 | "react-dom": "^17.0.2" 72 | }, 73 | "files": [ 74 | "dist" 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /src/context/yith-context.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Vault } from "@hyperion-framework/vault"; 3 | 4 | interface YithContextStore { 5 | expanded: boolean; 6 | sequences: Array; 7 | vault: Vault; 8 | } 9 | 10 | interface YithAction { 11 | type: string; 12 | expanded: boolean; 13 | sequences: Array; 14 | } 15 | 16 | const defaultState: YithContextStore = { 17 | expanded: false, 18 | sequences: [], 19 | vault: new Vault(), 20 | }; 21 | 22 | const YithStateContext = React.createContext(defaultState); 23 | const YithDispatchContext = React.createContext(defaultState); 24 | 25 | function yithReducer(state: YithContextStore, action: YithAction) { 26 | switch (action.type) { 27 | case "updateModal": { 28 | return { 29 | ...state, 30 | expanded: action.expanded, 31 | }; 32 | } 33 | case "updateSequences": { 34 | return { 35 | ...state, 36 | sequences: action.sequences, 37 | }; 38 | } 39 | default: { 40 | throw new Error(`Unhandled action type: ${action.type}`); 41 | } 42 | } 43 | } 44 | 45 | interface YithProviderProps { 46 | sequence: any; 47 | initialState?: YithContextStore; 48 | children: React.ReactNode; 49 | } 50 | 51 | const YithProvider: React.FC = ({ 52 | sequence, 53 | initialState = defaultState, 54 | children, 55 | }) => { 56 | const [state, dispatch] = React.useReducer< 57 | React.Reducer 58 | >(yithReducer, initialState); 59 | 60 | if ( 61 | state.sequences.filter(function (e) { 62 | return e.id === sequence.id; 63 | }).length === 0 64 | ) { 65 | state.sequences.push(sequence); 66 | } 67 | 68 | return ( 69 | 70 | 73 | {children} 74 | 75 | 76 | ); 77 | }; 78 | 79 | function useYithState() { 80 | const context = React.useContext(YithStateContext); 81 | if (context === undefined) { 82 | throw new Error("useYithState must be used within a YithProvider"); 83 | } 84 | return context; 85 | } 86 | 87 | function useYithDispatch() { 88 | const context = React.useContext(YithDispatchContext); 89 | if (context === undefined) { 90 | throw new Error("useYithDispatch must be used within a YithProvider"); 91 | } 92 | return context; 93 | } 94 | 95 | export { YithProvider, useYithState, useYithDispatch }; 96 | -------------------------------------------------------------------------------- /src/components/Viewer/Viewer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // @ts-ignore 3 | import findkey from "lodash.findkey"; 4 | import { ViewerControls, ViewerWrapper } from "./Viewer.styled"; 5 | import { DialogClose } from "@radix-ui/react-dialog"; 6 | import { Mirador } from "./Mirador"; 7 | import { getMiradorConfig } from "hooks/viewer/getMiradorConfig"; 8 | import { uuid } from "services/uuid"; 9 | import { Note } from "./Note"; 10 | import { getStepData } from "hooks/getStepData"; 11 | 12 | interface ViewerProps { 13 | manifestId: any; 14 | sequence: any; 15 | type: string; 16 | } 17 | 18 | export const Viewer: React.FC = ({ 19 | manifestId, 20 | sequence, 21 | type, 22 | }) => { 23 | /* 24 | * todo: allow presentation to send multiple windows 25 | */ 26 | 27 | const defaultKey: number = parseInt(findkey(sequence, { id: manifestId })); 28 | const [key, setKey] = React.useState(defaultKey); 29 | 30 | console.log(manifestId); 31 | 32 | const data = getStepData(sequence[key]); 33 | 34 | const doStep = (step: number) => { 35 | setKey(step); 36 | }; 37 | 38 | const prefix: string = `mirador-${uuid()}`; 39 | const config = getMiradorConfig(type); 40 | 41 | return ( 42 | 43 | 44 | Close Viewer 45 | {sequence.length > 1 && ( 46 | <> 47 | 53 | 59 | 60 | )} 61 | 62 | {type === "projection" && } 63 | 76 | 77 | ); 78 | }; 79 | interface RenderNavigationProps { 80 | label: any; 81 | stepKey: number; 82 | sequence: any; 83 | doStep: (arg0: number) => void; 84 | } 85 | 86 | const ViewerNavigation: React.FC = ({ 87 | label, 88 | stepKey, 89 | sequence, 90 | doStep, 91 | }) => { 92 | let disabled: boolean = false; 93 | let step: any = undefined; 94 | 95 | switch (label) { 96 | case "Previous": 97 | if (stepKey === 0) disabled = true; 98 | else step = stepKey + -1; 99 | break; 100 | case "Next": 101 | if (stepKey === sequence.length - 1) disabled = true; 102 | else step = stepKey + 1; 103 | break; 104 | default: 105 | console.error(`Label "${label}" is unknown.`); 106 | } 107 | 108 | return ( 109 | 112 | ); 113 | }; 114 | -------------------------------------------------------------------------------- /src/components/Previews/Interstitial.styled.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@stitches/react"; 2 | 3 | const InterstitialStyled = styled("figure", { 4 | margin: "0", 5 | width: "100%", 6 | 7 | "> div": { 8 | position: "relative", 9 | backgroundColor: "white", 10 | height: "100%", 11 | width: "100%", 12 | background: "white", 13 | display: "flex", 14 | 15 | "> div": { 16 | backgroundColor: "#fff", 17 | position: "absolute", 18 | top: "0", 19 | left: "0", 20 | width: "calc(100% - 4px)", 21 | height: "calc(100% - 4px)", 22 | zIndex: "0", 23 | display: "flex", 24 | justifyContent: "flex-end", 25 | alignItems: "flex-end", 26 | boxShadow: "0px 0px 8px #00000011", 27 | 28 | span: { 29 | position: "absolute", 30 | display: "inline", 31 | backgroundColor: "white", 32 | color: "#000000", 33 | padding: "0.5rem 1rem", 34 | alignSelf: "flex-end", 35 | opacity: "0", 36 | zIndex: "2", 37 | fontSize: "0.722rem", 38 | textTransform: "uppercase", 39 | fontWeight: "700", 40 | bottom: "1rem", 41 | boxShadow: "2px 2px 5px #00000011", 42 | transition: "all 200ms ease-in-out", 43 | }, 44 | 45 | img: { 46 | opacity: "1", 47 | width: "calc(100% - 0rem)", 48 | height: "calc(100% - 2rem)", 49 | objectFit: "contain", 50 | margin: "1rem 0", 51 | zIndex: "1", 52 | position: "relative", 53 | transition: "all 200ms ease-in-out", 54 | }, 55 | }, 56 | }, 57 | 58 | figcaption: { 59 | display: "flex", 60 | flexDirection: "column", 61 | justifyContent: "space-between", 62 | fontSize: "1rem", 63 | textAlign: "left", 64 | position: "relative", 65 | color: "#0F111A", 66 | zIndex: "1", 67 | left: "0", 68 | top: "0", 69 | padding: "2rem", 70 | 71 | "> span": { 72 | display: "flex", 73 | flexShrink: "1", 74 | fontWeight: "200", 75 | fontSize: "1.5rem", 76 | }, 77 | 78 | "> a": { 79 | display: "inline-flex", 80 | backgroundColor: "#0F111A", 81 | color: "#fff", 82 | width: "auto", 83 | margin: "0.5rem auto auto 0", 84 | padding: "0.5rem 1rem", 85 | zIndex: "2", 86 | fontSize: "0.722rem", 87 | textTransform: "uppercase", 88 | fontWeight: "700", 89 | transition: "all 200ms ease-in-out", 90 | opacity: "1", 91 | }, 92 | 93 | "> div": { 94 | display: "flex", 95 | flexDirection: "column", 96 | flexGrow: "1", 97 | justifyContent: "flex-end", 98 | fontWeight: "800", 99 | }, 100 | 101 | dl: { 102 | fontSize: "0.7222rem", 103 | marginBottom: "0", 104 | opacity: "0.7", 105 | fontWeight: "300", 106 | }, 107 | }, 108 | }); 109 | 110 | const LQIP = styled("div", { 111 | position: "absolute", 112 | display: "flex", 113 | left: "0", 114 | top: "0", 115 | width: "100%", 116 | height: "100%", 117 | zIndex: "0", 118 | opacity: "0", 119 | backgroundSize: "cover", 120 | filter: "blur(50px) contrast(1.15)", 121 | transform: "scale3d(1.15,1.15,1.15)", 122 | }); 123 | 124 | export { InterstitialStyled, LQIP }; 125 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { styled } from "@stitches/react"; 3 | import { YithProvider } from "./context/yith-context"; 4 | import { Manifest, ManifestProps } from "./components/Manifest"; 5 | import { Annotation, AnnotationProps } from "./components/Annotation"; 6 | import { Collection, CollectionProps } from "./components/Collection"; 7 | import { Canvas, CanvasProps } from "./components/Canvas"; 8 | import { Presentation, Projection } from "./screens"; 9 | import { ErrorBoundary } from "react-error-boundary"; 10 | import { uuid } from "./services/uuid"; 11 | 12 | interface YithProps { 13 | type: string; 14 | preview?: string; 15 | text?: string; 16 | children: React.ReactChild | React.ReactChild[]; 17 | } 18 | 19 | interface YithComposition { 20 | Annotation: React.FC; 21 | Canvas: React.FC; 22 | Collection: React.FC; 23 | Manifest: React.FC; 24 | } 25 | interface FallbackProps { 26 | error: any; 27 | resetErrorBoundary: any; 28 | } 29 | 30 | const ErrorFallback: React.FC = ({ error }) => { 31 | return ( 32 |
33 |

Something went wrong:

34 |
{error.message}
35 |
36 | ); 37 | }; 38 | 39 | export interface Sequence { 40 | id: string; 41 | items: Array; 42 | } 43 | 44 | const Yith: React.FC & YithComposition = ({ 45 | type, 46 | preview, 47 | text, 48 | children, 49 | }) => { 50 | if (!window) return null; 51 | 52 | const instance: string = `yith-${uuid()}`; 53 | 54 | let sequence: Sequence = { 55 | id: instance, 56 | items: [], 57 | }; 58 | 59 | // todo: write this as a hook OR two 60 | const clonedManifests = React.Children.toArray(children).map( 61 | (manifest: any) => { 62 | // add manifest to sequence 63 | 64 | sequence.items.push({ 65 | id: manifest.props.id, 66 | type: "Manifest", 67 | }); 68 | 69 | if (manifest.props.children) 70 | manifest.props.children.forEach((child: any) => { 71 | sequence.items.push({ 72 | id: child.props.id, 73 | type: child.type.name, 74 | manifestId: manifest.props.id, 75 | }); 76 | }); 77 | 78 | // clone and add instance/type 79 | const clonedManifest = React.cloneElement(manifest, { 80 | instance, 81 | preview, 82 | text, 83 | type, 84 | }); 85 | return clonedManifest; 86 | } 87 | ); 88 | 89 | const screen = (type: string) => { 90 | switch (type) { 91 | case "presentation": 92 | return ; 93 | case "projection": 94 | return ; 95 | default: 96 | return ( 97 | 98 | Error rendering screen type {type}. 99 | 100 | ); 101 | } 102 | }; 103 | 104 | return ( 105 | 106 | 107 | {screen(type)} 108 | 109 | 110 | ); 111 | }; 112 | 113 | Yith.Annotation = Annotation; 114 | Yith.Canvas = Canvas; 115 | Yith.Collection = Collection; 116 | Yith.Manifest = Manifest; 117 | 118 | const Screen = styled("div", { 119 | fontFamily: "inherit, system-ui, -apple-system, Helvetica, Arial, sans-serif", 120 | display: "flex", 121 | alignItems: "flex-start", 122 | }); 123 | 124 | export default Yith; 125 | -------------------------------------------------------------------------------- /public/fixtures/iiif/manifests/tenncities.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": ["https://iiif.io/api/presentation/3/context.json"], 3 | "id": "http://127.0.0.1:8080/fixtures/iiif/manifests/tenncities.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": ["Cabin near Knoxville"] 7 | }, 8 | "summary": { 9 | "en": [ 10 | "Although somewhat idealized, this log cabin is of a design typical of the Great Smoky Mountains before the advent of Great Smoky Mountains National Park. This particular view shows the front of the cabin, including steps, a makeshift porch, and a firepit." 11 | ] 12 | }, 13 | "metadata": [ 14 | { 15 | "label": { 16 | "en": ["Role Term"] 17 | }, 18 | "value": { 19 | "en": ["Unknown"] 20 | } 21 | }, 22 | { 23 | "label": { 24 | "en": ["Date"] 25 | }, 26 | "value": { 27 | "en": ["1911", "1911"] 28 | } 29 | }, 30 | { 31 | "label": { 32 | "en": ["Form"] 33 | }, 34 | "value": { 35 | "en": ["postcards"] 36 | } 37 | }, 38 | { 39 | "label": { 40 | "en": ["Topic"] 41 | }, 42 | "value": { 43 | "en": ["Log cabins", "Architectural photography"] 44 | } 45 | }, 46 | { 47 | "label": { 48 | "en": ["Coverage"] 49 | }, 50 | "value": { 51 | "en": ["Great Smoky Mountains (N.C. and Tenn.)"] 52 | } 53 | }, 54 | { 55 | "label": { 56 | "en": ["Publication Identifier"] 57 | }, 58 | "value": { 59 | "en": [] 60 | } 61 | } 62 | ], 63 | "rights": "http://rightsstatements.org/vocab/CNE/1.0/", 64 | "requiredStatement": { 65 | "label": { 66 | "en": ["Attribution"] 67 | }, 68 | "value": { 69 | "en": ["University of Tennessee, Knoxville. Libraries"] 70 | } 71 | }, 72 | "provider": [ 73 | { 74 | "id": "https://www.lib.utk.edu/about/", 75 | "type": "Agent", 76 | "label": { 77 | "en": ["University of Tennessee, Knoxville. Libraries"] 78 | }, 79 | "homepage": [ 80 | { 81 | "id": "https://www.lib.utk.edu/", 82 | "type": "Text", 83 | "label": { 84 | "en": ["University of Tennessee Libraries Homepage"] 85 | }, 86 | "format": "text/html" 87 | } 88 | ], 89 | "logo": [ 90 | { 91 | "id": "https://utkdigitalinitiatives.github.io/iiif-level-0/ut_libraries_centered/full/full/0/default.jpg", 92 | "type": "Image", 93 | "format": "image/jpeg", 94 | "width": 200, 95 | "height": 200 96 | } 97 | ] 98 | } 99 | ], 100 | "thumbnail": [ 101 | { 102 | "id": "https://digital.lib.utk.edu/iiif/2/collections~islandora~object~tenncities%3A343~datastream~JP2/info.json", 103 | "type": "Image", 104 | "format": "image/jpeg", 105 | "width": 200, 106 | "height": 200 107 | } 108 | ], 109 | "items": [ 110 | { 111 | "id": "https://digital.lib.utk.edu/assemble/manifest/tenncities/343/canvas/0", 112 | "type": "Canvas", 113 | "width": 4000, 114 | "height": 2545, 115 | "items": [ 116 | { 117 | "id": "https://digital.lib.utk.edu/assemble/manifest/tenncities/343/canvas/0/page/tenncities%3A343", 118 | "type": "AnnotationPage", 119 | "items": [ 120 | { 121 | "id": "https://digital.lib.utk.edu/assemble/manifest/tenncities/343/canvas/0/page/tenncities%3A343/60cb5b036ac13", 122 | "type": "Annotation", 123 | "motivation": "painting", 124 | "body": [ 125 | { 126 | "id": "https://digital.lib.utk.edu/iiif/2/collections~islandora~object~tenncities%3A343~datastream~JP2/full/full/0/default.jpg", 127 | "type": "Image", 128 | "width": 4000, 129 | "height": 2545, 130 | "format": "image/jpeg", 131 | "service": [ 132 | { 133 | "@id": "https://digital.lib.utk.edu/iiif/2/collections~islandora~object~tenncities%3A343~datastream~JP2", 134 | "@type": "http://iiif.io/api/image/2/context.json", 135 | "profile": "http://iiif.io/api/image/2/level2.json" 136 | } 137 | ] 138 | } 139 | ], 140 | "target": "https://digital.lib.utk.edu/assemble/manifest/tenncities/343/canvas/0" 141 | } 142 | ] 143 | } 144 | ] 145 | } 146 | ], 147 | "seeAlso": [ 148 | { 149 | "id": "https://digital.lib.utk.edu/collections/islandora/object/tenncities%3A343/datastream/MODS", 150 | "type": "Dataset", 151 | "label": { 152 | "en": ["Bibliographic Description in MODS"] 153 | }, 154 | "format": "application/xml", 155 | "profile": "http://www.loc.gov/standards/mods/v3/mods-3-5.xsd" 156 | } 157 | ] 158 | } 159 | --------------------------------------------------------------------------------