diff --git a/frontend/editor/src/core/components/shared/ButtonSelector.stories.tsx b/frontend/editor/src/core/components/shared/ButtonSelector.stories.tsx new file mode 100644 index 0000000000..1169c0b3a7 --- /dev/null +++ b/frontend/editor/src/core/components/shared/ButtonSelector.stories.tsx @@ -0,0 +1,42 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import ButtonSelector from "@app/components/shared/ButtonSelector"; + +/** Reproduces the redaction "Mode" picker (Automatic / Manual). */ +const meta: Meta = { + title: "Shared/ButtonSelector", + component: ButtonSelector, + parameters: { layout: "padded" }, + decorators: [ + (S) => ( +
+ +
+ ), + ], +}; +export default meta; +type Story = StoryObj; + +function ModeDemo({ disabled }: { disabled?: boolean }) { + const [value, setValue] = useState("automatic"); + return ( + + ); +} + +/** Enabled (files present): the selected segment must be dark + readable. */ +export const RedactionMode: Story = { render: () => }; + +/** Both options disabled (no files) — the selected segment must STILL be legible. */ +export const DisabledOptions: Story = { + render: () => , +}; diff --git a/frontend/editor/src/core/components/shared/FileSelectorPicker.module.css b/frontend/editor/src/core/components/shared/FileSelectorPicker.module.css index 7d9b0251db..89c57dd3fa 100644 --- a/frontend/editor/src/core/components/shared/FileSelectorPicker.module.css +++ b/frontend/editor/src/core/components/shared/FileSelectorPicker.module.css @@ -224,6 +224,7 @@ /* ── Individual file row ─────────────────────────── */ .fileItem { width: 100%; + --sui-btn-py: 0.625rem; padding: 0.625rem 0.875rem; display: flex; align-items: center; diff --git a/frontend/editor/src/core/components/shared/FileSidebarFileItem.css b/frontend/editor/src/core/components/shared/FileSidebarFileItem.css index 7bce3162dc..51278c1673 100644 --- a/frontend/editor/src/core/components/shared/FileSidebarFileItem.css +++ b/frontend/editor/src/core/components/shared/FileSidebarFileItem.css @@ -23,6 +23,7 @@ /* ---- File item ---- */ .file-sidebar-file-item { + position: relative; display: flex; align-items: center; gap: 10px; @@ -37,6 +38,31 @@ contain-intrinsic-size: auto 46px; } +.file-sidebar-file-actions { + position: absolute; + top: 50%; + right: 8px; + transform: translateY(-50%); + display: flex; + align-items: center; + gap: 2px; + pointer-events: none; +} + +.file-sidebar-file-item:hover .file-sidebar-file-actions, +.file-sidebar-file-item.viewed .file-sidebar-file-actions { + pointer-events: auto; +} + +/* Only shrink the name to clear the buttons while they're visible. */ +.file-sidebar-file-info { + transition: padding-right 0.12s ease; +} +.file-sidebar-file-item:hover .file-sidebar-file-info, +.file-sidebar-file-item.viewed .file-sidebar-file-info { + padding-right: 3.5rem; +} + .file-sidebar-file-item:hover:not(.selected) { background-color: rgba(59, 130, 246, 0.06); } diff --git a/frontend/editor/src/core/components/shared/FileSidebarFileItem.tsx b/frontend/editor/src/core/components/shared/FileSidebarFileItem.tsx index 8621466120..8647e18a68 100644 --- a/frontend/editor/src/core/components/shared/FileSidebarFileItem.tsx +++ b/frontend/editor/src/core/components/shared/FileSidebarFileItem.tsx @@ -365,133 +365,138 @@ export function FileItem({ )} - { - e.stopPropagation(); - onEyeClick(fileId, e); - }} - tabIndex={-1} - aria-label={ - isViewedInViewer - ? t("fileSidebar.fileItem.closeViewer", "Close viewer") - : t("fileSidebar.fileItem.openInViewer", "Open in viewer") - } - > - - - - {(onDelete || - (canSaveToCloud && onSaveToCloud) || - (hasVersionHistory && onVersionHistory)) && ( - - - e.stopPropagation()} - tabIndex={-1} - aria-label={t( - "fileSidebar.fileItem.moreActions", - "More actions", - )} - > - - - - e.stopPropagation()}> - {hasVersionHistory && onVersionHistory && ( - } - onClick={(e) => { - e.stopPropagation(); - onVersionHistory(fileId); - }} +
+ { + e.stopPropagation(); + onEyeClick(fileId, e); + }} + tabIndex={-1} + aria-label={ + isViewedInViewer + ? t("fileSidebar.fileItem.closeViewer", "Close viewer") + : t("fileSidebar.fileItem.openInViewer", "Open in viewer") + } + > + + + + {(onDelete || + (canSaveToCloud && onSaveToCloud) || + (hasVersionHistory && onVersionHistory)) && ( + + + e.stopPropagation()} + tabIndex={-1} + aria-label={t( + "fileSidebar.fileItem.moreActions", + "More actions", + )} > - {t("fileSidebar.fileItem.versionHistory", "Version history")} - - )} - {canSaveToCloud && - onSaveToCloud && - (() => { - const uploadLabel = isUploadedToCloud - ? t( - "fileSidebar.fileItem.updateOnServer", - "Update on server", - ) - : t( - "fileSidebar.fileItem.uploadToServer", - "Upload to server", - ); - return ( - -
- - } - onClick={(e) => { - e.stopPropagation(); - onSaveToCloud(fileId); - }} - > - {uploadLabel} - -
-
- ); - })()} - {onDelete && - (() => { - const deleteLabel = t( - "fileSidebar.fileItem.delete", - "Delete", - ); - return ( - -
- - } - onClick={(e) => { - e.stopPropagation(); - onDelete(fileId); - }} - > - {deleteLabel} - -
-
- ); - })()} - -
- )} + + + + e.stopPropagation()}> + {hasVersionHistory && onVersionHistory && ( + } + onClick={(e) => { + e.stopPropagation(); + onVersionHistory(fileId); + }} + > + {t( + "fileSidebar.fileItem.versionHistory", + "Version history", + )} + + )} + {canSaveToCloud && + onSaveToCloud && + (() => { + const uploadLabel = isUploadedToCloud + ? t( + "fileSidebar.fileItem.updateOnServer", + "Update on server", + ) + : t( + "fileSidebar.fileItem.uploadToServer", + "Upload to server", + ); + return ( + +
+ + } + onClick={(e) => { + e.stopPropagation(); + onSaveToCloud(fileId); + }} + > + {uploadLabel} + +
+
+ ); + })()} + {onDelete && + (() => { + const deleteLabel = t( + "fileSidebar.fileItem.delete", + "Delete", + ); + return ( + +
+ + } + onClick={(e) => { + e.stopPropagation(); + onDelete(fileId); + }} + > + {deleteLabel} + +
+
+ ); + })()} +
+
+ )} + {useRasterThumb && diff --git a/frontend/editor/src/core/components/tools/ToolPanel.css b/frontend/editor/src/core/components/tools/ToolPanel.css index a8ad41588f..e9cbb9b1b2 100644 --- a/frontend/editor/src/core/components/tools/ToolPanel.css +++ b/frontend/editor/src/core/components/tools/ToolPanel.css @@ -511,7 +511,8 @@ flex-direction: row; gap: 0.75rem; align-items: flex-start; - padding: 0.85rem 0.95rem; + --sui-btn-py: 1rem; + padding: 1rem 0.95rem; border: 1px solid var(--fullscreen-border-subtle-70); border-radius: 0.95rem; background: var(--fullscreen-bg-item); @@ -526,6 +527,11 @@ min-width: 0; } +.tool-panel__fullscreen-item .mantine-Button-label { + gap: 1rem; + align-items: flex-start; +} + .tool-panel__fullscreen-item:focus-visible { outline: 2px solid var(--fullscreen-accent-selected); outline-offset: 3px; diff --git a/frontend/editor/src/core/components/tools/certSign/CertificateFormatSettings.tsx b/frontend/editor/src/core/components/tools/certSign/CertificateFormatSettings.tsx index 5be808b492..cbc4b67163 100644 --- a/frontend/editor/src/core/components/tools/certSign/CertificateFormatSettings.tsx +++ b/frontend/editor/src/core/components/tools/certSign/CertificateFormatSettings.tsx @@ -1,4 +1,4 @@ -import { Stack } from "@mantine/core"; +import { SimpleGrid } from "@mantine/core"; import { Button } from "@app/ui/Button"; import { CertSignParameters } from "@app/hooks/tools/certSign/useCertSignParameters"; @@ -8,106 +8,27 @@ interface CertificateFormatSettingsProps { disabled?: boolean; } +const FORMATS = ["PKCS12", "PFX", "PEM", "JKS"] as const; + const CertificateFormatSettings = ({ parameters, onParameterChange, disabled = false, }: CertificateFormatSettingsProps) => { return ( - -
- {/* First row - PKCS#12 and PFX */} -
- - -
- {/* Second row - PEM and JKS */} -
- - -
-
-
+ + {FORMATS.map((format) => ( + )} - + + + {t("compare.edited.label", "Edited PDF")} + + {renderSlot("comparison")} + = { ], }, size: { control: "inline-radio", options: ["sm", "md", "lg", "xl"] }, + p: { + control: "inline-radio", + options: [undefined, "none", "xs", "sm", "md", "lg", "xl"], + }, + px: { + control: "inline-radio", + options: [undefined, "none", "xs", "sm", "md", "lg", "xl"], + }, + py: { + control: "inline-radio", + options: [undefined, "none", "xs", "sm", "md", "lg", "xl"], + }, justify: { control: "inline-radio", options: ["center", "start", "end", "between"], @@ -141,6 +153,34 @@ export const Accents: Story = { ), }; +/** Loading keeps the button's shape. A labelless loader collapses to an icon + square — unless `fullWidth`, which must always span its container (e.g. an + execute button whose label is briefly absent while files hydrate). */ +export const Loading: Story = { + render: (args) => ( +
+
+ ), +}; + +/** Disabled primary — dark mode keeps a muted accent instead of grey. */ +export const DisabledDark: Story = { + render: () => ( +
+
+
+
+ ), +}; + /** Real size differences. */ export const Sizes: Story = { render: (args) => ( @@ -152,15 +192,12 @@ export const Sizes: Story = { ), }; -/** - * `fontSize` scales the label text independently of `size` (which sets height). - * Unset inherits the `size`-derived default (md for a default button). - */ -export const FontSizes: Story = { +/** Padding override via `p` (both axes); `px`/`py` override a single axis. */ +export const Padding: Story = { render: (args) => ( - {(["xs", "sm", "md", "lg", "xl"] as const).map((fontSize) => ( - diff --git a/frontend/editor/src/portal/components/infrastructure/ApiKeyCard.tsx b/frontend/editor/src/portal/components/infrastructure/ApiKeyCard.tsx index af0d0526e0..8b99973ac8 100644 --- a/frontend/editor/src/portal/components/infrastructure/ApiKeyCard.tsx +++ b/frontend/editor/src/portal/components/infrastructure/ApiKeyCard.tsx @@ -24,30 +24,34 @@ export function ApiKeyCard({ {open && ( diff --git a/frontend/editor/src/portal/views/Home.css b/frontend/editor/src/portal/views/Home.css index 70752c714b..e1af68b768 100644 --- a/frontend/editor/src/portal/views/Home.css +++ b/frontend/editor/src/portal/views/Home.css @@ -61,12 +61,9 @@ gap: 0.375rem; } +/* Visual container only — layout (icon / text / arrow) is handled by the + shared Button's leftSection / label / rightSection, not overridden here. */ .portal-home__quick-row { - display: grid; - grid-template-columns: auto 1fr auto; - align-items: center; - gap: 0.625rem; - padding: 0.5rem 0.625rem; background: var(--color-bg-subtle); border: 1px solid var(--color-border-light); border-radius: var(--radius-md); @@ -81,6 +78,14 @@ border-color: var(--color-border); } +/* Let the label grow so the arrow (rightSection) is pushed to the far right, + and don't clip the stacked subtitle. */ +.portal-home__quick-row .mantine-Button-label { + flex: 1 1 auto; + justify-content: flex-start; + overflow: visible; +} + .portal-home__quick-icon { display: inline-flex; align-items: center; @@ -95,6 +100,7 @@ .portal-home__quick-text { display: flex; flex-direction: column; + align-items: flex-start; min-width: 0; } diff --git a/frontend/editor/src/portal/views/Home.tsx b/frontend/editor/src/portal/views/Home.tsx index b9a42fbab8..a1829d5476 100644 --- a/frontend/editor/src/portal/views/Home.tsx +++ b/frontend/editor/src/portal/views/Home.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { Button, Card } from "@app/ui"; import { useTier } from "@portal/contexts/TierContext"; -import { useView } from "@portal/contexts/ViewContext"; +import { useView, type ViewId } from "@portal/contexts/ViewContext"; import { HomeHero } from "@portal/components/HomeHero"; import { HomeGreeting } from "@portal/components/HomeGreeting"; import { RecentActivity } from "@portal/components/RecentActivity"; @@ -13,6 +13,37 @@ import "@portal/views/Home.css"; /* Quick actions card */ /* ──────────────────────────────────────────────────────────────────────── */ +/** Rows for the Quick Actions list. Each `view` navigates the portal. */ +const QUICK_ACTIONS: Array<{ + key: string; + glyph: string; + bg: string; + fg: string; + view: ViewId; +}> = [ + { + key: "buildPipeline", + glyph: "⌃", + bg: "var(--color-purple-light)", + fg: "var(--color-purple)", + view: "pipelines", + }, + { + key: "connectSource", + glyph: "⇢", + bg: "var(--color-green-light)", + fg: "var(--color-green-dark)", + view: "sources", + }, + { + key: "issueApiKey", + glyph: "⚙", + bg: "var(--color-amber-light)", + fg: "var(--color-amber-dark)", + view: "infrastructure", + }, +]; + function QuickActions() { const { t } = useTranslation(); const { setActiveView } = useView(); @@ -27,78 +58,40 @@ function QuickActions() {
- - - + + + {t(`portal.home.quickActions.${action.key}.title`)} + + {t(`portal.home.quickActions.${action.key}.blurb`)} + + + ))}
); diff --git a/frontend/editor/src/portal/views/Infrastructure.css b/frontend/editor/src/portal/views/Infrastructure.css index f0a2f55da9..bca7406359 100644 --- a/frontend/editor/src/portal/views/Infrastructure.css +++ b/frontend/editor/src/portal/views/Infrastructure.css @@ -205,11 +205,8 @@ } .portal-infra__key-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; width: 100%; + --sui-btn-py: 0.875rem; /* 14px */ padding: 0.875rem 1.125rem; background: transparent; text-align: left; diff --git a/frontend/editor/src/proprietary/auth/ui/LoginRightCarousel.tsx b/frontend/editor/src/proprietary/auth/ui/LoginRightCarousel.tsx index f6ad17ec7e..490aea923c 100644 --- a/frontend/editor/src/proprietary/auth/ui/LoginRightCarousel.tsx +++ b/frontend/editor/src/proprietary/auth/ui/LoginRightCarousel.tsx @@ -1,5 +1,5 @@ import { memo, useEffect, useMemo, useRef, useState } from "react"; -import { Button } from "@app/ui/Button"; +import { CarouselDots } from "@app/ui/CarouselDots"; import bgDefault from "@app/assets/login/LoginBackgroundPanel.png"; export type ImageSlide = { @@ -196,42 +196,21 @@ function LoginRightCarousel({ ))} {/* Dot navigation */} -
- {Array.from({ length: totalSlides }).map((_, i) => ( -
+ /> ); } diff --git a/frontend/editor/src/proprietary/auth/ui/auth.css b/frontend/editor/src/proprietary/auth/ui/auth.css index 249d6cdda5..31f1eb480a 100644 --- a/frontend/editor/src/proprietary/auth/ui/auth.css +++ b/frontend/editor/src/proprietary/auth/ui/auth.css @@ -172,6 +172,19 @@ align-items: center; } +/* These OAuth buttons are shared