diff --git a/frontend/src/__mocks__/api/orgs/[id]/crawlconfigs/[id].js b/frontend/src/__mocks__/api/orgs/[id]/crawlconfigs/[id].js new file mode 100644 index 0000000000..e161780b35 --- /dev/null +++ b/frontend/src/__mocks__/api/orgs/[id]/crawlconfigs/[id].js @@ -0,0 +1,77 @@ +import user from "@/__mocks__/api/users/me"; + +// API v1.18.0 +export default { + name: "", + description: null, + created: "2025-01-23T06:23:32Z", + createdBy: user.id, + modified: "2025-01-28T19:53:15Z", + modifiedBy: user.id, + autoAddCollections: [], + inactive: false, + rev: 2, + crawlAttemptCount: 3, + crawlCount: 3, + crawlSuccessfulCount: 3, + totalSize: 61275, + lastCrawlId: "manual-20250205210838-a5e3cf23-f3c", + lastCrawlStartTime: "2025-02-05T21:08:38Z", + lastStartedBy: user.id, + lastCrawlTime: "2025-02-05T21:09:20Z", + lastCrawlState: "complete", + lastCrawlSize: 61275, + lastRun: "2025-02-05T21:09:20Z", + isCrawlRunning: false, + crawlFilenameTemplate: null, + id: "a5f3cf23-f3ce-4ceb-a232-f2946185370f", + schedule: "", + jobType: "custom", + config: { + seeds: null, + seedFileId: null, + scopeType: "page", + include: null, + exclude: [], + depth: -1, + limit: null, + extraHops: 0, + lang: "en", + blockAds: false, + behaviorTimeout: null, + pageLoadTimeout: null, + pageExtraDelay: null, + postLoadDelay: null, + workers: null, + headless: null, + generateWACZ: null, + combineWARC: null, + useSitemap: false, + failOnFailedSeed: false, + logging: null, + behaviors: "autoscroll,autoplay,autofetch,siteSpecific", + customBehaviors: [], + userAgent: "", + selectLinks: ["a[href]->href"], + clickSelector: "a", + }, + tags: [], + crawlTimeout: 0, + maxCrawlSize: 0, + scale: 1, + browserWindows: 4, + oid: "x_example_org_id_x", + profileid: null, + crawlerChannel: "beta", + proxyId: null, + firstSeed: "https://example.com/", + seedCount: 1, + lastCrawlStopping: false, + lastCrawlShouldPause: false, + lastCrawlPausedAt: null, + lastCrawlPausedExpiry: null, + profileName: null, + createdByName: user.name, + modifiedByName: user.name, + lastStartedByName: user.name, +}; diff --git a/frontend/src/components/ui/overflow-dropdown.ts b/frontend/src/components/ui/overflow-dropdown.ts index 79c5b52646..bafc589fc3 100644 --- a/frontend/src/components/ui/overflow-dropdown.ts +++ b/frontend/src/components/ui/overflow-dropdown.ts @@ -11,6 +11,7 @@ import { import { ifDefined } from "lit/directives/if-defined.js"; import { TailwindElement } from "@/classes/TailwindElement"; +import type { WorkflowActionMenu } from "@/features/crawl-workflows/workflow-action-menu/workflow-action-menu"; /** * Dropdown for additional actions. @@ -40,8 +41,11 @@ export class OverflowDropdown extends TailwindElement { @query("sl-dropdown") private readonly dropdown?: SlDropdown; - @queryAssignedElements({ selector: "sl-menu", flatten: true }) - private readonly menu!: SlMenu[]; + @queryAssignedElements({ + selector: "sl-menu, btrix-workflow-action-menu", + flatten: true, + }) + private readonly menu!: (SlMenu | WorkflowActionMenu)[]; render() { return html` @@ -49,6 +53,10 @@ export class OverflowDropdown extends TailwindElement { ?disabled=${!this.hasMenuItems} hoist distance=${ifDefined(this.raised ? "4" : undefined)} + @btrix-select=${() => { + void this.dropdown?.hide(); + this.dropdown?.focusOnTrigger(); + }} > = CustomEvent<{ item: T }>; + +declare global { + interface GlobalEventHandlersEventMap { + "btrix-select": BtrixSelectEvent; + } +} diff --git a/frontend/src/features/crawl-workflows/index.ts b/frontend/src/features/crawl-workflows/index.ts index dc074e1560..57a46ba5a5 100644 --- a/frontend/src/features/crawl-workflows/index.ts +++ b/frontend/src/features/crawl-workflows/index.ts @@ -5,6 +5,7 @@ import("./link-selector-table"); import("./new-workflow-dialog"); import("./queue-exclusion-form"); import("./queue-exclusion-table"); +import("./workflow-action-menu/workflow-action-menu"); import("./workflow-editor"); import("./workflow-list"); import("./workflow-schedule-filter"); diff --git a/frontend/src/features/crawl-workflows/workflow-action-menu/index.ts b/frontend/src/features/crawl-workflows/workflow-action-menu/index.ts new file mode 100644 index 0000000000..be6c1dd2d0 --- /dev/null +++ b/frontend/src/features/crawl-workflows/workflow-action-menu/index.ts @@ -0,0 +1 @@ +export { WorkflowActionMenu } from "./workflow-action-menu"; diff --git a/frontend/src/features/crawl-workflows/workflow-action-menu/types.ts b/frontend/src/features/crawl-workflows/workflow-action-menu/types.ts new file mode 100644 index 0000000000..0a23b6cec4 --- /dev/null +++ b/frontend/src/features/crawl-workflows/workflow-action-menu/types.ts @@ -0,0 +1,14 @@ +import type { BtrixSelectEvent } from "@/events/btrix-select"; + +export enum Action { + Run = "run", + TogglePauseResume = "togglePauseResume", + Stop = "stop", + Cancel = "cancel", + EditBrowserWindows = "editBrowserWindows", + EditExclusions = "editExclusions", + Duplicate = "duplicate", + Delete = "delete", +} + +export type BtrixSelectActionEvent = BtrixSelectEvent<{ action: Action }>; diff --git a/frontend/src/features/crawl-workflows/workflow-action-menu/workflow-action-menu.ts b/frontend/src/features/crawl-workflows/workflow-action-menu/workflow-action-menu.ts new file mode 100644 index 0000000000..87093a59de --- /dev/null +++ b/frontend/src/features/crawl-workflows/workflow-action-menu/workflow-action-menu.ts @@ -0,0 +1,263 @@ +import { localized, msg } from "@lit/localize"; +import type { SlSelectEvent } from "@shoelace-style/shoelace"; +import { html, nothing } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { when } from "lit/directives/when.js"; + +import { Action, type BtrixSelectActionEvent } from "./types"; + +import { BtrixElement } from "@/classes/BtrixElement"; +import { ClipboardController } from "@/controllers/clipboard"; +import { WorkflowTab } from "@/routes"; +import type { Crawl, ListWorkflow, Workflow } from "@/types/crawler"; +import { isNotFailed, isSuccessfullyFinished } from "@/utils/crawler"; +import { isArchivingDisabled } from "@/utils/orgs"; + +@customElement("btrix-workflow-action-menu") +@localized() +export class WorkflowActionMenu extends BtrixElement { + @property({ type: Object }) + workflow?: ListWorkflow | Workflow; + + @property({ type: Object }) + latestCrawl?: Crawl | null; + + @property({ type: Object }) + logTotals?: { errors: number; behaviors: number } | null; + + @property({ type: Boolean }) + hidePauseResume?: boolean; + + @property({ type: Boolean }) + disablePauseResume?: boolean; + + @property({ type: Boolean }) + cancelingRun?: boolean; + + render() { + const workflow = this.workflow; + + if (!workflow) return; + + const canCrawl = this.appState.isCrawler; + const archivingDisabled = isArchivingDisabled(this.org, true); + const paused = workflow.lastCrawlState === "paused"; + const crawling = + workflow.isCrawlRunning && + !workflow.lastCrawlStopping && + !workflow.lastCrawlShouldPause && + workflow.lastCrawlState === "running"; + + return html` { + e.stopPropagation(); + const action = e.detail.item.dataset["action"]; + + this.dispatchEvent( + new CustomEvent("btrix-select", { + detail: { item: { ...e.detail.item, action: action as Action } }, + bubbles: true, + composed: true, + }), + ); + }} + > + ${when( + canCrawl, + () => + html`${when( + workflow.isCrawlRunning, + () => html` + ${when( + !this.hidePauseResume && + !this.disablePauseResume && + !this.cancelingRun, + () => + paused + ? html` + + + ${msg("Resume Crawl")} + + ` + : html` + + + ${msg("Pause Crawl")} + + `, + )} + + + + ${msg("Stop Crawl")} + + + + ${msg(html`Cancel & Discard Crawl`)} + + `, + () => html` + + + ${msg("Run Crawl")} + + `, + )} `, + )} + ${when( + canCrawl, + () => + html`${when( + workflow.isCrawlRunning && + !workflow.lastCrawlStopping && + !this.cancelingRun, + () => html` + + + ${msg("Edit Browser Windows")} + + + + ${msg("Edit Exclusions")} + + `, + )} + + this.navigate.to( + `/orgs/${this.appState.orgSlug}/workflows/${workflow.id}?edit`, + )} + > + + ${msg("Edit Workflow Settings")} + + + + ${msg("Duplicate Workflow")} + + `, + )} + ${when( + this.latestCrawl && isNotFailed(this.latestCrawl), + () => html` + + + ${msg("Latest Crawl")} + ${this.renderLatestCrawlMenu(this.latestCrawl!)} + + + `, + )} + + + ClipboardController.copyToClipboard(workflow.tags.join(", "))} + ?disabled=${!workflow.tags.length} + > + + ${msg("Copy Tags")} + + + ClipboardController.copyToClipboard(workflow.id)} + > + + ${msg("Copy Workflow ID")} + + + ${when( + canCrawl && !workflow.crawlCount, + () => html` + + + + ${msg("Delete Workflow")} + + `, + )} + `; + } + + private renderLatestCrawlMenu(latestCrawl: Crawl) { + const authToken = this.authState?.headers.Authorization.split(" ")[1]; + const logTotals = this.logTotals; + + return html` + + + + ${msg("Download Item")} + ${latestCrawl.fileSize + ? html` ${this.localize.bytes(latestCrawl.fileSize)}` + : nothing} + + + + + ${msg("Download Log")} + + + ${when( + isSuccessfullyFinished(latestCrawl), + () => html` + + this.navigate.to( + `${this.navigate.orgBasePath}/workflows/${latestCrawl.cid}/${WorkflowTab.Crawls}/${latestCrawl.id}`, + )} + > + + ${msg("View Item Details")} + + `, + )} + ClipboardController.copyToClipboard(latestCrawl.id)} + > + + ${msg("Copy Item ID")} + + + `; + } +} diff --git a/frontend/src/pages/org/workflow-detail.ts b/frontend/src/pages/org/workflow-detail.ts index 4b7948f28b..a99289267e 100644 --- a/frontend/src/pages/org/workflow-detail.ts +++ b/frontend/src/pages/org/workflow-detail.ts @@ -1,6 +1,6 @@ import { localized, msg, str } from "@lit/localize"; import { Task, TaskStatus } from "@lit/task"; -import type { SlSelect } from "@shoelace-style/shoelace"; +import type { SlDropdown, SlSelect } from "@shoelace-style/shoelace"; import clsx from "clsx"; import { html, nothing, type PropertyValues, type TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; @@ -23,6 +23,10 @@ import { import { ClipboardController } from "@/controllers/clipboard"; import { CrawlStatus } from "@/features/archived-items/crawl-status"; import { ExclusionEditor } from "@/features/crawl-workflows/exclusion-editor"; +import { + Action, + type BtrixSelectActionEvent, +} from "@/features/crawl-workflows/workflow-action-menu/types"; import { pageError } from "@/layouts/pageError"; import { pageNav, type Breadcrumb } from "@/layouts/pageHeader"; import { WorkflowTab } from "@/routes"; @@ -900,7 +904,6 @@ export class WorkflowDetail extends BtrixElement { private readonly renderActions = () => { if (!this.workflow) return; - const workflow = this.workflow; const archivingDisabled = isArchivingDisabled(this.org, true); const cancelStopLoading = this.isCancelingRun; @@ -909,6 +912,7 @@ export class WorkflowDetail extends BtrixElement { const hidePauseResume = !this.lastCrawlId || this.isCancelingRun || + this.workflow.lastCrawlState === "starting" || this.workflow.lastCrawlStopping; const disablePauseResume = this.disablePauseResume || @@ -970,199 +974,63 @@ export class WorkflowDetail extends BtrixElement { this.renderRunNowButton, )} - + ${msg("Actions")} - - ${when( - workflow.isCrawlRunning, - // HACK shoelace doesn't current have a way to override non-hover - // color without resetting the --sl-color-neutral-700 variable - () => html` - ${when(!hidePauseResume && !disablePauseResume, () => - paused - ? html` - void this.pauseResumeTask.run()} - > - - ${msg("Resume Crawl")} - - ` - : html` - void this.pauseResumeTask.run()} - > - - ${msg("Pause Crawl")} - - `, - )} - - (this.openDialogName = "stop")} - ?disabled=${workflow.lastCrawlStopping || this.isCancelingRun} - > - - ${msg("Stop Crawl")} - - (this.openDialogName = "cancel")} - > - - ${msg(html`Cancel & Discard Crawl`)} - - `, - () => html` - void this.runNowTask.run()} - > - - ${msg("Run Crawl")} - - `, - )} - - ${when( - workflow.isCrawlRunning && !workflow.lastCrawlStopping, - () => html` - (this.openDialogName = "scale")}> - - ${msg("Edit Browser Windows")} - - (this.openDialogName = "exclusions")} - ?disabled=${!this.isCrawling} - > - - ${msg("Edit Exclusions")} - - `, - )} - - this.navigate.to( - `/orgs/${this.appState.orgSlug}/workflows/${workflow.id}?edit`, - )} - > - - ${msg("Edit Workflow Settings")} - - void this.duplicateConfig()} - > - - ${msg("Duplicate Workflow")} - - ${when( - workflow.lastCrawlId, - () => html` - - - ${this.tabLabels.latest} ${this.renderLatestCrawlMenu()} - - `, - )} - - - ClipboardController.copyToClipboard(workflow.tags.join(", "))} - ?disabled=${!workflow.tags.length} - > - - ${msg("Copy Tags")} - - ClipboardController.copyToClipboard(workflow.id)} - > - - ${msg("Copy Workflow ID")} - - - ${when( - !workflow.crawlCount, - () => html` - - (this.openDialogName = "delete")} - > - - ${msg("Delete Workflow")} - - `, - )} - + `; }; - private renderLatestCrawlMenu() { - const authToken = this.authState?.headers.Authorization.split(" ")[1]; - const latestCrawl = this.latestCrawlTask.value; - const logTotals = this.logTotalsTask.value; - - return html` - - - - ${msg("Download Item")} - ${latestCrawl?.fileSize - ? html` ${this.localize.bytes(latestCrawl.fileSize)}` - : nothing} - + private readonly onSelectAction = (e: BtrixSelectActionEvent) => { + const dropdown = e.currentTarget as SlDropdown; - - - ${msg("Download Log")} - - - + switch (e.detail.item.action) { + case Action.Run: + void this.runNowTask.run(); + break; + case Action.TogglePauseResume: + void this.pauseResumeTask.run(); + break; + case Action.Stop: + this.openDialogName = "stop"; + break; + case Action.Cancel: + this.openDialogName = "cancel"; + break; + case Action.EditBrowserWindows: + this.openDialogName = "scale"; + break; + case Action.EditExclusions: + this.openDialogName = "exclusions"; + break; + case Action.Duplicate: + void this.duplicateConfig(); + break; + case Action.Delete: + this.openDialogName = "delete"; + break; + default: + console.debug("unknown workflow action:", e.detail.item.action); + break; + } - ${when( - this.archivedItemId, - (id) => html` - - this.navigate.to( - `${this.basePath}/${WorkflowTab.Crawls}/${id}`, - )} - > - - ${msg("View Item Details")} - - `, - )} - - ClipboardController.copyToClipboard(this.lastCrawlId || "")} - ?disabled=${!this.lastCrawlId} - > - - ${msg("Copy Item ID")} - - - `; - } + void dropdown.hide(); + dropdown.focusOnTrigger(); + }; private renderDetails() { const relativeDate = ( diff --git a/frontend/src/pages/org/workflows-list.ts b/frontend/src/pages/org/workflows-list.ts index eb89203ec0..857fd69a7c 100644 --- a/frontend/src/pages/org/workflows-list.ts +++ b/frontend/src/pages/org/workflows-list.ts @@ -24,6 +24,10 @@ import { type SelectEvent } from "@/components/ui/search-combobox"; import { ClipboardController } from "@/controllers/clipboard"; import { SearchParamsController } from "@/controllers/searchParams"; import type { SelectJobTypeEvent } from "@/features/crawl-workflows/new-workflow-dialog"; +import { + Action, + type BtrixSelectActionEvent, +} from "@/features/crawl-workflows/workflow-action-menu/types"; import { type BtrixChangeWorkflowProfileFilterEvent } from "@/features/crawl-workflows/workflow-profile-filter"; import type { BtrixChangeWorkflowScheduleFilterEvent } from "@/features/crawl-workflows/workflow-schedule-filter"; import type { BtrixChangeWorkflowTagFilterEvent } from "@/features/crawl-workflows/workflow-tag-filter"; @@ -794,11 +798,59 @@ export class WorkflowsList extends BtrixElement { private readonly renderWorkflowItem = (workflow: ListWorkflow) => html` - ${this.renderMenuItems(workflow)} + { + switch (e.detail.item.action) { + case Action.Run: + void this.runNow(workflow); + break; + case Action.TogglePauseResume: + // TODO + break; + case Action.Stop: + void this.stop(workflow.lastCrawlId); + break; + case Action.Cancel: + void this.cancel(workflow.lastCrawlId); + break; + case Action.EditBrowserWindows: + this.navigate.to( + `${this.navigate.orgBasePath}/workflows/${workflow.id}/${WorkflowTab.LatestCrawl}`, + { + dialog: "scale", + }, + ); + break; + case Action.EditExclusions: + this.navigate.to( + `${this.navigate.orgBasePath}/workflows/${workflow.id}/${WorkflowTab.LatestCrawl}`, + { + dialog: "exclusions", + }, + ); + break; + case Action.Duplicate: + void this.duplicateConfig(workflow); + break; + case Action.Delete: { + this.workflowToDelete = workflow; + await this.updateComplete; + void this.deleteDialog?.show(); + break; + } + default: + console.debug("unknown workflow action:", e.detail.item.action); + break; + } + }} + > `; - private renderMenuItems(workflow: ListWorkflow) { + private renderMenu(workflow: ListWorkflow) { return html` ${when( workflow.isCrawlRunning && this.appState.isCrawler, diff --git a/frontend/src/stories/decorators/orgDecorator.ts b/frontend/src/stories/decorators/orgDecorator.ts index 4ec4a7294e..b9dbd908cf 100644 --- a/frontend/src/stories/decorators/orgDecorator.ts +++ b/frontend/src/stories/decorators/orgDecorator.ts @@ -8,6 +8,8 @@ import { AppStateService } from "@/utils/state"; const { users, usage, quotas, ...org } = orgMock; +export { orgMock }; + export type StorybookOrgProps = { orgUsers?: boolean; orgUsage?: boolean; diff --git a/frontend/src/stories/design/action-menus.mdx b/frontend/src/stories/design/action-menus.mdx index c7603a046b..9a2aa57ac6 100644 --- a/frontend/src/stories/design/action-menus.mdx +++ b/frontend/src/stories/design/action-menus.mdx @@ -1,4 +1,5 @@ -import { Meta } from "@storybook/blocks"; +import { Meta, Canvas } from '@storybook/addon-docs/blocks'; +import { NoRuns } from '../features/crawl-workflows/WorkflowActionMenu.stories'; @@ -27,3 +28,7 @@ should be ordered as follows: | Actions related to exporting/downloading (eg: download archived item) | | Copy information to clipboard (eg: copy share link)
Copy IDs to clipboard (eg: copy item ID) | | Destructive actions (eg: delete item) | + +## Example + + diff --git a/frontend/src/stories/features/crawl-workflows/WorkflowActionMenu.stories.ts b/frontend/src/stories/features/crawl-workflows/WorkflowActionMenu.stories.ts new file mode 100644 index 0000000000..8452f9797a --- /dev/null +++ b/frontend/src/stories/features/crawl-workflows/WorkflowActionMenu.stories.ts @@ -0,0 +1,132 @@ +import type { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; +import type { DecoratorFunction } from "storybook/internal/types"; + +import { argTypes } from "../excludeContainerProperties"; + +import "@/features/crawl-workflows/workflow-action-menu"; + +import workflowMock from "@/__mocks__/api/orgs/[id]/crawlconfigs/[id]"; +import type { WorkflowActionMenu } from "@/features/crawl-workflows/workflow-action-menu"; +import { orgDecorator, orgMock } from "@/stories/decorators/orgDecorator"; +import type { Workflow } from "@/types/crawler"; +import { AccessCode } from "@/types/org"; +import appState from "@/utils/state"; + +const mockAppState = ({ role }: { role: AccessCode }) => { + return { + ...appState, + org: orgMock, + isCrawler: role >= AccessCode.crawler, + isAdmin: role >= AccessCode.owner, + userOrg: { + default: true, + id: orgMock.id, + name: orgMock.name, + slug: orgMock.slug, + role, + }, + orgId: workflowMock.oid, + }; +}; + +const meta = { + title: "Features/Workflow Action Menu", + component: "btrix-workflow-action-menu", + subcomponents: { + CustomBehaviorsTableRow: "btrix-workflow-action-menu-row", + }, + tags: ["autodocs"], + decorators: [orgDecorator as DecoratorFunction], + render: (args) => html` + + `, + argTypes: { + ...argTypes, + }, + args: { + appState: mockAppState({ role: 20 }), + workflow: workflowMock as unknown as Workflow, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** + * Users with a "crawler" role will see the following options. + */ +export const Crawler: Story = { + args: { + workflow: { + ...meta.args.workflow, + }, + }, +}; + +/** + * A workflow can be deleted if it's never been run. + */ +export const NoRuns: Story = { + args: { + workflow: { + ...meta.args.workflow, + crawlCount: 0, + }, + }, +}; + +/** + * Additional actions are displayed depending on the workflow run state. + */ +export const Running: Story = { + args: { + workflow: { + ...meta.args.workflow, + isCrawlRunning: true, + lastCrawlState: "running", + }, + }, +}; + +export const Paused: Story = { + args: { + workflow: { + ...meta.args.workflow, + isCrawlRunning: true, + lastCrawlState: "paused", + }, + }, +}; + +export const Canceling: Story = { + args: { + workflow: { + ...meta.args.workflow, + isCrawlRunning: true, + lastCrawlState: "running", + }, + cancelingRun: true, + }, +}; + +/** + * Users with a "viewer" role will only see copy-to-clipboard options. + */ +export const Viewer: Story = { + args: { + appState: mockAppState({ role: 10 }), + workflow: { + ...meta.args.workflow, + tags: ["sample-tag-1", "sample-tag-2"], + }, + }, +}; diff --git a/frontend/src/theme.stylesheet.css b/frontend/src/theme.stylesheet.css index 82512fc087..2ad070739a 100644 --- a/frontend/src/theme.stylesheet.css +++ b/frontend/src/theme.stylesheet.css @@ -230,6 +230,21 @@ border: 1px solid var(--sl-panel-border-color); } + /* Adjust menu item hover and focus styles */ + sl-menu-item, + btrix-menu-item-link { + @apply part-[base]:text-neutral-700 part-[base]:hover:bg-cyan-50/50 part-[base]:hover:text-cyan-700 part-[base]:focus-visible:bg-cyan-50/50; + } + + /* Add menu item variants */ + .menu-item-success { + @apply part-[base]:text-success part-[base]:hover:bg-success-50 part-[base]:hover:text-success-700 part-[base]:focus-visible:bg-success-50; + } + + .menu-item-danger { + @apply part-[base]:text-danger part-[base]:hover:bg-danger-50 part-[base]:hover:text-danger-700 part-[base]:focus-visible:bg-danger-50; + } + /* Validation styles */ /** * FIXME Use [data-user-invalid] selector exclusion table is migrated diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 7f598638fe..38b99be4d3 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -6003,7 +6003,7 @@ fast-glob@^3.1.1: merge2 "^1.3.0" micromatch "^4.0.8" -fast-glob@^3.2.11, fast-glob@^3.2.2, fast-glob@^3.3.0, fast-glob@^3.3.1, fast-glob@^3.3.2: +fast-glob@^3.2.11, fast-glob@^3.2.2, fast-glob@^3.3.1, fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -7288,10 +7288,10 @@ jest-worker@^29.7.0: merge-stream "^2.0.0" supports-color "^8.0.0" -jiti@^1.19.1: - version "1.21.0" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" - integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== +jiti@^1.21.6: + version "1.21.7" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.7.tgz#9dd81043424a3d28458b193d965f0d18a2300ba9" + integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== js-levenshtein-esm@^1.2.0: version "1.2.0" @@ -7525,7 +7525,7 @@ lighthouse-logger@^1.0.0: debug "^2.6.9" marky "^1.2.2" -lilconfig@2.1.0, lilconfig@^2.1.0: +lilconfig@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== @@ -7540,6 +7540,11 @@ lilconfig@^3.0.0: resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.0.0.tgz#f8067feb033b5b74dab4602a5f5029420be749bc" integrity sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g== +lilconfig@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4" + integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -8430,12 +8435,7 @@ nanoid@^3.1.25, nanoid@^3.3.4: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== -nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== - -nanoid@^3.3.8: +nanoid@^3.3.11, nanoid@^3.3.8: version "3.3.11" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== @@ -9050,7 +9050,7 @@ postcss-lit@^1.1.1: "@babel/traverse" "^7.16.0" lilconfig "^2.0.6" -postcss-load-config@^4.0.1: +postcss-load-config@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== @@ -9116,14 +9116,14 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" -postcss-nested@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c" - integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ== +postcss-nested@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.2.0.tgz#4c2d22ab5f20b9cb61e2c5c5915950784d068131" + integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ== dependencies: - postcss-selector-parser "^6.0.11" + postcss-selector-parser "^6.1.1" -postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: version "6.0.11" resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz" integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== @@ -9131,6 +9131,14 @@ postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.2, postcss-selecto cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^6.1.1, postcss-selector-parser@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-selector-parser@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz#4d6af97eba65d73bc4d84bcb343e865d7dd16262" @@ -9153,15 +9161,6 @@ postcss@^8.4.19, postcss@^8.4.5: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.4.23: - version "8.4.33" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742" - integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg== - dependencies: - nanoid "^3.3.7" - picocolors "^1.0.0" - source-map-js "^1.0.2" - postcss@^8.4.33: version "8.5.3" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb" @@ -9171,6 +9170,15 @@ postcss@^8.4.33: picocolors "^1.1.1" source-map-js "^1.2.1" +postcss@^8.4.47: + version "8.5.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + postinstall-postinstall@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" @@ -9636,7 +9644,7 @@ resolve@^1.1.7, resolve@^1.17.0, resolve@^1.19.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.20.0: +resolve@^1.20.0, resolve@^1.22.8: version "1.22.10" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== @@ -9645,7 +9653,7 @@ resolve@^1.20.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.22.1, resolve@^1.22.2, resolve@^1.22.4: +resolve@^1.22.1, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -10408,7 +10416,7 @@ style-mod@^4.0.0, style-mod@^4.1.0, style-mod@^4.1.2: resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67" integrity sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw== -sucrase@^3.32.0: +sucrase@^3.35.0: version "3.35.0" resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== @@ -10470,32 +10478,32 @@ table-layout@^1.0.2: wordwrapjs "^4.0.0" tailwindcss@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.1.tgz#f512ca5d1dd4c9503c7d3d28a968f1ad8f5c839d" - integrity sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA== + version "3.4.17" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.17.tgz#ae8406c0f96696a631c790768ff319d46d5e5a63" + integrity sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og== dependencies: "@alloc/quick-lru" "^5.2.0" arg "^5.0.2" - chokidar "^3.5.3" + chokidar "^3.6.0" didyoumean "^1.2.2" dlv "^1.1.3" - fast-glob "^3.3.0" + fast-glob "^3.3.2" glob-parent "^6.0.2" is-glob "^4.0.3" - jiti "^1.19.1" - lilconfig "^2.1.0" - micromatch "^4.0.5" + jiti "^1.21.6" + lilconfig "^3.1.3" + micromatch "^4.0.8" normalize-path "^3.0.0" object-hash "^3.0.0" - picocolors "^1.0.0" - postcss "^8.4.23" + picocolors "^1.1.1" + postcss "^8.4.47" postcss-import "^15.1.0" postcss-js "^4.0.1" - postcss-load-config "^4.0.1" - postcss-nested "^6.0.1" - postcss-selector-parser "^6.0.11" - resolve "^1.22.2" - sucrase "^3.32.0" + postcss-load-config "^4.0.2" + postcss-nested "^6.2.0" + postcss-selector-parser "^6.1.2" + resolve "^1.22.8" + sucrase "^3.35.0" tapable@^1.0.0: version "1.1.3"