mirror of
https://github.com/coder/coder.git
synced 2026-09-21 12:44:32 +08:00
fix(site): WCAG 2.1 AA remediation — landmarks, semantics, and a11y tooling (#22746)
## Summary Targeted WCAG 2.1 AA accessibility remediation — continuation of #22673 — addressing remaining semantic, landmark, and tooling gaps identified in the frontend accessibility review. ### Changes #### Document semantics (WCAG 3.1.1) - **`site/index.html`**: Added `<html lang="en">` root wrapper so screen readers and browser features correctly identify the document language. #### Landmark & bypass (WCAG 1.3.1, 2.4.1) - **`DashboardLayout.tsx`**: Replaced `<div id="main-content">` with `<main id="main-content">` so assistive technology exposes a proper main landmark and the skip link targets a semantic region. #### Table header relationships (WCAG 1.3.1) - **`Table.tsx`**: `TableHead` now renders `scope="col"` by default (overridable via prop), giving data cells an explicit header relationship. #### Semantic interactive controls (WCAG 2.1.1, 4.1.2) - **`AuditLogRow.tsx`**: Replaced `<div role="button" tabIndex={0}>` with native `<button type="button">`, removing the manual keyboard handler (native button provides Enter/Space for free). - **`Autocomplete.tsx`**: Replaced clear `<span role="button" tabIndex={0}>` with native `<button type="button" aria-label="Clear selection">`. #### Reduced motion (WCAG 2.3.3 best practice) - **`index.css`**: Added global `@media (prefers-reduced-motion: reduce)` block that suppresses non-essential animations and transitions. #### Accessibility regression tooling - **Storybook**: Added `@storybook/addon-a11y` (version-matched to existing Storybook 10.x). - **vitest-axe**: Added `vitest-axe` with setup wiring and an exemplar `Table.axe.test.tsx` that runs axe-core assertions in vitest. ### Test plan - 12 new/updated tests pass across 5 test files: - `DashboardLayout.test.tsx` — main landmark + skip link behavior - `Table.test.tsx` — scope default + override - `Table.axe.test.tsx` — axe-core violation scan - `AuditPage.test.tsx` — keyboard toggle with native button - `Autocomplete.test.tsx` — clear control semantics - `pnpm lint` clean (biome, TypeScript, circular deps) - Manual keyboard traversal: skip link → main content, audit row toggle, autocomplete clear
This commit is contained in:
@@ -3,6 +3,7 @@ export default {
|
||||
|
||||
addons: [
|
||||
"@chromatic-com/storybook",
|
||||
"@storybook/addon-a11y",
|
||||
"@storybook/addon-docs",
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-themes",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
.########+ -########. #########+ ########## #### .#### ###########
|
||||
-->
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Coder</title>
|
||||
@@ -61,3 +62,4 @@
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
"@chromatic-com/storybook": "5.0.1",
|
||||
"@octokit/types": "12.6.0",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@storybook/addon-a11y": "10.2.10",
|
||||
"@storybook/addon-docs": "10.2.10",
|
||||
"@storybook/addon-links": "10.2.10",
|
||||
"@storybook/addon-themes": "10.2.10",
|
||||
|
||||
Generated
+20
@@ -311,6 +311,9 @@ importers:
|
||||
'@playwright/test':
|
||||
specifier: 1.50.1
|
||||
version: 1.50.1
|
||||
'@storybook/addon-a11y':
|
||||
specifier: 10.2.10
|
||||
version: 10.2.10(storybook@10.2.10(@testing-library/dom@10.4.0)(prettier@3.4.1)(react-dom@19.2.2(react@19.2.2))(react@19.2.2))
|
||||
'@storybook/addon-docs':
|
||||
specifier: 10.2.10
|
||||
version: 10.2.10(@types/react@19.2.7)(esbuild@0.25.12)(rollup@4.53.3)(storybook@10.2.10(@testing-library/dom@10.4.0)(prettier@3.4.1)(react-dom@19.2.2(react@19.2.2))(react@19.2.2))(vite@7.2.6(@types/node@20.19.25)(jiti@1.21.7)(yaml@2.7.0))
|
||||
@@ -2313,6 +2316,11 @@ packages:
|
||||
'@standard-schema/spec@1.0.0':
|
||||
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==, tarball: https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz}
|
||||
|
||||
'@storybook/addon-a11y@10.2.10':
|
||||
resolution: {integrity: sha512-1S9pDXgvbHhBStGarCvfJ3/rfcaiAcQHRhuM3Nk4WGSIYtC1LCSRuzYdDYU0aNRpdCbCrUA7kUCbqvIE3tH+3Q==, tarball: https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.2.10.tgz}
|
||||
peerDependencies:
|
||||
storybook: ^10.2.10
|
||||
|
||||
'@storybook/addon-docs@10.2.10':
|
||||
resolution: {integrity: sha512-2wIYtdvZIzPbQ5194M5Igpy8faNbQ135nuO5ZaZ2VuttqGr+IJcGnDP42zYwbAsGs28G8ohpkbSgIzVyJWUhPQ==, tarball: https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.2.10.tgz}
|
||||
peerDependencies:
|
||||
@@ -3036,6 +3044,10 @@ packages:
|
||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==, tarball: https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
axe-core@4.11.1:
|
||||
resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==, tarball: https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
axios@1.13.2:
|
||||
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==, tarball: https://registry.npmjs.org/axios/-/axios-1.13.2.tgz}
|
||||
|
||||
@@ -8653,6 +8665,12 @@ snapshots:
|
||||
|
||||
'@standard-schema/spec@1.0.0': {}
|
||||
|
||||
'@storybook/addon-a11y@10.2.10(storybook@10.2.10(@testing-library/dom@10.4.0)(prettier@3.4.1)(react-dom@19.2.2(react@19.2.2))(react@19.2.2))':
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
axe-core: 4.11.1
|
||||
storybook: 10.2.10(@testing-library/dom@10.4.0)(prettier@3.4.1)(react-dom@19.2.2(react@19.2.2))(react@19.2.2)
|
||||
|
||||
'@storybook/addon-docs@10.2.10(@types/react@19.2.7)(esbuild@0.25.12)(rollup@4.53.3)(storybook@10.2.10(@testing-library/dom@10.4.0)(prettier@3.4.1)(react-dom@19.2.2(react@19.2.2))(react@19.2.2))(vite@7.2.6(@types/node@20.19.25)(jiti@1.21.7)(yaml@2.7.0))':
|
||||
dependencies:
|
||||
'@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.2)
|
||||
@@ -9414,6 +9432,8 @@ snapshots:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.0.0
|
||||
|
||||
axe-core@4.11.1: {}
|
||||
|
||||
axios@1.13.2:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Avatar } from "components/Avatar/Avatar";
|
||||
import { AvatarData } from "components/Avatar/AvatarData";
|
||||
import { Check } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { expect, screen, userEvent, waitFor, within } from "storybook/test";
|
||||
import { expect, fn, screen, userEvent, waitFor, within } from "storybook/test";
|
||||
import { Autocomplete } from "./Autocomplete";
|
||||
|
||||
const meta: Meta<typeof Autocomplete> = {
|
||||
@@ -222,13 +222,22 @@ export const SearchAndFilter: Story = {
|
||||
};
|
||||
|
||||
export const ClearSelection: Story = {
|
||||
render: function ClearSelectionStory() {
|
||||
args: {
|
||||
onChange: fn<(value: unknown) => void>(),
|
||||
},
|
||||
render: function ClearSelectionStory(args) {
|
||||
const [value, setValue] = useState<SimpleOption | null>(simpleOptions[0]);
|
||||
const handleChange = (newValue: SimpleOption | null) => {
|
||||
args.onChange(newValue);
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-80">
|
||||
<Autocomplete
|
||||
{...args}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
onChange={handleChange}
|
||||
options={simpleOptions}
|
||||
getOptionValue={(opt) => opt.id}
|
||||
getOptionLabel={(opt) => opt.name}
|
||||
@@ -237,13 +246,23 @@ export const ClearSelection: Story = {
|
||||
</div>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const trigger = canvas.getByRole("button", { name: /mango/i });
|
||||
expect(trigger).toHaveTextContent("Mango");
|
||||
|
||||
const clearButton = canvas.getByRole("button", { name: "Clear selection" });
|
||||
const onChangeSpy = args.onChange as ReturnType<
|
||||
typeof fn<(value: unknown) => void>
|
||||
>;
|
||||
onChangeSpy.mockClear();
|
||||
|
||||
const clearButton = canvas.getByLabelText("Clear selection");
|
||||
expect(clearButton).toHaveAttribute("role", "button");
|
||||
expect(clearButton).toHaveAttribute("tabindex", "0");
|
||||
expect(clearButton.tagName).toBe("SPAN");
|
||||
|
||||
await userEvent.click(clearButton);
|
||||
await waitFor(() => expect(onChangeSpy).toHaveBeenCalledWith(null));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
|
||||
@@ -181,7 +181,7 @@ export function Autocomplete<TOption>({
|
||||
handleClear(e);
|
||||
}
|
||||
}}
|
||||
className="flex items-center justify-center size-5 rounded hover:bg-surface-secondary transition-colors"
|
||||
className="flex items-center justify-center size-5 rounded hover:bg-surface-secondary transition-colors cursor-pointer"
|
||||
aria-label="Clear selection"
|
||||
>
|
||||
<X className="size-4 text-content-secondary hover:text-content-primary" />
|
||||
|
||||
@@ -23,7 +23,7 @@ export const FullPageForm: FC<FullPageFormProps> = ({
|
||||
{detail && <PageHeaderSubtitle>{detail}</PageHeaderSubtitle>}
|
||||
</PageHeader>
|
||||
|
||||
<main>{children}</main>
|
||||
<div>{children}</div>
|
||||
</Margins>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ export const FullPageHorizontalForm: FC<FullPageHorizontalFormProps> = ({
|
||||
{detail && <PageHeaderSubtitle>{detail}</PageHeaderSubtitle>}
|
||||
</PageHeader>
|
||||
|
||||
<main>{children}</main>
|
||||
<div>{children}</div>
|
||||
</Margins>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, within } from "storybook/test";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -87,4 +88,29 @@ const meta: Meta<typeof Table> = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Table>;
|
||||
|
||||
export const Default: Story = {};
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const invoiceHeader = canvas.getByRole("columnheader", { name: "Invoice" });
|
||||
|
||||
expect(invoiceHeader).toHaveAttribute("scope", "col");
|
||||
},
|
||||
};
|
||||
|
||||
export const ScopeOverride: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead scope="row">Invoice</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const invoiceHeader = canvas.getByRole("rowheader", { name: "Invoice" });
|
||||
|
||||
expect(invoiceHeader).toHaveAttribute("scope", "row");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -106,6 +106,7 @@ export const TableRow: React.FC<TableRowProps> = ({
|
||||
|
||||
export const TableHead: React.FC<React.ComponentPropsWithRef<"th">> = ({
|
||||
className,
|
||||
scope = "col",
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
@@ -115,6 +116,7 @@ export const TableHead: React.FC<React.ComponentPropsWithRef<"th">> = ({
|
||||
"[&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className,
|
||||
)}
|
||||
scope={scope}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -217,3 +217,14 @@
|
||||
@apply text-xs font-mono font-medium;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,9 +34,10 @@ test("renders a skip link before navigation content", async () => {
|
||||
name: "Skip to main content",
|
||||
});
|
||||
const navigation = screen.getAllByRole("navigation")[0];
|
||||
const mainContent = document.getElementById("main-content");
|
||||
const mainContent = screen.getByRole("main");
|
||||
|
||||
expect(skipToContentLink).toHaveAttribute("href", "#main-content");
|
||||
expect(mainContent).toHaveAttribute("id", "main-content");
|
||||
expect(mainContent).toHaveAttribute("tabindex", "-1");
|
||||
expect(
|
||||
skipToContentLink.compareDocumentPosition(navigation) &
|
||||
|
||||
@@ -39,7 +39,7 @@ export const DashboardLayout: FC = () => {
|
||||
</a>
|
||||
<Navbar />
|
||||
|
||||
<div
|
||||
<main
|
||||
id="main-content"
|
||||
tabIndex={-1}
|
||||
className={cn(
|
||||
@@ -50,7 +50,7 @@ export const DashboardLayout: FC = () => {
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<DeploymentBanner />
|
||||
|
||||
|
||||
@@ -54,11 +54,11 @@ const DeploymentSettingsLayout: FC = () => {
|
||||
<section className="px-10 max-w-screen-2xl mx-auto">
|
||||
<div className="flex flex-row gap-28 py-10">
|
||||
<DeploymentSidebar />
|
||||
<main css={{ flexGrow: 1 }}>
|
||||
<div css={{ flexGrow: 1 }}>
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ const OrganizationSidebarLayout: FC = () => {
|
||||
return (
|
||||
<div className="flex flex-row flex-1 min-h-0 w-full">
|
||||
<OrganizationSidebar />
|
||||
<main className="flex flex-col items-center flex-1 min-h-0 h-full overflow-y-auto w-full px-10 pt-10">
|
||||
<div className="flex flex-col items-center flex-1 min-h-0 h-full overflow-y-auto w-full px-10 pt-10">
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,9 +15,10 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import { InfoIcon, NetworkIcon } from "lucide-react";
|
||||
import { type FC, useState } from "react";
|
||||
import { type FC, type KeyboardEvent, useState } from "react";
|
||||
import { Link as RouterLink } from "react-router";
|
||||
import userAgentParser from "ua-parser-js";
|
||||
import { cn } from "utils/cn";
|
||||
import { buildReasonLabels } from "utils/workspace";
|
||||
import { AuditLogDescription } from "./AuditLogDescription/AuditLogDescription";
|
||||
import { AuditLogDiff } from "./AuditLogDiff/AuditLogDiff";
|
||||
@@ -75,16 +76,22 @@ export const AuditLogRow: FC<AuditLogRowProps> = ({
|
||||
<TableCell className="!p-0 border-0 border-b text-base">
|
||||
<Collapsible open={isDiffOpen} onOpenChange={setIsDiffOpen}>
|
||||
<div
|
||||
className="flex flex-row items-center gap-4 py-4 px-8"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={toggle}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
toggle();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"flex flex-row items-center gap-4 py-4 px-8",
|
||||
shouldDisplayDiff && "cursor-pointer",
|
||||
)}
|
||||
{...(shouldDisplayDiff && {
|
||||
tabIndex: 0,
|
||||
role: "button",
|
||||
"aria-expanded": isDiffOpen,
|
||||
onClick: toggle,
|
||||
onKeyDown: (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
toggle();
|
||||
}
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-4 flex-1">
|
||||
<div className="flex flex-row items-center gap-4 w-full">
|
||||
|
||||
@@ -8,13 +8,7 @@ import {
|
||||
waitForLoaderToBeRemoved,
|
||||
} from "testHelpers/renderHelpers";
|
||||
import { server } from "testHelpers/server";
|
||||
import {
|
||||
createEvent,
|
||||
fireEvent,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from "@testing-library/react";
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { API } from "api/api";
|
||||
import type { AuditLogsRequest } from "api/typesGenerated";
|
||||
@@ -91,47 +85,53 @@ describe("AuditPage", () => {
|
||||
audit_logs: [MockAuditLog],
|
||||
count: 1,
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
|
||||
await renderPage();
|
||||
|
||||
const row = screen.getByTestId(`audit-log-row-${MockAuditLog.id}`);
|
||||
const expandableRowButton = within(row).getByRole("button");
|
||||
|
||||
expect(expandableRowButton).toHaveAttribute("role", "button");
|
||||
expect(expandableRowButton).toHaveAttribute("tabindex", "0");
|
||||
expect(expandableRowButton.tagName).toBe("DIV");
|
||||
expect(screen.queryByText(/ttl:/i)).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.keyDown(expandableRowButton, { key: "Enter" });
|
||||
expandableRowButton.focus();
|
||||
await user.keyboard("{Enter}");
|
||||
|
||||
expect(screen.getAllByText(/ttl:/i)).toHaveLength(2);
|
||||
|
||||
fireEvent.keyDown(expandableRowButton, { key: "Enter" });
|
||||
await user.keyboard("{Enter}");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/ttl:/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("toggles an expandable audit row with Space and prevents default", async () => {
|
||||
it("toggles an expandable audit row with Space", async () => {
|
||||
vi.spyOn(API, "getAuditLogs").mockResolvedValue({
|
||||
audit_logs: [MockAuditLog],
|
||||
count: 1,
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
|
||||
await renderPage();
|
||||
|
||||
const row = screen.getByTestId(`audit-log-row-${MockAuditLog.id}`);
|
||||
const expandableRowButton = within(row).getByRole("button");
|
||||
const spaceEvent = createEvent.keyDown(expandableRowButton, {
|
||||
key: " ",
|
||||
code: "Space",
|
||||
});
|
||||
const preventDefaultSpy = vi.spyOn(spaceEvent, "preventDefault");
|
||||
|
||||
fireEvent(expandableRowButton, spaceEvent);
|
||||
expect(expandableRowButton).toHaveAttribute("role", "button");
|
||||
expect(expandableRowButton).toHaveAttribute("tabindex", "0");
|
||||
expect(expandableRowButton.tagName).toBe("DIV");
|
||||
expect(screen.queryByText(/ttl:/i)).not.toBeInTheDocument();
|
||||
|
||||
expandableRowButton.focus();
|
||||
await user.keyboard(" ");
|
||||
|
||||
expect(preventDefaultSpy).toHaveBeenCalled();
|
||||
expect(screen.getAllByText(/ttl:/i)).toHaveLength(2);
|
||||
|
||||
fireEvent.keyDown(expandableRowButton, { key: " " });
|
||||
await user.keyboard(" ");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/ttl:/i)).not.toBeInTheDocument();
|
||||
|
||||
@@ -83,7 +83,7 @@ export const HealthyDot: FC<HealthyDotProps> = ({ severity }) => {
|
||||
|
||||
export const Main: FC<HTMLAttributes<HTMLDivElement>> = (props) => {
|
||||
return (
|
||||
<main
|
||||
<div
|
||||
css={{
|
||||
padding: `0 ${CONTENT_PADDING}px ${CONTENT_PADDING}px`,
|
||||
display: "flex",
|
||||
|
||||
@@ -21,7 +21,7 @@ const CreateOrganizationPage: FC = () => {
|
||||
const error = createOrganizationMutation.error;
|
||||
|
||||
return (
|
||||
<main className="py-7">
|
||||
<div className="py-7">
|
||||
<RequirePermission isFeatureVisible={permissions.createOrganization}>
|
||||
<CreateOrganizationPageView
|
||||
error={error}
|
||||
@@ -38,7 +38,7 @@ const CreateOrganizationPage: FC = () => {
|
||||
}}
|
||||
/>
|
||||
</RequirePermission>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ const TasksPage: FC = () => {
|
||||
<PageHeaderSubtitle>Automate tasks with AI</PageHeaderSubtitle>
|
||||
</PageHeader>
|
||||
|
||||
<main className="pb-8">
|
||||
<div className="pb-8">
|
||||
<TaskPrompt
|
||||
templates={aiTemplatesQuery.data}
|
||||
error={aiTemplatesQuery.error}
|
||||
@@ -295,7 +295,7 @@ const TasksPage: FC = () => {
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<BatchDeleteConfirmation
|
||||
open={isDeleteDialogOpen}
|
||||
|
||||
@@ -66,9 +66,9 @@ export const TemplateSettingsLayout: FC = () => {
|
||||
>
|
||||
<Sidebar template={templateQuery.data} />
|
||||
<Suspense fallback={<Loader />}>
|
||||
<main css={{ width: "100%" }}>
|
||||
<div className="w-full">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</Suspense>
|
||||
</TemplateSettings.Provider>
|
||||
)}
|
||||
|
||||
@@ -17,9 +17,9 @@ const Layout: FC = () => {
|
||||
<div className="flex flex-row gap-12 py-12">
|
||||
<Sidebar user={me} />
|
||||
<Suspense fallback={<Loader />}>
|
||||
<main className="w-full max-w-full">
|
||||
<div className="w-full max-w-full">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
</Margins>
|
||||
|
||||
@@ -73,9 +73,9 @@ export const WorkspaceSettingsLayout: FC = () => {
|
||||
>
|
||||
<Sidebar />
|
||||
<Suspense fallback={<Loader />}>
|
||||
<main css={{ width: "100%" }}>
|
||||
<div className="w-full">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</Suspense>
|
||||
</WorkspaceSettings.Provider>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user