mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
fix(ui): shared Button content-sizing + padding props, and button call-site cleanups (#6914)
## Summary A batch of shared **design-system** fixes (Button, SegmentedControl, Chip, a new CarouselDots) and the consumer/call-site cleanups they unlock, following the button consolidation (#6787). Also includes dark-theme token alignment and some portal/auth polish that rides on the same components. The shared Button now sizes to its content instead of clipping it, gains per-axis padding controls, and no longer misbehaves while loading or disabled; several call sites are then migrated onto the proper component APIs. ## Shared components (`core/ui`) ### Button - **Content-driven height.** `--button-height` is now a `min-height`, not a fixed cap. Single-line buttons still land exactly on the shared control-height scale (pixel-aligned with `ActionIcon` / `SegmentedControl`), while taller content — wrapped labels, stacked title + subtitle rows — grows the button instead of being clipped mid-glyph. Short content is re-centered with `align-content`, **without** overriding the root `display`, so a consumer's own layout (e.g. a full-width list row) isn't disturbed. - **Padding props.** New `p` / `px` / `py` props (`none`/`xs`/`sm`/`md`/`lg`/`xl`) override the size-based padding per axis. Vertical padding is applied through a `--sui-btn-py` CSS variable, so consumers can also set it from their own class. - **Loading no longer collapses.** A `fullWidth` button is never treated as icon-only, so an execute button whose label is momentarily absent while files hydrate (e.g. `ScopedOperationButton`) keeps its full width with a centered spinner instead of shrinking to an icon-sized square for a split second. - **Disabled in dark mode.** A disabled *primary* button keeps a muted version of its own accent fill (`opacity: 0.55`) instead of Mantine's near-black `--mantine-color-disabled`, which blended into dark surfaces and made the button all but disappear. Loading spinners are excluded so they stay full-strength. No breaking API changes — buttons that don't opt in render exactly as before. ### SegmentedControl - Fixed a bug where a segment marked `disabled` that also happened to be the currently-selected value was rendered disabled, leaving the active segment un-selectable/greyed. A disabled option is now only disabled when it isn't the current value. ### CarouselDots (new) - New shared dots indicator component (with Storybook story), used by the login carousel. ### Chip / theme - Dark-theme tokens in `theme.css` aligned to the portal's `tokens.css` so the editor and portal (Processor) dark modes stop drifting (chrome surfaces lift off the darker canvas); plus a Chip dark-mode styling fix and a small `mantineTheme` cleanup. ## Consumer / call-site cleanups - **Compare** tool: the swap control is now a regular shared Button placed **between** the Original and Edited file cards (the bespoke full-height vertical swap button and its CSS were removed), and the file cards fill the full available width. - **Certificate format**: replaced the inline-styled buttons with clean two-state (primary / secondary) buttons. - **ToolPicker**: restored the label selectors that #6787 renamed to the never-emitted `.sui-btn__label`, and fixed the sidebar-search row clipping. - **File sidebar**: "View all files" row fix; `FileSidebarFileItem` migrated off `display:flex` + `gap` on the Button root (which no longer reaches the nested label) onto `leftSection` / `rightSection` + a stacked label. ## Portal / auth polish - Portal button consolidation and styling across Header, SettingsModal, Home, Infrastructure, ApiKeyCard, and PopularUseCases. - **Login**: onboarding text now shows the default starting username / password; login carousel uses the new CarouselDots; desktop OAuth styling tweak. ## Verification - Storybook: button sizes measure exactly on the control-height scale and match `ActionIcon`; icon-only buttons stay square and centered; `fullWidth` loading buttons hold full width; disabled dark-mode primary buttons render as a muted accent rather than grey. - Single-line buttons are pixel-identical before/after; only buttons whose content previously overflowed a fixed height render differently (they now fit rather than clip). - `task frontend:lint` clean; typecheck shows only the pre-existing third-party `node_modules` noise also present on `main`.
This commit is contained in:
@@ -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<typeof ButtonSelector> = {
|
||||
title: "Shared/ButtonSelector",
|
||||
component: ButtonSelector,
|
||||
parameters: { layout: "padded" },
|
||||
decorators: [
|
||||
(S) => (
|
||||
<div style={{ maxWidth: "22rem" }}>
|
||||
<S />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ButtonSelector>;
|
||||
|
||||
function ModeDemo({ disabled }: { disabled?: boolean }) {
|
||||
const [value, setValue] = useState("automatic");
|
||||
return (
|
||||
<ButtonSelector
|
||||
label="Mode"
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
options={[
|
||||
{ value: "automatic", label: "Automatic", disabled },
|
||||
{ value: "manual", label: "Manual", disabled },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** Enabled (files present): the selected segment must be dark + readable. */
|
||||
export const RedactionMode: Story = { render: () => <ModeDemo /> };
|
||||
|
||||
/** Both options disabled (no files) — the selected segment must STILL be legible. */
|
||||
export const DisabledOptions: Story = {
|
||||
render: () => <ModeDemo disabled />,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -365,133 +365,138 @@ export function FileItem({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ActionIcon
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
className="file-sidebar-eye-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEyeClick(fileId, e);
|
||||
}}
|
||||
tabIndex={-1}
|
||||
aria-label={
|
||||
isViewedInViewer
|
||||
? t("fileSidebar.fileItem.closeViewer", "Close viewer")
|
||||
: t("fileSidebar.fileItem.openInViewer", "Open in viewer")
|
||||
}
|
||||
>
|
||||
<VisibilityOutlinedIcon
|
||||
className="file-sidebar-eye-open"
|
||||
sx={{ fontSize: "1.1rem" }}
|
||||
/>
|
||||
<VisibilityOffOutlinedIcon
|
||||
className="file-sidebar-eye-closed"
|
||||
sx={{ fontSize: "1.1rem" }}
|
||||
/>
|
||||
</ActionIcon>
|
||||
{(onDelete ||
|
||||
(canSaveToCloud && onSaveToCloud) ||
|
||||
(hasVersionHistory && onVersionHistory)) && (
|
||||
<Menu position="bottom-end" withinPortal shadow="md" width={190}>
|
||||
<Menu.Target>
|
||||
<ActionIcon
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
className="file-sidebar-kebab-btn"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
tabIndex={-1}
|
||||
aria-label={t(
|
||||
"fileSidebar.fileItem.moreActions",
|
||||
"More actions",
|
||||
)}
|
||||
>
|
||||
<MoreVertIcon sx={{ fontSize: "1.1rem" }} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown onClick={(e) => e.stopPropagation()}>
|
||||
{hasVersionHistory && onVersionHistory && (
|
||||
<Menu.Item
|
||||
leftSection={<HistoryIcon sx={{ fontSize: 16 }} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onVersionHistory(fileId);
|
||||
}}
|
||||
<div className="file-sidebar-file-actions">
|
||||
<ActionIcon
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
className="file-sidebar-eye-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEyeClick(fileId, e);
|
||||
}}
|
||||
tabIndex={-1}
|
||||
aria-label={
|
||||
isViewedInViewer
|
||||
? t("fileSidebar.fileItem.closeViewer", "Close viewer")
|
||||
: t("fileSidebar.fileItem.openInViewer", "Open in viewer")
|
||||
}
|
||||
>
|
||||
<VisibilityOutlinedIcon
|
||||
className="file-sidebar-eye-open"
|
||||
sx={{ fontSize: "1.1rem" }}
|
||||
/>
|
||||
<VisibilityOffOutlinedIcon
|
||||
className="file-sidebar-eye-closed"
|
||||
sx={{ fontSize: "1.1rem" }}
|
||||
/>
|
||||
</ActionIcon>
|
||||
{(onDelete ||
|
||||
(canSaveToCloud && onSaveToCloud) ||
|
||||
(hasVersionHistory && onVersionHistory)) && (
|
||||
<Menu position="bottom-end" withinPortal shadow="md" width={190}>
|
||||
<Menu.Target>
|
||||
<ActionIcon
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
className="file-sidebar-kebab-btn"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
tabIndex={-1}
|
||||
aria-label={t(
|
||||
"fileSidebar.fileItem.moreActions",
|
||||
"More actions",
|
||||
)}
|
||||
>
|
||||
{t("fileSidebar.fileItem.versionHistory", "Version history")}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canSaveToCloud &&
|
||||
onSaveToCloud &&
|
||||
(() => {
|
||||
const uploadLabel = isUploadedToCloud
|
||||
? t(
|
||||
"fileSidebar.fileItem.updateOnServer",
|
||||
"Update on server",
|
||||
)
|
||||
: t(
|
||||
"fileSidebar.fileItem.uploadToServer",
|
||||
"Upload to server",
|
||||
);
|
||||
return (
|
||||
<Tooltip
|
||||
label={enforcingTooltip(uploadLabel)}
|
||||
disabled={!policyEnforcing}
|
||||
position="left"
|
||||
offset={6}
|
||||
withArrow
|
||||
>
|
||||
<div>
|
||||
<Menu.Item
|
||||
disabled={policyEnforcing}
|
||||
leftSection={
|
||||
<CloudUploadOutlinedIcon sx={{ fontSize: 16 }} />
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSaveToCloud(fileId);
|
||||
}}
|
||||
>
|
||||
{uploadLabel}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})()}
|
||||
{onDelete &&
|
||||
(() => {
|
||||
const deleteLabel = t(
|
||||
"fileSidebar.fileItem.delete",
|
||||
"Delete",
|
||||
);
|
||||
return (
|
||||
<Tooltip
|
||||
label={enforcingTooltip(deleteLabel)}
|
||||
disabled={!policyEnforcing}
|
||||
position="left"
|
||||
offset={6}
|
||||
withArrow
|
||||
>
|
||||
<div>
|
||||
<Menu.Item
|
||||
disabled={policyEnforcing}
|
||||
color="red"
|
||||
leftSection={
|
||||
<DeleteOutlineIcon sx={{ fontSize: 16 }} />
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(fileId);
|
||||
}}
|
||||
>
|
||||
{deleteLabel}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})()}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
)}
|
||||
<MoreVertIcon sx={{ fontSize: "1.1rem" }} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown onClick={(e) => e.stopPropagation()}>
|
||||
{hasVersionHistory && onVersionHistory && (
|
||||
<Menu.Item
|
||||
leftSection={<HistoryIcon sx={{ fontSize: 16 }} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onVersionHistory(fileId);
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
"fileSidebar.fileItem.versionHistory",
|
||||
"Version history",
|
||||
)}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canSaveToCloud &&
|
||||
onSaveToCloud &&
|
||||
(() => {
|
||||
const uploadLabel = isUploadedToCloud
|
||||
? t(
|
||||
"fileSidebar.fileItem.updateOnServer",
|
||||
"Update on server",
|
||||
)
|
||||
: t(
|
||||
"fileSidebar.fileItem.uploadToServer",
|
||||
"Upload to server",
|
||||
);
|
||||
return (
|
||||
<Tooltip
|
||||
label={enforcingTooltip(uploadLabel)}
|
||||
disabled={!policyEnforcing}
|
||||
position="left"
|
||||
offset={6}
|
||||
withArrow
|
||||
>
|
||||
<div>
|
||||
<Menu.Item
|
||||
disabled={policyEnforcing}
|
||||
leftSection={
|
||||
<CloudUploadOutlinedIcon sx={{ fontSize: 16 }} />
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSaveToCloud(fileId);
|
||||
}}
|
||||
>
|
||||
{uploadLabel}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})()}
|
||||
{onDelete &&
|
||||
(() => {
|
||||
const deleteLabel = t(
|
||||
"fileSidebar.fileItem.delete",
|
||||
"Delete",
|
||||
);
|
||||
return (
|
||||
<Tooltip
|
||||
label={enforcingTooltip(deleteLabel)}
|
||||
disabled={!policyEnforcing}
|
||||
position="left"
|
||||
offset={6}
|
||||
withArrow
|
||||
>
|
||||
<div>
|
||||
<Menu.Item
|
||||
disabled={policyEnforcing}
|
||||
color="red"
|
||||
leftSection={
|
||||
<DeleteOutlineIcon sx={{ fontSize: 16 }} />
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(fileId);
|
||||
}}
|
||||
>
|
||||
{deleteLabel}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})()}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{useRasterThumb &&
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<Stack gap="md">
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
||||
{/* First row - PKCS#12 and PFX */}
|
||||
<div style={{ display: "flex", gap: "4px" }}>
|
||||
<Button
|
||||
variant={parameters.certType === "PKCS12" ? "primary" : "secondary"}
|
||||
onClick={() => onParameterChange("certType", "PKCS12")}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: "auto",
|
||||
minHeight: "40px",
|
||||
fontSize: "11px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
lineHeight: "1.1",
|
||||
fontSize: "11px",
|
||||
}}
|
||||
>
|
||||
PKCS12
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant={parameters.certType === "PFX" ? "primary" : "secondary"}
|
||||
onClick={() => onParameterChange("certType", "PFX")}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: "auto",
|
||||
minHeight: "40px",
|
||||
fontSize: "11px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
lineHeight: "1.1",
|
||||
fontSize: "11px",
|
||||
}}
|
||||
>
|
||||
PFX
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{/* Second row - PEM and JKS */}
|
||||
<div style={{ display: "flex", gap: "4px" }}>
|
||||
<Button
|
||||
variant={parameters.certType === "PEM" ? "primary" : "secondary"}
|
||||
onClick={() => onParameterChange("certType", "PEM")}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: "auto",
|
||||
minHeight: "40px",
|
||||
fontSize: "11px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
lineHeight: "1.1",
|
||||
fontSize: "11px",
|
||||
}}
|
||||
>
|
||||
PEM
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant={parameters.certType === "JKS" ? "primary" : "secondary"}
|
||||
onClick={() => onParameterChange("certType", "JKS")}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: "auto",
|
||||
minHeight: "40px",
|
||||
fontSize: "11px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
lineHeight: "1.1",
|
||||
fontSize: "11px",
|
||||
}}
|
||||
>
|
||||
JKS
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
<SimpleGrid cols={2} spacing="xs">
|
||||
{FORMATS.map((format) => (
|
||||
<Button
|
||||
key={format}
|
||||
variant={parameters.certType === format ? "primary" : "secondary"}
|
||||
accent="default"
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
onClick={() => onParameterChange("certType", format)}
|
||||
text={format}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -521,7 +521,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Compare tool — file selection step (Original / Edited + swap column) */
|
||||
/* Compare tool — file selection step (Original / Edited stacked vertically) */
|
||||
.compare-step-selection {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -532,50 +532,15 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Thumbnails + swap only — swap top aligns with first thumbnail, not “Original PDF” heading */
|
||||
.compare-step-selection__thumbs-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Stacked file cards fill the full available width */
|
||||
.compare-step-selection__thumbs-col {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Full-height bar to the right of both cards */
|
||||
.compare-step-selection__swap {
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
align-self: stretch;
|
||||
margin: 0;
|
||||
padding: 0.5rem 0.2rem;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.compare-step-selection__swap-icon {
|
||||
font-size: 1.35rem !important;
|
||||
}
|
||||
|
||||
.compare-step-selection__swap-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.15;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Pixel compare mode */
|
||||
.compare-pixel-workbench {
|
||||
padding: 16px 24px 32px;
|
||||
|
||||
@@ -240,6 +240,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({
|
||||
variant="tertiary"
|
||||
accent="neutral"
|
||||
size="sm"
|
||||
p="none"
|
||||
fullWidth
|
||||
justify="start"
|
||||
className="tool-button"
|
||||
@@ -264,6 +265,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({
|
||||
variant="tertiary"
|
||||
accent="neutral"
|
||||
size="sm"
|
||||
p="sm"
|
||||
fullWidth
|
||||
justify="start"
|
||||
className="tool-button"
|
||||
@@ -284,6 +286,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({
|
||||
accent="neutral"
|
||||
onClick={() => handleClick(id)}
|
||||
size="sm"
|
||||
p="sm"
|
||||
fullWidth
|
||||
justify="start"
|
||||
className="tool-button"
|
||||
|
||||
@@ -64,18 +64,23 @@
|
||||
--tool-button-selected-bg: var(--mantine-color-dark-4);
|
||||
}
|
||||
|
||||
/* Compact tool buttons */
|
||||
/* Compact tool buttons (padding via the Button `p`/`py` props).
|
||||
List rows hug their content — drop the shared Button's min-height floor
|
||||
so height = icon/label only and rows sit tight against each other. */
|
||||
.tool-button {
|
||||
font-size: 0.875rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.tool-button .sui-btn__label {
|
||||
.tool-button.sui-btn.mantine-Button-root {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tool-button .mantine-Button-label {
|
||||
font-size: 0.84rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
/* ≥1.4 so the overflow:hidden label box contains glyph descenders (g/y/p);
|
||||
row height is icon-bound (~24px) so this doesn't change row size. */
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tool-button-icon {
|
||||
@@ -121,13 +126,7 @@
|
||||
padding: 0.5rem var(--mantine-spacing-sm) var(--mantine-spacing-sm);
|
||||
}
|
||||
|
||||
.tool-picker__compact .tool-button {
|
||||
padding-top: 0.65rem;
|
||||
padding-bottom: 0.65rem;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.tool-picker__compact .tool-button .sui-btn__label {
|
||||
.tool-picker__compact .tool-button .mantine-Button-label {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -442,10 +442,10 @@
|
||||
--gray-800: 218 222 245;
|
||||
--gray-900: 238 241 252;
|
||||
|
||||
/* Dark semantic colors for Tailwind */
|
||||
--surface: 19 23 41;
|
||||
--background: 9 11 24;
|
||||
--border: 28 35 64;
|
||||
/* Dark semantic colors for Tailwind — aligned with portal tokens.css */
|
||||
--surface: 21 28 46; /* #151c2e — portal --color-surface */
|
||||
--background: 9 12 20; /* #090c14 — portal --color-bg */
|
||||
--border: 40 50 72; /* #283248 — portal --color-border */
|
||||
|
||||
/* Dark theme Mantine colors */
|
||||
--color-red-50: #2d1b1b;
|
||||
@@ -524,14 +524,18 @@
|
||||
--color-yellow-800: #fef08a;
|
||||
--color-yellow-900: #fef9c3;
|
||||
|
||||
/* Dark theme semantic colors — deep navy/indigo palette */
|
||||
--bg-surface: #131729;
|
||||
--bg-raised: #0d1020;
|
||||
--bg-muted: #0d1020;
|
||||
--bg-background: #090b18;
|
||||
--bg-toolbar: #090b18;
|
||||
--bg-file-manager: #090b18;
|
||||
--bg-file-list: #131729;
|
||||
/* Dark theme semantic colors — deep navy/indigo palette, aligned with portal
|
||||
tokens.css so the editor and portal (Processor) dark modes don't drift.
|
||||
Chrome surfaces (--bg-toolbar, --bg-file-manager) sit at the portal's
|
||||
sidebar level (#0d1120) so the left/right sidebars lift off the darker
|
||||
canvas (--bg-background) instead of blending into it. */
|
||||
--bg-surface: #151c2e; /* portal --color-surface */
|
||||
--bg-raised: #0d1120; /* portal --color-bg-alt */
|
||||
--bg-muted: #0d1120;
|
||||
--bg-background: #090c14; /* portal --color-bg */
|
||||
--bg-toolbar: #0d1120; /* portal --color-sidebar-bg — lifted off the canvas */
|
||||
--bg-file-manager: #0d1120; /* portal --color-sidebar-bg */
|
||||
--bg-file-list: #151c2e; /* portal --color-surface */
|
||||
--btn-open-file: #4f8ef5;
|
||||
--text-primary: #e8eaf6;
|
||||
--text-secondary: #9299b0;
|
||||
@@ -540,11 +544,11 @@
|
||||
--text-always-dark: #1f2937;
|
||||
--text-always-dark-muted: #6b7280;
|
||||
--border-subtle: rgba(255, 255, 255, 0.05);
|
||||
--border-default: #1c2340;
|
||||
--border-strong: #2d3560;
|
||||
--border-hover: #3a4680;
|
||||
--hover-bg: #181d38;
|
||||
--active-bg: #232a50;
|
||||
--border-default: #1e2840; /* portal --color-border-light */
|
||||
--border-strong: #283248; /* portal --color-border */
|
||||
--border-hover: #3d4f6a; /* portal --color-border-hover */
|
||||
--hover-bg: #1c2640; /* portal --color-bg-hover */
|
||||
--active-bg: #243044; /* portal --color-bg-muted */
|
||||
--automation-entry-hover-bg: var(--color-gray-200);
|
||||
|
||||
/* Icon colors for dark mode */
|
||||
@@ -615,7 +619,7 @@
|
||||
--header-selected-bg: #2040a0;
|
||||
--header-selected-fg: #ffffff;
|
||||
/* file card background (dark) */
|
||||
--file-card-bg: #0d1020;
|
||||
--file-card-bg: #0d1120;
|
||||
--accordion-item-bg: #1c2340; /* accordion item background - more distinguishable */
|
||||
|
||||
/* shadows */
|
||||
|
||||
@@ -370,22 +370,5 @@ export const mantineTheme = createTheme({
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
SegmentedControl: {
|
||||
styles: {
|
||||
root: {
|
||||
backgroundColor: "var(--bg-muted)",
|
||||
border: "1px solid var(--border-subtle)",
|
||||
},
|
||||
control: {
|
||||
color: "var(--text-secondary)",
|
||||
"&[dataActive]": {
|
||||
backgroundColor: "var(--bg-surface)",
|
||||
color: "var(--text-primary)",
|
||||
boxShadow: "var(--shadow-sm)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -498,32 +498,27 @@ const Compare = (props: BaseToolProps) => {
|
||||
{t("compare.original.label", "Original PDF")}
|
||||
</Text>
|
||||
|
||||
<div className="compare-step-selection__thumbs-row">
|
||||
<Stack gap="sm" className="compare-step-selection__thumbs-col">
|
||||
{renderSlot("base")}
|
||||
<Text fw={700} size="sm" style={{ margin: 0 }}>
|
||||
{t("compare.edited.label", "Edited PDF")}
|
||||
</Text>
|
||||
{renderSlot("comparison")}
|
||||
</Stack>
|
||||
<Stack gap="sm" className="compare-step-selection__thumbs-col">
|
||||
{renderSlot("base")}
|
||||
|
||||
{hasBothSelected && (
|
||||
<Button
|
||||
className="compare-step-selection__swap"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleSwap}
|
||||
disabled={base.operation.isLoading}
|
||||
aria-label={t("compare.swap.label", "Swap")}
|
||||
leftSection={<SwapVertRoundedIcon fontSize="small" />}
|
||||
style={{ alignSelf: "center" }}
|
||||
>
|
||||
<SwapVertRoundedIcon
|
||||
className="compare-step-selection__swap-icon"
|
||||
fontSize="inherit"
|
||||
/>
|
||||
<span className="compare-step-selection__swap-label">
|
||||
{t("compare.swap.label", "Swap")}
|
||||
</span>
|
||||
{t("compare.swap.label", "Swap")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Text fw={700} size="sm" style={{ margin: 0 }}>
|
||||
{t("compare.edited.label", "Edited PDF")}
|
||||
</Text>
|
||||
{renderSlot("comparison")}
|
||||
</Stack>
|
||||
|
||||
<Modal
|
||||
opened={swapConfirmOpen}
|
||||
|
||||
@@ -35,7 +35,37 @@
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.sui-btn .mantine-Button-label {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ---- height: --button-height is a minimum, not a cap ---- */
|
||||
/* Single-line content computes to exactly --button-height (same as ActionIcon /
|
||||
SegmentedControl via CONTROL_HEIGHT); taller content (wrapped label, stacked
|
||||
rows) grows the button instead of clipping mid-glyph. Under height:auto the
|
||||
inner's height:100% resolves to auto on its own, so block-level
|
||||
align-content re-centers short content — deliberately NOT flex on the root:
|
||||
that display would out-specificity consumers' own display (e.g. a
|
||||
width:100% list row) and turn them inline-level. */
|
||||
.sui-btn.mantine-Button-root {
|
||||
height: auto;
|
||||
min-height: var(--button-height);
|
||||
align-content: center;
|
||||
/* Vertical padding via the `py` prop; Mantine has no --button-padding-y.
|
||||
Default 0 keeps height-driven layout unchanged for every other button. */
|
||||
padding-block: var(--sui-btn-py, 0);
|
||||
}
|
||||
|
||||
/* no-hover: !important to beat Mantine's own :hover background rule. */
|
||||
.sui-btn--no-hover:not(:disabled):not([data-disabled]):hover {
|
||||
background: var(--button-bg, transparent) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"]
|
||||
.sui-btn--primary.mantine-Button-root:disabled:not([data-loading]),
|
||||
[data-theme="dark"]
|
||||
.sui-btn--primary.mantine-Button-root[data-disabled]:not([data-loading]) {
|
||||
background: var(--button-bg);
|
||||
color: var(--button-color);
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
@@ -72,6 +72,18 @@ const meta: Meta<typeof Button> = {
|
||||
],
|
||||
},
|
||||
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) => (
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: 12, width: 320 }}
|
||||
>
|
||||
<Button {...args} loading text="Save changes" />
|
||||
<Button {...args} loading leftSection={<Plus />} text="Save changes" />
|
||||
{/* fullWidth + loading + no label — must fill width, not shrink to a square */}
|
||||
<Button {...args} text={undefined} fullWidth loading />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
/** Disabled primary — dark mode keeps a muted accent instead of grey. */
|
||||
export const DisabledDark: Story = {
|
||||
render: () => (
|
||||
<div style={{ background: "var(--bg-surface)", padding: 24, width: 360 }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<Button data-d="active" fullWidth text="Merge (8 files)" />
|
||||
<Button data-d="disabled" fullWidth disabled text="Merge (8 files)" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
/** 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) => (
|
||||
<Wrap>
|
||||
{(["xs", "sm", "md", "lg", "xl"] as const).map((fontSize) => (
|
||||
<Button key={fontSize} {...args} fontSize={fontSize} text={fontSize} />
|
||||
{(["none", "xs", "sm", "md", "lg", "xl"] as const).map((p) => (
|
||||
<Button key={p} {...args} p={p} text={p} />
|
||||
))}
|
||||
</Wrap>
|
||||
),
|
||||
|
||||
@@ -6,7 +6,13 @@ import type {
|
||||
ElementType,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
import { CONTROL_HEIGHT } from "@app/ui/controlSizes";
|
||||
import {
|
||||
CONTROL_HEIGHT,
|
||||
CONTROL_PADDING,
|
||||
type ControlFontSize,
|
||||
type ControlPadding,
|
||||
resolveFontSize,
|
||||
} from "@app/ui/controlSizes";
|
||||
import "@app/ui/Button.css";
|
||||
|
||||
/** primary=solid, secondary=outlined, tertiary=ghost (tinted hover), quiet=plain (no bg, hovers to text colour). */
|
||||
@@ -23,11 +29,11 @@ export type ButtonAccent =
|
||||
| "warning";
|
||||
export type ButtonSize = "sm" | "md" | "lg" | "xl";
|
||||
/**
|
||||
* Text-size scale, independent of `size` (which sets the control height). Unset
|
||||
* inherits the `size`-derived default — i.e. `md` for a default button — so
|
||||
* existing buttons are unchanged; set it to size the label on its own axis.
|
||||
* Relative label-size scale, applied on top of the `size`-derived base. `md`
|
||||
* leaves the base unchanged; other values scale it up/down. Unset inherits
|
||||
* Mantine's size default, so existing buttons are unchanged.
|
||||
*/
|
||||
export type ButtonFontSize = "xs" | "sm" | "md" | "lg" | "xl";
|
||||
export type ButtonFontSize = ControlFontSize;
|
||||
/** `between` pins leftSection/label/rightSection to left/center/right (toolbar rows). */
|
||||
export type ButtonJustify = "center" | "start" | "end" | "between";
|
||||
export type ButtonShape = "default" | "circle" | "pill";
|
||||
@@ -36,8 +42,14 @@ type ButtonOwnProps = {
|
||||
variant?: ButtonVariant;
|
||||
accent?: ButtonAccent;
|
||||
size?: ButtonSize;
|
||||
/** Label text size, independent of `size`. Defaults to the `size`-derived value. */
|
||||
/** Label size relative to `size`. Defaults to the `size`-derived value. */
|
||||
fontSize?: ButtonFontSize;
|
||||
/** Padding override for both axes */
|
||||
p?: ControlPadding;
|
||||
/** Horizontal-padding override */
|
||||
px?: ControlPadding;
|
||||
/** Vertical-padding override */
|
||||
py?: ControlPadding;
|
||||
justify?: ButtonJustify;
|
||||
shape?: ButtonShape;
|
||||
/** Alternative to children; use one or the other. */
|
||||
@@ -105,23 +117,16 @@ const MANTINE_JUSTIFY: Record<ButtonJustify, string> = {
|
||||
between: "space-between",
|
||||
};
|
||||
|
||||
// Maps the fontSize scale onto Mantine's font-size tokens (xs 12 → xl 20px),
|
||||
// the same tokens `size` resolves to — so fontSize is a drop-in override.
|
||||
const FONT_SIZE_VAR: Record<ButtonFontSize, string> = {
|
||||
xs: "var(--mantine-font-size-xs)",
|
||||
sm: "var(--mantine-font-size-sm)",
|
||||
md: "var(--mantine-font-size-md)",
|
||||
lg: "var(--mantine-font-size-lg)",
|
||||
xl: "var(--mantine-font-size-xl)",
|
||||
};
|
||||
|
||||
const ButtonRoot = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
function ButtonRoot(
|
||||
{
|
||||
variant = "primary",
|
||||
accent = "default",
|
||||
size = "md",
|
||||
size = "sm",
|
||||
fontSize,
|
||||
p,
|
||||
px,
|
||||
py,
|
||||
justify = "center",
|
||||
shape = "default",
|
||||
text,
|
||||
@@ -142,7 +147,15 @@ const ButtonRoot = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
) {
|
||||
const label = text ?? children;
|
||||
const hasLabel = label != null && label !== false && label !== "";
|
||||
const iconOnly = !hasLabel && (!!leftSection || !!rightSection || loading);
|
||||
// A labelless button collapses to a square icon-only button — but never when
|
||||
// `fullWidth` is set (it must span its container, e.g. a loading execute
|
||||
// button whose label is momentarily absent while files hydrate).
|
||||
const iconOnly =
|
||||
!hasLabel && !fullWidth && (!!leftSection || !!rightSection || loading);
|
||||
|
||||
// px/py override p for their axis; each stays undefined (= size default) if unset.
|
||||
const padX = px ?? p;
|
||||
const padY = py ?? p;
|
||||
|
||||
// Sections flank a label → spread them without requiring justify="between".
|
||||
const effectiveJustify =
|
||||
@@ -210,10 +223,19 @@ const ButtonRoot = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
style={{
|
||||
...(accentVars as CSSProperties),
|
||||
...({ "--button-height": CONTROL_HEIGHT[size] } as CSSProperties),
|
||||
// fontSize decouples label text size from `size`; unset leaves Mantine's
|
||||
// size-derived --button-fz in place.
|
||||
// Relative label size, scaled off the `size` base (unset → Mantine default).
|
||||
...(fontSize
|
||||
? ({ "--button-fz": FONT_SIZE_VAR[fontSize] } as CSSProperties)
|
||||
? ({
|
||||
"--button-fz": resolveFontSize(size, fontSize),
|
||||
} as CSSProperties)
|
||||
: {}),
|
||||
// Padding overrides (inline to beat Mantine's size-based value).
|
||||
// px → Mantine's own var; py → our --sui-btn-py (Mantine has no vertical padding).
|
||||
...(padX
|
||||
? ({ "--button-padding-x": CONTROL_PADDING[padX] } as CSSProperties)
|
||||
: {}),
|
||||
...(padY
|
||||
? ({ "--sui-btn-py": CONTROL_PADDING[padY] } as CSSProperties)
|
||||
: {}),
|
||||
// Icon-only: zero the size padding inline so the lone icon centres.
|
||||
...(iconOnly ? ({ "--button-padding-x": "0" } as CSSProperties) : {}),
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/* Carousel pagination dots — active dot widens into a pill. */
|
||||
.sui-carousel-dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.sui-carousel-dots__dot {
|
||||
width: 0.4375rem;
|
||||
height: 0.4375rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--sui-dot-color, var(--color-text-5));
|
||||
opacity: 0.4;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
opacity var(--motion-fast),
|
||||
background var(--motion-fast),
|
||||
width var(--motion-fast);
|
||||
}
|
||||
|
||||
.sui-carousel-dots__dot:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.sui-carousel-dots__dot:focus-visible {
|
||||
outline: 2px solid var(--sui-dot-active-color, var(--color-blue));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.sui-carousel-dots__dot.is-active {
|
||||
width: 1rem;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--sui-dot-active-color, var(--color-blue));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* onImage: white dots over dark photography / gradients. */
|
||||
.sui-carousel-dots--onImage {
|
||||
--sui-dot-color: #ffffff;
|
||||
--sui-dot-active-color: #ffffff;
|
||||
}
|
||||
|
||||
.sui-carousel-dots--onImage .sui-carousel-dots__dot {
|
||||
opacity: 0.5;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.sui-carousel-dots--onImage .sui-carousel-dots__dot.is-active {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { CarouselDots } from "@app/ui/CarouselDots";
|
||||
|
||||
const meta: Meta<typeof CarouselDots> = {
|
||||
title: "Primitives/CarouselDots",
|
||||
component: CarouselDots,
|
||||
tags: ["autodocs"],
|
||||
parameters: { layout: "centered" },
|
||||
args: { count: 3, activeIndex: 0, tone: "default" },
|
||||
argTypes: {
|
||||
count: { control: { type: "number", min: 2, max: 8 } },
|
||||
activeIndex: { control: { type: "number", min: 0, max: 7 } },
|
||||
tone: { control: "inline-radio", options: ["default", "onImage"] },
|
||||
},
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CarouselDots>;
|
||||
|
||||
/** Click a dot to move the active pill. */
|
||||
export const Interactive: Story = {
|
||||
render: (args) => {
|
||||
const [index, setIndex] = useState(0);
|
||||
return (
|
||||
<CarouselDots
|
||||
{...args}
|
||||
activeIndex={index}
|
||||
onSelect={setIndex}
|
||||
label="Slides"
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
/** Blue pill on a light surface (portal hero, light backgrounds). */
|
||||
export const Default: Story = { args: { activeIndex: 1 } };
|
||||
|
||||
/** White dots for use over dark photography (auth carousel). */
|
||||
export const OnImage: Story = {
|
||||
args: { activeIndex: 1, tone: "onImage" },
|
||||
decorators: [
|
||||
(S) => (
|
||||
<div style={{ background: "#1e293b", padding: "2rem", borderRadius: 12 }}>
|
||||
<S />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { CSSProperties } from "react";
|
||||
import "@app/ui/CarouselDots.css";
|
||||
|
||||
/** default = blue pill on light surfaces; onImage = white dots over dark photography. */
|
||||
export type CarouselDotsTone = "default" | "onImage";
|
||||
|
||||
export interface CarouselDotsProps {
|
||||
/** Number of slides / dots. */
|
||||
count: number;
|
||||
/** Zero-based index of the active slide. */
|
||||
activeIndex: number;
|
||||
/** Called with the clicked dot's index; omit to render non-interactive dots. */
|
||||
onSelect?: (index: number) => void;
|
||||
/** Accessible label for the dot group. */
|
||||
label?: string;
|
||||
/** Accessible label for each dot; receives the zero-based index. */
|
||||
dotLabel?: (index: number) => string;
|
||||
tone?: CarouselDotsTone;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
/** Carousel pagination indicator: active dot is a wider pill, inactive dots are ovals. */
|
||||
export function CarouselDots({
|
||||
count,
|
||||
activeIndex,
|
||||
onSelect,
|
||||
label,
|
||||
dotLabel,
|
||||
tone = "default",
|
||||
className,
|
||||
style,
|
||||
}: CarouselDotsProps) {
|
||||
const classes = [
|
||||
"sui-carousel-dots",
|
||||
`sui-carousel-dots--${tone}`,
|
||||
className ?? "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<div className={classes} style={style} role="group" aria-label={label}>
|
||||
{Array.from({ length: Math.max(0, count) }).map((_, i) => {
|
||||
const active = i === activeIndex;
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
className={"sui-carousel-dots__dot" + (active ? " is-active" : "")}
|
||||
aria-current={active ? "true" : undefined}
|
||||
aria-label={dotLabel ? dotLabel(i) : `Go to slide ${i + 1}`}
|
||||
onClick={onSelect ? () => onSelect(i) : undefined}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -48,6 +48,17 @@
|
||||
.sui-chip__label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Center the label precisely. Mantine sets line-height: var(--pill-height) on
|
||||
the label, which positions the text by its font baseline and reads slightly
|
||||
high; flex-centering with line-height:1 centres the ink in the pill. */
|
||||
.sui-chip .mantine-Pill-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
overflow: visible;
|
||||
}
|
||||
.sui-chip__spinner {
|
||||
width: 0.75em;
|
||||
height: 0.75em;
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.sui-mantine-wrapper .mantine-Input-input {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* SUI focus ring — matches .sui-input:focus-within */
|
||||
.sui-mantine-wrapper[data-focused],
|
||||
.sui-mantine-wrapper:focus-within {
|
||||
|
||||
@@ -13,9 +13,16 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-block: 0;
|
||||
}
|
||||
|
||||
.sui-seg .mantine-SegmentedControl-label[data-active][data-disabled] {
|
||||
color: var(--mantine-color-text);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Mantine wraps label content in .innerLabel; flex it so an icon + text sit
|
||||
* centered together with a consistent gap. Without this the inline-block icon
|
||||
* aligns to the text baseline and rides ~3px high. */
|
||||
|
||||
@@ -54,7 +54,7 @@ export function SegmentedControl<T extends string>({
|
||||
value,
|
||||
onChange,
|
||||
accent = "default",
|
||||
size = "md",
|
||||
size = "sm",
|
||||
variant = "primary",
|
||||
fullWidth = false,
|
||||
disabled = false,
|
||||
@@ -72,11 +72,10 @@ export function SegmentedControl<T extends string>({
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
// Drive Mantine's indicator/label colour vars from the accent palette.
|
||||
const accentVars =
|
||||
variant === "primary"
|
||||
? { "--sc-color": "var(--_solid)", "--sc-label-color": "var(--_on)" }
|
||||
: { "--sc-color": "var(--_tint)", "--sc-label-color": "var(--_text)" };
|
||||
variant === "secondary"
|
||||
? { "--sc-color": "var(--_tint)", "--sc-label-color": "var(--_text)" }
|
||||
: {};
|
||||
|
||||
return (
|
||||
<MantineSegmentedControl
|
||||
|
||||
@@ -7,3 +7,44 @@ export const CONTROL_HEIGHT = {
|
||||
} as const;
|
||||
|
||||
export type ControlSize = keyof typeof CONTROL_HEIGHT;
|
||||
|
||||
/** Optional padding-override scale for Button (`p`/`px`/`py` props). Undefined = Mantine's size-based default. */
|
||||
export const CONTROL_PADDING = {
|
||||
none: "0",
|
||||
xs: "0.5rem",
|
||||
sm: "0.75rem",
|
||||
md: "1rem",
|
||||
lg: "1.25rem",
|
||||
xl: "1.5rem",
|
||||
} as const;
|
||||
|
||||
export type ControlPadding = keyof typeof CONTROL_PADDING;
|
||||
|
||||
/**
|
||||
* Shared label font-size scale (rem). Both a control's `size` and its optional
|
||||
* `fontSize` prop index into this same scale — `md` is the neutral point.
|
||||
*/
|
||||
export const CONTROL_FONT_SIZE = {
|
||||
xs: 0.75,
|
||||
sm: 0.875,
|
||||
md: 1.0,
|
||||
lg: 1.125,
|
||||
xl: 1.25,
|
||||
} as const;
|
||||
|
||||
export type ControlFontSize = keyof typeof CONTROL_FONT_SIZE;
|
||||
|
||||
/**
|
||||
* Resolve a label font-size from the control `size` and a relative `fontSize`.
|
||||
* The base comes from `size`; `fontSize` scales it relative to `md` (= no
|
||||
* change). Because it scales the size-derived base rather than replacing it, an
|
||||
* `xl` button with `md` text stays larger than a `sm` button with `lg` text.
|
||||
*/
|
||||
export function resolveFontSize(
|
||||
size: ControlSize,
|
||||
fontSize: ControlFontSize,
|
||||
): string {
|
||||
const base = CONTROL_FONT_SIZE[size];
|
||||
const factor = CONTROL_FONT_SIZE[fontSize] / CONTROL_FONT_SIZE.md;
|
||||
return `${(base * factor).toFixed(3)}rem`;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export * from "@app/ui/NavItem";
|
||||
export * from "@app/ui/PanelHeader";
|
||||
export * from "@app/ui/CodeBlock";
|
||||
export * from "@app/ui/SectionDivider";
|
||||
export * from "@app/ui/CarouselDots";
|
||||
export * from "@app/ui/Card";
|
||||
export * from "@app/ui/Modal";
|
||||
export * from "@app/ui/SettingsShell";
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0.75rem 1rem; /* 12px 16px */
|
||||
/* Vertical padding via the shared Button's --sui-btn-py (beats the Button's
|
||||
own padding-block); horizontal stays in the shorthand. */
|
||||
--sui-btn-py: 1rem; /* 16px (md) */
|
||||
padding: 1rem 1rem; /* 16px */
|
||||
border: 1px solid var(--auth-input-border-light-only);
|
||||
border-radius: 0.75rem; /* 12px */
|
||||
background-color: var(--auth-card-bg-light-only);
|
||||
|
||||
@@ -46,6 +46,17 @@
|
||||
border-color: var(--color-search-border-hover);
|
||||
}
|
||||
|
||||
/* The icon / placeholder / ⌘K live inside the shared Button's label, so the
|
||||
label must fill the button for the placeholder's flex:1 to push ⌘K to the
|
||||
far right (otherwise the label is content-width and everything left-packs). */
|
||||
.portal-header__search .mantine-Button-inner {
|
||||
width: 100%;
|
||||
}
|
||||
.portal-header__search .mantine-Button-label {
|
||||
flex: 1;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.portal-header__search-placeholder {
|
||||
flex: 1 1 auto;
|
||||
text-align: left;
|
||||
@@ -104,9 +115,7 @@
|
||||
}
|
||||
|
||||
.portal-header__tier-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
--sui-btn-py: 0.3125rem;
|
||||
padding: 0.3125rem 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-2);
|
||||
|
||||
@@ -52,15 +52,18 @@ function TierSwitcher() {
|
||||
<Button
|
||||
type="button"
|
||||
variant="quiet"
|
||||
justify="start"
|
||||
className="portal-header__tier-btn"
|
||||
leftSection={
|
||||
<span
|
||||
className="portal-header__tier-dot"
|
||||
style={{ background: info.dotColor }}
|
||||
aria-hidden
|
||||
/>
|
||||
}
|
||||
rightSection={<ChevronDownIcon size={14} />}
|
||||
>
|
||||
<span
|
||||
className="portal-header__tier-dot"
|
||||
style={{ background: info.dotColor }}
|
||||
aria-hidden
|
||||
/>
|
||||
<span className="portal-header__tier-label">{info.label}</span>
|
||||
<ChevronDownIcon size={14} />
|
||||
</Button>
|
||||
</Dropdown.Trigger>
|
||||
<Dropdown.Menu width="12rem">
|
||||
|
||||
@@ -15,25 +15,29 @@ export function ApiKeyCard({ apiKey }: { apiKey: ApiKey }) {
|
||||
<Card padding="default" className="portal-infra__key">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
fullWidth
|
||||
justify="between"
|
||||
className="portal-infra__key-head"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
aria-expanded={open}
|
||||
rightSection={
|
||||
<span className="portal-infra__key-head-right">
|
||||
<StatusBadge tone={KEY_TONE[apiKey.status]} size="sm">
|
||||
{KEY_LABEL[apiKey.status]}
|
||||
</StatusBadge>
|
||||
<span
|
||||
className={"portal-infra__chevron" + (open ? " is-open" : "")}
|
||||
aria-hidden
|
||||
>
|
||||
›
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<span className="portal-infra__key-id">
|
||||
<span className="portal-infra__cell-strong">{apiKey.name}</span>
|
||||
<code className="portal-infra__cell-code">{apiKey.prefix}</code>
|
||||
</span>
|
||||
<span className="portal-infra__key-head-right">
|
||||
<StatusBadge tone={KEY_TONE[apiKey.status]} size="sm">
|
||||
{t(KEY_LABEL[apiKey.status])}
|
||||
</StatusBadge>
|
||||
<span
|
||||
className={"portal-infra__chevron" + (open ? " is-open" : "")}
|
||||
aria-hidden
|
||||
>
|
||||
›
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
{open && (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="portal-home__quick-list">
|
||||
<Button
|
||||
variant="quiet"
|
||||
type="button"
|
||||
className="portal-home__quick-row"
|
||||
onClick={() => setActiveView("pipelines")}
|
||||
>
|
||||
<span
|
||||
className="portal-home__quick-icon"
|
||||
style={{
|
||||
background: "var(--color-purple-light)",
|
||||
color: "var(--color-purple)",
|
||||
}}
|
||||
aria-hidden
|
||||
{QUICK_ACTIONS.map((action) => (
|
||||
<Button
|
||||
key={action.key}
|
||||
variant="quiet"
|
||||
justify="start"
|
||||
fullWidth
|
||||
px="sm"
|
||||
py="sm"
|
||||
type="button"
|
||||
className="portal-home__quick-row"
|
||||
onClick={() => setActiveView(action.view)}
|
||||
leftSection={
|
||||
<span
|
||||
className="portal-home__quick-icon"
|
||||
style={{ background: action.bg, color: action.fg }}
|
||||
aria-hidden
|
||||
>
|
||||
{action.glyph}
|
||||
</span>
|
||||
}
|
||||
rightSection={
|
||||
<span className="portal-home__quick-arrow" aria-hidden>
|
||||
→
|
||||
</span>
|
||||
}
|
||||
>
|
||||
⌃
|
||||
</span>
|
||||
<span className="portal-home__quick-text">
|
||||
<strong>{t("portal.home.quickActions.buildPipeline.title")}</strong>
|
||||
<span>{t("portal.home.quickActions.buildPipeline.blurb")}</span>
|
||||
</span>
|
||||
<span className="portal-home__quick-arrow" aria-hidden>
|
||||
→
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="quiet"
|
||||
type="button"
|
||||
className="portal-home__quick-row"
|
||||
onClick={() => setActiveView("sources")}
|
||||
>
|
||||
<span
|
||||
className="portal-home__quick-icon"
|
||||
style={{
|
||||
background: "var(--color-green-light)",
|
||||
color: "var(--color-green-dark)",
|
||||
}}
|
||||
aria-hidden
|
||||
>
|
||||
⇢
|
||||
</span>
|
||||
<span className="portal-home__quick-text">
|
||||
<strong>{t("portal.home.quickActions.connectSource.title")}</strong>
|
||||
<span>{t("portal.home.quickActions.connectSource.blurb")}</span>
|
||||
</span>
|
||||
<span className="portal-home__quick-arrow" aria-hidden>
|
||||
→
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="quiet"
|
||||
type="button"
|
||||
className="portal-home__quick-row"
|
||||
onClick={() => setActiveView("infrastructure")}
|
||||
>
|
||||
<span
|
||||
className="portal-home__quick-icon"
|
||||
style={{
|
||||
background: "var(--color-amber-light)",
|
||||
color: "var(--color-amber-dark)",
|
||||
}}
|
||||
aria-hidden
|
||||
>
|
||||
⚙
|
||||
</span>
|
||||
<span className="portal-home__quick-text">
|
||||
<strong>{t("portal.home.quickActions.issueApiKey.title")}</strong>
|
||||
<span>{t("portal.home.quickActions.issueApiKey.blurb")}</span>
|
||||
</span>
|
||||
<span className="portal-home__quick-arrow" aria-hidden>
|
||||
→
|
||||
</span>
|
||||
</Button>
|
||||
<span className="portal-home__quick-text">
|
||||
<strong>
|
||||
{t(`portal.home.quickActions.${action.key}.title`)}
|
||||
</strong>
|
||||
<span>{t(`portal.home.quickActions.${action.key}.blurb`)}</span>
|
||||
</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */}
|
||||
<div
|
||||
<CarouselDots
|
||||
tone="onImage"
|
||||
count={totalSlides}
|
||||
activeIndex={index}
|
||||
onSelect={setIndex}
|
||||
label="Slides"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 16,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
gap: 10,
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: totalSlides }).map((_, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
variant="tertiary"
|
||||
aria-label={`Go to slide ${i + 1}`}
|
||||
onClick={() => setIndex(i)}
|
||||
style={{
|
||||
width: "5px",
|
||||
height: "5px",
|
||||
borderRadius: "50%",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
backgroundColor:
|
||||
i === index ? "#ffffff" : "rgba(255,255,255,0.5)",
|
||||
boxShadow: "0 2px 6px rgba(0,0,0,0.25)",
|
||||
display: "block",
|
||||
flexShrink: 0,
|
||||
padding: 0,
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,6 +172,19 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* These OAuth buttons are shared <Button>s. Pin their pill shape + height so the
|
||||
auth styling wins over the design-system Button base regardless of CSS load
|
||||
order — Storybook injects the Button/Mantine CSS after auth.css (the app loads
|
||||
it before), which otherwise flips these single-class rules to the Button's
|
||||
defaults (rounded-rect + 36px). Higher specificity keeps both identical. */
|
||||
.oauth-button-vertical.sui-btn.mantine-Button-root {
|
||||
border-radius: 999px;
|
||||
min-height: 3.5rem; /* 56px */
|
||||
}
|
||||
.oauth-button-fullwidth.sui-btn.mantine-Button-root {
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.oauth-button-icon {
|
||||
width: 3.75rem; /* 60px */
|
||||
height: 3.75rem; /* 60px */
|
||||
@@ -214,7 +227,10 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.875rem 1.5rem; /* 14px 24px */
|
||||
/* Vertical padding is driven by the shared Button's --sui-btn-py so it beats
|
||||
the Button's own padding-block; horizontal stays in the shorthand. */
|
||||
--sui-btn-py: 1rem; /* 16px (md) */
|
||||
padding: 1rem 1.5rem; /* 16px 24px */
|
||||
border: 1px solid var(--auth-input-border-light-only);
|
||||
border-radius: 999px;
|
||||
background-color: var(--auth-card-bg-light-only);
|
||||
@@ -718,7 +734,10 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.75rem 1rem;
|
||||
/* Vertical padding via the shared Button's --sui-btn-py (beats the Button's
|
||||
own padding-block); horizontal stays in the shorthand. */
|
||||
--sui-btn-py: 1rem; /* 16px (md) */
|
||||
padding: 1rem 1rem; /* 16px */
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 100px;
|
||||
background-color: #ffffff;
|
||||
@@ -798,7 +817,10 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.75rem 1rem;
|
||||
/* Vertical padding via the shared Button's --sui-btn-py (beats the Button's
|
||||
own padding-block); horizontal stays in the shorthand. */
|
||||
--sui-btn-py: 1rem; /* 16px (md) */
|
||||
padding: 1rem 1rem; /* 16px */
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 100px;
|
||||
background-color: #ffffff;
|
||||
|
||||
@@ -171,7 +171,8 @@
|
||||
.pol-row-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 0.8125rem;
|
||||
/* Match a normal `sm` button label so icon-bearing rows aren't smaller. */
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
@@ -508,19 +508,19 @@ export default function Login() {
|
||||
size="sm"
|
||||
fw={600}
|
||||
ta="center"
|
||||
style={{ color: "var(--text-always-dark)" }}
|
||||
style={{ color: "var(--text-primary)" }}
|
||||
>
|
||||
{t("login.defaultCredentials", "Default Login Credentials")}
|
||||
</Text>
|
||||
<Text
|
||||
size="sm"
|
||||
ta="center"
|
||||
style={{ color: "var(--text-always-dark)" }}
|
||||
style={{ color: "var(--text-primary)" }}
|
||||
>
|
||||
<Text
|
||||
component="span"
|
||||
fw={600}
|
||||
style={{ color: "var(--text-always-dark)" }}
|
||||
style={{ color: "var(--text-primary)" }}
|
||||
>
|
||||
{t("login.username", "Username")}:
|
||||
</Text>{" "}
|
||||
@@ -529,12 +529,12 @@ export default function Login() {
|
||||
<Text
|
||||
size="sm"
|
||||
ta="center"
|
||||
style={{ color: "var(--text-always-dark)" }}
|
||||
style={{ color: "var(--text-primary)" }}
|
||||
>
|
||||
<Text
|
||||
component="span"
|
||||
fw={600}
|
||||
style={{ color: "var(--text-always-dark)" }}
|
||||
style={{ color: "var(--text-primary)" }}
|
||||
>
|
||||
{t("login.password", "Password")}:
|
||||
</Text>{" "}
|
||||
@@ -544,7 +544,7 @@ export default function Login() {
|
||||
size="xs"
|
||||
ta="center"
|
||||
mt="xs"
|
||||
style={{ color: "var(--text-always-dark-muted)" }}
|
||||
style={{ color: "var(--text-muted)" }}
|
||||
>
|
||||
{t(
|
||||
"login.changePasswordWarning",
|
||||
|
||||
Reference in New Issue
Block a user