+
+
+
+ ${this.renderContent()}
+
+
+ `;
+ }
+
+ onTagClick = (ev :CustomEvent
) =>{
+ navigate(this, this.isActive(`/ui/doc/api/${ev.detail}`)? `/ui/doc/api/`:`/ui/doc/api/${ev.detail}`);
+ }
+
+ download = ()=>{
+ const el = document.createElement("a");
+ el.setAttribute("href", `data:application/json;base64,`+btoa(JSON.stringify(definitions, null, 2)));
+ el.setAttribute("download", "openapi.json");
+ el.style.display = 'none';
+ document.body.appendChild(el);
+ el.click();
+ document.body.removeChild(el);
+ }
+}
+
+@customElement("tag-block")
+export class TagBlock extends LitElement{
+ @property({attribute: true, type: String})
+ name :string;
+ @property({attribute: false, type: String})
+ description :string;
+
+ @property({attribute: true, reflect: true, type: Boolean})
+ expanded: boolean;
+
+ @property({attribute:false, type: String})
+ selected ?:string;
+
+ handleClick = (e:MouseEvent)=>{
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(new CustomEvent("select", {detail: this.name}));
+ }
+
+ handleSelect = (e:CustomEvent)=>{
+
+ }
+
+ paths() :Array<[string, Path]> {
+ return Object.entries(definitions.paths);
+ }
+
+ operations() :Array<[string, Method, Parameters, Operation ]>{
+ return operations.filter(op=>op[3].tags?.indexOf(this.name)!= -1);
+ }
+
+ render(){
+ return html`
+
+
${this.expanded?this.operations().map(([pathname, method, parameters, operation])=>{
+ return html``
+ }):null}
+
`
+ }
+ static styles = [
+ styles,
+ apiStyles,
+ ];
+}
+
+
+@customElement("op-line")
+export class OperationLine extends LitElement{
+ @property({attribute: true, reflect: true, type: String})
+ method :Method;
+ @property({attribute: true, reflect: true, type: String})
+ pathname :string;
+
+ @property({attribute: false, type: Object})
+ operation :Operation;
+
+ @property({attribute: false, type: Object})
+ parameters :Parameters;
+
+ @property({attribute: true, reflect: true, type: Boolean})
+ expanded :boolean = false;
+
+ createRenderRoot() {
+ return this;
+ }
+
+ connectedCallback(): void {
+ this.classList.add("path-line");
+ super.connectedCallback();
+ }
+
+ protected update(changedProperties: Map): void {
+ if(changedProperties.has("pathname")){
+ this.id = this.pathname;
+ }
+ super.update(changedProperties);
+ }
+
+
+ onclick = (ev: MouseEvent) => {
+ ev.stopPropagation();
+ this.expanded = !this.expanded;
+ }
+
+ render(){
+ if(this.expanded) console.log("Operation :", this.parameters, this.operation);
+ const methodName = (this.method.startsWith("x-"))? this.method.slice(2) :this.method;
+ return html`
+
+ ${methodName}
+
+
+ ${this.pathname}
+ ${(this.expanded && this.parameters?.length)? html`
+ Parameters
+ ${this.parameters.map(p=>html`
+
${p.name}
+
+ ${unsafeHTML(p.description)}
+
+
`)}
+
+ `:null}
+
+ ${unsafeHTML(this.operation.description)}
+
+ `;
+ }
+}
\ No newline at end of file
diff --git a/source/ui/screens/Doc/oas.d.ts b/source/ui/screens/Doc/oas.d.ts
new file mode 100644
index 00000000..a0a00544
--- /dev/null
+++ b/source/ui/screens/Doc/oas.d.ts
@@ -0,0 +1,287 @@
+export interface Openapi2 {
+ openapi: string;
+ info: Info;
+ servers: Server[];
+ tags: Tag[];
+ paths: Paths;
+ components: Components;
+}
+
+export interface Components {
+ parameters: Parameters;
+ responses: Responses;
+ schemas: Schemas;
+}
+
+export interface File {
+ name: string;
+ in: string;
+ required: boolean;
+ schema: AuthorClass;
+ description: string;
+ examples?: Examples;
+ example?: string;
+}
+
+export interface Examples {
+ folder: Document;
+ thumbnail: Document;
+ document: Document;
+}
+
+export interface Document {
+ summary: string;
+ value: string;
+}
+
+export interface AuthorClass {
+ type: Type;
+}
+
+export enum Type {
+ Boolean = "boolean",
+ Object = "object",
+ String = "string",
+}
+
+export interface Responses {
+ HTTPError: HTTPError;
+}
+
+export interface HTTPError {
+ description: string;
+ content: HTTPErrorContent;
+}
+
+export interface HTTPErrorContent {
+ "application/json": PurpleApplicationJSON;
+ "text/plain": TextPlain;
+ "text/html": TextHTML;
+}
+
+export interface PurpleApplicationJSON {
+ schema: PurpleSchema;
+}
+
+export interface PurpleSchema {
+ type: Type;
+ required: string[];
+ properties: SchemaProperties;
+}
+
+export interface SchemaProperties {
+ code: Code;
+ message: AuthorClass;
+}
+
+export interface Code {
+ description: string;
+ type: string;
+ format: string;
+ minimum: number;
+ maximum: number;
+}
+
+export interface TextHTML {
+ schema: AuthorClass;
+}
+
+export interface TextPlain {
+ schema: Username;
+}
+
+export interface Username {
+ type: Type;
+ example: string;
+}
+
+export interface Schemas {
+ Scene: Scene;
+ User: User;
+ Uid: Uid;
+ AccessType: AccessType;
+}
+
+export interface AccessType {
+ type: Type;
+ enum: string[];
+}
+
+export interface Scene {
+ type: Type;
+ required: string[];
+ properties: SceneProperties;
+}
+
+export interface SceneProperties {
+ ctime: Time;
+ mtime: Time;
+ author_id: AuthorID;
+ author: AuthorClass;
+ id: AuthorID;
+ name: AuthorClass;
+ thumb: Thumb;
+ access: Access;
+}
+
+export interface Access {
+ type: Type;
+ required: string[];
+ properties: AccessProperties;
+}
+
+export interface AccessProperties {
+ default: Any;
+ any: Any;
+ user: Any;
+}
+
+export interface Any {
+ "$ref:\"#/components/schemas/AccessType\"": null;
+}
+
+export interface AuthorID {
+ "$ref:\"#/components/schemas/Uid\"": null;
+}
+
+export interface Time {
+ type: Type;
+ format: string;
+}
+
+export interface Thumb {
+ type: Type;
+ desciption: string;
+}
+
+export interface Uid {
+ type: Type;
+ pattern: string;
+ description: string;
+}
+
+export interface User {
+ type: Type;
+ required: string[];
+ properties: UserProperties;
+}
+
+export interface UserProperties {
+ uid: UidElement;
+ username: Username;
+ isAdministrator: AuthorClass;
+}
+
+export interface UidElement {
+ $ref: string;
+}
+
+export interface Info {
+ title: string;
+ version: string;
+ summary: string;
+ description: string;
+ contact: Contact;
+ license: License;
+}
+
+export interface Contact {
+ name: string;
+ url: string;
+ email: string;
+}
+
+export interface License {
+ name: string;
+ url: string;
+}
+
+
+export type Paths = Record;
+
+export type Path = Record &{
+ summary?: string,
+ parameters: Parameters,
+};
+
+export type Parameters = Parameter[]
+
+export interface Parameter{
+ name :string;
+ in :"path"|"query";
+
+ description :string;
+}
+
+export interface Operation {
+ tags: string[];
+ operationId: string;
+ description: string;
+ responses?: { [key: string]: Response };
+ requestBody?: RequestBody;
+}
+
+export type Method = "get"|"post"|"put"|"delete"|"patch"|"x-mkcol"|"x-propfind"|"x-copy"|"x-move"
+
+export interface RequestBody {
+ description: string;
+ required: boolean;
+ content: RequestBodyContent;
+}
+
+export interface RequestBodyContent {
+ "application/json"?: TextHTML;
+ "model/gltf-binary"?: ModelGltfBinary;
+}
+
+export interface ModelGltfBinary {
+ schema: ModelGltfBinarySchema;
+}
+
+export interface ModelGltfBinarySchema {
+ type: Type;
+ format: string;
+ description: string;
+}
+
+export interface Response {
+ description?: string;
+ content?: ResponseContent;
+ $ref?: string;
+}
+
+export interface ResponseContent {
+ "application/json": FluffyApplicationJSON;
+}
+
+export interface FluffyApplicationJSON {
+ schema: FluffySchema;
+}
+
+export interface FluffySchema {
+ type: string;
+ items: Items;
+}
+
+export interface Items {
+ type?: Type;
+ required?: string[];
+ properties?: ItemsProperties;
+ $ref?: string;
+}
+
+export interface ItemsProperties {
+ uid: UidElement;
+ username: AuthorClass;
+ access: UidElement;
+}
+
+
+export interface Server {
+ url: string;
+}
+
+export interface Tag {
+ name: string;
+ description: string;
+}
diff --git a/source/ui/screens/Doc/openapi.yml b/source/ui/screens/Doc/openapi.yml
new file mode 100644
index 00000000..f4761806
--- /dev/null
+++ b/source/ui/screens/Doc/openapi.yml
@@ -0,0 +1,380 @@
+openapi: '3.1.0'
+info:
+ title: eCorpus
+ version: '1.0.0'
+ summary: HTTP API for eCorpus
+ description: |
+ This HTTP API provides all necessary routes to access and edit scenes stored on an eCorpus instance under the `/scenes` path.
+ Additionally it provides a number of namespaced utilities
+ for **users** management (`/users`),
+ **authentication** and ACL edition (`/auth`),
+ changes **history** management (`/history`)
+ or gathering scenes under **collections** (`/tags`).
+
+ It provides some webDAV utility routes for the `/scenes` resources
+ but is far from [Class 1](http://www.webdav.org/specs/rfc4918.html#rfc.section.18.1) Compliance:
+ Only routes that are necessary for proper [Voyager](https://smithsonian.github.io/dpo-voyager/) support are implemented.
+ WebDAV-specific methods are defined as an extension with a `x-` prefix to prevent breaking openAPI tooling
+
+ Other namespaces tends to adhere to a stricter REST philosophy where possible.
+ contact:
+ name: eCorpus Support
+ url: https://github.com/Holusion/eCorpus
+ email: contact@holusion.com
+ license:
+ name: Apache 2.0
+ url: https://www.apache.org/licenses/LICENSE-2.0.html
+
+servers:
+ - url: https://ecorpus.holusion.com
+
+tags:
+ - name: admin
+ description: Administrative tasks routes
+ - name: auth
+ description: Authentication, access control querying and edition routes.
+ - name: history
+ description: |
+ history management utilities for the `/scenes` namespace.
+ Scene names in `/history` directly and uniquely maps to scenes in `/scenes`.
+ - name: scenes
+ description: |
+ Where all the actual data is stored
+ API design for the `/scenes/*` makes use of the liberal definition of
+ [GET for collections](https://datatracker.ietf.org/doc/html/rfc2518#section-8.4) in the webDAV specification
+ to return well-defined JSON documents for those queries, allowing most use cases to bypass cumbersome PROPFIND queries
+ - name: tags
+ description: |
+ collections (tags) management routes.
+ - name: users
+ description: Users management
+
+paths:
+ /scenes:
+ summary: Collection of the scenes stored on this service
+ get:
+ tags: [scenes]
+ operationId: getScenes
+ description: |
+ get a list of scenes with optional search parameters. Similar to PROPFIND but will return JSON.
+ Provides advanced search and pagination semantics
+ responses:
+ '200':
+ description: a list of scenes matching this query
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Scene"
+ post:
+ tags: [scenes]
+ operationId: postScenes
+ description: import an archive of scenes to be extracted into the `scenes/` folder
+ x-propfind:
+ tags: [scenes]
+ operationId: propfindScenes
+ description: fetch all readable content in `scenes/`
+ /scenes/{scene}:
+ parameters:
+ - $ref: '#/components/parameters/scene'
+ get:
+ tags: [scenes]
+ operationId: getScene
+ description: get a scene's metadata.
+ x-mkcol:
+ tags: [scenes]
+ operationId: mkScene
+ description: creates a new empty scene. This scene will essentially be invisible until populated
+ x-propfind:
+ tags: [scenes]
+ operationId: propfindScene
+ description: fetch the scene's content
+ delete:
+ tags: [scenes]
+ operationId: deleteScene
+ description: Archives a scene
+ post:
+ tags: [scenes]
+ operationId: postScene
+ description: creates a new scene using attached data
+ requestBody:
+ description: "scene initialization data"
+ required: true
+ content:
+ model/gltf-binary:
+ schema:
+ type: string
+ format: binary
+ description: A `.glb` model file
+
+ patch:
+ tags: [scenes]
+ operationId: patchScene
+ description: Edit scene's metadata
+ requestBody:
+ description: Scene patch data
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ /scenes/{scene}/{file}:
+ parameters:
+ - $ref: '#/components/parameters/scene'
+ - $ref: '#/components/parameters/file'
+ get:
+ tags: [scenes]
+ operationId: getFile
+ description: get a file in scene
+ put:
+ tags: [scenes]
+ operationId: putFile
+ description: overwrite the file with new content
+ x-copy:
+ tags: [scenes]
+ operationId: copyFile
+ description: copy a file to another location in the same scene
+ x-move:
+ tags: [scenes]
+ operationId: moveFile
+ description: move a file to another location in the same scene
+ delete:
+ tags: [scenes]
+ operationId: deleteFile
+ description: archives a file. It is still accessible through the history API
+ x-mkcol:
+ tags: [scenes]
+ operationId: mkFolder
+ description: creates a folder in a scene
+ x-propfind:
+ tags: [scenes]
+ operationId: propfindFile
+ description: get a file's properties
+ /history/{scene}:
+ parameters:
+ - $ref: '#/components/parameters/scene'
+ get:
+ tags: [history]
+ operationId: getHistory
+ description: get a full history of a scene's modifications
+ post:
+ tags: [history]
+ operationId: postHistory
+ description: edit a scene's history
+ /history/{scene}/files:
+ parameters:
+ - $ref: '#/components/parameters/scene'
+ get:
+ tags: [history]
+ operationId: getFileHistory
+ description: list all files in the scenes in their current state
+ /tags:
+ get:
+ tags: [tags]
+ operationId: getTags
+ description: get a list of tags on this server
+ /tags/{tag}:
+ parameters:
+ - name: tag
+ in: path
+ required: true
+ schema: {type: "string"}
+ description: name of a tag
+ get:
+ tags: [tags]
+ operationId: getTag
+ description: get all scenes associated with this tag
+ /users:
+ get:
+ tags: [users]
+ operationId: getUsers
+ description: get a list of registered users
+ responses:
+ '200':
+ description: An array of all users registered on this server
+ content:
+ application/json:
+ schema:
+ type: array
+ items: { $ref: "#/components/schemas/User"}
+ '401':
+ $ref: "#/components/responses/HTTPError"
+ post:
+ tags: [users]
+ operationId: postUser
+ description: create a new user
+ /users/{uid}:
+ parameters:
+ - name: uid
+ in: path
+ required: true
+ schema: {type: "string", pattern: '^\d+$'}
+ description: unique ID of an user (stays stable through user renames)
+ delete:
+ tags: [users]
+ operationId: deleteUser
+ description: delete a user
+ patch:
+ tags: [users]
+ operationId: patchUser
+ description: change a user's data
+ /auth:
+ get:
+ tags: [auth]
+ operationId: getAuth
+ description: get login data
+ post:
+ tags: [auth]
+ operationId: postAuth
+ description: log-in to the server
+ /auth/login/{username}/link:
+ parameters:
+ - name: username
+ in: path
+ required: true
+ schema: {type: "string"}
+ description: human-readable unique name of an user
+ get:
+ tags: [auth]
+ operationId: getAuthLink
+ description: get a login link for this user
+ post:
+ tags: [auth]
+ operationId: postAuthLink
+ description: generate and send a login link for this user
+ /auth/logout:
+ post:
+ tags: [auth]
+ operationId: postLogout
+ description: delete this request's credentials
+ /auth/access/{scene}:
+ parameters:
+ - $ref: '#/components/parameters/scene'
+ get:
+ tags: [auth]
+ operationId: getAccess
+ description: get a scene's access rights
+ responses:
+ '200':
+ description: Access map defined for this scene
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: object
+ required: ["uid", "username", "access"]
+ properties:
+ uid: { $ref: "#/components/schemas/Uid"}
+ username: {type: string}
+ access: { $ref: "#/components/schemas/AccessType" }
+ '401':
+ $ref: "#/components/responses/HTTPError"
+ patch:
+ tags: [auth]
+ operationId: patchAccess
+ description: edit a scene's access rights
+# Administrative data. Might contain server configuration routes in the future
+ /admin/stats:
+ get:
+ tags: [admin]
+ operationId: getAdminStats
+ description: get server stats
+ /admin/mailtest:
+ post:
+ tags: [admin]
+ operationId: postAdminMailtest
+ description: sends a test email
+
+components:
+ parameters:
+ scene:
+ name: scene
+ in: path
+ required: true
+ schema: {type: string }
+ description: unique name of a scene
+ example: foo
+ file:
+ name: file
+ in: path
+ required: true
+ schema: {type: "string"}
+ description: |
+ relative path to a scene's file.
+ Might contain slashs, though openAPI spec won't allow them in test queries
+ examples:
+ folder:
+ summary: a file in a nested folder
+ value: models/foo.glb
+ thumbnail:
+ summary: a thumbnail for this scene
+ value: "scene-image-thumb.jpg"
+ document:
+ summary: a voyager scene document file
+ value: scene.svx.json
+ responses:
+ 'HTTPError':
+ description: Generic HTTP error response whose content depends on the request's "Accept" header
+ content:
+ application/json:
+ schema:
+ type: object
+ required: ["code", "message"]
+ properties:
+ code:
+ description: |
+ [HTTP Status](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) code
+ type: number
+ format: int32
+ minimum: 100
+ maximum: 599
+ message:
+ type: string
+ text/plain:
+ schema:
+ type: "string"
+ example: "Bad Request"
+ text/html:
+ schema:
+ type: "string"
+
+ schemas:
+ Scene:
+ type: object
+ required: ["ctime", "mtime", "author_id", "author", "id", "access"]
+ properties:
+ ctime: {type: "string", format: "date-time"}
+ mtime: {type: "string", format: "date-time"}
+ author_id: {$ref:"#/components/schemas/Uid"}
+ author: {type: "string"}
+ id: {$ref:"#/components/schemas/Uid"}
+ name: {type: "string"}
+ thumb: {type: "string", desciption: "URI to the scene's thumbnail representation if it exists"}
+ access: {
+ type: object,
+ required: ["any", "default"],
+ properties: {
+ default: {$ref:"#/components/schemas/AccessType"},
+ any: {$ref:"#/components/schemas/AccessType"},
+ user: {$ref:"#/components/schemas/AccessType"},
+ }
+ }
+ User:
+ type: object
+ required: ["uid", "username", "isAdministrator"]
+ properties:
+ uid: {$ref: "#/components/schemas/Uid"}
+ username: {type: "string", example: "alice" }
+ isAdministrator: {type: "boolean"}
+ Uid:
+ type: "string"
+ pattern: '^\d+$'
+ description:
+ string representation of unique IDs.
+ Applicable for users or scenes, but uids are not expected to be unique across namespaces
+ UIDs are often stringified to prevent rounding errors on large intergers in the javascript engine
+ AccessType:
+ type: string
+ enum: ["none", "read", "write", "admin"]
diff --git a/source/ui/state/router.ts b/source/ui/state/router.ts
index 1847aea8..51f4ca09 100644
--- a/source/ui/state/router.ts
+++ b/source/ui/state/router.ts
@@ -8,14 +8,14 @@ export interface RouteParams{
export type RouteHandler = (props :RouteParams)=> TemplateResult;
-export interface Route{
+export interface RouteDeclaration{
pattern :RegExp;
content :RouteHandler;
}
export declare class Router{
path :string;
- static routes :Route[];
+ static routes :RouteDeclaration[];
get route() :URL;
isActive(pathname :string, strict?:boolean) :boolean;
renderContent() :TemplateResult;
diff --git a/source/ui/styles/apidoc.scss b/source/ui/styles/apidoc.scss
new file mode 100644
index 00000000..bff43695
--- /dev/null
+++ b/source/ui/styles/apidoc.scss
@@ -0,0 +1,184 @@
+
+.tag-line{
+ border-bottom: 1px solid var(--color-secondary);
+}
+
+:host(:hover:not([expanded])) .tag-line {
+ background-color: var(--color-highlight);
+}
+
+.tag-header{
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ cursor: pointer;
+ h4{
+ flex-grow: 1;
+ padding-right: 1rem;
+ font-family: var(--font-body);
+ color: var(--color-text);
+ }
+
+ .tag-summary{
+ max-width: 50%;
+ p{
+ text-align: left;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ }
+
+}
+
+
+:host([expanded]) .tag-header{
+ cursor: initial;
+ .tag-summary{
+ p{
+ white-space: wrap;
+ }
+ }
+
+}
+.tag-body{
+ box-sizing: border-box;
+ max-height: 0;
+ overflow: hidden;
+ display: grid;
+ grid-template-columns: auto minmax(auto, 1fr) 1fr 24px;
+ grid-auto-rows: auto;
+ align-items: baseline;
+ transition: max-height .4s ease;
+ &:not(:empty){
+ padding-bottom: 1rem;
+ }
+}
+
+:host([expanded]) .tag-body{
+ max-height: 1200px;
+ overflow-y: auto;
+}
+
+.path-line{
+ display: grid;
+ grid-column: 1 / span 4;
+ grid-template-columns: auto minmax(auto, 1fr) 1fr 24px;
+ grid-template-columns: subgrid;
+ grid-template-rows: subgrid;
+ align-items: baseline;
+ padding: 4px;
+ &:nth-child(2n){
+ background-color: rgba(0, 0, 0, 0.1);
+ }
+ &:nth-child(2n+1){
+ background-color: rgba(0, 0, 0, 0.3);
+ }
+
+ &:hover:not([expanded]){
+ background-color: var(--color-highlight2);
+ }
+
+ &:not(.expanded){
+ cursor: pointer;
+ }
+
+ > *{
+ min-width: 0;
+ }
+
+ .method{
+ font-weight: bold;
+ box-sizing: border-box;
+ width: 12ch;
+ padding: .5rem .25ch;
+ background-color: var(--color-highlight);
+ text-transform: uppercase;
+ text-align: center;
+ border-radius: 4px;
+
+ &.get{
+ background-color: #2f8132;
+ }
+ &.post{
+ background-color: #186faf;
+ }
+ &.put{
+ background-color: #FD7E14;
+ }
+ &.delete{
+ background-color: #dc3545;
+ }
+ &.patch{
+ background-color: #ffc107;
+ }
+ &.mkcol{
+ background-color: #38b3f9;
+ }
+ &.propfind{
+ background-color: #28a745;
+ }
+ &.copy, &.move{
+ background-color: #6f42c1;
+ }
+ }
+
+ .pathname{
+ padding-left: .5rem;
+ }
+
+ .op-summary > p{
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ text-align: right;
+ margin: 0;
+ }
+ &[expanded] .op-summary > p{
+ white-space: wrap;
+ text-align: left;
+ }
+
+ .caret{
+ width: 16px;
+ color: var(--color-text);
+ fill: currentColor;
+ }
+
+ .operation-parameters{
+ display: flex;
+ justify-content: space-between;
+ > * {
+ flex-grow: 1;
+ &:first-child{
+ flex: 0 0 25%;
+ }
+ }
+ }
+}
+code{
+ background-color: rgba(230, 185, 0, 0.1);
+}
+a[href], a:-webkit-any-link {
+ text-decoration: none;
+ &:hover{
+ box-shadow: inset 0 -2px 0 #E6B900;
+ }
+}
+
+
+.caret{
+ box-sizing: border-box;
+ width: 1.5rem;
+ padding-left: .5rem;
+ color: var(--color-text);
+
+ &:hover{
+ color: #aaa;
+ }
+ fill: currentColor;
+ cursor: pointer;
+ svg{
+ margin-bottom: -8px;
+ }
+}
\ No newline at end of file
diff --git a/source/ui/styles/layout.scss b/source/ui/styles/layout.scss
index 327956cf..7f5e75d2 100644
--- a/source/ui/styles/layout.scss
+++ b/source/ui/styles/layout.scss
@@ -80,6 +80,9 @@ corpus-list, home-page {
'header'
'content';
}
+ > *{
+ min-width: 0;
+ }
.grid-header{
grid-area: header;
}
diff --git a/source/ui/styles/theme.scss b/source/ui/styles/theme.scss
index 4b50e8e8..16ee6b75 100644
--- a/source/ui/styles/theme.scss
+++ b/source/ui/styles/theme.scss
@@ -2,6 +2,6 @@
@import url("https://fonts.googleapis.com/css2?family=Noto+Serif&display=swap");
html:root {
- --font-body: 'Open Sans', 'Liberation', 'Roboto', 'sans-serif';
+ --font-body: 'Open Sans', 'Liberation Sans', 'Roboto', 'sans-serif';
--font-heading: 'Noto Serif', 'serif';
}
\ No newline at end of file
diff --git a/source/ui/webpack.config.js b/source/ui/webpack.config.js
index 6b4775d2..8a89eae8 100644
--- a/source/ui/webpack.config.js
+++ b/source/ui/webpack.config.js
@@ -154,6 +154,10 @@ module.exports = function createAppConfig(env, argv={})
publicPath: "/dist/",
}
},
+ {
+ test: /openapi\.ya?ml$/i,
+ use: path.resolve(dirs.source,"oas-loader.mjs")
+ }
]
},