Add new WIP QA Review page (#1500)

Resolves https://github.com/webrecorder/browsertrix-cloud/issues/1493

<!-- Fixes #issue_number -->

### 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 <hi@emma.cafe>
This commit is contained in:
sua yoo 2024-02-20 00:26:38 -08:00 committed by GitHub
parent a8e3ff1141
commit 91ff95c8e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 443 additions and 186 deletions

View File

@ -46,5 +46,6 @@
}
],
"eslint.workingDirectories": ["./frontend"],
"eslint.nodePath": "./frontend/node_modules"
"eslint.nodePath": "./frontend/node_modules",
"tailwindCSS.experimental.classRegex": ["tw`([^`]*)"]
}

View File

@ -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`
<span class=${this.variant}>
<span
class="h-4.5 ${{
success: tw`bg-success-500 text-neutral-0`,
warning: tw`bg-warning-600 text-neutral-0`,
danger: tw`bg-danger-500 text-neutral-0`,
neutral: tw`bg-neutral-100 text-neutral-600`,
"high-contrast": tw`bg-neutral-600 text-neutral-0`,
primary: tw`bg-primary text-neutral-0`,
}[
this.variant
]} inline-flex items-center justify-center rounded-sm px-2 align-[1px] text-xs"
>
<slot></slot>
</span>
`;

View File

@ -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)}

View File

@ -1,5 +1,6 @@
import "./alert";
import "./badge";
import "./navigation";
import("./button");
import("./code");
import("./combobox");

View File

@ -0,0 +1 @@
export * from "./navigation-button";

View File

@ -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
* <btrix-navigation-button>Click me</btrix-navigation-button>
* ```
*/
@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<this>) {
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}
>
<slot></slot>
</${tag}>`;
}
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();
}
}
}

View File

@ -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 {

View File

@ -664,6 +664,9 @@ export class App extends LiteElement {
// falls through
}
case "components":
return html`<btrix-components></btrix-components>`;
default:
return this.renderNotFoundPage();
}

View File

@ -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<this> | Map<PropertyKey, unknown>,
): 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`
<nav class="mb-7">
<a
class="text-sm font-medium text-neutral-500 hover:text-neutral-600"
href=${`${crawlBaseUrl}`}
@click=${this.navigate.link}
>
<sl-icon
name="arrow-left"
class="inline-block align-middle"
></sl-icon>
<span class="inline-block align-middle">
${msg("Back to")} ${itemName}
</span>
</a>
</nav>
<article>
<header class="mainHeader outline">
<h1>${msg("Review")} &mdash; ${itemName}</h1>
</header>
<section class="main outline">
<nav>
<btrix-button
id="screenshot-tab"
href=${`${crawlBaseUrl}/review/screenshots`}
variant=${this.tab === "screenshots" ? "primary" : "neutral"}
?raised=${this.tab === "screenshots"}
@click=${this.navigate.link}
>${msg("Screenshots")}</btrix-button
>
<btrix-button
id="replay-tab"
href=${`${crawlBaseUrl}/review/replay`}
variant=${this.tab === "replay" ? "primary" : "neutral"}
?raised=${this.tab === "replay"}
@click=${this.navigate.link}
>${msg("Replay")}</btrix-button
>
</nav>
<div role="region" aria-labelledby="${this.tab}-tab">
${choose(
this.tab,
[
["screenshots", this.renderScreenshots],
["replay", this.renderReplay],
],
() => html`<btrix-not-found></btrix-not-found>`,
)}
</div>
</section>
<h2 class="pageListHeader outline">
${msg("Pages List")} <sl-button>${msg("Finish Review")}</sl-button>
</h2>
<section class="pageList outline">[page list]</section>
</article>
`;
}
private readonly renderScreenshots = () => {
return html`[screenshots]`;
};
private readonly renderReplay = () => {
return html`[replay]`;
};
private async fetchArchivedItem(): Promise<void> {
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<ArchivedItem> {
const apiPath = `/orgs/${this.orgId}/all-crawls/${this.itemId}`;
return this.api.fetch<ArchivedItem>(apiPath, this.authState!);
}
}

View File

@ -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<ResourceName>;
export type OrgTab =
| "home"
| "crawls"
| "workflows"
| "items"
| "browser-profiles"
| "collections"
| "settings";
type Params = {
export type OrgParams = {
home: Record<string, never>;
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;
itemId?: string;
new?: ResourceName;
};
collections: {
collectionId?: string;
collectionTab?: string;
itemType?: Crawl["type"];
jobType?: JobType;
};
settings: {
settingsTab?: "information" | "members";
new?: ResourceName;
};
};
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`
<div class="flex min-h-full flex-col">
${this.renderStorageAlert()} ${this.renderExecutionMinutesAlert()}
${this.renderOrgNavBar()}
<main>
<div
class="mx-auto box-border w-full max-w-screen-desktop px-3 py-7"
<main
class="${noMaxWidth
? "w-full"
: "w-full max-w-screen-desktop"} mx-auto box-border flex flex-1 flex-col p-3 pt-7"
aria-labelledby="${this.orgTab}-tab"
>
${tabPanelContent}
</div>
</main>
${this.renderNewResourceDialogs()}
</div>
`;
}
@ -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` <btrix-archived-item-qa
class="flex-1"
.authState=${this.authState!}
orgId=${this.orgId}
itemId=${params.itemId}
tab=${params.qaTab}
?isCrawler=${this.isCrawler}
></btrix-archived-item-qa>`;
}
return html` <btrix-archived-item-detail
.authState=${this.authState!}
orgId=${this.orgId}
crawlId=${this.params.itemId}
collectionId=${this.params.collectionId || ""}
workflowId=${this.params.workflowId || ""}
itemType=${this.params.itemType || "crawl"}
crawlId=${params.itemId}
collectionId=${params.collectionId || ""}
workflowId=${params.workflowId || ""}
itemType=${params.itemType || "crawl"}
?isCrawler=${this.isCrawler}
></btrix-archived-item-detail>`;
}
@ -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}
></btrix-archived-items>`;
}
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`<btrix-browser-profiles-detail
.authState=${this.authState!}
.orgId=${this.orgId}
profileId=${this.params.browserProfileId}
profileId=${params.browserProfileId}
></btrix-browser-profiles-detail>`;
}
if (this.params.browserId) {
if (params.browserId) {
return html`<btrix-browser-profiles-new
.authState=${this.authState!}
.orgId=${this.orgId}
.browserId=${this.params.browserId}
.browserId=${params.browserId}
></btrix-browser-profiles-new>`;
}
@ -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`<btrix-collection-detail
.authState=${this.authState!}
orgId=${this.orgId}
userId=${this.userInfo!.id}
collectionId=${this.params.collectionId}
collectionTab=${(this.params.collectionTab as
| CollectionTab
| undefined) || "replay"}
collectionId=${params.collectionId}
collectionTab=${(params.collectionTab as CollectionTab | undefined) ||
"replay"}
?isCrawler=${this.isCrawler}
></btrix-collection-detail>`;
}
@ -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",

View File

@ -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))",

View File

@ -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<string, string>}
*/
const makeColorPalette = (color) =>
colorGrades.reduce((acc, v) => ({
colorGrades.reduce(
/**
* @param {Record<string, string>} 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",
},
};
}