├── .babelrc ├── .github └── workflows │ ├── chromatic.yml │ ├── playwright.yml │ └── test.yml ├── .gitignore ├── .node-version ├── .prettierignore ├── .prettierrc ├── .storybook ├── decorators │ ├── OrgContextDecorator.svelte │ ├── TipOfDayContextDecorator.svelte │ ├── UserContextDecorator.svelte │ └── ViewerContextDecorator.svelte ├── main.ts ├── preview-head.html └── preview.ts ├── AUTHORS.md ├── CHANGELOG.md ├── Makefile ├── README.md ├── embeds.js ├── jsconfig.json ├── license.md ├── local.builder.yml ├── local.yml ├── netlify.toml ├── package-lock.json ├── package.json ├── playwright.config.js ├── plugins └── cache-bust │ ├── index.js │ └── manifest.yml ├── public ├── _headers ├── _redirects ├── dc_logo.png ├── favicon.png ├── fonts │ ├── SourceCodePro-Italic-VariableFont_wght.ttf │ ├── SourceCodePro-VariableFont_wght.ttf │ ├── source-sans-pro-v21-latin-600.eot │ ├── source-sans-pro-v21-latin-600.svg │ ├── source-sans-pro-v21-latin-600.ttf │ ├── source-sans-pro-v21-latin-600.woff │ ├── source-sans-pro-v21-latin-600.woff2 │ ├── source-sans-pro-v21-latin-700.eot │ ├── source-sans-pro-v21-latin-700.svg │ ├── source-sans-pro-v21-latin-700.ttf │ ├── source-sans-pro-v21-latin-700.woff │ ├── source-sans-pro-v21-latin-700.woff2 │ ├── source-sans-pro-v21-latin-italic.eot │ ├── source-sans-pro-v21-latin-italic.svg │ ├── source-sans-pro-v21-latin-italic.ttf │ ├── source-sans-pro-v21-latin-italic.woff │ ├── source-sans-pro-v21-latin-italic.woff2 │ ├── source-sans-pro-v21-latin-regular.eot │ ├── source-sans-pro-v21-latin-regular.svg │ ├── source-sans-pro-v21-latin-regular.ttf │ ├── source-sans-pro-v21-latin-regular.woff │ └── source-sans-pro-v21-latin-regular.woff2 └── mockServiceWorker.js ├── src ├── app.d.ts ├── app.html ├── config │ ├── config.js │ ├── embed.js │ ├── production.js │ ├── remote.js │ └── staging.js ├── embed │ ├── documentLoader.js │ ├── enhance.js │ ├── iframeSizer.js │ ├── noteLoader.js │ └── projectLoader.js ├── hooks.client.ts ├── hooks.server.ts ├── hooks.ts ├── langs │ └── json │ │ ├── README.md │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── fr.json │ │ ├── it.json │ │ ├── ru.json │ │ ├── scratch.json │ │ └── uk.json ├── legacy │ ├── README.md │ ├── modification │ │ ├── Modification.svelte │ │ ├── modification.js │ │ ├── modification.test.js │ │ ├── modifySpec.js │ │ └── modifySpec.test.js │ ├── search │ │ ├── parse.js │ │ ├── parse.test.js │ │ ├── search.js │ │ ├── searchFields.js │ │ └── searchFields.test.js │ └── util │ │ ├── README.md │ │ ├── array.js │ │ ├── batchDelay.js │ │ ├── batchDelay.test.js │ │ ├── bounds.js │ │ ├── cache.js │ │ ├── callEvery.js │ │ ├── closure.js │ │ ├── coalesceHighlights.js │ │ ├── coalesceHighlights.test.js │ │ ├── copy.js │ │ ├── data.js │ │ ├── dom.js │ │ ├── domPurify.js │ │ ├── easing.js │ │ ├── epsilon.js │ │ ├── iframe.js │ │ ├── markup.js │ │ ├── paginate.js │ │ ├── storageManager.js │ │ ├── string.js │ │ ├── string.test.js │ │ ├── textareaResize.js │ │ ├── timeout.js │ │ ├── transition.js │ │ ├── url.js │ │ ├── url.test.js │ │ ├── visibility.js │ │ └── wrapLoad.js ├── lib │ ├── api │ │ ├── accounts.ts │ │ ├── addons.ts │ │ ├── collaborators.ts │ │ ├── documents.ts │ │ ├── embed.ts │ │ ├── feedback.ts │ │ ├── flatpages.ts │ │ ├── notes.ts │ │ ├── projects.ts │ │ ├── sections.ts │ │ ├── tests │ │ │ ├── accounts.test.ts │ │ │ ├── addons.test.ts │ │ │ ├── collaborators.test.ts │ │ │ ├── documents.test.ts │ │ │ ├── embed.test.ts │ │ │ ├── flatpages.test.ts │ │ │ ├── notes.test.ts │ │ │ ├── projects.test.ts │ │ │ └── sections.test.ts │ │ └── types.d.ts │ ├── components │ │ ├── accounts │ │ │ ├── Avatar.svelte │ │ │ ├── Mailkey.svelte │ │ │ ├── Unverified.svelte │ │ │ ├── UserMenu.svelte │ │ │ ├── stories │ │ │ │ ├── Avatar.stories.svelte │ │ │ │ ├── Mailkey.stories.svelte │ │ │ │ ├── Unverified.stories.svelte │ │ │ │ └── UserMenu.stories.svelte │ │ │ └── tests │ │ │ │ ├── Mailkey.test.ts │ │ │ │ ├── UserMenu.test.ts │ │ │ │ └── __snapshots__ │ │ │ │ ├── Mailkey.test.ts.snap │ │ │ │ └── UserMenu.test.ts.snap │ │ ├── addons │ │ │ ├── AddOnListItem.svelte │ │ │ ├── AddOnMeta.svelte │ │ │ ├── AddOnPin.svelte │ │ │ ├── AddOnsNavigation.svelte │ │ │ ├── DocumentList.svelte │ │ │ ├── History.svelte │ │ │ ├── HistoryEvent.svelte │ │ │ ├── Scheduled.svelte │ │ │ ├── ScheduledEvent.svelte │ │ │ ├── stories │ │ │ │ ├── AddOnListItem.stories.svelte │ │ │ │ ├── AddOnMeta.stories.svelte │ │ │ │ ├── AddOnsNavigation.stories.svelte │ │ │ │ ├── History.stories.svelte │ │ │ │ ├── HistoryEvent.stories.svelte │ │ │ │ ├── Scheduled.stories.svelte │ │ │ │ └── ScheduledEvent.stories.svelte │ │ │ └── tests │ │ │ │ ├── AddOnListItem.test.ts │ │ │ │ ├── AddOnsNavigation.test.ts │ │ │ │ └── __snapshots__ │ │ │ │ ├── AddOnListItem.test.ts.snap │ │ │ │ └── AddOnsNavigation.test.ts.snap │ │ ├── common │ │ │ ├── Access.svelte │ │ │ ├── Action.svelte │ │ │ ├── Badge.svelte │ │ │ ├── Button.svelte │ │ │ ├── Card.svelte │ │ │ ├── Copy.svelte │ │ │ ├── Dropdown.svelte │ │ │ ├── Empty.svelte │ │ │ ├── Error.svelte │ │ │ ├── Field.svelte │ │ │ ├── FieldLabel.svelte │ │ │ ├── Flex.svelte │ │ │ ├── Highlight.svelte │ │ │ ├── HighlightGroup.svelte │ │ │ ├── KV.svelte │ │ │ ├── Logo.svelte │ │ │ ├── Menu.svelte │ │ │ ├── MenuInsert.svelte │ │ │ ├── MenuItem.svelte │ │ │ ├── Metadata.svelte │ │ │ ├── PageToolbar.svelte │ │ │ ├── Paginator.svelte │ │ │ ├── Pin.svelte │ │ │ ├── PlausibleTracker.svelte │ │ │ ├── Premium.svelte │ │ │ ├── RelativeTime.svelte │ │ │ ├── ShowSize.svelte │ │ │ ├── SignedIn.svelte │ │ │ ├── Tab.svelte │ │ │ ├── Tip.svelte │ │ │ ├── TipOfDay.svelte │ │ │ ├── Toast.svelte │ │ │ ├── Tooltip.svelte │ │ │ ├── stories │ │ │ │ ├── Action.stories.svelte │ │ │ │ ├── Badge.stories.svelte │ │ │ │ ├── Button.stories.svelte │ │ │ │ ├── Card.stories.svelte │ │ │ │ ├── Dropdown.stories.svelte │ │ │ │ ├── Empty.stories.svelte │ │ │ │ ├── Error.stories.svelte │ │ │ │ ├── FieldLabel.stories.svelte │ │ │ │ ├── Highlight.stories.svelte │ │ │ │ ├── HighlightGroup.stories.svelte │ │ │ │ ├── KV.stories.svelte │ │ │ │ ├── Logo.stories.svelte │ │ │ │ ├── Metadata.stories.svelte │ │ │ │ ├── PageToolbar.stories.svelte │ │ │ │ ├── Paginator.stories.svelte │ │ │ │ ├── Pin.stories.svelte │ │ │ │ ├── RelativeTime.stories.svelte │ │ │ │ ├── Tab.stories.svelte │ │ │ │ ├── Tip.stories.svelte │ │ │ │ ├── Toast.stories.svelte │ │ │ │ └── Tooltip.stories.svelte │ │ │ └── tests │ │ │ │ ├── Button.test.ts │ │ │ │ ├── ShowSize.demo.svelte │ │ │ │ ├── ShowSize.test.ts │ │ │ │ └── Toast.test.ts │ │ ├── documents │ │ │ ├── CustomizeEmbed.svelte │ │ │ ├── Data.svelte │ │ │ ├── DocumentListItem.svelte │ │ │ ├── Header.svelte │ │ │ ├── Metadata.svelte │ │ │ ├── NoteHighlights.svelte │ │ │ ├── PageHighlights.svelte │ │ │ ├── Pending.svelte │ │ │ ├── Projects.svelte │ │ │ ├── ResultsList.svelte │ │ │ ├── Revisions.svelte │ │ │ ├── Share.svelte │ │ │ ├── Thumbnail.svelte │ │ │ ├── VisibleFields.svelte │ │ │ ├── sidebar │ │ │ │ ├── Notes.svelte │ │ │ │ └── Sections.svelte │ │ │ ├── stories │ │ │ │ ├── CustomizeEmbed.stories.svelte │ │ │ │ ├── Data.stories.svelte │ │ │ │ ├── DocumentListItem.stories.svelte │ │ │ │ ├── Header.stories.svelte │ │ │ │ ├── Metadata.stories.svelte │ │ │ │ ├── NoteHighlights.stories.svelte │ │ │ │ ├── PageHighlights.stories.svelte │ │ │ │ ├── Pending.stories.svelte │ │ │ │ ├── Projects.stories.svelte │ │ │ │ ├── ResultsList.stories.svelte │ │ │ │ ├── Revisions.stories.svelte │ │ │ │ ├── Share.stories.svelte │ │ │ │ ├── Thumbnail.stories.svelte │ │ │ │ └── VisibleFields.stories.svelte │ │ │ └── tests │ │ │ │ ├── DocumentListItem.test.ts │ │ │ │ ├── ResultsList.test.ts │ │ │ │ ├── Share.test.ts │ │ │ │ └── __snapshots__ │ │ │ │ └── DocumentListItem.test.ts.snap │ │ ├── embeds │ │ │ ├── DocumentEmbed.svelte │ │ │ └── stories │ │ │ │ └── DocumentEmbed.stories.svelte │ │ ├── forms │ │ │ ├── AddOnDispatch.svelte │ │ │ ├── ConfirmDelete.svelte │ │ │ ├── ConfirmRedaction.svelte │ │ │ ├── DeleteProject.svelte │ │ │ ├── Edit.svelte │ │ │ ├── EditData.svelte │ │ │ ├── EditDataMany.svelte │ │ │ ├── EditMany.svelte │ │ │ ├── EditNote.svelte │ │ │ ├── EditProject.svelte │ │ │ ├── EditSectionRow.svelte │ │ │ ├── EditSections.svelte │ │ │ ├── InviteCollaborator.svelte │ │ │ ├── Projects.svelte │ │ │ ├── RemoveCollaborator.svelte │ │ │ ├── Reprocess.svelte │ │ │ ├── RevisionControl.svelte │ │ │ ├── Search.svelte │ │ │ ├── UpdateCollaborator.svelte │ │ │ ├── Upload.svelte │ │ │ ├── UploadListItem.svelte │ │ │ ├── UserFeedback.svelte │ │ │ ├── stories │ │ │ │ ├── AddOnDispatch.stories.svelte │ │ │ │ ├── ConfirmDelete.stories.svelte │ │ │ │ ├── ConfirmRedaction.stories.svelte │ │ │ │ ├── DeleteProject.stories.svelte │ │ │ │ ├── Edit.stories.svelte │ │ │ │ ├── EditData.stories.svelte │ │ │ │ ├── EditNote.stories.svelte │ │ │ │ ├── EditProject.stories.svelte │ │ │ │ ├── InviteCollaborator.stories.svelte │ │ │ │ ├── Projects.stories.svelte │ │ │ │ ├── RemoveCollaborator.stories.svelte │ │ │ │ ├── Reprocess.stories.svelte │ │ │ │ ├── RevisionControl.stories.svelte │ │ │ │ ├── Search.stories.svelte │ │ │ │ ├── Sections.stories.svelte │ │ │ │ ├── UpdateCollaborator.stories.svelte │ │ │ │ ├── Upload.stories.svelte │ │ │ │ ├── UploadListItem.stories.svelte │ │ │ │ └── UserFeedback.stories.svelte │ │ │ └── tests │ │ │ │ └── Upload.test.ts │ │ ├── icons │ │ │ ├── Credit.svelte │ │ │ ├── Pin.svelte │ │ │ ├── Premium.svelte │ │ │ └── Star.svelte │ │ ├── inputs │ │ │ ├── AccessLevel.svelte │ │ │ ├── ArrayField.svelte │ │ │ ├── Checkbox.svelte │ │ │ ├── Choices.svelte │ │ │ ├── Dropzone.svelte │ │ │ ├── Field.svelte │ │ │ ├── File.svelte │ │ │ ├── KeyValue.svelte │ │ │ ├── Language.svelte │ │ │ ├── Number.svelte │ │ │ ├── ProjectAccess.svelte │ │ │ ├── Select.svelte │ │ │ ├── Selection.svelte │ │ │ ├── Switch.svelte │ │ │ ├── Text.svelte │ │ │ ├── TextArea.svelte │ │ │ ├── generator.ts │ │ │ ├── stories │ │ │ │ ├── AccessLevel.stories.svelte │ │ │ │ ├── Checkbox.stories.svelte │ │ │ │ ├── Dropzone.stories.svelte │ │ │ │ ├── File.stories.svelte │ │ │ │ ├── KeyValue.stories.svelte │ │ │ │ ├── Language.stories.svelte │ │ │ │ ├── ProjectAccess.stories.svelte │ │ │ │ ├── Select.stories.svelte │ │ │ │ ├── Switch.stories.svelte │ │ │ │ ├── Text.stories.svelte │ │ │ │ └── Textarea.stories.svelte │ │ │ └── tests │ │ │ │ └── Dropzone.test.ts │ │ ├── layouts │ │ │ ├── AddOnBrowser.svelte │ │ │ ├── AddOnLayout.svelte │ │ │ ├── AppLayout.svelte │ │ │ ├── ContentLayout.svelte │ │ │ ├── DocumentBrowser.svelte │ │ │ ├── DocumentLayout.svelte │ │ │ ├── DocumentListToolbar.svelte │ │ │ ├── EmbedLayout.svelte │ │ │ ├── Error.svelte │ │ │ ├── Flatpage.svelte │ │ │ ├── Modal.svelte │ │ │ ├── Navigation.svelte │ │ │ ├── Portal.svelte │ │ │ ├── Project.svelte │ │ │ ├── Sidebar.svelte │ │ │ ├── SidebarLayout.svelte │ │ │ ├── Toaster.svelte │ │ │ ├── stories │ │ │ │ ├── AddOnBrowser.stories.svelte │ │ │ │ ├── AddOnLayout.stories.svelte │ │ │ │ ├── AppLayout.stories.svelte │ │ │ │ ├── Dialog.demo.svelte │ │ │ │ ├── DocumentBrowser.stories.svelte │ │ │ │ ├── DocumentLayout.stories.svelte │ │ │ │ ├── EmbedLayout.stories.svelte │ │ │ │ ├── Error.stories.svelte │ │ │ │ ├── Flatpage.stories.svelte │ │ │ │ ├── Modal.stories.svelte │ │ │ │ ├── Project.stories.svelte │ │ │ │ ├── SidebarLayout.stories.svelte │ │ │ │ └── Toaster.stories.svelte │ │ │ └── tests │ │ │ │ └── Toaster.test.ts │ │ ├── navigation │ │ │ ├── Breadcrumbs.svelte │ │ │ ├── HelpMenu.svelte │ │ │ ├── LanguageMenu.svelte │ │ │ ├── OrgMenu.svelte │ │ │ ├── stories │ │ │ │ ├── Breadcrumbs.stories.svelte │ │ │ │ ├── LanguageMenu.stories.svelte │ │ │ │ └── OrgMenu.stories.svelte │ │ │ └── tests │ │ │ │ └── Breadcrumbs.test.ts │ │ ├── onboarding │ │ │ ├── GuidedTour.svelte │ │ │ └── scripts.ts │ │ ├── premium-credits │ │ │ ├── CreditMeter.svelte │ │ │ ├── PremiumBadge.svelte │ │ │ ├── Price.svelte │ │ │ ├── UpgradePrompt.svelte │ │ │ └── stories │ │ │ │ ├── CreditMeter.stories.svelte │ │ │ │ ├── PremiumBadge.stories.svelte │ │ │ │ ├── Price.stories.svelte │ │ │ │ └── UpgradePrompt.stories.svelte │ │ ├── processing │ │ │ ├── AddOns.svelte │ │ │ ├── Documents.svelte │ │ │ ├── Process.svelte │ │ │ ├── ProcessContext.svelte │ │ │ ├── ProcessDropdown.svelte │ │ │ ├── ProcessSummary.svelte │ │ │ └── stories │ │ │ │ ├── AddOns.stories.svelte │ │ │ │ ├── Documents.stories.svelte │ │ │ │ ├── Process.stories.svelte │ │ │ │ ├── ProcessDropdown.stories.svelte │ │ │ │ └── ProcessSummary.stories.svelte │ │ ├── projects │ │ │ ├── Collaborators.svelte │ │ │ ├── ProjectHeader.svelte │ │ │ ├── ProjectListItem.svelte │ │ │ ├── ProjectPin.svelte │ │ │ ├── ProjectShare.svelte │ │ │ └── stories │ │ │ │ ├── Collaborators.stories.svelte │ │ │ │ ├── ProjectHeader.stories.svelte │ │ │ │ ├── ProjectListItem.stories.svelte │ │ │ │ └── ProjectShare.stories.svelte │ │ ├── sidebar │ │ │ ├── AddOns.svelte │ │ │ ├── DocumentActions.svelte │ │ │ ├── Documents.svelte │ │ │ ├── ProjectActions.svelte │ │ │ ├── Projects.svelte │ │ │ ├── SidebarGroup.svelte │ │ │ ├── SidebarItem.svelte │ │ │ ├── UploadButton.svelte │ │ │ ├── ViewerActions.svelte │ │ │ └── stories │ │ │ │ ├── AddOns.stories.svelte │ │ │ │ ├── DocumentActions.stories.svelte │ │ │ │ ├── Documents.stories.svelte │ │ │ │ ├── ProjectActions.stories.svelte │ │ │ │ ├── Projects.stories.svelte │ │ │ │ ├── SidebarGroup.stories.svelte │ │ │ │ ├── SidebarItem.stories.svelte │ │ │ │ └── ViewerActions.stories.svelte │ │ └── viewer │ │ │ ├── AnnotationLayer.svelte │ │ │ ├── AnnotationToolbar.svelte │ │ │ ├── Grid.svelte │ │ │ ├── LoadingToolbar.svelte │ │ │ ├── Note.svelte │ │ │ ├── NoteTab.svelte │ │ │ ├── Notes.svelte │ │ │ ├── PDF.svelte │ │ │ ├── PDFPage.svelte │ │ │ ├── Page.svelte │ │ │ ├── PageActions.svelte │ │ │ ├── PaginationToolbar.svelte │ │ │ ├── README.md │ │ │ ├── ReadingToolbar.svelte │ │ │ ├── RedactionLayer.svelte │ │ │ ├── RedactionToolbar.svelte │ │ │ ├── Search.svelte │ │ │ ├── Text.svelte │ │ │ ├── Viewer.svelte │ │ │ ├── ViewerContext.svelte │ │ │ ├── Zoom.svelte │ │ │ └── stories │ │ │ ├── AnnotationLayer.stories.svelte │ │ │ ├── AnnotationToolbar.stories.svelte │ │ │ ├── Grid.stories.svelte │ │ │ ├── LoadingToolbar.stories.svelte │ │ │ ├── Note.stories.svelte │ │ │ ├── NoteTab.stories.svelte │ │ │ ├── Notes.stories.svelte │ │ │ ├── PDF.stories.svelte │ │ │ ├── PDFPage.stories.svelte │ │ │ ├── PaginationToolbar.stories.svelte │ │ │ ├── ReadingToolbar.stories.svelte │ │ │ ├── RedactionLayer.stories.svelte │ │ │ ├── RedactionToolbar.stories.svelte │ │ │ ├── Search.stories.svelte │ │ │ ├── Section.stories.svelte │ │ │ ├── Text.stories.svelte │ │ │ ├── Viewer.stories.svelte │ │ │ └── Zoom.stories.svelte │ ├── i18n │ │ └── index.js │ ├── load │ │ └── document.ts │ └── utils │ │ ├── api.ts │ │ ├── array.ts │ │ ├── copy.ts │ │ ├── embed.ts │ │ ├── files.ts │ │ ├── index.ts │ │ ├── language.ts │ │ ├── layout.ts │ │ ├── logging.ts │ │ ├── markdown.ts │ │ ├── navigation.ts │ │ ├── pageSize.ts │ │ ├── permissions.ts │ │ ├── scroll.ts │ │ ├── search.ts │ │ ├── slugify.ts │ │ ├── storage.ts │ │ ├── tests │ │ ├── __snapshots__ │ │ │ ├── embed.test.ts.snap │ │ │ └── viewer.test.ts.snap │ │ ├── api.test.ts │ │ ├── array.test.ts │ │ ├── copy.test.ts │ │ ├── embed.test.ts │ │ ├── files.test.ts │ │ ├── language.test.ts │ │ ├── navigation.test.ts │ │ ├── pageSize.test.ts │ │ ├── permissions.test.ts │ │ ├── scroll.test.ts │ │ ├── search.test.ts │ │ ├── storage.test.ts │ │ └── viewer.test.ts │ │ └── viewer.ts ├── routes │ ├── (app) │ │ ├── +error.svelte │ │ ├── +layout.server.ts │ │ ├── +layout.svelte │ │ ├── +layout.ts │ │ ├── +page.server.ts │ │ ├── +page.ts │ │ ├── add-ons │ │ │ ├── +error.svelte │ │ │ ├── +layout.ts │ │ │ ├── +page.svelte │ │ │ ├── +page.ts │ │ │ ├── [owner] │ │ │ │ └── [repo] │ │ │ │ │ ├── +error.svelte │ │ │ │ │ ├── +page.server.ts │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── +page.ts │ │ │ │ │ └── [event] │ │ │ │ │ ├── +page.server.ts │ │ │ │ │ ├── +page.svelte │ │ │ │ │ └── +page.ts │ │ │ └── runs │ │ │ │ └── +page.svelte │ │ ├── app │ │ │ └── +page.ts │ │ ├── documents │ │ │ ├── +error.svelte │ │ │ ├── +layout.ts │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ ├── +page.ts │ │ │ ├── [id]-[slug].[format] │ │ │ │ └── +page.ts │ │ │ ├── [id]-[slug] │ │ │ │ ├── +error.svelte │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ ├── +page.ts │ │ │ │ ├── annotations │ │ │ │ │ └── [note_id] │ │ │ │ │ │ └── +page.ts │ │ │ │ └── pages │ │ │ │ │ └── [page] │ │ │ │ │ └── +page.ts │ │ │ └── [id] │ │ │ │ ├── +page.ts │ │ │ │ ├── annotations │ │ │ │ └── [note_id] │ │ │ │ │ └── +page.ts │ │ │ │ └── pages │ │ │ │ └── [page] │ │ │ │ └── +page.ts │ │ ├── projects │ │ │ ├── +error.svelte │ │ │ ├── +layout.ts │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ ├── +page.ts │ │ │ ├── [id]-[slug] │ │ │ │ ├── +error.svelte │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ └── +page.ts │ │ │ └── [id] │ │ │ │ └── +page.ts │ │ └── upload │ │ │ ├── +error.svelte │ │ │ ├── +page.svelte │ │ │ └── +page.ts │ ├── (pages) │ │ ├── +error.svelte │ │ ├── about │ │ │ ├── +page.svelte │ │ │ └── +page.ts │ │ ├── help │ │ │ └── [...path] │ │ │ │ ├── +page.svelte │ │ │ │ └── +page.ts │ │ └── home │ │ │ ├── +page.svelte │ │ │ └── +page.ts │ ├── +error.svelte │ ├── +layout.svelte │ ├── +layout.ts │ └── embed │ │ ├── +layout.ts │ │ ├── documents │ │ ├── [id]-[slug] │ │ │ ├── +page.svelte │ │ │ └── +page.ts │ │ └── [id] │ │ │ ├── +page.ts │ │ │ ├── annotations │ │ │ └── [note_id] │ │ │ │ ├── +page.svelte │ │ │ │ └── +page.ts │ │ │ └── pages │ │ │ └── [page] │ │ │ ├── +page.svelte │ │ │ ├── +page.ts │ │ │ ├── Annotation.svelte │ │ │ └── Note.svelte │ │ ├── projects │ │ └── [project_id]-[slug] │ │ │ ├── +page.svelte │ │ │ └── +page.ts │ │ └── stories │ │ ├── note-embed.stories.svelte │ │ ├── page-embed.stories.svelte │ │ └── project-embed.stories.svelte ├── service-worker.ts ├── style │ ├── README.md │ ├── kit.css │ ├── legacy.css │ └── variables.css └── test │ ├── components │ └── UserContext.demo.svelte │ ├── fixtures │ ├── accounts.ts │ ├── addons.ts │ ├── addons │ │ └── progress.ts │ ├── common.ts │ ├── documents.ts │ ├── documents │ │ ├── create.json │ │ ├── document-expanded.json │ │ ├── document.json │ │ ├── document.txt.json │ │ ├── documents-expanded.json │ │ ├── documents.json │ │ ├── examples │ │ │ ├── agreement-between-conservatives-and-liberal-democrats-to-form-a-coalition-government.pdf │ │ │ ├── lefler-thesis.json │ │ │ ├── the-santa-anas-p1.position.json │ │ │ ├── the-santa-anas.json │ │ │ └── the-santa-anas.pdf │ │ ├── highlight.json │ │ ├── pending.json │ │ ├── pending.ts │ │ ├── redactions.json │ │ ├── revisions.json │ │ └── search-highlight.json │ ├── flatpages.ts │ ├── notes.ts │ ├── notes │ │ ├── note-expanded.json │ │ ├── note.json │ │ ├── notes-expanded.json │ │ └── notes.json │ ├── oembed.json │ ├── projects.ts │ ├── projects │ │ ├── project-documents-2.json │ │ ├── project-documents-expanded.json │ │ ├── project-documents.json │ │ ├── project-users.json │ │ └── project.json │ └── sections.json │ └── handlers │ ├── accounts.ts │ ├── addons.ts │ ├── documents.ts │ ├── feedback.ts │ ├── oembed.ts │ ├── projects.ts │ ├── utils.ts │ └── viewer.ts ├── static ├── 404.html ├── _redirects ├── apple-touch-icon-120x120-precomposed.png ├── apple-touch-icon-152x152-precomposed.png ├── apple-touch-icon-152x152.png ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon.ico ├── favicon.png ├── fonts-cyrillic.css ├── fonts-latin.css ├── fonts.css ├── fonts │ ├── SourceCodePro-Italic-VariableFont_wght.ttf │ ├── SourceCodePro-VariableFont_wght.ttf │ ├── source-sans-pro-cyrillic-600.woff │ ├── source-sans-pro-cyrillic-600.woff2 │ ├── source-sans-pro-cyrillic-700.woff │ ├── source-sans-pro-cyrillic-700.woff2 │ ├── source-sans-pro-cyrillic-italic.woff │ ├── source-sans-pro-cyrillic-italic.woff2 │ ├── source-sans-pro-cyrillic-menuset-600.woff │ ├── source-sans-pro-cyrillic-menuset-600.woff2 │ ├── source-sans-pro-cyrillic-regular.woff │ ├── source-sans-pro-cyrillic-regular.woff2 │ ├── source-sans-pro-latin-menuset-600.woff │ ├── source-sans-pro-latin-menuset-600.woff2 │ ├── source-sans-pro-v21-latin-600.eot │ ├── source-sans-pro-v21-latin-600.svg │ ├── source-sans-pro-v21-latin-600.ttf │ ├── source-sans-pro-v21-latin-600.woff │ ├── source-sans-pro-v21-latin-600.woff2 │ ├── source-sans-pro-v21-latin-700.eot │ ├── source-sans-pro-v21-latin-700.svg │ ├── source-sans-pro-v21-latin-700.ttf │ ├── source-sans-pro-v21-latin-700.woff │ ├── source-sans-pro-v21-latin-700.woff2 │ ├── source-sans-pro-v21-latin-italic.eot │ ├── source-sans-pro-v21-latin-italic.svg │ ├── source-sans-pro-v21-latin-italic.ttf │ ├── source-sans-pro-v21-latin-italic.woff │ ├── source-sans-pro-v21-latin-italic.woff2 │ ├── source-sans-pro-v21-latin-regular.eot │ ├── source-sans-pro-v21-latin-regular.svg │ ├── source-sans-pro-v21-latin-regular.ttf │ ├── source-sans-pro-v21-latin-regular.woff │ └── source-sans-pro-v21-latin-regular.woff2 └── robots.txt ├── svelte.config.js ├── tests ├── README.md └── fixtures │ ├── Small pdf.pdf │ ├── production.json │ ├── staging.json │ └── the-nature-of-the-firm-CPEC11.pdf ├── tsconfig.json ├── tsconfig.test.json ├── utility ├── README.md ├── find-all-assets.sh ├── find-asset.sh ├── find-unused-assets.sh ├── language_transformer.py ├── purge.sh └── translate.py ├── vite.config.js └── vitest-setup.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": "> 0.25%, not dead" 7 | } 8 | ], 9 | "@babel/preset-typescript" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | # this workflow still refers to the legacy codebase 2 | # tests need to be updated before this is useful again 3 | name: Playwright Tests 4 | 5 | on: 6 | pull_request: 7 | branches: [master] 8 | 9 | env: 10 | URL: "https://deploy-preview-${{ github.event.number }}.muckcloud.com" 11 | PLAYWRIGHT_TEST_BASE_URL: "https://deploy-preview-${{ github.event.number }}.muckcloud.com" 12 | NODE_ENV: staging 13 | 14 | jobs: 15 | wait: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: cygnetdigital/wait_for_response@v2.0.0 20 | with: 21 | url: ${{ env.URL }} 22 | responseCode: "200" 23 | timeout: 120000 24 | interval: 3000 25 | 26 | test: 27 | timeout-minutes: 60 28 | runs-on: ubuntu-latest 29 | needs: wait 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Use Node.js 20.x 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: "20.x" 37 | 38 | - name: Install dependencies 39 | run: npm ci 40 | 41 | - name: Install Playwright 42 | run: npx playwright install --with-deps 43 | 44 | - name: Run Playwright tests 45 | run: npx playwright test 46 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | unit-test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Use Node.js 20.x 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: "20.x" 19 | - run: npm ci 20 | - run: npm run build 21 | env: 22 | NODE_ENV: production 23 | - run: npm run test:coverage 24 | - name: Report Coverage 25 | uses: davelosert/vitest-coverage-report-action@v2 26 | 27 | check: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Use Node.js 20.x 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: "20.x" 36 | - run: npm ci 37 | - run: npm run build 38 | env: 39 | NODE_ENV: production 40 | - run: npm run check 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | ._* 3 | node_modules 4 | public/bundle.* 5 | public/index.html 6 | public/docs 7 | public/test 8 | package-lock.json 9 | yarn.lock 10 | stats.json 11 | public_test/main.* 12 | 13 | # Webpack chunks 14 | public/[0-9]*.*.* 15 | public/*.map 16 | public/src_* 17 | public/vendors-* 18 | public/node_modules_* 19 | 20 | # Embed bundles 21 | public/embed/* 22 | public/notes/* 23 | public/viewer/* 24 | public/assets/ 25 | 26 | static/embed/* 27 | static/notes/* 28 | static/viewer/* 29 | 30 | # Netlify 31 | .netlify 32 | 33 | # Local test env file 34 | .env 35 | .env.* 36 | .envrc 37 | playwright-report 38 | test-results 39 | coverage 40 | certs 41 | 42 | # local fixtures, everyone should generate their own 43 | tests/fixtures/development.json 44 | 45 | .vscode 46 | vite.config.js.timestamp-* 47 | vite.config.ts.timestamp-* 48 | *.log 49 | 50 | # sveltekit 51 | /build 52 | /.svelte-kit 53 | /package 54 | 55 | scratch/ 56 | build/ 57 | dist/ 58 | storybook-static/ 59 | 60 | # Sentry Config File 61 | .sentryclirc 62 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.14.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore files for PNPM, NPM and YARN 2 | pnpm-lock.yaml 3 | package-lock.json 4 | yarn.lock 5 | 6 | public/ 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSameLine": false, 3 | "plugins": ["prettier-plugin-svelte"], 4 | "printWidth": 80, 5 | "semi": true, 6 | "singleQuote": false, 7 | "svelteAllowShorthand": true, 8 | "svelteIndentScriptAndStyle": true, 9 | "svelteSortOrder": "options-scripts-markup-styles", 10 | "svelteStrictMode": false, 11 | "tabWidth": 2, 12 | "trailingComma": "all", 13 | "useTabs": false 14 | } 15 | -------------------------------------------------------------------------------- /.storybook/decorators/OrgContextDecorator.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.storybook/decorators/TipOfDayContextDecorator.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.storybook/decorators/UserContextDecorator.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.storybook/decorators/ViewerContextDecorator.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/svelte-vite"; 2 | 3 | const config: StorybookConfig = { 4 | core: { 5 | disableTelemetry: true, // 👈 Disables telemetry 6 | }, 7 | stories: [ 8 | "../src/lib/**/*.stories.@(js|jsx|ts|tsx|svelte)", 9 | "../src/routes/**/*.stories.@(js|jsx|ts|tsx|svelte)", 10 | ], 11 | staticDirs: ["../public", "../static"], 12 | addons: [ 13 | "@storybook/addon-links", 14 | "@storybook/addon-essentials", 15 | "@storybook/addon-interactions", 16 | "@storybook/addon-svelte-csf", 17 | "storybook-addon-cookie", 18 | ], 19 | framework: { 20 | name: "@storybook/sveltekit", 21 | options: {}, 22 | }, 23 | docs: { 24 | autodocs: "tag", 25 | }, 26 | 27 | viteFinal(config) { 28 | config.resolve = { 29 | alias: { 30 | "@": new URL("../src", import.meta.url).toString(), 31 | "@/*": new URL("../src/*", import.meta.url).toString(), 32 | }, 33 | }; 34 | 35 | config.build = { 36 | ...config.build, 37 | target: "esnext", 38 | }; 39 | 40 | return config; 41 | }, 42 | }; 43 | 44 | export default config; 45 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | docker volume create nodemodules && docker compose -f local.builder.yml run --rm install 3 | 4 | npmlist: 5 | docker compose -f local.builder.yml run --rm npmlist 6 | 7 | build: 8 | docker compose -f local.builder.yml run --rm build 9 | 10 | build-browser-test: 11 | docker compose -f local.builder.yml build browser-test 12 | 13 | dev: 14 | docker compose -f local.yml up documentcloud_frontend 15 | 16 | preview: 17 | docker compose -f local.yml up preview 18 | 19 | down: 20 | docker compose -f local.yml down 21 | 22 | clean: 23 | @echo deleting Webpack chunks 24 | rm -f public/index.html public/[0-9]*.*.* public/bundle.*.js public/bundle.*.css public/bundle.*.txt public/*.map public/*.*.js 25 | rm -rf public/assets public/notes public/viewer public/embed 26 | @echo deleting built files 27 | rm -rf build .svelte-kit .netlify playwright-report 28 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm run build" 3 | publish = "build" 4 | 5 | [[context.production.plugins]] 6 | package = "/plugins/cache-bust" 7 | 8 | [[plugins]] 9 | package = "@netlify/plugin-lighthouse" 10 | # https://github.com/netlify/netlify-plugin-lighthouse 11 | # Set minimum thresholds for each report area 12 | # [plugins.inputs.thresholds] 13 | # performance = 0.9 14 | # 15 | # to audit a path other than / 16 | # route1 audit will use the top level thresholds 17 | [[plugins.inputs.audits]] 18 | path = "documents/" 19 | -------------------------------------------------------------------------------- /plugins/cache-bust/index.js: -------------------------------------------------------------------------------- 1 | // bust cache after a production deploy succeeds 2 | 3 | const { CLOUDFLARE_ID, CLOUDFLARE_TOKEN } = process.env; 4 | const PURGE_URL = `https://api.cloudflare.com/client/v4 5 | /zones/${CLOUDFLARE_ID}/purge_cache`; 6 | 7 | export async function onSuccess() { 8 | if (!CLOUDFLARE_ID) { 9 | console.error("CLOUDFLARE_ID is not set."); 10 | return; 11 | } 12 | 13 | const resp = await fetch(PURGE_URL, { 14 | method: "POST", 15 | headers: { 16 | authorization: `Bearer ${CLOUDFLARE_TOKEN}`, 17 | "content-type": "application/json", 18 | }, 19 | body: JSON.stringify({ purge_everything: true }), 20 | }); 21 | 22 | if (!resp.ok) { 23 | console.error("Error purging cache."); 24 | console.error(`${resp.status}: ${resp.statusText}`); 25 | } 26 | 27 | const result = await resp.json(); 28 | 29 | console.log(JSON.stringify(result, null, 2)); 30 | } 31 | -------------------------------------------------------------------------------- /plugins/cache-bust/manifest.yml: -------------------------------------------------------------------------------- 1 | # cache bust plugin 2 | 3 | name: cache-bust 4 | -------------------------------------------------------------------------------- /public/_headers: -------------------------------------------------------------------------------- 1 | 2 | # generated css and js files all have hashes in their names, so can be 3 | # cached forever 4 | 5 | /assets/*.css 6 | Cache-Control: public, max-age=604800, immutable 7 | 8 | /assets/*.js 9 | Cache-Control: public, max-age=604800, immutable 10 | 11 | 12 | # favicon.png and global.css are checked into the repo and change infrequently 13 | # will set them to cache forever on cloudflare, and manually purge if necessary 14 | # will set to cache for one day in the browser 15 | 16 | /favicon.png 17 | Cache-Control: public, max-age=86400, s-maxage=604800, immutable 18 | 19 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | http://beta.documentcloud.org/* https://www.documentcloud.org/:splat 301! 2 | https://beta.documentcloud.org/* https://www.documentcloud.org/:splat 301! 3 | 4 | /api/oembed.json https://api.www.documentcloud.org/api/oembed/ 5 | /documents/:id/:path https://api.www.documentcloud.org/files/documents/:id/:path 6 | /assets/* /assets/:splat 7 | /* /index.html 200 8 | -------------------------------------------------------------------------------- /public/dc_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/dc_logo.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/favicon.png -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Italic-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/SourceCodePro-Italic-VariableFont_wght.ttf -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/SourceCodePro-VariableFont_wght.ttf -------------------------------------------------------------------------------- /public/fonts/source-sans-pro-v21-latin-600.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/source-sans-pro-v21-latin-600.eot -------------------------------------------------------------------------------- /public/fonts/source-sans-pro-v21-latin-600.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/source-sans-pro-v21-latin-600.ttf -------------------------------------------------------------------------------- /public/fonts/source-sans-pro-v21-latin-600.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/source-sans-pro-v21-latin-600.woff -------------------------------------------------------------------------------- /public/fonts/source-sans-pro-v21-latin-600.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/source-sans-pro-v21-latin-600.woff2 -------------------------------------------------------------------------------- /public/fonts/source-sans-pro-v21-latin-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/source-sans-pro-v21-latin-700.eot -------------------------------------------------------------------------------- /public/fonts/source-sans-pro-v21-latin-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/source-sans-pro-v21-latin-700.ttf -------------------------------------------------------------------------------- /public/fonts/source-sans-pro-v21-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/source-sans-pro-v21-latin-700.woff -------------------------------------------------------------------------------- /public/fonts/source-sans-pro-v21-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/source-sans-pro-v21-latin-700.woff2 -------------------------------------------------------------------------------- /public/fonts/source-sans-pro-v21-latin-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/source-sans-pro-v21-latin-italic.eot -------------------------------------------------------------------------------- /public/fonts/source-sans-pro-v21-latin-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/source-sans-pro-v21-latin-italic.ttf -------------------------------------------------------------------------------- /public/fonts/source-sans-pro-v21-latin-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/source-sans-pro-v21-latin-italic.woff -------------------------------------------------------------------------------- /public/fonts/source-sans-pro-v21-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/source-sans-pro-v21-latin-italic.woff2 -------------------------------------------------------------------------------- /public/fonts/source-sans-pro-v21-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/source-sans-pro-v21-latin-regular.eot -------------------------------------------------------------------------------- /public/fonts/source-sans-pro-v21-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/source-sans-pro-v21-latin-regular.ttf -------------------------------------------------------------------------------- /public/fonts/source-sans-pro-v21-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/source-sans-pro-v21-latin-regular.woff -------------------------------------------------------------------------------- /public/fonts/source-sans-pro-v21-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/public/fonts/source-sans-pro-v21-latin-regular.woff2 -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | interface PageData { 8 | flash?: { 9 | message: string; 10 | status?: "info" | "warning" | "success" | "error"; 11 | lifespan?: number | null; 12 | }; 13 | } 14 | // interface PageState {} 15 | interface Platform { 16 | context: any; 17 | } 18 | } 19 | 20 | namespace svelteHTML { 21 | interface HTMLAttributes { 22 | "on:vite:preloadError"?: (event: any) => any; 23 | } 24 | 25 | interface HTMLProps { 26 | "on:vite:preloadError"?: (event: any) => any; 27 | } 28 | } 29 | } 30 | 31 | export {}; 32 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | %sveltekit.head% 10 | 11 | 12 |
%sveltekit.body%
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/config/embed.js: -------------------------------------------------------------------------------- 1 | // dedicated, minimal config required for embeds 2 | import * as staging from "./staging.js"; 3 | import * as production from "./production.js"; 4 | 5 | let APP_URL = "https://www.dev.documentcloud.org/"; 6 | let EMBED_URL = "https://www.dev.documentcloud.org/"; 7 | 8 | if (process.env.NODE_ENV === "staging") { 9 | APP_URL = staging.APP_URL; 10 | EMBED_URL = staging.EMBED_URL; 11 | } 12 | 13 | if (process.env.NODE_ENV === "production") { 14 | APP_URL = production.APP_URL; 15 | EMBED_URL = production.EMBED_URL; 16 | } 17 | 18 | export { APP_URL, EMBED_URL }; 19 | -------------------------------------------------------------------------------- /src/config/production.js: -------------------------------------------------------------------------------- 1 | export const DC_BASE = "https://api.www.documentcloud.org"; 2 | export const APP_URL = "https://www.documentcloud.org/"; 3 | export const EMBED_URL = "https://embed.documentcloud.org/"; 4 | export const SQUARELET_BASE = "https://accounts.muckrock.com"; 5 | export const STAFF_ONLY_S3_URL = 6 | "https://s3.console.aws.amazon.com/s3/buckets/s3.documentcloud.org?region=us-east-1&prefix=documents/$$ID$$/&showversions=false"; 7 | -------------------------------------------------------------------------------- /src/config/remote.js: -------------------------------------------------------------------------------- 1 | export const DC_BASE = "https://api.muckcloud.com"; 2 | export const APP_URL = "https://local.muckcloud.com:5173"; 3 | export const EMBED_URL = APP_URL; 4 | export const SQUARELET_BASE = "https://squarelet-staging.herokuapp.com"; 5 | export const STAFF_ONLY_S3_URL = 6 | "https://s3.console.aws.amazon.com/s3/buckets/documentcloud-staging-files?region=us-east-1&prefix=documents/$$ID$$/&showversions=false"; 7 | -------------------------------------------------------------------------------- /src/config/staging.js: -------------------------------------------------------------------------------- 1 | export const DC_BASE = "https://api.muckcloud.com"; 2 | export const APP_URL = process.env.DEPLOY_PRIME_URL || "https://muckcloud.com/"; 3 | export const EMBED_URL = APP_URL; 4 | export const SQUARELET_BASE = "https://squarelet-staging.herokuapp.com"; 5 | export const STAFF_ONLY_S3_URL = 6 | "https://s3.console.aws.amazon.com/s3/buckets/documentcloud-staging-files?region=us-east-1&prefix=documents/$$ID$$/&showversions=false"; 7 | -------------------------------------------------------------------------------- /src/hooks.client.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/src/hooks.client.ts -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | // universal hooks 2 | 3 | export { reroute } from "./lib/utils/embed"; 4 | -------------------------------------------------------------------------------- /src/langs/json/README.md: -------------------------------------------------------------------------------- 1 | # Languages 2 | 3 | This folder contains language files that translate text on the site. 4 | 5 | We use [`svelte-i18n`](https://github.com/kaisermann/svelte-i18n/tree/main) to manage language settings. See its [formatting documentation](https://github.com/kaisermann/svelte-i18n/blob/main/docs/Formatting.md) to handle localization of numbers and dates. 6 | 7 | Use the [find-unused](https://github.com/eyeseast/find-unused) utility to clean out unused translation strings. 8 | -------------------------------------------------------------------------------- /src/legacy/README.md: -------------------------------------------------------------------------------- 1 | # Legacy 2 | 3 | Files from the previous DocumentCloud that are not used, but represent un-implemented features in the SvelteKit version. 4 | 5 | ## Search 6 | 7 | Files for parsing and formatting a string into search terms. 8 | 9 | ## Modification 10 | 11 | Logic for modifying PDFs in the viewer 12 | -------------------------------------------------------------------------------- /src/legacy/modification/Modification.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | -------------------------------------------------------------------------------- /src/legacy/util/README.md: -------------------------------------------------------------------------------- 1 | Common utility functions to handle simple, general purpose tasks 2 | -------------------------------------------------------------------------------- /src/legacy/util/batchDelay.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { batchDelay } from "./batchDelay.js"; 3 | import { timeout } from "./timeout.js"; 4 | 5 | const timeShrink = 5; 6 | 7 | test("batch delay works", async () => { 8 | const expected = [1000 / timeShrink, 3000 / timeShrink, 1000 / timeShrink]; 9 | const results = await batchDelay( 10 | expected, 11 | 1, 12 | 1000 / timeShrink, 13 | async (x) => { 14 | await timeout(x[0]); 15 | return [x[0] * 2]; 16 | }, 17 | ); 18 | expect(results).toEqual(expected.map((x) => x * 2)); 19 | }); 20 | -------------------------------------------------------------------------------- /src/legacy/util/bounds.js: -------------------------------------------------------------------------------- 1 | export function ensureBounds(x, min = 0, max = 1) { 2 | return Math.max(Math.min(x, max), min); 3 | } 4 | -------------------------------------------------------------------------------- /src/legacy/util/callEvery.js: -------------------------------------------------------------------------------- 1 | export function callEvery(fn, times) { 2 | let count = 0; 3 | 4 | return () => { 5 | if (count++ % times == 0) { 6 | fn(); 7 | } 8 | }; 9 | } 10 | 11 | export function callEveryAsync(fn, times) { 12 | let count = 0; 13 | 14 | return async () => { 15 | if (count++ % times == 0) { 16 | await fn(); 17 | } 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/legacy/util/closure.js: -------------------------------------------------------------------------------- 1 | export function smoothify(fn) { 2 | let timer = null; 3 | 4 | return (...args) => { 5 | if (timer != null) { 6 | cancelAnimationFrame(timer); 7 | timer = null; 8 | } 9 | 10 | timer = requestAnimationFrame(() => { 11 | timer = null; 12 | fn(...args); 13 | }); 14 | }; 15 | } 16 | 17 | export function ignoreFirst(closure) { 18 | // Ignore first invocation of a function 19 | let first = true; 20 | return (...args) => { 21 | if (first) { 22 | first = false; 23 | } else { 24 | closure(...args); 25 | } 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/legacy/util/copy.js: -------------------------------------------------------------------------------- 1 | import { pushToast } from "../common/Toast.svelte"; 2 | 3 | export function copy(elem) { 4 | // Copy text within an element 5 | elem.select(); 6 | document.execCommand("copy"); 7 | 8 | // Show toast 9 | pushToast("Copied to clipboard"); 10 | } 11 | -------------------------------------------------------------------------------- /src/legacy/util/data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Patches the provided data with the specified user data. 3 | * @param {Object} data The starting data. 4 | * @param {Object} me The user data. 5 | */ 6 | export function injectMe(data, me) { 7 | data.user = me; 8 | data.organization = me.organization; 9 | return data; 10 | } 11 | -------------------------------------------------------------------------------- /src/legacy/util/dom.js: -------------------------------------------------------------------------------- 1 | export function elementInside(elem, parent) { 2 | if (elem == null) return false; 3 | if (elem == parent) return true; 4 | return elementInside(elem.parentNode, parent); 5 | } 6 | -------------------------------------------------------------------------------- /src/legacy/util/domPurify.js: -------------------------------------------------------------------------------- 1 | import { Svue } from "svue"; 2 | 3 | export const domPurify = new Svue({ 4 | data() { 5 | return { 6 | domPurify: null, 7 | }; 8 | }, 9 | }); 10 | 11 | export async function loadDompurify() { 12 | if (domPurify.domPurify !== null) return; 13 | return import("dompurify").then((module) => { 14 | domPurify.domPurify = module.default ? module.default : module; 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/legacy/util/easing.js: -------------------------------------------------------------------------------- 1 | import { ensureBounds } from "./bounds.js"; 2 | 3 | function sigmoidHelper(t, a) { 4 | return 1 / (1 + Math.exp(-a * t)) - 0.5; 5 | } 6 | 7 | // from https://hackernoon.com/ease-in-out-the-sigmoid-factory-c5116d8abce9 8 | export function sigmoid(t, k = 2) { 9 | return (0.5 / sigmoidHelper(1, k)) * sigmoidHelper(2 * t - 1, k) + 0.5; 10 | } 11 | 12 | export function zeroUntilEnd(t) { 13 | if (t == 1) return 1; 14 | return 0; 15 | } 16 | 17 | export function scale(tweener, ratio, keepInBounds = true) { 18 | return (t) => { 19 | let value = tweener(t / ratio); 20 | if (keepInBounds) value = ensureBounds(value); 21 | return value; 22 | }; 23 | } 24 | 25 | export function interp(tweener, a, b) { 26 | return (t) => tweener(t) * (b - a) + a; 27 | } 28 | -------------------------------------------------------------------------------- /src/legacy/util/epsilon.js: -------------------------------------------------------------------------------- 1 | export function withinPercent(num1, num2, eps) { 2 | if (num1 == null || num2 == null || num2 == 0) return false; 3 | 4 | const ratio = Math.max(num1, num2) / Math.min(num1, num2); 5 | return ratio - 1 <= eps; 6 | } 7 | 8 | export function closeEnough(x, y, epsilon = 0.000001) { 9 | return Math.abs(x - y) < epsilon; 10 | } 11 | -------------------------------------------------------------------------------- /src/legacy/util/iframe.js: -------------------------------------------------------------------------------- 1 | export function inIframe() { 2 | // from https://stackoverflow.com/a/326076 3 | try { 4 | return window.self !== window.top; 5 | } catch (e) { 6 | return true; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/legacy/util/paginate.js: -------------------------------------------------------------------------------- 1 | import session from "@/api/session"; 2 | import { queryBuilder } from "./url.js"; 3 | 4 | import { MAX_PER_PAGE } from "../../config/config.js"; 5 | 6 | /** 7 | * Requests the specified URL and paginates through to return all 8 | * results if additional pages are present. 9 | * @param {string} url The API url to request. 10 | * @param {number?} perPage If present, the per page to request 11 | */ 12 | export async function grabAllPages(url, perPage = MAX_PER_PAGE) { 13 | if (perPage) url = queryBuilder(url, { per_page: perPage }); 14 | const { data } = await session.get(url); 15 | if (data.next) { 16 | // Grab the next page 17 | const next = await grabAllPages(data.next); 18 | return data.results.concat(next); 19 | } else { 20 | return data.results; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/legacy/util/storageManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A localStorage utility class that can be 3 | * used to remember keyed preferences/options 4 | */ 5 | 6 | const KEY_PREFIX = "__documentcloud_"; 7 | 8 | export class StorageManager { 9 | constructor(key) { 10 | this.key = (subkey) => `${KEY_PREFIX}${key}_${subkey}`; 11 | } 12 | 13 | get(key, defaultValue) { 14 | try { 15 | const value = JSON.parse(localStorage.getItem(this.key(key))); 16 | if (value == null) return defaultValue; 17 | return value; 18 | } catch (e) { 19 | // Local storage not available 20 | return defaultValue; 21 | } 22 | } 23 | 24 | set(key, value) { 25 | try { 26 | localStorage.setItem(this.key(key), JSON.stringify(value)); 27 | } catch (e) { 28 | // Local storage not available 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/legacy/util/string.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { allIndices, nFormatter } from "./string.js"; 3 | 4 | test("string all indices", () => { 5 | expect(allIndices("the cat and the hat", "the")).toEqual([0, 12]); 6 | }); 7 | 8 | test("nFormatter", () => { 9 | const tests = [ 10 | { num: 0, digits: 1, result: "0" }, 11 | { num: 12, digits: 1, result: "12" }, 12 | { num: 123, digits: 1, result: "123" }, 13 | { num: 1234, digits: 1, result: "1.2K" }, 14 | { num: 123.456, digits: 1, result: "123.5" }, 15 | { num: 123.456, digits: 2, result: "123.46" }, 16 | { num: 123.456, digits: 4, result: "123.456" }, 17 | { num: 759878, digits: 0, result: "760K" }, 18 | { num: 759878, digits: 1, result: "759.9K" }, 19 | { num: 100000000, digits: 1, result: "100M" }, 20 | { num: 299792458, digits: 1, result: "299.8M" }, 21 | { num: 759878, digits: 0, result: "760K" }, 22 | ]; 23 | tests.forEach(function (test) { 24 | expect(nFormatter(test.num, test.digits)).toEqual(test.result); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/legacy/util/textareaResize.js: -------------------------------------------------------------------------------- 1 | // Adapted from https://svelte.dev/repl/ead0f1fcd2d4402bbbd64eca1d665341?version=3.14.1 2 | 3 | function resize({ target, offset = 2 }) { 4 | target.style.height = "1px"; 5 | 6 | target.style.height = +target.scrollHeight + offset + "px"; 7 | } 8 | 9 | export function textAreaResize(el, offset = 2) { 10 | if (offset != 0) { 11 | el.style.overflow = "auto"; 12 | el.style.boxSizing = "border-box"; 13 | } 14 | 15 | const resizer = () => resize({ target: el, offset }); 16 | 17 | el.addEventListener("input", resizer); 18 | window.addEventListener("resize", resizer); 19 | 20 | resizer(); 21 | 22 | return { 23 | destroy: () => { 24 | el.removeEventListener("input", resizer); 25 | window.removeEventListener("resize", resizer); 26 | }, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/legacy/util/timeout.js: -------------------------------------------------------------------------------- 1 | export function timeout(ms) { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | } 4 | -------------------------------------------------------------------------------- /src/legacy/util/transition.js: -------------------------------------------------------------------------------- 1 | import { cubicOut } from "svelte/easing"; 2 | 3 | export function slideHorizontal( 4 | node, 5 | { delay = 0, duration = 400, easing = cubicOut }, 6 | ) { 7 | const style = getComputedStyle(node); 8 | const opacity = +style.opacity; 9 | const width = parseFloat(style.width); 10 | const padding_left = parseFloat(style.paddingLeft); 11 | const padding_right = parseFloat(style.paddingRight); 12 | const margin_left = parseFloat(style.marginLeft); 13 | const margin_right = parseFloat(style.marginRight); 14 | const border_left_width = parseFloat(style.borderLeftWidth); 15 | const border_right_width = parseFloat(style.borderRightWidth); 16 | 17 | return { 18 | delay, 19 | duration, 20 | easing, 21 | css: (t) => 22 | `overflow: hidden;` + 23 | `opacity: ${Math.min(t * 20, 1) * opacity};` + 24 | `width: ${t * width}px;` + 25 | `padding-left: ${t * padding_left}px;` + 26 | `padding-right: ${t * padding_right}px;` + 27 | `margin-left: ${t * margin_left}px;` + 28 | `margin-right: ${t * margin_right}px;` + 29 | `border-left-width: ${t * border_left_width}px;` + 30 | `border-right-width: ${t * border_right_width}px;`, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/legacy/util/visibility.js: -------------------------------------------------------------------------------- 1 | // Adapted from https://svelte.dev/repl/ead0f1fcd2d4402bbbd64eca1d665341?version=3.14.1 2 | 3 | export function showIfFullyVisible(el) { 4 | // for very old browsers we don't support, bail 5 | if (typeof window.IntersectionObserver === "undefined") return; 6 | 7 | const setVisible = (visibility) => { 8 | el.style.visibility = visibility ? "visible" : "hidden"; 9 | }; 10 | 11 | let observer = new IntersectionObserver( 12 | (e) => { 13 | if (e == null || e.length != 1) return; 14 | setVisible( 15 | e[0].intersectionRatio == 1 || 16 | (e[0].intersectionRect.width > 0 && 17 | e[0].intersectionRect.width >= e[0].boundingClientRect.width), 18 | ); 19 | }, 20 | { 21 | threshold: 1, 22 | margin: "30px 0 30px 0", 23 | }, 24 | ); 25 | observer.observe(el); 26 | 27 | return { 28 | destroy: () => { 29 | if (observer != null) observer.unobserve(el); 30 | }, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/legacy/util/wrapLoad.js: -------------------------------------------------------------------------------- 1 | export async function wrapLoadSeparate(loadWritable, errorStore, fn) { 2 | loadWritable.set(true); 3 | try { 4 | return await fn(); 5 | } catch (e) { 6 | console.error(e); 7 | errorStore.error = e; 8 | } finally { 9 | loadWritable.set(false); 10 | } 11 | } 12 | 13 | export async function wrapSeparate(loadStore, errorStore, fn) { 14 | if (loadStore != null) loadStore.loading = true; 15 | try { 16 | return await fn(); 17 | } catch (e) { 18 | console.error(e); 19 | errorStore.error = e; 20 | } finally { 21 | if (loadStore != null) loadStore.loading = false; 22 | } 23 | } 24 | 25 | export async function wrapLoad(store, fn) { 26 | return await wrapSeparate(store, store, fn); 27 | } 28 | 29 | export async function wrapMultiple(store, ...fns) { 30 | return await wrapLoad(store, () => Promise.all(fns.map((fn) => fn()))); 31 | } 32 | 33 | export async function wrapMultipleSeparate(loadStore, errorStore, ...fns) { 34 | return await wrapSeparate(loadStore, errorStore, () => 35 | Promise.all(fns.map((fn) => fn())), 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/api/embed.ts: -------------------------------------------------------------------------------- 1 | // api utilities for embeds 2 | 3 | import type { OEmbed } from "./types"; 4 | import { BASE_API_URL } from "@/config/config.js"; 5 | 6 | /** 7 | * Generate an oembed URL for a given DocumentCloud URL 8 | * @export 9 | */ 10 | export function embedUrl(url: URL | string): URL { 11 | const endpoint = new URL("oembed/", BASE_API_URL); 12 | endpoint.searchParams.set("url", url.toString()); 13 | return endpoint; 14 | } 15 | 16 | /** 17 | * Fetch embed code from the OEmbed API endpoint 18 | * @deprecated 19 | * @export 20 | */ 21 | export async function getEmbed(url: URL | string): Promise { 22 | const endpoint = embedUrl(url); 23 | 24 | return fetch(endpoint, { credentials: "include" }).then((r) => r.json()); 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/api/flatpages.ts: -------------------------------------------------------------------------------- 1 | import type { APIResponse, Flatpage } from "./types"; 2 | 3 | import { BASE_API_URL } from "@/config/config"; 4 | import { getApiResponse } from "$lib/utils/api"; 5 | 6 | /** 7 | * Return the tip of the day flatpage 8 | * @param fetch 9 | */ 10 | export async function getTipOfDay( 11 | fetch = globalThis.fetch, 12 | ): Promise { 13 | const { data, error } = await get("/tipofday/", fetch); 14 | if (!error) { 15 | return data; 16 | } 17 | } 18 | 19 | /** 20 | * Get a single flat page 21 | * @param path 22 | * @param fetch 23 | */ 24 | export async function get( 25 | path: string, 26 | fetch = globalThis.fetch, 27 | ): Promise> { 28 | if (!path.startsWith("/")) { 29 | path = "/" + path; 30 | } 31 | 32 | const endpoint = new URL("flatpages" + path, BASE_API_URL); 33 | const resp = await fetch(endpoint, { credentials: "include" }).catch( 34 | console.warn, 35 | ); 36 | 37 | return getApiResponse(resp); 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/api/tests/embed.test.ts: -------------------------------------------------------------------------------- 1 | import type { Document, Note } from "../types"; 2 | 3 | import { test, describe, expect } from "vitest"; 4 | 5 | import { BASE_API_URL } from "@/config/config.js"; 6 | import { embedUrl } from "../embed"; 7 | 8 | import * as documents from "../documents"; 9 | import * as notes from "../notes.js"; 10 | 11 | import document from "@/test/fixtures/documents/document.json"; 12 | import note from "@/test/fixtures/notes/note.json"; 13 | 14 | describe("embed tests", () => { 15 | test("embedUrl", () => { 16 | // document 17 | const docUrl = documents.canonicalUrl(document as Document); 18 | 19 | expect(embedUrl(docUrl)).toStrictEqual( 20 | new URL( 21 | `oembed/?url=${encodeURIComponent(docUrl.toString())}`, 22 | BASE_API_URL, 23 | ), 24 | ); 25 | 26 | // note 27 | const noteUrl = notes.noteUrl(document as Document, note as Note); 28 | expect(embedUrl(noteUrl)).toStrictEqual( 29 | new URL( 30 | `oembed/?url=${encodeURIComponent(noteUrl.toString())}`, 31 | BASE_API_URL, 32 | ), 33 | ); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/lib/api/tests/flatpages.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, describe, expect, test, vi } from "vitest"; 2 | 3 | import { BASE_API_URL } from "@/config/config"; 4 | import { flatpage } from "@/test/fixtures/flatpages"; 5 | import * as flatpages from "../flatpages"; 6 | 7 | describe("flatpages", () => { 8 | afterEach(() => { 9 | vi.restoreAllMocks(); 10 | }); 11 | 12 | test("flatpages.get", async () => { 13 | const mockFetch = vi.fn().mockImplementation(async (endpoint, options) => { 14 | return { 15 | ok: true, 16 | status: 200, 17 | async json() { 18 | return flatpage; 19 | }, 20 | }; 21 | }); 22 | 23 | const { error, data } = await flatpages.get(flatpage.url, mockFetch); 24 | 25 | expect(error).toBeUndefined(); 26 | expect(data).toEqual(flatpage); 27 | 28 | expect(mockFetch).toHaveBeenCalledWith( 29 | new URL("/api/flatpages" + flatpage.url, BASE_API_URL), 30 | { 31 | credentials: "include", 32 | }, 33 | ); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/lib/components/accounts/Avatar.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | {#if user} 11 | {#if user.avatar_url} 12 | Avatar 13 | {:else} 14 | 15 | {/if} 16 | {:else if org} 17 | {#if org.avatar_url} 18 | Avatar 19 | {:else} 20 | 21 | {/if} 22 | {/if} 23 |
24 | 25 | 42 | -------------------------------------------------------------------------------- /src/lib/components/accounts/stories/Avatar.stories.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | 19 | 20 | 29 | 30 | 31 | 40 | -------------------------------------------------------------------------------- /src/lib/components/accounts/stories/Mailkey.stories.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | 19 | 23 | 27 | -------------------------------------------------------------------------------- /src/lib/components/accounts/stories/Unverified.stories.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/accounts/tests/Mailkey.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | import { render } from "@testing-library/svelte"; 3 | import Mailkey from "../Mailkey.svelte"; 4 | 5 | test("Mailkey", () => { 6 | let result = render(Mailkey); 7 | expect(result.container).toMatchSnapshot(); 8 | result = render(Mailkey, {message: 'Something happened'}); 9 | expect(result.container).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /src/lib/components/accounts/tests/UserMenu.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | import { render } from "@testing-library/svelte"; 3 | import { me as meFixture } from "@/test/fixtures/accounts"; 4 | import UserMenu from "../UserMenu.svelte"; 5 | 6 | test("UserMenu", async () => { 7 | let result = render(UserMenu, {user: meFixture}); 8 | expect(result.container).toMatchSnapshot(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/lib/components/addons/stories/AddOnMeta.stories.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 17 | 18 | 19 | 33 | -------------------------------------------------------------------------------- /src/lib/components/addons/stories/AddOnsNavigation.stories.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/lib/components/addons/stories/History.stories.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | 24 | 29 | 34 | 39 | 44 | -------------------------------------------------------------------------------- /src/lib/components/addons/stories/Scheduled.stories.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /src/lib/components/addons/stories/ScheduledEvent.stories.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/lib/components/addons/tests/AddOnListItem.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | import { render } from "@testing-library/svelte"; 3 | 4 | import AddOnListItem from "../AddOnListItem.svelte"; 5 | import { addon, premiumAddon } from "@/test/fixtures/addons"; 6 | 7 | test("AddOnListItem", () => { 8 | const result = render(AddOnListItem, { addon }); 9 | expect(result.getByRole("heading").textContent).toEqual(addon.name); 10 | expect(result.container).toMatchSnapshot(); 11 | const premium = render(AddOnListItem, { addon: premiumAddon }); 12 | expect(premium.getByRole("status").textContent).toContain("Premium"); 13 | expect(premium.container).toMatchSnapshot(); 14 | }); 15 | -------------------------------------------------------------------------------- /src/lib/components/addons/tests/AddOnsNavigation.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | import { render } from "@testing-library/svelte"; 3 | import AddOnsNavigation from "../AddOnsNavigation.svelte"; 4 | 5 | test("AddOnsNavigation", () => { 6 | const result = render(AddOnsNavigation); 7 | expect(result.container).toMatchSnapshot(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/lib/components/common/Badge.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
13 | {#if $$slots.icon}{/if} 14 | {label} 15 |
16 | 17 | 41 | -------------------------------------------------------------------------------- /src/lib/components/common/Card.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | -------------------------------------------------------------------------------- /src/lib/components/common/Copy.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /src/lib/components/common/Empty.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | {#if icon}{/if} 9 | 10 |
11 | 12 | 28 | -------------------------------------------------------------------------------- /src/lib/components/common/Error.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 7 | 8 |
9 | 10 | 25 | -------------------------------------------------------------------------------- /src/lib/components/common/Field.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 19 | 20 | {#if $$slots.help}
{/if} 21 |
22 | 23 | 30 | -------------------------------------------------------------------------------- /src/lib/components/common/Menu.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 30 | -------------------------------------------------------------------------------- /src/lib/components/common/MenuInsert.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/lib/components/common/Metadata.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 13 | 36 | -------------------------------------------------------------------------------- /src/lib/components/common/PageToolbar.svelte: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 |
11 | {#if $$slots.left} 12 |
13 | 14 |
15 | {/if} 16 | {#if $$slots.center} 17 |
18 | 19 |
20 | {/if} 21 | {#if $$slots.right} 22 |
23 | 24 |
25 | {/if} 26 |
27 | 28 | 54 | -------------------------------------------------------------------------------- /src/lib/components/common/PlausibleTracker.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/common/Premium.svelte: -------------------------------------------------------------------------------- 1 | 6 | 20 | 21 | {#if isPremium} 22 | 23 | {:else} 24 | 25 | {/if} 26 | -------------------------------------------------------------------------------- /src/lib/components/common/ShowSize.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | {#if size >= 1 && size <= MAX_EDIT_BATCH} 8 | 9 | 10 | {:else if size > MAX_EDIT_BATCH} 11 | 12 | 13 | {:else} 14 | 15 | 16 | {/if} 17 | -------------------------------------------------------------------------------- /src/lib/components/common/SignedIn.svelte: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | {#if isSignedIn($me)} 13 | 14 | {:else} 15 | 16 | {/if} 17 | -------------------------------------------------------------------------------- /src/lib/components/common/stories/Action.stories.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | Open 16 | 17 | 18 | 19 | Edit 20 | 21 | 22 | 23 | Edit 24 | 25 | -------------------------------------------------------------------------------- /src/lib/components/common/stories/Badge.stories.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | 22 | 23 | 31 | -------------------------------------------------------------------------------- /src/lib/components/common/stories/Empty.stories.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | Nothing to see here! 15 | 16 | 17 | 18 | No results found for search "FYSA" 19 | 20 | -------------------------------------------------------------------------------- /src/lib/components/common/stories/Error.stories.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | Something has gone terribly wrong! 14 | 15 | -------------------------------------------------------------------------------- /src/lib/components/common/stories/FieldLabel.stories.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | 23 | 24 | 25 | Never-Ending Salad & Breadsticks 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/lib/components/common/stories/HighlightGroup.stories.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/lib/components/common/stories/KV.stories.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 | 40 |
41 |
42 | 43 | 48 | -------------------------------------------------------------------------------- /src/lib/components/common/stories/Logo.stories.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /src/lib/components/common/stories/Metadata.stories.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | March 21, 2024 17 | 18 | 19 | 20 | 21 | March 21, 2024 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/lib/components/common/stories/PageToolbar.stories.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 |

Left

18 |

Center

19 |

Right

20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/lib/components/common/stories/Pin.stories.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/lib/components/common/stories/RelativeTime.stories.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/lib/components/common/stories/Tooltip.stories.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/lib/components/common/tests/Button.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { render, screen } from "@testing-library/svelte"; 3 | 4 | import Button from "../Button.svelte"; 5 | 6 | describe("Button", () => { 7 | it("renders a default button", () => { 8 | render(Button); 9 | 10 | const button = screen.getByRole("button"); 11 | 12 | expect(button).toBeInTheDocument(); 13 | expect(button.tagName).toStrictEqual("BUTTON"); 14 | }); 15 | 16 | it("renders a link when href is used", () => { 17 | render(Button, { props: { href: "https://www.documentcloud.org" } }); 18 | 19 | const link = screen.getByText(/Submit/); 20 | 21 | expect(link).toBeInTheDocument(); 22 | expect(link.tagName).toStrictEqual("A"); 23 | }); 24 | 25 | it("sets name and value properties in button mode", () => { 26 | render(Button, { props: { name: "action", value: "add" } }); 27 | 28 | const button = screen.getByRole("button"); 29 | 30 | expect(button).toBeInTheDocument(); 31 | expect(button.getAttribute("name")).toEqual("action"); 32 | expect(button.getAttribute("value")).toEqual("add"); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/lib/components/common/tests/ShowSize.demo.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 |

{size} items

11 | Zero items 12 | Too many items! 13 |
14 | -------------------------------------------------------------------------------- /src/lib/components/common/tests/ShowSize.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { render } from "@testing-library/svelte"; 3 | import ShowSizeDemo from "./ShowSize.demo.svelte"; 4 | import { MAX_EDIT_BATCH } from "@/config/config.js"; 5 | 6 | describe("ShowSize.svelte", () => { 7 | it("renders default slot when size is within valid range", () => { 8 | const { getByText } = render(ShowSizeDemo, { 9 | props: { 10 | size: 5, 11 | }, 12 | }); 13 | 14 | expect(getByText("5 items")).toBeTruthy(); 15 | }); 16 | 17 | it("renders oversize slot when size exceeds MAX_EDIT_BATCH", () => { 18 | const { getByText } = render(ShowSizeDemo, { 19 | props: { 20 | size: MAX_EDIT_BATCH + 1, 21 | }, 22 | }); 23 | expect(getByText("Too many items!")).toBeTruthy(); 24 | }); 25 | 26 | it("renders empty slot when size is 0", () => { 27 | const { getByText } = render(ShowSizeDemo, { 28 | props: { 29 | size: 0, 30 | }, 31 | }); 32 | expect(getByText("Zero items")).toBeTruthy(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/lib/components/common/tests/Toast.test.ts: -------------------------------------------------------------------------------- 1 | import { vi, describe, it, expect } from "vitest"; 2 | import { render } from "@testing-library/svelte"; 3 | import { userEvent } from "@testing-library/user-event"; 4 | import Toast from "../Toast.svelte"; 5 | 6 | describe("Toast", () => { 7 | it("calls close when user clicks close button", async () => { 8 | const user = userEvent.setup(); 9 | const container = render(Toast, { lifespan: 1000 }); 10 | // Spy on the "close" event 11 | const closeSpy = vi.fn(); 12 | container.component.$on("close", closeSpy); 13 | // Click the close button 14 | const closeButton = container.getByRole("button"); 15 | await user.click(closeButton); 16 | // Check if the "close" event was dispatched 17 | expect(closeSpy).toHaveBeenCalledTimes(1); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/lib/components/documents/Pending.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {#if pending.length > 0} 12 | 19 | 20 |

{$_("processingBar.processing")}

21 |

22 | {$_("processingBar.processingDocuments", { 23 | values: { n: pending.length }, 24 | })} 25 |

26 |
27 | {/if} 28 | -------------------------------------------------------------------------------- /src/lib/components/documents/stories/CustomizeEmbed.stories.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/lib/components/documents/stories/Metadata.stories.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/lib/components/documents/stories/NoteHighlights.stories.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/lib/components/documents/stories/PageHighlights.stories.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/lib/components/documents/stories/Pending.stories.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/lib/components/documents/stories/Projects.stories.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/lib/components/documents/stories/Revisions.stories.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/lib/components/documents/stories/Thumbnail.stories.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/lib/components/documents/tests/DocumentListItem.test.ts: -------------------------------------------------------------------------------- 1 | import { vi, describe, it, beforeEach, expect } from "vitest"; 2 | import { render } from "@testing-library/svelte"; 3 | import { document } from "@/test/fixtures/documents"; 4 | import DocumentListItem from "../DocumentListItem.svelte"; 5 | 6 | describe("DocumentListItem", async () => { 7 | beforeEach(() => { 8 | // Mock Date.now() to return a fixed timestamp 9 | vi.spyOn(Date, "now").mockReturnValue(1620000000000); // Mocked timestamp 10 | }); 11 | it("renders", () => { 12 | let result = render(DocumentListItem, { document }); 13 | expect(result.container).toMatchSnapshot(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/lib/components/embeds/stories/DocumentEmbed.stories.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 31 | 32 | 33 | 37 | 38 | 43 | -------------------------------------------------------------------------------- /src/lib/components/forms/DeleteProject.svelte: -------------------------------------------------------------------------------- 1 | 4 | 22 | 23 |
24 |

{$_("projects.delete.really", { values: { project: project.title } })}

25 | 26 | 27 | 31 | 34 | 35 |
36 | 37 | 45 | -------------------------------------------------------------------------------- /src/lib/components/forms/stories/ConfirmRedaction.stories.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 |
20 | 21 |
22 |
23 | -------------------------------------------------------------------------------- /src/lib/components/forms/stories/DeleteProject.stories.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/forms/stories/EditData.stories.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/lib/components/forms/stories/EditNote.stories.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/forms/stories/EditProject.stories.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 |
16 | 17 |
18 |
19 | 20 | 21 |
22 | 23 |
24 |
25 | 26 | 31 | -------------------------------------------------------------------------------- /src/lib/components/forms/stories/InviteCollaborator.stories.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 |
17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /src/lib/components/forms/stories/Projects.stories.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | -------------------------------------------------------------------------------- /src/lib/components/forms/stories/RemoveCollaborator.stories.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/lib/components/forms/stories/Reprocess.stories.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 |
23 |

24 | {$_("dialogReprocessDialog.title")} 25 |

26 |
27 |
28 |
29 | 30 | 31 | 32 |
33 |

34 | {$_("dialogReprocessDialog.title")} 35 |

36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /src/lib/components/forms/stories/RevisionControl.stories.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | action("change")} /> 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/lib/components/forms/stories/Sections.stories.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 |
21 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /src/lib/components/forms/stories/UpdateCollaborator.stories.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/lib/components/forms/stories/UserFeedback.stories.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 |
21 | 22 |
23 |
24 | 25 | 26 |
27 | 28 |
29 |
30 | 31 | 32 |
33 | 34 |
35 |
36 | 37 | 38 |
39 | 40 |
41 |
42 | -------------------------------------------------------------------------------- /src/lib/components/icons/Premium.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | {title} 9 | 14 | 19 | 20 | 21 | 27 | -------------------------------------------------------------------------------- /src/lib/components/inputs/Choices.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /src/lib/components/inputs/Language.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 |