From 91ff95c8e9cfa6be1b524d8c361c715947498f1c Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 20 Feb 2024 00:26:38 -0800 Subject: [PATCH] Add new WIP QA Review page (#1500) Resolves https://github.com/webrecorder/browsertrix-cloud/issues/1493 ### Changes Adds WIP QA page with basic grid layout sections and navigation. ### Manual testing Page can be access by adding `/review/screenshots` or `/review/replay` to a crawl detail page URL. For example: ``` /orgs/suas-dev-sandbox-2/items/crawl/manual-20240124023524-422e41d6-97d/review/screenshots ``` --------- Co-authored-by: emma --- .vscode/settings.json | 3 +- frontend/src/components/ui/badge.ts | 63 +++--- frontend/src/components/ui/button.ts | 97 ++-------- frontend/src/components/ui/index.ts | 1 + .../src/components/ui/navigation/index.ts | 1 + .../ui/navigation/navigation-button.ts | 99 ++++++++++ frontend/src/components/ui/tab-list.ts | 11 +- frontend/src/index.ts | 3 + frontend/src/pages/org/archived-item-qa.ts | 182 ++++++++++++++++++ frontend/src/pages/org/index.ts | 135 ++++++++----- frontend/src/routes.ts | 2 +- frontend/tailwind.config.js | 32 ++- 12 files changed, 443 insertions(+), 186 deletions(-) create mode 100644 frontend/src/components/ui/navigation/index.ts create mode 100644 frontend/src/components/ui/navigation/navigation-button.ts create mode 100644 frontend/src/pages/org/archived-item-qa.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index d965e066..2130e66a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -46,5 +46,6 @@ } ], "eslint.workingDirectories": ["./frontend"], - "eslint.nodePath": "./frontend/node_modules" + "eslint.nodePath": "./frontend/node_modules", + "tailwindCSS.experimental.classRegex": ["tw`([^`]*)"] } diff --git a/frontend/src/components/ui/badge.ts b/frontend/src/components/ui/badge.ts index f3b5831f..3141d7e2 100644 --- a/frontend/src/components/ui/badge.ts +++ b/frontend/src/components/ui/badge.ts @@ -1,4 +1,6 @@ -import { LitElement, html, css } from "lit"; +import { TailwindElement } from "@/classes/TailwindElement"; +import { tw } from "@/utils/tailwind"; +import { html } from "lit"; import { customElement, property } from "lit/decorators.js"; /** @@ -10,48 +12,33 @@ import { customElement, property } from "lit/decorators.js"; * ``` */ @customElement("btrix-badge") -export class Badge extends LitElement { +export class Badge extends TailwindElement { @property({ type: String }) - variant: "success" | "warning" | "danger" | "neutral" = "neutral"; + variant: + | "success" + | "warning" + | "danger" + | "neutral" + | "primary" + | "high-contrast" = "neutral"; - // postcss-lit-disable-next-line - static styles = css` - :host > span { - display: inline-flex; - align-items: center; - justify-content: center; - font-size: var(--sl-font-size-x-small); - line-height: 1.125rem; - height: 1.125rem; - padding: 0 0.5rem; - border-radius: var(--sl-border-radius-small); - vertical-align: 1px; - } - - .success { - background-color: var(--sl-color-success-500); - color: var(--sl-color-neutral-0); - } - - .warning { - background-color: var(--sl-color-warning-600); - color: var(--sl-color-neutral-0); - } - - .danger { - background-color: var(--sl-color-danger-500); - color: var(--sl-color-neutral-0); - } - - .neutral { - background-color: var(--sl-color-neutral-100); - color: var(--sl-color-neutral-600); - } - `; + @property({ type: String, reflect: true }) + role: string | null = "status"; render() { return html` - + `; diff --git a/frontend/src/components/ui/button.ts b/frontend/src/components/ui/button.ts index 96c3c3ac..67351a30 100644 --- a/frontend/src/components/ui/button.ts +++ b/frontend/src/components/ui/button.ts @@ -1,10 +1,11 @@ /* eslint-disable lit/binding-positions */ /* eslint-disable lit/no-invalid-html */ -import { LitElement, css } from "lit"; +import { css } from "lit"; import { html, literal } from "lit/static-html.js"; import { customElement, property } from "lit/decorators.js"; -import { classMap } from "lit/directives/class-map.js"; import { ifDefined } from "lit/directives/if-defined.js"; +import { TailwindElement } from "@/classes/TailwindElement"; +import { tw } from "@/utils/tailwind"; /** * Custom styled button @@ -15,7 +16,7 @@ import { ifDefined } from "lit/directives/if-defined.js"; * ``` */ @customElement("btrix-button") -export class Button extends LitElement { +export class Button extends TailwindElement { @property({ type: String }) type: "submit" | "button" = "button"; @@ -40,7 +41,6 @@ export class Button extends LitElement { @property({ type: Boolean }) icon = false; - // postcss-lit-disable-next-line static styles = css` :host { display: inline-block; @@ -50,89 +50,24 @@ export class Button extends LitElement { display: block; font-size: 1rem; } - - .button { - all: unset; - display: flex; - gap: var(--sl-spacing-x-small); - align-items: center; - justify-content: center; - border-radius: var(--sl-border-radius-small); - box-sizing: border-box; - font-weight: 500; - text-align: center; - cursor: pointer; - transform: translateY(0px); - transition: - background-color 0.15s, - box-shadow 0.15s, - color 0.15s, - transform 0.15s; - } - - .button[disabled] { - cursor: not-allowed; - background-color: var(--sl-color-neutral-100) !important; - color: var(--sl-color-neutral-300) !important; - } - - .button.icon { - min-width: 1.5rem; - min-height: 1.5rem; - padding: 0 var(--sl-spacing-2x-small); - } - - .button:not(.icon) { - height: var(--sl-input-height-small); - padding: 0 var(--sl-spacing-x-small); - } - - .raised { - box-shadow: var(--sl-shadow-x-small); - } - - :not([aria-disabled]) .raised:not([disabled]):hover { - box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1); - transform: translateY(1px); - } - - .primary { - background-color: var(--sl-color-blue-50); - color: var(--sl-color-blue-600); - } - - :not([aria-disabled]) .primary:hover { - background-color: var(--sl-color-blue-100); - } - - .danger { - background-color: var(--sl-color-danger-50); - color: var(--sl-color-danger-600); - } - - :not([aria-disabled]) .danger:hover { - background-color: var(--sl-color-danger-100); - } - - .neutral { - color: var(--sl-color-neutral-600); - } - - .neutral:hover { - color: var(--sl-color-blue-500); - } `; render() { const tag = this.href ? literal`a` : literal`button`; return html`<${tag} type=${this.type === "submit" ? "submit" : "button"} - class=${classMap({ - button: true, - [this.variant]: true, - icon: this.icon, - raised: this.raised, - })} + class=${[ + tw`flex h-6 cursor-pointer items-center justify-center gap-2 rounded-sm text-center font-medium transition-all disabled:cursor-not-allowed disabled:text-neutral-300`, + this.icon ? tw`min-h-6 min-w-6 px-1` : tw`h-6 px-2`, + this.raised ? tw`shadow-sm` : "", + { + primary: tw`bg-blue-50 text-blue-600 shadow-blue-800/20 hover:bg-blue-100`, + danger: tw`shadow-danger-800/20 bg-danger-50 text-danger-600 hover:bg-danger-100`, + neutral: tw`text-neutral-600 hover:text-blue-600`, + }[this.variant], + ] + .filter(Boolean) + .join(" ")} ?disabled=${this.disabled} href=${ifDefined(this.href)} aria-label=${ifDefined(this.label)} diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts index 0b88e810..b84a11ec 100644 --- a/frontend/src/components/ui/index.ts +++ b/frontend/src/components/ui/index.ts @@ -1,5 +1,6 @@ import "./alert"; import "./badge"; +import "./navigation"; import("./button"); import("./code"); import("./combobox"); diff --git a/frontend/src/components/ui/navigation/index.ts b/frontend/src/components/ui/navigation/index.ts new file mode 100644 index 00000000..5b57be94 --- /dev/null +++ b/frontend/src/components/ui/navigation/index.ts @@ -0,0 +1 @@ +export * from "./navigation-button"; diff --git a/frontend/src/components/ui/navigation/navigation-button.ts b/frontend/src/components/ui/navigation/navigation-button.ts new file mode 100644 index 00000000..8245743e --- /dev/null +++ b/frontend/src/components/ui/navigation/navigation-button.ts @@ -0,0 +1,99 @@ +/* eslint-disable lit/binding-positions */ +/* eslint-disable lit/no-invalid-html */ +import { type PropertyValueMap, css } from "lit"; +import { html, literal } from "lit/static-html.js"; +import { customElement, property } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; +import { TailwindElement } from "@/classes/TailwindElement"; +import { tw } from "@/utils/tailwind"; + +/** + * Custom styled button + * + * Usage example: + * ```ts + * Click me + * ``` + */ +@customElement("btrix-navigation-button") +export class Button extends TailwindElement { + @property({ type: Boolean }) + active = false; + + @property({ type: String }) + type: "submit" | "button" = "button"; + + @property({ type: String }) + label?: string; + + @property({ type: String }) + href?: string; + + @property({ type: Boolean }) + disabled = false; + + @property({ type: Boolean }) + icon = false; + + @property({ type: String, reflect: true }) + role: ARIAMixin["role"] = "tab"; + + protected willUpdate(changedProperties: PropertyValueMap) { + if (changedProperties.has("active")) { + this.ariaSelected = this.active ? "true" : null; + } + } + + static styles = css` + :host { + display: inline-block; + } + + ::slotted(sl-icon) { + display: block; + font-size: 1rem; + } + `; + + render() { + const tag = this.href ? literal`a` : literal`button`; + return html`<${tag} + type=${this.type === "submit" ? "submit" : "button"} + class=${[ + tw`flex w-full cursor-pointer items-center justify-start gap-2 rounded-sm px-2 py-4 font-medium outline-primary-600 transition hover:transition-none focus-visible:outline focus-visible:outline-3 focus-visible:outline-offset-1 disabled:cursor-not-allowed disabled:bg-transparent disabled:opacity-50`, + this.icon ? tw`min-h-6 min-w-6` : tw`h-6`, + this.active + ? tw`bg-blue-100 text-blue-600 shadow-sm shadow-blue-900/40 hover:bg-blue-100` + : tw`text-neutral-600 hover:bg-blue-50`, + ] + .filter(Boolean) + .join(" ")} + ?disabled=${this.disabled} + href=${ifDefined(this.href)} + aria-label=${ifDefined(this.label)} + @click=${this.handleClick} + > + + `; + } + + private handleClick(e: MouseEvent) { + if (this.disabled) { + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (this.type === "submit") { + this.submit(); + } + } + + private submit() { + const form = (this.closest("form") || this.closest("form"))!; + + if (form) { + form.submit(); + } + } +} diff --git a/frontend/src/components/ui/tab-list.ts b/frontend/src/components/ui/tab-list.ts index b537568f..988c366c 100644 --- a/frontend/src/components/ui/tab-list.ts +++ b/frontend/src/components/ui/tab-list.ts @@ -4,8 +4,7 @@ import { property, queryAsync, customElement } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; const DEFAULT_PANEL_ID = "default-panel"; -// Breakpoint in pixels for 2-column layout -const TWO_COL_SCREEN_MIN = 1032; +export const TWO_COL_SCREEN_MIN_CSS = css`64.5rem`; /** * Tab list @@ -92,7 +91,7 @@ export class TabList extends LitElement { grid-gap: 1.5rem; } - @media only screen and (min-width: ${TWO_COL_SCREEN_MIN}px) { + @media only screen and (min-width: ${TWO_COL_SCREEN_MIN_CSS}) { .container { grid-template-areas: ". header" @@ -105,7 +104,7 @@ export class TabList extends LitElement { grid-area: menu; } - @media only screen and (min-width: ${TWO_COL_SCREEN_MIN}px) { + @media only screen and (min-width: ${TWO_COL_SCREEN_MIN_CSS}) { .navWrapper { overflow: initial; } @@ -140,7 +139,7 @@ export class TabList extends LitElement { margin-left: var(--track-width); } - @media only screen and (min-width: ${TWO_COL_SCREEN_MIN}px) { + @media only screen and (min-width: ${TWO_COL_SCREEN_MIN_CSS}) { .tablist { display: block; } @@ -165,7 +164,7 @@ export class TabList extends LitElement { background-color: var(--sl-color-blue-500); } - @media only screen and (min-width: ${TWO_COL_SCREEN_MIN}px) { + @media only screen and (min-width: ${TWO_COL_SCREEN_MIN_CSS}) { .tablist, .show-indicator .track, .show-indicator .indicator { diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 7c5bd09e..7afd5ed2 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -664,6 +664,9 @@ export class App extends LiteElement { // falls through } + case "components": + return html``; + default: return this.renderNotFoundPage(); } diff --git a/frontend/src/pages/org/archived-item-qa.ts b/frontend/src/pages/org/archived-item-qa.ts new file mode 100644 index 00000000..f7f9ff1b --- /dev/null +++ b/frontend/src/pages/org/archived-item-qa.ts @@ -0,0 +1,182 @@ +import { html, css, nothing, type PropertyValues } from "lit"; +import { state, property, customElement } from "lit/decorators.js"; +import { msg, localized } from "@lit/localize"; +import { choose } from "lit/directives/choose.js"; + +import { TailwindElement } from "@/classes/TailwindElement"; +import { type AuthState } from "@/utils/AuthService"; +import { TWO_COL_SCREEN_MIN_CSS } from "@/components/ui/tab-list"; +import { NavigateController } from "@/controllers/navigate"; +import { APIController } from "@/controllers/api"; +import { NotifyController } from "@/controllers/notify"; +import { renderName } from "@/utils/crawler"; +import { type ArchivedItem } from "@/types/crawler"; + +export type QATab = "screenshots" | "replay"; + +@localized() +@customElement("btrix-archived-item-qa") +export class ArchivedItemQA extends TailwindElement { + static styles = css` + :host { + height: inherit; + display: flex; + flex-direction: column; + } + + article { + flex-grow: 1; + display: grid; + grid-gap: 1rem; + grid-template: + "mainHeader" + "main" + "pageListHeader" + "pageList"; + grid-template-rows: repeat(4, max-content); + } + + @media only screen and (min-width: ${TWO_COL_SCREEN_MIN_CSS}) { + article { + grid-template: + "mainHeader pageListHeader" + "main pageList"; + grid-template-columns: 1fr 24rem; + grid-template-rows: min-content 1fr; + } + } + + .mainHeader { + grid-area: mainHeader; + } + + .pageListHeader { + grid-area: pageListHeader; + } + + .main { + grid-area: main; + } + + .pageList { + grid-area: pageList; + } + `; + + @property({ type: Object }) + authState?: AuthState; + + @property({ type: String }) + orgId?: string; + + @property({ type: String }) + itemId?: string; + + @property({ type: Boolean }) + isCrawler = false; + + @property({ type: String }) + tab: QATab = "screenshots"; + + @state() + private item?: ArchivedItem; + + private readonly api = new APIController(this); + private readonly navigate = new NavigateController(this); + private readonly notify = new NotifyController(this); + + protected willUpdate( + changedProperties: PropertyValues | Map, + ): void { + if (changedProperties.has("itemId") && this.itemId) { + void this.fetchArchivedItem(); + } + } + + render() { + const crawlBaseUrl = `${this.navigate.orgBasePath}/items/crawl/${this.itemId}`; + const itemName = this.item ? renderName(this.item) : nothing; + return html` + + +
+
+

${msg("Review")} — ${itemName}

+
+
+ +
+ ${choose( + this.tab, + [ + ["screenshots", this.renderScreenshots], + ["replay", this.renderReplay], + ], + () => html``, + )} +
+
+

+ ${msg("Pages List")} ${msg("Finish Review")} +

+
[page list]
+
+ `; + } + + private readonly renderScreenshots = () => { + return html`[screenshots]`; + }; + + private readonly renderReplay = () => { + return html`[replay]`; + }; + + private async fetchArchivedItem(): Promise { + try { + this.item = await this.getArchivedItem(); + } catch { + this.notify.toast({ + message: msg("Sorry, couldn't retrieve archived item at this time."), + variant: "danger", + icon: "exclamation-octagon", + }); + } + } + + private async getArchivedItem(): Promise { + const apiPath = `/orgs/${this.orgId}/all-crawls/${this.itemId}`; + return this.api.fetch(apiPath, this.authState!); + } +} diff --git a/frontend/src/pages/org/index.ts b/frontend/src/pages/org/index.ts index 423f8fff..f885d881 100644 --- a/frontend/src/pages/org/index.ts +++ b/frontend/src/pages/org/index.ts @@ -16,6 +16,7 @@ import "./workflows-list"; import "./workflows-new"; import "./archived-item-detail"; import "./archived-items"; +import "./archived-item-qa"; import "./collections-list"; import "./collection-detail"; import "./browser-profiles-detail"; @@ -30,6 +31,7 @@ import type { OrgRemoveMemberEvent, } from "./settings"; import type { Tab as CollectionTab } from "./collection-detail"; +import type { QATab } from "./archived-item-qa"; import type { SelectJobTypeEvent } from "@/features/crawl-workflows/new-workflow-dialog"; import type { QuotaUpdateDetail } from "@/controllers/api"; import { type TemplateResult } from "lit"; @@ -39,27 +41,34 @@ import type { CollectionSavedEvent } from "@/features/collections/collection-met const RESOURCE_NAMES = ["workflow", "collection", "browser-profile", "upload"]; type ResourceName = (typeof RESOURCE_NAMES)[number]; export type SelectNewDialogEvent = CustomEvent; -export type OrgTab = - | "home" - | "crawls" - | "workflows" - | "items" - | "browser-profiles" - | "collections" - | "settings"; - -type Params = { - workflowId?: string; - browserProfileId?: string; - browserId?: string; - itemId?: string; - collectionId?: string; - collectionTab?: string; - itemType?: Crawl["type"]; - jobType?: JobType; - settingsTab?: "information" | "members"; - new?: ResourceName; +export type OrgParams = { + home: Record; + workflows: { + workflowId?: string; + jobType?: JobType; + new?: ResourceName; + }; + items: { + itemType?: Crawl["type"]; + itemId?: string; + qaTab?: QATab; + workflowId?: string; + collectionId?: string; + }; + "browser-profiles": { + browserProfileId?: string; + browserId?: string; + new?: ResourceName; + }; + collections: { + collectionId?: string; + collectionTab?: string; + }; + settings: { + settingsTab?: "information" | "members"; + }; }; +export type OrgTab = keyof OrgParams; const defaultTab = "home"; @@ -90,7 +99,7 @@ export class Org extends LiteElement { orgPath!: string; @property({ type: Object }) - params!: Params; + params: OrgParams[OrgTab] = {}; @property({ type: String }) orgTab: OrgTab = defaultTab; @@ -253,7 +262,7 @@ export class Org extends LiteElement { tabPanelContent = this.renderDashboard(); break; case "items": - tabPanelContent = this.renderArchive(); + tabPanelContent = this.renderArchivedItem(); break; case "workflows": tabPanelContent = this.renderWorkflows(); @@ -278,18 +287,23 @@ export class Org extends LiteElement { break; } + const noMaxWidth = + this.orgTab === "items" && (this.params as OrgParams["items"]).qaTab; + return html` - ${this.renderStorageAlert()} ${this.renderExecutionMinutesAlert()} - ${this.renderOrgNavBar()} -
-
+ ${this.renderStorageAlert()} ${this.renderExecutionMinutesAlert()} + ${this.renderOrgNavBar()} +
${tabPanelContent} -
-
- ${this.renderNewResourceDialogs()} + + ${this.renderNewResourceDialogs()} + `; } @@ -499,15 +513,28 @@ export class Org extends LiteElement { `; } - private renderArchive() { - if (this.params.itemId) { + private renderArchivedItem() { + const params = this.params as OrgParams["items"]; + + if (params.itemId) { + if (params.qaTab) { + return html` `; + } + return html` `; } @@ -518,17 +545,17 @@ export class Org extends LiteElement { orgId=${this.orgId} ?orgStorageQuotaReached=${this.orgStorageQuotaReached} ?isCrawler=${this.isCrawler} - itemType=${ifDefined(this.params.itemType || undefined)} + itemType=${ifDefined(params.itemType || undefined)} @select-new-dialog=${this.onSelectNewDialog} >`; } private renderWorkflows() { - const isEditing = Object.prototype.hasOwnProperty.call(this.params, "edit"); + const params = this.params as OrgParams["workflows"]; + const isEditing = Object.prototype.hasOwnProperty.call(params, "edit"); const isNewResourceTab = - Object.prototype.hasOwnProperty.call(this.params, "new") && - this.params.jobType; - const workflowId = this.params.workflowId; + Object.prototype.hasOwnProperty.call(params, "new") && params.jobType; + const workflowId = params.workflowId; if (workflowId) { return html` @@ -557,7 +584,7 @@ export class Org extends LiteElement { ?isCrawler=${this.isCrawler} .initialWorkflow=${workflow} .initialSeeds=${seeds} - jobType=${ifDefined(this.params.jobType)} + jobType=${ifDefined(params.jobType)} ?orgStorageQuotaReached=${this.orgStorageQuotaReached} ?orgExecutionMinutesQuotaReached=${this.orgExecutionMinutesQuotaReached} @select-new-dialog=${this.onSelectNewDialog} @@ -576,19 +603,21 @@ export class Org extends LiteElement { } private renderBrowserProfiles() { - if (this.params.browserProfileId) { + const params = this.params as OrgParams["browser-profiles"]; + + if (params.browserProfileId) { return html``; } - if (this.params.browserId) { + if (params.browserId) { return html``; } @@ -600,15 +629,16 @@ export class Org extends LiteElement { } private renderCollections() { - if (this.params.collectionId) { + const params = this.params as OrgParams["collections"]; + + if (params.collectionId) { return html``; } @@ -623,7 +653,8 @@ export class Org extends LiteElement { private renderOrgSettings() { if (!this.userInfo || !this.org) return; - const activePanel = this.params.settingsTab || "information"; + const params = this.params as OrgParams["settings"]; + const activePanel = params.settingsTab || "information"; const isAddingMember = Object.prototype.hasOwnProperty.call( this.params, "invite", diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 9d297cb0..0f8d36e7 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -14,7 +14,7 @@ export const ROUTES = { "/orgs/:slug", // Org sections: "(/workflows(/crawls)(/crawl/:workflowId))", - "(/items(/:itemType(/:itemId)))", + "(/items(/:itemType(/:itemId(/review/:qaTab))))", "(/collections(/new)(/view/:collectionId(/:collectionTab)))", "(/browser-profiles(/profile(/browser/:browserId)(/:browserProfileId)))", "(/settings(/:settingsTab))", diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 11447460..b734f0f4 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -40,11 +40,23 @@ function makeTheme() { // Map color grading: const colorGrades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900]; + /** + * @param {string} color + * @returns {Record} + */ const makeColorPalette = (color) => - colorGrades.reduce((acc, v) => ({ - ...acc, - [v]: `var(--sl-color-${color}-${v})`, - })); + colorGrades.reduce( + /** + * @param {Record} acc + * @param {number} v + * @returns + */ + (acc, v) => ({ + ...acc, + [v]: `var(--sl-color-${color}-${v})`, + }), + {}, + ); return { // https://github.com/tailwindlabs/tailwindcss/blob/52ab3154392ba3d7a05cae643694384e72dc24b2/stubs/defaultConfig.stub.js @@ -52,9 +64,9 @@ function makeTheme() { current: "currentColor", ...colors.map(makeColorPalette), primary, - success: `var(--success)`, - warning: `var(--warning)`, - danger: `var(--danger)`, + success: { ...makeColorPalette("success"), DEFAULT: `var(--success)` }, + warning: { ...makeColorPalette("warning"), DEFAULT: `var(--warning)` }, + danger: { ...makeColorPalette("danger"), DEFAULT: `var(--danger)` }, neutral: { ...makeColorPalette("neutral"), // Shoelace supports additional neutral variables: @@ -120,6 +132,12 @@ function makeTheme() { fast: "var(--sl-transition-fast)", "x-fast": "var(--sl-transition-x-fast)", }, + outlineWidth: { + 3: "3px", + }, + outlineOffset: { + 3: "3px", + }, }; }