fix: Sync user guide to correct workflow section (#2592)

Resolves https://github.com/webrecorder/browsertrix/issues/2560

## Changes

- Syncs workflow current form section with user guide section.
- Stickies "User Guide" button to top of viewport so that user guide can
be opened.
- Makes content behind user guide clickable (fixes issues with stickied
elements shifting when user guide is not contained to the parent
element.)
- Decreases size of user guide text when embedded in an iframe.
- Refactors overflow scrim to reuse CSS variables.

---------
Co-authored-by: Emma Segal-Grossman <hi@emma.cafe>
This commit is contained in:
sua yoo 2025-05-08 14:41:35 -07:00 committed by GitHub
parent 652e8a6085
commit 6b510fe89c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 138 additions and 51 deletions

View File

@ -0,0 +1,9 @@
if (window.self !== window.top) {
// Within iframe--assume this is an iframe embedded in the Browsertrix app.
const style = document.createElement("style");
// Decrease text size without decreasing element size and overall spacing
style.innerText = `.md-typeset { font-size: 0.7rem; }`;
window.document.body.appendChild(style);
}

View File

@ -6,6 +6,7 @@ extra_css:
- stylesheets/extra.css
extra_javascript:
- js/insertversion.js
- js/embed.js
theme:
name: material
custom_dir: docs/overrides

View File

@ -64,6 +64,7 @@ import type {
ExclusionChangeEvent,
QueueExclusionTable,
} from "@/features/crawl-workflows/queue-exclusion-table";
import type { UserGuideEventMap } from "@/index";
import { infoCol, inputCol } from "@/layouts/columns";
import { pageSectionsWithNav } from "@/layouts/pageSectionsWithNav";
import { panel } from "@/layouts/panel";
@ -105,7 +106,9 @@ import {
getDefaultFormState,
getInitialFormState,
getServerDefaults,
makeUserGuideEvent,
SECTIONS,
workflowTabToGuideHash,
type FormState,
type WorkflowDefaults,
} from "@/utils/workflow";
@ -420,6 +423,7 @@ export class WorkflowEditor extends BtrixElement {
nav: this.renderNav(),
main: this.renderFormSections(),
sticky: true,
stickyTopClassname: tw`lg:top-16`,
})}
${this.renderFooter()}
</form>
@ -485,6 +489,10 @@ export class WorkflowEditor extends BtrixElement {
this.updateProgressState({
activeTab: name,
});
if (this.appState.userGuideOpen) {
this.dispatchEvent(makeUserGuideEvent(name));
}
}
track(AnalyticsTrackEvent.ExpandWorkflowFormSection, {
@ -2160,6 +2168,21 @@ https://archiveweb.page/images/${"logo.svg"}`}
await this.updateComplete;
void this.scrollToActivePanel();
if (this.appState.userGuideOpen) {
this.dispatchEvent(
new CustomEvent<UserGuideEventMap["btrix-user-guide-show"]["detail"]>(
"btrix-user-guide-show",
{
detail: {
path: `user-guide/workflow-setup/#${workflowTabToGuideHash[step]}`,
},
bubbles: true,
composed: true,
},
),
);
}
};
private onKeyDown(event: KeyboardEvent) {

View File

@ -339,7 +339,9 @@ export class App extends BtrixElement {
<div class="min-w-screen flex min-h-screen flex-col">
${this.renderSuperadminBanner()} ${this.renderNavBar()}
${this.renderAlertBanner()}
<main class="relative flex flex-auto md:min-h-[calc(100vh-3.125rem)]">
<main
class="relative flex flex-auto transition-[padding] md:min-h-[calc(100vh-3.125rem)]"
>
${this.renderPage()}
</main>
<div class="border-t border-neutral-100">${this.renderFooter()}</div>
@ -356,14 +358,27 @@ export class App extends BtrixElement {
<sl-drawer
id="userGuideDrawer"
label=${msg("User Guide")}
style="--body-spacing: 0; --footer-spacing: var(--sl-spacing-2x-small);"
class="[--body-spacing:0] [--footer-spacing:var(--sl-spacing-2x-small)] [--size:31rem] part-[base]:fixed part-[base]:z-50 part-[panel]:[border-left:1px_solid_var(--sl-panel-border-color)]"
?open=${this.appState.userGuideOpen}
contained
@sl-hide=${() => AppStateService.updateUserGuideOpen(false)}
@sl-after-hide=${() => {
// FIXME There might be a way to handle this in Mkdocs, but updating
// only the hash doesn't seem to update the docs view
const iframe = this.userGuideDrawer.querySelector("iframe");
if (!iframe) return;
const src = iframe.src;
iframe.src = src.slice(0, src.indexOf("#"));
}}
>
<span slot="label" class="flex items-center gap-3">
<sl-icon name="book" class=""></sl-icon>
<span>${msg("User Guide")}</span>
</span>
<iframe
class="size-full transition-opacity duration-slow"
class="size-full text-xs transition-opacity duration-slow"
src="${this.docsUrl}user-guide/workflow-setup/"
></iframe>
<sl-button
@ -952,7 +967,9 @@ export class App extends BtrixElement {
iframe.src = this.fullDocsUrl;
}
void this.userGuideDrawer.show();
if (!this.appState.userGuideOpen) {
AppStateService.updateUserGuideOpen(true);
}
} else {
console.debug("user guide iframe not found");
}

View File

@ -8,11 +8,13 @@ export function pageSectionsWithNav({
main,
placement = "start",
sticky = false,
stickyTopClassname,
}: {
nav: TemplateResult;
main: TemplateResult;
placement?: "start" | "top";
sticky?: boolean;
stickyTopClassname?: string; // e.g. `lg:top-0`
}) {
return html`
<div
@ -24,7 +26,8 @@ export function pageSectionsWithNav({
<div
class=${clsx(
tw`flex flex-1 flex-col gap-2`,
sticky && tw`lg:sticky lg:top-2 lg:self-start`,
sticky &&
[tw`lg:sticky lg:self-start`, stickyTopClassname || tw`lg:top-2`],
placement === "start" ? tw`lg:max-w-[16.5rem]` : tw`lg:flex-row`,
)}
part="tabs"

View File

@ -592,7 +592,9 @@ export class WorkflowDetail extends BtrixElement {
private readonly renderEditor = () => html`
<div class="col-span-1">${this.renderBreadcrumbs()}</div>
<header class="col-span-1 mb-3 flex flex-wrap gap-2">
<header
class="scrim scrim-to-b z-10 col-span-1 mb-3 flex flex-wrap gap-2 before:-top-3 lg:sticky lg:top-3"
>
<btrix-detail-page-title .item=${this.workflow}></btrix-detail-page-title>
</header>

View File

@ -1,4 +1,5 @@
import { localized, msg } from "@lit/localize";
import clsx from "clsx";
import { mergeDeep } from "immutable";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@ -7,33 +8,18 @@ import type { PartialDeep } from "type-fest";
import { ScopeType, type Seed, type WorkflowParams } from "./types";
import type { UserGuideEventMap } from "@/index";
import { pageNav, type Breadcrumb } from "@/layouts/pageHeader";
import { WorkflowScopeType } from "@/types/workflow";
import LiteElement, { html } from "@/utils/LiteElement";
import { tw } from "@/utils/tailwind";
import {
DEFAULT_AUTOCLICK_SELECTOR,
DEFAULT_SELECT_LINKS,
makeUserGuideEvent,
type SectionsEnum,
type FormState as WorkflowFormState,
} from "@/utils/workflow";
type GuideHash =
| "scope"
| "limits"
| "browser-settings"
| "scheduling"
| "metadata"
| "review-settings";
const workflowTabToGuideHash: Record<string, GuideHash> = {
crawlSetup: "scope",
crawlLimits: "limits",
browserSettings: "browser-settings",
crawlScheduling: "scheduling",
crawlMetadata: "metadata",
confirmSettings: "review-settings",
};
/**
* Usage:
* ```ts
@ -55,23 +41,6 @@ export class WorkflowsNew extends LiteElement {
@property({ type: Object })
initialWorkflow?: WorkflowParams;
private userGuideHashLink: GuideHash = "scope";
connectedCallback(): void {
super.connectedCallback();
this.userGuideHashLink =
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
workflowTabToGuideHash[window.location.hash.slice(1) as GuideHash] ||
"scope";
window.addEventListener("hashchange", () => {
const hashValue = window.location.hash.slice(1) as GuideHash;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
this.userGuideHashLink = workflowTabToGuideHash[hashValue] || "scope";
});
}
private get defaultNewWorkflow(): WorkflowParams {
return {
name: "",
@ -125,21 +94,20 @@ export class WorkflowsNew extends LiteElement {
return html`
<div class="mb-5">${this.renderBreadcrumbs()}</div>
<header class="flex items-center justify-between">
<header
class="scrim scrim-to-b z-10 flex flex-wrap items-start justify-between gap-2 to-white before:-top-3 lg:sticky lg:top-3"
>
<h2 class="mb-6 text-xl font-semibold">${msg("New Crawl Workflow")}</h2>
<sl-button
size="small"
class=${clsx(
tw`transition-opacity`,
this.appState.userGuideOpen && tw`pointer-events-none opacity-0`,
)}
?disabled=${this.appState.userGuideOpen}
@click=${() => {
this.dispatchEvent(
new CustomEvent<
UserGuideEventMap["btrix-user-guide-show"]["detail"]
>("btrix-user-guide-show", {
detail: {
path: `user-guide/workflow-setup/#${this.userGuideHashLink}`,
},
bubbles: true,
composed: true,
}),
makeUserGuideEvent(window.location.hash.slice(1) as SectionsEnum),
);
}}
>

View File

@ -96,6 +96,15 @@
/* Transition */
--sl-transition-x-fast: 100ms;
/*
*
* Browsertrix theme tokens
*
*/
/* Overflow scrim */
--btrix-overflow-scroll-scrim-color: var(--sl-panel-background-color);
--btrix-overflow-scrim-width: 3rem;
}
body {
@ -446,6 +455,16 @@
sl-button.button-card::part(label) {
@apply flex flex-1 flex-col justify-center gap-2 text-left;
}
.scrim:before {
@apply pointer-events-none absolute -z-10;
}
.scrim-to-b:before {
@apply w-full bg-gradient-to-b from-white;
height: var(--btrix-overflow-scrim-width);
--tw-gradient-from: var(--btrix-overflow-scroll-scrim-color, white);
}
}
/* Following styles won't work with layers */

View File

@ -47,6 +47,8 @@ export function makeAppStateService() {
// Org details
org: OrgData | null | undefined = undefined;
userGuideOpen = false;
// Since org slug is used to ID an org, use `userOrg`
// to retrieve the basic org info like name and ID
// before other org details are available
@ -159,6 +161,11 @@ export function makeAppStateService() {
}
}
@unlock()
updateUserGuideOpen(open: boolean) {
appState.userGuideOpen = open;
}
@transaction()
@unlock()
resetAll() {

View File

@ -4,6 +4,7 @@ import { z } from "zod";
import { getAppSettings } from "./app";
import type { Tags } from "@/components/ui/tag-input";
import type { UserGuideEventMap } from "@/index";
import {
Behavior,
ScopeType,
@ -37,6 +38,43 @@ export const SECTIONS = [
export const sectionsEnum = z.enum(SECTIONS);
export type SectionsEnum = z.infer<typeof sectionsEnum>;
export enum GuideHash {
Scope = "scope",
Limits = "crawl-limits",
Behaviors = "page-behavior",
BrowserSettings = "browser-settings",
Scheduling = "scheduling",
Metadata = "metadata",
}
export const workflowTabToGuideHash: Record<SectionsEnum, GuideHash> = {
scope: GuideHash.Scope,
limits: GuideHash.Limits,
behaviors: GuideHash.Behaviors,
browserSettings: GuideHash.BrowserSettings,
scheduling: GuideHash.Scheduling,
metadata: GuideHash.Metadata,
};
export function makeUserGuideEvent(
section: SectionsEnum,
): UserGuideEventMap["btrix-user-guide-show"] {
const userGuideHash =
(workflowTabToGuideHash[section] as GuideHash | undefined) ||
GuideHash.Scope;
return new CustomEvent<UserGuideEventMap["btrix-user-guide-show"]["detail"]>(
"btrix-user-guide-show",
{
detail: {
path: `user-guide/workflow-setup/#${userGuideHash}`,
},
bubbles: true,
composed: true,
},
);
}
export function defaultLabel(value: unknown): string {
if (value === Infinity) {
return msg("Default: Unlimited");