diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx
index 7e694ec..68260a9 100644
--- a/src/components/Badge.tsx
+++ b/src/components/Badge.tsx
@@ -4,22 +4,22 @@ import { BadgeShape, ThemeVariant, getVariantColors } from "./utils";
interface BadgeProps extends React.HTMLAttributes {
$variant?: ThemeVariant;
$shape?: BadgeShape;
- $isInteractive?: boolean;
+ $isClickable?: boolean;
}
/**
* Badge component for compact status/label pills.
- * Displays colored badges with different variants (success, warning, error, info) and shapes.
+ * Displays colored badges with different variants (primary, success, warning, error, accent) and shapes.
* Supports interactive mode with hover effects for clickable badges.
* @param $variant - Visual variant using ThemeVariant enum.
* @param $shape - Shape of the badge: BadgeShape.Pill (fully rounded ends) or BadgeShape.Rounded (medium border radius). Defaults to Pill.
- * @param $isInteractive - When true, adds hover/active states including translateY animation and shadow for clickable badges. Defaults to false.
+ * @param $isClickable - When true, adds hover/active states including translateY animation and shadow for clickable badges. Defaults to false.
* @example
* // Simple success badge
* Active
* @example
* // Interactive warning badge with rounded shape
- * Click me
+ * Click me
*/
export const Badge = styled.span`
display: inline-flex;
@@ -31,19 +31,19 @@ export const Badge = styled.span`
border-radius: ${({ $shape = BadgeShape.Pill, theme }) =>
$shape === BadgeShape.Rounded ? theme.radius.md : theme.radius.full};
border: 1px solid
- ${({ $variant = ThemeVariant.Info, theme }) =>
+ ${({ $variant = ThemeVariant.Primary, theme }) =>
getVariantColors(theme, $variant).border};
font: ${({ theme }) => theme.typography.small.font};
line-height: ${({ theme }) => theme.fonts.lineHeight.mini};
- background: ${({ $variant = ThemeVariant.Info, theme }) =>
+ background: ${({ $variant = ThemeVariant.Primary, theme }) =>
getVariantColors(theme, $variant).bg};
- color: ${({ $variant = ThemeVariant.Info, theme }) =>
+ color: ${({ $variant = ThemeVariant.Primary, theme }) =>
getVariantColors(theme, $variant).text};
- ${({ $isInteractive, theme }) =>
- $isInteractive
+ ${({ $isClickable, theme }) =>
+ $isClickable
? `
cursor: pointer;
transition: transform ${theme.transitions.base},
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 529e611..0df0780 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -1,9 +1,6 @@
-import { Children } from "react";
import styled from "styled-components";
-import { ControlLabel } from "./Typography";
import {
- ButtonWidth,
- getThemeSizeValuesByThemeSize,
+ getControlSize,
getVariantColors,
ThemeSize,
ThemeVariant,
@@ -13,85 +10,72 @@ export interface ButtonProps extends React.ButtonHTMLAttributesSubmit
+ *
* @example
- * // Ghost error button with auto width
- *
+ * // Ghost error button
+ *
* @example
* // Large success button
*
*/
-export const Button = styled.button.attrs(({ children }) => ({
- children: Children.map(children, (child) =>
- typeof child === "string" || typeof child === "number"
- ? {child}
- : child,
- ),
-}))`
+export const Button = styled.button`
display: inline-flex;
align-items: center;
justify-content: center;
- gap: ${({ $size = ThemeSize.Medium }) =>
- getThemeSizeValuesByThemeSize($size)};
- ${({ $width }) => ($width === ButtonWidth.Full ? "width: 100%;" : "")}
+ ${({ $fullWidth }) => ($fullWidth ? "width: 100%;" : "")}
${({ $size = ThemeSize.Medium, theme }) => {
- switch ($size) {
- case ThemeSize.Small:
- return `
- padding: ${theme.spacing.xs} ${theme.spacing.sm};
- `;
- case ThemeSize.Medium:
- return `
- padding: ${theme.spacing.sm} ${theme.spacing.md};
- `;
- case ThemeSize.Large:
- default:
- return `
- padding: ${theme.spacing.md} ${theme.spacing.lg};
- `;
- }
+ const { height, paddingX, gap } = getControlSize(theme, $size);
+ return `
+ gap: ${gap};
+ height: ${height};
+ padding: 0 ${paddingX};
+ `;
}}
border-radius: ${({ theme }) => theme.radius.md};
- ${({ $variant = ThemeVariant.Info, $ghost, theme }) => {
+ ${({ $variant = ThemeVariant.Primary, $ghost, theme }) => {
const colors = getVariantColors(theme, $variant);
-
+ const darken = (amount: string) =>
+ `color-mix(in srgb, ${colors.solidBg}, ${theme.colors.shade} ${amount})`;
+ const hoverBg = $ghost
+ ? colors.solidBg
+ : darken(theme.interactions.hoverDarken);
+ const activeBg = darken(theme.interactions.activeDarken);
+ const disabledBorder = $ghost
+ ? theme.colors.gray[500]
+ : theme.colors.gray[300];
+
+ // A filled button always has a border in the same color as its fill.
return `
- border: 1px solid ${colors.border};
+ border: 1px solid ${$ghost ? colors.border : colors.solidBg};
background-color: ${$ghost ? "transparent" : colors.solidBg};
color: ${$ghost ? colors.border : colors.solidText};
&:hover:not(:disabled) {
- background-color: color-mix(
- in srgb,
- ${colors.solidBg},
- ${theme.colors.shade} ${theme.interactions.hoverDarken}
- );
+ border-color: ${hoverBg};
+ background-color: ${hoverBg};
}
&:active:not(:disabled) {
- background-color: color-mix(
- in srgb,
- ${colors.solidBg},
- ${theme.colors.shade} ${theme.interactions.activeDarken}
- );
+ border-color: ${activeBg};
+ background-color: ${activeBg};
}
${
@@ -105,7 +89,7 @@ export const Button = styled.button.attrs(({ children }) => ({
}
&:disabled {
- border-color: ${theme.colors.gray[500]};
+ border-color: ${disabledBorder};
background-color: ${$ghost ? "transparent" : theme.colors.gray[300]};
opacity: ${theme.interactions.disabledOpacity};
@@ -117,8 +101,9 @@ export const Button = styled.button.attrs(({ children }) => ({
}}
font: ${({ theme }) => theme.typography.bodyMedium.font};
+ font-size: ${({ $size = ThemeSize.Medium, theme }) =>
+ getControlSize(theme, $size).fontSize};
- cursor: pointer;
transition:
background-color ${({ theme }) => theme.transitions.fast},
border-color ${({ theme }) => theme.transitions.fast},
diff --git a/src/components/CodeEditor.tsx b/src/components/CodeEditor.tsx
index 4f39af8..c645b13 100644
--- a/src/components/CodeEditor.tsx
+++ b/src/components/CodeEditor.tsx
@@ -12,7 +12,6 @@ const EditorTextarea = styled.textarea<{
$minHeight?: string;
}>`
width: 100%;
- box-sizing: border-box;
min-height: ${({ $minHeight = "400px" }) => $minHeight};
padding: ${({ theme }) => theme.spacing.md};
@@ -24,7 +23,7 @@ const EditorTextarea = styled.textarea<{
background-color: ${({ theme }) => theme.colors.background.secondary};
color: ${({ theme }) => theme.colors.text.primary};
- font: ${({ theme }) => theme.typography.code_small.font};
+ font: ${({ theme }) => theme.typography.code.font};
resize: vertical;
@@ -44,8 +43,8 @@ const EditorTextarea = styled.textarea<{
export interface CodeEditorProps extends React.HTMLAttributes {
$value: string;
$onChange: (value: string) => void;
- $placeholder?: string;
- $disabled?: boolean;
+ placeholder?: string;
+ disabled?: boolean;
$hasError?: boolean;
$minHeight?: string;
}
@@ -55,22 +54,22 @@ export interface CodeEditorProps extends React.HTMLAttributes {
* It is a controlled component and relies on `$value` / `$onChange` for state.
* @param $value - Current text content of the editor.
* @param $onChange - Callback invoked with new text when the content changes.
- * @param $placeholder - Optional placeholder text when editor is empty.
- * @param $disabled - When true the editor is non-editable and shows disabled visuals.
+ * @param placeholder - Optional placeholder text when editor is empty.
+ * @param disabled - When true the editor is non-editable and shows disabled visuals.
* @param $hasError - When true the editor displays an error border state.
* @param $minHeight - Optional CSS min-height for the editor area (defaults to `400px`).
* @example:
*
*/
export const CodeEditor: React.FC = ({
$value,
$onChange,
- $placeholder,
- $disabled,
+ placeholder,
+ disabled,
$hasError,
$minHeight,
...rest
@@ -80,8 +79,8 @@ export const CodeEditor: React.FC = ({
$onChange(e.target.value)}
- placeholder={$placeholder}
- disabled={$disabled}
+ placeholder={placeholder}
+ disabled={disabled}
$hasError={$hasError}
$minHeight={$minHeight}
/>
diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx
index 8ed44f6..0ea3290 100644
--- a/src/components/ConfirmDialog.tsx
+++ b/src/components/ConfirmDialog.tsx
@@ -52,7 +52,7 @@ export const ConfirmDialog: React.FC = ({
{...rest}
>
- {$title}
+ {$title}
{$description}
diff --git a/src/components/Containers.tsx b/src/components/Containers.tsx
index 31d90bc..97109ad 100644
--- a/src/components/Containers.tsx
+++ b/src/components/Containers.tsx
@@ -31,8 +31,6 @@ export const PageContainer = styled.div`
background-position: center;
background-repeat: no-repeat;
`}
-
- padding: 0;
`;
export const CenteredCardContent = styled.div`
diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx
index b71340c..5aa0851 100644
--- a/src/components/CopyButton.tsx
+++ b/src/components/CopyButton.tsx
@@ -5,31 +5,30 @@ import toast from "react-hot-toast";
import { formatErrorForToast } from "@/api/utils";
import { Button } from "./Button";
import Icon from "./Icon";
-import { ThemeSize } from "./utils";
export interface CopyButtonProps extends React.ButtonHTMLAttributes {
- $text: string;
+ $value: string;
}
/**
* Small icon-only button to copy text to clipboard.
- * @param $text - The text to copy to clipboard.
+ * @param $value - The text to copy to the clipboard.
* @param title - The tooltip/title for the button (default: "Copy").
* @param disabled - Whether the button is disabled (default: false).
* @example
- *
+ *
*/
export const CopyButton: React.FC = ({
- $text,
+ $value,
disabled = false,
...rest
}) => {
const handleClick = async (e: React.MouseEvent) => {
e.stopPropagation();
- if (disabled || !$text) return;
+ if (disabled || !$value) return;
try {
- await navigator.clipboard.writeText($text);
+ await navigator.clipboard.writeText($value);
toast.success("Copied to clipboard");
} catch (err) {
toast.error(formatErrorForToast(err, "Failed to copy to clipboard"));
@@ -40,7 +39,6 @@ export const CopyButton: React.FC = ({
*
* @example
- * // Info notification (default)
+ * // Primary notification (default)
*
* This is an informational message.
*
@@ -38,12 +38,12 @@ export const NotificationCard = styled.div`
border-radius: ${({ theme }) => theme.radius.md};
border: 1px solid
- ${({ $variant = ThemeVariant.Info, theme }) =>
+ ${({ $variant = ThemeVariant.Primary, theme }) =>
getVariantColors(theme, $variant).border};
- background: ${({ $variant = ThemeVariant.Info, theme }) =>
+ background: ${({ $variant = ThemeVariant.Primary, theme }) =>
getVariantColors(theme, $variant).bg};
- color: ${({ $variant = ThemeVariant.Info, theme }) =>
+ color: ${({ $variant = ThemeVariant.Primary, theme }) =>
getVariantColors(theme, $variant).text};
font: ${({ theme }) => theme.typography.body.font};
diff --git a/src/components/Popover.tsx b/src/components/Popover.tsx
index 4e2b1bc..637c3de 100644
--- a/src/components/Popover.tsx
+++ b/src/components/Popover.tsx
@@ -1,6 +1,5 @@
import React, { createContext, useContext } from "react";
import styled from "styled-components";
-import { ControlLabel } from "./Typography";
const PopoverContext = createContext(undefined);
@@ -13,13 +12,7 @@ const PopoverTrigger = styled.button<{ $anchorName: string }>`
padding: ${({ theme }) => `${theme.spacing.sm} ${theme.spacing.md}`};
- border: 0;
border-radius: ${({ theme }) => theme.radius.md};
- background: transparent;
-
- color: inherit;
-
- cursor: pointer;
&:hover {
background: ${({ theme }) => theme.colors.info.bg};
@@ -38,7 +31,6 @@ const PopoverContent = styled.div<{ $anchorName: string }>`
position-area: block-end span-inline-end;
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
- box-sizing: border-box;
inline-size: max-content;
min-inline-size: min(10rem, calc(100vw - 1rem));
max-inline-size: min(18rem, calc(100vw - 1rem));
@@ -75,21 +67,16 @@ const PopoverItemButton = styled.button`
align-items: center;
gap: ${({ theme }) => theme.spacing.sm};
- box-sizing: border-box;
inline-size: 100%;
padding: ${({ theme }) => `${theme.spacing.sm} ${theme.spacing.md}`};
- border: 0;
border-radius: ${({ theme }) => theme.radius.md};
- background: transparent;
color: ${({ theme }) => theme.colors.info.border};
font: ${({ theme }) => theme.typography.caption.font};
text-align: start;
- cursor: pointer;
-
&:hover:not(:disabled) {
background: color-mix(
in srgb,
@@ -165,11 +152,7 @@ export const PopoverItem: React.FC = ({ children, ...props })
popoverTargetAction="hide"
{...props}
>
- {React.Children.map(children, (child) =>
- typeof child === "string" || typeof child === "number"
- ? {child}
- : child,
- )}
+ {children}
);
};
diff --git a/src/components/RadioGroup.tsx b/src/components/RadioGroup.tsx
index 07ee2a1..acefa9a 100644
--- a/src/components/RadioGroup.tsx
+++ b/src/components/RadioGroup.tsx
@@ -6,16 +6,20 @@ const RadioGroupContainer = styled.div`
gap: ${({ theme }) => theme.spacing.md};
`;
-const RadioLabel = styled.label<{ $disabled?: boolean }>`
+const RadioLabel = styled.label`
display: flex;
align-items: center;
gap: ${({ theme }) => theme.spacing.xs};
- cursor: ${({ $disabled }) => ($disabled ? "not-allowed" : "pointer")};
- opacity: ${({ $disabled }) => ($disabled ? 0.6 : 1)};
+ cursor: pointer;
font: ${({ theme }) => theme.typography.body.font};
color: ${({ theme }) => theme.colors.text.primary};
+
+ &:has(input:disabled) {
+ opacity: ${({ theme }) => theme.interactions.disabledOpacity};
+ cursor: not-allowed;
+ }
`;
const RadioInput = styled.input`
@@ -66,7 +70,7 @@ interface RadioGroupProps extends React.HTMLAttributes {
$options: RadioOption[];
$value: string;
$onChange: (value: string) => void;
- $disabled?: boolean;
+ disabled?: boolean;
}
/**
@@ -78,7 +82,7 @@ interface RadioGroupProps extends React.HTMLAttributes {
* @param $options - Array of { label, value } option objects to render as radio buttons.
* @param $value - Currently selected option value (controlled component pattern).
* @param $onChange - Callback fired with the newly selected value when user clicks an option.
- * @param $disabled - When true, disables all options in the group with reduced opacity and no-allowed cursor. Defaults to false.
+ * @param disabled - When true, disables all options in the group with reduced opacity and no-allowed cursor. Defaults to false.
* @example
* const [selected, setSelected] = useState("option1");
* = ({
$options,
$value,
$onChange,
- $disabled = false,
+ disabled = false,
...rest
}) => {
return (
{$options.map((option) => (
-
+
$onChange(e.target.value)}
- disabled={$disabled}
+ disabled={disabled}
/>
{option.label}
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index 355eb3f..f73212e 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -91,7 +91,6 @@ export const NavItem = styled.a<{ $isActive?: boolean }>`
border-radius: ${({ theme }) => theme.radius.md};
color: ${({ theme }) => theme.colors.text.secondary};
- border: none;
padding: ${({ theme }) => `${theme.spacing.sm} ${theme.spacing.lg}`};
gap: ${({ theme }) => theme.spacing.md};
diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx
index c9132fb..2e3d3ec 100644
--- a/src/components/Spinner.tsx
+++ b/src/components/Spinner.tsx
@@ -1,25 +1,18 @@
import { spin } from "@/theme/animations";
-import { theme } from "@/theme/theme";
import { IconCircle, IconLoader2 } from "@tabler/icons-react";
import React from "react";
-import styled from "styled-components";
+import styled, { useTheme } from "styled-components";
import {
- getThemePixelSizeValuesByThemeSize,
- getThemeSizeValuesByThemeSize,
+ getIconPixelSize,
getVariantColors,
ThemeSize,
ThemeVariant,
} from "./utils";
-const Ring = styled.div<{
- $size: ThemeSize;
- $borderWidth: number;
-}>`
+const Ring = styled.div<{ $px: number }>`
position: relative;
- width: ${({ $size, $borderWidth }) =>
- `calc(${getThemeSizeValuesByThemeSize($size)} + ${$borderWidth * 2}px)`};
- height: ${({ $size, $borderWidth }) =>
- `calc(${getThemeSizeValuesByThemeSize($size)} + ${$borderWidth * 2}px)`};
+ width: ${({ $px }) => `${$px}px`};
+ height: ${({ $px }) => `${$px}px`};
svg {
position: absolute;
@@ -33,20 +26,20 @@ const Ring = styled.div<{
}
`;
-const SpinnerContainer = styled.div<{
- $variant: ThemeVariant;
- $size: ThemeSize;
-}>`
+const SpinnerContainer = styled.div<{ $variant: ThemeVariant; $px: number }>`
display: flex;
align-items: center;
justify-content: center;
+ width: ${({ $px }) => `${$px}px`};
+ height: ${({ $px }) => `${$px}px`};
+
border-radius: 50%;
background-color: ${({ $variant, theme }) =>
getVariantColors(theme, $variant).solidBg};
border: 1px solid
- ${({ $variant, theme }) => getVariantColors(theme, $variant).border};
+ ${({ $variant, theme }) => getVariantColors(theme, $variant).solidBg};
`;
export interface SpinnerProps extends React.HTMLAttributes {
@@ -60,18 +53,18 @@ export interface SpinnerProps extends React.HTMLAttributes {
/**
* Animated circular spinner component for indicating loading states.
* Can be centered in a container or positioned using className.
- * @param $size - Size of the spinning element
- * @param $color - Custom hex/rgb color string for the spinning accent color. Overrides $variant if provided.
- * @param $trackColor - Custom hex/rgb color string for the background ring track. Uses theme colors if not specified.
- * @param $variant - Theme color variant: "primary", "success", "error", "warning", "info", "accent", "secondary". Determines accent color from theme. Defaults to "primary".
- * @param $withContainer - When true, wraps the spinner in a centered flex container with padding. Defaults to false.
+ * @param $size - Size on the icon scale (see Icon). The ring is drawn around it, so the spinner is 1.5x that size. Defaults to Medium.
+ * @param $color - Custom CSS color for the spinning arc. Overrides $variant if provided.
+ * @param $trackColor - Custom CSS color for the background ring track. Uses theme colors if not specified.
+ * @param $variant - Theme color variant: "primary", "success", "error", "warning", "accent". Defaults to "primary".
+ * @param $withContainer - When true, draws a white spinner on a filled circle in the variant color, matching a round Icon of the same size. Defaults to false.
* @param className - Optional CSS class name for custom positioning or styling overrides.
* @example
* // Default medium primary spinner
*
* @example
- * // Large success spinner, centered
- *
+ * // Spinner next to round icons
+ *
* @example
* // Custom size and colors
*
@@ -85,31 +78,40 @@ export const Spinner: React.FC = ({
className,
...rest
}) => {
- // Note: We can't access theme context here in the component function,
- // so we continue using the static theme for optional color overrides.
- // This is acceptable since $color and $trackColor are explicit overrides.
- const resolvedColor = $color || getVariantColors(theme, $variant).solidBg;
- const resolvedTrackColor =
- $trackColor || getVariantColors(theme, $variant).solidText;
+ const colors = getVariantColors(useTheme(), $variant);
+ const arcColor = $color || ($withContainer ? colors.solidText : colors.solidBg);
+ const trackColor = $trackColor || ($withContainer ? colors.solidText : colors.bg);
+ const trackOpacity = $withContainer && !$trackColor ? 0.35 : 1;
- const pixelSize = getThemePixelSizeValuesByThemeSize($size);
+ const pixelSize = getIconPixelSize($size);
const borderWidth = Math.max(2, pixelSize / 4);
- const stroke = (borderWidth / (pixelSize + borderWidth * 2)) * 24;
+ // In a container the spinner sits inside the circle like the glyph of a round Icon.
+ const ringSize = $withContainer ? pixelSize : pixelSize + borderWidth * 2;
+ const stroke = $withContainer ? 2.5 : (borderWidth / ringSize) * 24;
const spinner = (
-
-
+
+
);
return $withContainer ? (
-
+
{spinner}
) : (
diff --git a/src/components/Table.tsx b/src/components/Table.tsx
index a1ba760..d8aa5da 100644
--- a/src/components/Table.tsx
+++ b/src/components/Table.tsx
@@ -33,8 +33,6 @@ export const Table = styled.table`
font: ${({ theme }) => theme.typography.body.font};
color: ${({ theme }) => theme.colors.table.rowText};
-
- background: transparent;
`;
/**
diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx
index 1cdaaa3..6c1ca2b 100644
--- a/src/components/Tabs.tsx
+++ b/src/components/Tabs.tsx
@@ -1,6 +1,5 @@
import React, { ReactNode, useState } from "react";
import styled from "styled-components";
-import { ControlLabel } from "./Typography";
import { TabsContext, TabsContextType, useTabs } from "./utils";
interface TabOption {
@@ -10,19 +9,19 @@ interface TabOption {
export interface TabsProviderProps {
children: ReactNode;
- initialTab: string;
+ $initialTab: string;
}
/**
* Provider component that manages tab state using React Context.
* Must wrap the Tabs component and any child components that need to access tab state via useTabs hook.
* Provides activeTab, selectTab, and isActive methods to all descendants through context.
- * @param initialTab - The id of the tab that should be active on mount. Must match one of the tab ids.
+ * @param $initialTab - The id of the tab that should be active on mount. Must match one of the tab ids.
* @param children - Child components that will have access to tab context. Should include Tabs component and content that responds to tab changes.
* @example
* // Basic tabs with provider
- *
- *
+ *
@@ -37,9 +36,9 @@ export interface TabsProviderProps {
*/
export const TabsProvider: React.FC = ({
children,
- initialTab,
+ $initialTab,
}) => {
- const [activeTab, setActiveTab] = useState(initialTab);
+ const [activeTab, setActiveTab] = useState($initialTab);
const value: TabsContextType = {
activeTab,
@@ -51,7 +50,7 @@ export const TabsProvider: React.FC = ({
};
export interface TabsProps extends React.HTMLAttributes {
- tabs: TabOption[];
+ $tabs: TabOption[];
}
const TabsContainer = styled.div`
@@ -68,9 +67,6 @@ const TabButton = styled.button<{ $active: boolean }>`
padding: ${({ theme }) => `${theme.spacing.sm} ${theme.spacing.md}`};
- background: none;
- border: none;
-
border-bottom: 2px solid
${({ theme, $active }) =>
$active ? theme.colors.primary.main : "transparent"};
@@ -79,7 +75,6 @@ const TabButton = styled.button<{ $active: boolean }>`
$active ? theme.colors.primary.main : theme.colors.text.secondary};
font: ${({ theme }) => theme.typography.body.font};
- cursor: pointer;
transition: all ${({ theme }) => theme.transitions.base};
&:hover {
@@ -95,11 +90,11 @@ const TabButton = styled.button<{ $active: boolean }>`
* Renders a horizontal list of tab buttons with active state.
* Must be used within a `TabsProvider` to access tab state management.
* Tab selection is controlled through the `useTabs` context.
- * @param tabs - Array of tab objects with unique stable id and display label. Each tab requires: { id: string, label: ReactNode }.
+ * @param $tabs - Array of tab objects with unique stable id and display label. Each tab requires: { id: string, label: ReactNode }.
* @example
* // Simple text tabs
- *
- *
+ * `
*
* @example
* // Tabs with icons
- * List> },
* { id: "grid", label: <> Grid> }
* ]} />
*/
-export const Tabs: React.FC = ({ tabs, ...rest }) => {
+export const Tabs: React.FC = ({ $tabs, ...rest }) => {
const { activeTab, selectTab } = useTabs();
return (
- {tabs.map((tab) => (
+ {$tabs.map((tab) => (
selectTab(tab.id)}
>
- {typeof tab.label === "string" || typeof tab.label === "number"
- ? {tab.label}
- : tab.label}
+ {tab.label}
))}
diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx
index c669196..7dfb5ed 100644
--- a/src/components/Typography.tsx
+++ b/src/components/Typography.tsx
@@ -1,10 +1,6 @@
import { ReactNode, useEffect, useRef, useState } from "react";
import styled from "styled-components";
-export const ControlLabel = styled.span`
- align-self: baseline;
-`;
-
export interface TextProps extends React.HTMLAttributes {
$color?: string;
}
@@ -20,7 +16,6 @@ export interface TextProps extends React.HTMLAttributes {
export const Text = styled.p`
font: ${({ theme }) => theme.typography.body.font};
color: ${({ $color, theme }) => $color || theme.colors.text.secondary};
- margin: 0;
`;
interface ExpandableTextContainerProps extends React.HTMLAttributes {
@@ -44,13 +39,8 @@ const ExpandableTextContainer = styled.div`
`;
const ExpandToggle = styled.button`
- background: none;
- border: none;
-
color: ${({ theme }) => theme.colors.primary.main};
font: ${({ theme }) => theme.typography.small.font};
- cursor: pointer;
- padding: 0;
margin-top: ${({ theme }) => theme.spacing.xs};
text-decoration: underline;
@@ -62,8 +52,8 @@ const ExpandToggle = styled.button`
export interface ExpandableTextProps {
$text: ReactNode;
$lineClamp?: number;
- $showMoreText?: string;
- $showLessText?: string;
+ $showMoreLabel?: string;
+ $showLessLabel?: string;
}
/**
@@ -71,8 +61,8 @@ export interface ExpandableTextProps {
* Clamps text to 2 lines by default with ellipsis, and adds a toggle button only when content overflows.
* @param $text - Content to display.
* @param $lineClamp - Number of lines to clamp when collapsed. Defaults to 2.
- * @param $showMoreText - Custom text for the expand button. Defaults to "Show more".
- * @param $showLessText - Custom text for the collapse button. Defaults to "Show less".
+ * @param $showMoreLabel - Custom label for the expand button. Defaults to "Show more".
+ * @param $showLessLabel - Custom label for the collapse button. Defaults to "Show less".
* @example
* // Simple text expansion
*
@@ -80,8 +70,8 @@ export interface ExpandableTextProps {
* // Custom button labels
*
* @example
* Formatted content with bold text} />
@@ -89,8 +79,8 @@ export interface ExpandableTextProps {
export const ExpandableText: React.FC = ({
$text,
$lineClamp,
- $showMoreText,
- $showLessText,
+ $showMoreLabel,
+ $showLessLabel,
}) => {
const [expanded, setExpanded] = useState(false);
const [isClamped, setIsClamped] = useState(false);
@@ -124,8 +114,8 @@ export const ExpandableText: React.FC = ({
{isClamped && (
setExpanded(!expanded)}>
{expanded
- ? $showLessText || "Show less"
- : $showMoreText || "Show more"}
+ ? $showLessLabel || "Show less"
+ : $showMoreLabel || "Show more"}
)}
diff --git a/src/components/cards/Card.tsx b/src/components/cards/Card.tsx
index 3504ff9..5d2a28a 100644
--- a/src/components/cards/Card.tsx
+++ b/src/components/cards/Card.tsx
@@ -11,7 +11,6 @@ import styled from "styled-components";
*
*/
export const CardContainer = styled.div`
- box-sizing: border-box;
width: 100%;
background: ${({ theme }) => theme.colors.background.primary};
@@ -55,9 +54,7 @@ export const CardHeader = styled.div`
* Execution Report
*/
export const CardTitle = styled.h2`
- margin: 0;
-
- font: ${({ theme }) => theme.typography.bodyMedium.font};
+ font: ${({ theme }) => theme.typography.h2.font};
color: ${({ theme }) => theme.colors.text.primary};
`;
/**
@@ -74,7 +71,7 @@ export const CardBody = styled.div`
* Card footer section for actions, aligned to the right.
* @example
*
- * Cancel
+ * Cancel
* Save
*
*/
diff --git a/src/components/cards/ExpandableCard.tsx b/src/components/cards/ExpandableCard.tsx
index 0020f13..420414c 100644
--- a/src/components/cards/ExpandableCard.tsx
+++ b/src/components/cards/ExpandableCard.tsx
@@ -9,11 +9,6 @@ const ToggleButton = styled.span`
display: inline-flex;
align-items: center;
justify-content: center;
-
- padding: 0;
- border: none;
- background: transparent;
- color: inherit;
`;
const ToggleIconWrapper = styled.span<{ $expanded: boolean }>`
diff --git a/src/components/utils.test.ts b/src/components/utils.test.ts
index 2981f14..79a4a5c 100644
--- a/src/components/utils.test.ts
+++ b/src/components/utils.test.ts
@@ -1,8 +1,7 @@
import { darkTheme, theme } from "@/theme/theme";
import { describe, expect, test } from "vitest";
import {
- getThemePixelSizeValuesByThemeSize,
- getThemeSizeValuesByThemeSize,
+ getIconPixelSize,
getVariantColors,
ThemeSize,
ThemeVariant,
@@ -20,18 +19,12 @@ describe("Component utility functions", () => {
expect(getVariantColors(theme, ThemeVariant.Error)).toEqual(
theme.colors.error,
);
- expect(getVariantColors(theme, ThemeVariant.Info)).toEqual(
- theme.colors.info,
- );
expect(getVariantColors(theme, ThemeVariant.Primary)).toEqual(
theme.colors.info,
);
expect(getVariantColors(theme, ThemeVariant.Accent)).toEqual(
theme.colors.accent,
);
- expect(getVariantColors(theme, ThemeVariant.Secondary)).toEqual(
- theme.colors.accent,
- );
});
test("returns correct colors for all variants with dark theme", () => {
@@ -44,18 +37,12 @@ describe("Component utility functions", () => {
expect(getVariantColors(darkTheme, ThemeVariant.Error)).toEqual(
darkTheme.colors.error,
);
- expect(getVariantColors(darkTheme, ThemeVariant.Info)).toEqual(
- darkTheme.colors.info,
- );
expect(getVariantColors(darkTheme, ThemeVariant.Primary)).toEqual(
darkTheme.colors.info,
);
expect(getVariantColors(darkTheme, ThemeVariant.Accent)).toEqual(
darkTheme.colors.accent,
);
- expect(getVariantColors(darkTheme, ThemeVariant.Secondary)).toEqual(
- darkTheme.colors.accent,
- );
});
test("returns different colors for light and dark themes", () => {
@@ -72,7 +59,7 @@ describe("Component utility functions", () => {
ThemeVariant.Success,
ThemeVariant.Warning,
ThemeVariant.Error,
- ThemeVariant.Info,
+ ThemeVariant.Primary,
ThemeVariant.Accent,
];
@@ -92,89 +79,29 @@ describe("Component utility functions", () => {
});
});
- test("getThemeSizeValuesByThemeSize returns correct size values for all sizes", () => {
- expect(getThemeSizeValuesByThemeSize(ThemeSize.ExtraSmall)).toBe(
- theme.size.xs,
- );
- expect(getThemeSizeValuesByThemeSize(ThemeSize.Small)).toBe(theme.size.sm);
- expect(getThemeSizeValuesByThemeSize(ThemeSize.Medium)).toBe(theme.size.md);
- expect(getThemeSizeValuesByThemeSize(ThemeSize.Large)).toBe(theme.size.lg);
- expect(getThemeSizeValuesByThemeSize(ThemeSize.ExtraLarge)).toBe(
- theme.size.xl,
- );
- expect(getThemeSizeValuesByThemeSize(ThemeSize.TwoXL)).toBe(
- theme.size["2xl"],
- );
- expect(getThemeSizeValuesByThemeSize(ThemeSize.ThreeXL)).toBe(
- theme.size["3xl"],
- );
- expect(getThemeSizeValuesByThemeSize(ThemeSize.FourXL)).toBe(
- theme.size["4xl"],
- );
+ test("getIconPixelSize returns correct pixel values for all sizes", () => {
+ expect(getIconPixelSize(ThemeSize.Small)).toBe(theme.icon_size.sm);
+ expect(getIconPixelSize(ThemeSize.Medium)).toBe(theme.icon_size.md);
+ expect(getIconPixelSize(ThemeSize.Large)).toBe(theme.icon_size.lg);
});
- test("getThemeSizeValuesByThemeSize returns md as fallback for invalid size", () => {
+ test("getIconPixelSize returns md pixel value as fallback for invalid size", () => {
const invalidSize = "invalid" as ThemeSize;
- expect(getThemeSizeValuesByThemeSize(invalidSize)).toBe(theme.size.md);
- });
-
- test("getThemePixelSizeValuesByThemeSize returns correct pixel values for all sizes", () => {
- expect(getThemePixelSizeValuesByThemeSize(ThemeSize.ExtraSmall)).toBe(
- theme.size_pixels.xs,
- );
- expect(getThemePixelSizeValuesByThemeSize(ThemeSize.Small)).toBe(
- theme.size_pixels.sm,
- );
- expect(getThemePixelSizeValuesByThemeSize(ThemeSize.Medium)).toBe(
- theme.size_pixels.md,
- );
- expect(getThemePixelSizeValuesByThemeSize(ThemeSize.Large)).toBe(
- theme.size_pixels.lg,
- );
- expect(getThemePixelSizeValuesByThemeSize(ThemeSize.ExtraLarge)).toBe(
- theme.size_pixels.xl,
- );
- expect(getThemePixelSizeValuesByThemeSize(ThemeSize.TwoXL)).toBe(
- theme.size_pixels["2xl"],
- );
- expect(getThemePixelSizeValuesByThemeSize(ThemeSize.ThreeXL)).toBe(
- theme.size_pixels["3xl"],
- );
- expect(getThemePixelSizeValuesByThemeSize(ThemeSize.FourXL)).toBe(
- theme.size_pixels["4xl"],
+ expect(getIconPixelSize(invalidSize)).toBe(
+ theme.icon_size.md,
);
});
- test("getThemePixelSizeValuesByThemeSize returns md pixel value as fallback for invalid size", () => {
- const invalidSize = "invalid" as ThemeSize;
- expect(getThemePixelSizeValuesByThemeSize(invalidSize)).toBe(
- theme.size_pixels.md,
- );
- });
-
- test("getThemePixelSizeValuesByThemeSize returns numeric pixel values", () => {
+ test("getIconPixelSize returns numeric pixel values", () => {
// All pixel values should be numbers
- expect(typeof getThemePixelSizeValuesByThemeSize(ThemeSize.Small)).toBe(
+ expect(typeof getIconPixelSize(ThemeSize.Small)).toBe(
"number",
);
- expect(typeof getThemePixelSizeValuesByThemeSize(ThemeSize.Medium)).toBe(
+ expect(typeof getIconPixelSize(ThemeSize.Medium)).toBe(
"number",
);
- expect(typeof getThemePixelSizeValuesByThemeSize(ThemeSize.Large)).toBe(
+ expect(typeof getIconPixelSize(ThemeSize.Large)).toBe(
"number",
);
});
-
- test("getThemeSizeValuesByThemeSize returns string size values", () => {
- // All size values should be strings (rem values)
- expect(typeof getThemeSizeValuesByThemeSize(ThemeSize.Small)).toBe(
- "string",
- );
- expect(typeof getThemeSizeValuesByThemeSize(ThemeSize.Medium)).toBe(
- "string",
- );
- expect(typeof getThemeSizeValuesByThemeSize(ThemeSize.Large)).toBe(
- "string",
- );
- });
});
diff --git a/src/components/utils.ts b/src/components/utils.ts
index 8f3fe7c..64935f4 100644
--- a/src/components/utils.ts
+++ b/src/components/utils.ts
@@ -14,47 +14,24 @@ export const enum ThemeVariant {
Success = "success",
Warning = "warning",
Error = "error",
- Info = "info",
Accent = "accent",
Primary = "primary",
- Secondary = "secondary",
}
/**
- * This enum defines the size options available for theming components.
- * The sizes map to rem values defined in the theme size settings.
+ * Size options for components: buttons, form controls, icons and spinners.
+ * Controls map to theme.control_height, icons and spinners to theme.icon_size.
*/
export const enum ThemeSize {
- ExtraSmall = "xs",
Small = "sm",
Medium = "md",
Large = "lg",
- ExtraLarge = "xl",
- TwoXL = "2xl",
- ThreeXL = "3xl",
- FourXL = "4xl",
}
-const THEME_SIZE_VALUES_BY_THEME_SIZE: Record = {
- [ThemeSize.ExtraSmall]: theme.size.xs,
- [ThemeSize.Small]: theme.size.sm,
- [ThemeSize.Medium]: theme.size.md,
- [ThemeSize.Large]: theme.size.lg,
- [ThemeSize.ExtraLarge]: theme.size.xl,
- [ThemeSize.TwoXL]: theme.size["2xl"],
- [ThemeSize.ThreeXL]: theme.size["3xl"],
- [ThemeSize.FourXL]: theme.size["4xl"],
-};
-
-const THEME_PIXEL_SIZE_VALUES_BY_THEME_SIZE: Record = {
- [ThemeSize.ExtraSmall]: theme.size_pixels.xs,
- [ThemeSize.Small]: theme.size_pixels.sm,
- [ThemeSize.Medium]: theme.size_pixels.md,
- [ThemeSize.Large]: theme.size_pixels.lg,
- [ThemeSize.ExtraLarge]: theme.size_pixels.xl,
- [ThemeSize.TwoXL]: theme.size_pixels["2xl"],
- [ThemeSize.ThreeXL]: theme.size_pixels["3xl"],
- [ThemeSize.FourXL]: theme.size_pixels["4xl"],
+const ICON_PIXEL_SIZE_BY_THEME_SIZE: Record = {
+ [ThemeSize.Small]: theme.icon_size.sm,
+ [ThemeSize.Medium]: theme.icon_size.md,
+ [ThemeSize.Large]: theme.icon_size.lg,
};
export const enum BadgeShape {
@@ -62,11 +39,6 @@ export const enum BadgeShape {
Rounded = "rounded",
}
-export const enum ButtonWidth {
- Full = "full",
- Auto = "auto",
-}
-
export const TabsContext = createContext(
undefined,
);
@@ -128,35 +100,52 @@ export const getVariantColors = (
[ThemeVariant.Success]: "success",
[ThemeVariant.Warning]: "warning",
[ThemeVariant.Error]: "error",
- [ThemeVariant.Info]: "info",
[ThemeVariant.Primary]: "info",
[ThemeVariant.Accent]: "accent",
- [ThemeVariant.Secondary]: "accent",
};
return themeObj.colors[variantMap[variant]] as VariantColors;
};
/**
- * Maps ThemeSize enum to actual size values from the theme.
+ * Maps ThemeSize to an icon size in pixels (Icon and Spinner): sm 12, md 16, lg 20.
* @param size - The ThemeSize to map.
- * @returns The corresponding size value from the theme.
+ * @returns The icon size in pixels. Medium (16px) matches the body text.
* @example
- * const sizeValue = getThemeSizeValuesByThemeSize(ThemeSize.Large);
+ * const px = getIconPixelSize(ThemeSize.Large); // 20
*/
-export const getThemeSizeValuesByThemeSize = (size: ThemeSize) => {
- return THEME_SIZE_VALUES_BY_THEME_SIZE[size] ?? theme.size.md;
+export const getIconPixelSize = (size: ThemeSize) => {
+ return ICON_PIXEL_SIZE_BY_THEME_SIZE[size] ?? theme.icon_size.md;
};
/**
- * Unfortunately, sometimes we need pixel values instead of rem values. This is to perform some calculations.
- * when for example we want the container of an icon to be as big as the icon itself plus some percentage that
- * scales with the icon size.
- * This utility function maps ThemeSize to pixel values from the theme, so that, at least, we have a single source of truth for sizes.
- * @param size - The ThemeSize to map.
- * @returns The corresponding pixel size value from the theme.
+ * Dimensions shared by buttons, inputs and selects, so that controls of the
+ * same size have the same height: sm 28px, md 34px, lg 42px.
+ * @returns height, horizontal padding, gap between icon and label, and font size.
* @example
- * const pixelSizeValue = getThemePixelSizeValuesByThemeSize(ThemeSize.Large); // returns "16px"
+ * const { height, paddingX } = getControlSize(theme, ThemeSize.Small);
*/
-export const getThemePixelSizeValuesByThemeSize = (size: ThemeSize) => {
- return THEME_PIXEL_SIZE_VALUES_BY_THEME_SIZE[size] ?? theme.size_pixels.md;
+export const getControlSize = (themeObj: Theme, size: ThemeSize) => {
+ switch (size) {
+ case ThemeSize.Small:
+ return {
+ height: themeObj.control_height.sm,
+ paddingX: themeObj.spacing.sm,
+ gap: themeObj.spacing.xs,
+ fontSize: themeObj.fonts.size.sm,
+ };
+ case ThemeSize.Large:
+ return {
+ height: themeObj.control_height.lg,
+ paddingX: themeObj.spacing.md,
+ gap: themeObj.spacing.sm,
+ fontSize: themeObj.fonts.size.base,
+ };
+ default:
+ return {
+ height: themeObj.control_height.md,
+ paddingX: themeObj.spacing.sm,
+ gap: themeObj.spacing.sm,
+ fontSize: themeObj.fonts.size.base,
+ };
+ }
};
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx
index 6032158..a52eedd 100644
--- a/src/pages/LoginPage.tsx
+++ b/src/pages/LoginPage.tsx
@@ -7,7 +7,6 @@ import logo from "@/assets/soarca-logo-cropped.svg";
import {
Button,
- ButtonWidth,
FormContainer,
FormGroup,
FormLabel,
@@ -47,14 +46,14 @@ export const LoginPage: React.FC = () => {
Log in
Log in with OIDC
@@ -67,11 +66,11 @@ export const LoginPage: React.FC = () => {
$align="center"
$justify="space-between"
>
-
+
Forgot password
-
+
Help
diff --git a/src/pages/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx
index c254474..8792845 100644
--- a/src/pages/NotFoundPage.tsx
+++ b/src/pages/NotFoundPage.tsx
@@ -4,7 +4,6 @@ import { useNavigate } from "react-router";
import soarcaBackground from "@/assets/soarca-background.jpg";
import {
Button,
- ButtonWidth,
Icon,
PageContainer,
ThemeVariant,
@@ -27,14 +26,14 @@ export const NotFoundPage: React.FC = () => {
navigate(-1)}
>
Go Back
navigate(PATHS.BASE)}
>
Take me home
@@ -64,7 +63,7 @@ const ErrorContainer = styled.div`
box-shadow: ${({ theme }) => theme.shadows.base};
/* Sizing */
- width: 28rem;
+ width: 32rem;
text-align: center;
`;
@@ -73,14 +72,12 @@ const ErrorCode = styled.h1`
font-size: 5rem;
font-weight: ${({ theme }) => theme.fonts.weight.bold};
color: ${({ theme }) => theme.colors.primary.main};
- margin: 0;
line-height: 1;
`;
const ErrorTitle = styled.h2`
/* Typography */
color: ${({ theme }) => theme.colors.text.primary};
- margin: 0;
font: ${({ theme }) => theme.typography.h2.font};
`;
@@ -88,7 +85,6 @@ const ErrorDescription = styled.p`
/* Typography & Layout */
color: ${({ theme }) => theme.colors.text.secondary};
max-width: 400px;
- margin: 0;
font: ${({ theme }) => theme.typography.body.font};
`;
diff --git a/src/pages/main-page/MainPage.styles.tsx b/src/pages/main-page/MainPage.styles.tsx
index 253ea76..113244a 100644
--- a/src/pages/main-page/MainPage.styles.tsx
+++ b/src/pages/main-page/MainPage.styles.tsx
@@ -21,7 +21,7 @@ export const MainHeader = styled.header`
background-color: ${({ theme }) => theme.colors.background.primary};
border-bottom: 1px solid ${({ theme }) => theme.colors.border.light};
padding: ${({ theme }) => theme.spacing.lg} ${({ theme }) => theme.spacing.xl};
- min-height: 2rem;
+ min-height: 4rem;
@media (max-width: 768px) {
.mobile-logo {
@@ -39,6 +39,7 @@ export const MainHeader = styled.header`
export const MainContent = styled.main`
flex: 1;
overflow-y: auto;
+ scrollbar-gutter: stable;
padding: ${({ theme }) => theme.spacing.lg};
min-width: 550px;
background-color: ${({ theme }) => theme.colors.secondary.bg};
@@ -46,11 +47,8 @@ export const MainContent = styled.main`
export const MobileMenuButton = styled.button`
display: none;
- background: none;
- border: none;
color: ${({ theme }) => theme.colors.text.primary};
- cursor: pointer;
@media (max-width: 768px) {
display: flex;
diff --git a/src/pages/main-page/MainPage.tsx b/src/pages/main-page/MainPage.tsx
index 370e716..bb3b09b 100644
--- a/src/pages/main-page/MainPage.tsx
+++ b/src/pages/main-page/MainPage.tsx
@@ -19,8 +19,6 @@ import { PATHS, SOARCA_DOC_URL } from "@/utils";
import {
Button,
- ButtonWidth,
- ControlLabel,
Icon,
ImageContainer,
Link,
@@ -91,8 +89,7 @@ export const MainPage: React.FC = () => {
handleNavigation(PATHS.PLAYBOOKS.NEW)}
>
@@ -113,19 +110,19 @@ export const MainPage: React.FC = () => {
onClick={() => handleNavigation(route.path)}
>
- {route.label}
+ {route.label}
))}
- Docs
+ Docs
@@ -144,9 +141,9 @@ export const MainPage: React.FC = () => {
onClick={sidebarOpen ? closeSidebar : openSidebar}
>
{sidebarOpen ? (
-
+
) : (
-
+
)}
diff --git a/src/pages/main-page/SettingsPage.tsx b/src/pages/main-page/SettingsPage.tsx
index 33f95cd..a1e0043 100644
--- a/src/pages/main-page/SettingsPage.tsx
+++ b/src/pages/main-page/SettingsPage.tsx
@@ -67,7 +67,7 @@ export const SettingsPage: React.FC = () => {
{isLoading ? (
-
+
) : (
diff --git a/src/pages/main-page/monitoring-page/ExecutionDetailPage.styles.tsx b/src/pages/main-page/monitoring-page/ExecutionDetailPage.styles.tsx
index 01a32a8..d7152c4 100644
--- a/src/pages/main-page/monitoring-page/ExecutionDetailPage.styles.tsx
+++ b/src/pages/main-page/monitoring-page/ExecutionDetailPage.styles.tsx
@@ -2,7 +2,6 @@ import styled from "styled-components";
export const DetailsGrid = styled.div`
width: 100%;
- box-sizing: border-box;
display: grid;
/* minmax(0, ...) stops long unbreakable values (IDs, commands) from
widening a track and pushing content out of the grid */
diff --git a/src/pages/main-page/monitoring-page/ExecutionDetailPage.tsx b/src/pages/main-page/monitoring-page/ExecutionDetailPage.tsx
index 88b80a9..068deaa 100644
--- a/src/pages/main-page/monitoring-page/ExecutionDetailPage.tsx
+++ b/src/pages/main-page/monitoring-page/ExecutionDetailPage.tsx
@@ -12,7 +12,6 @@ import {
CardHeader,
CardTitle,
CenteredCardContent,
- ControlLabel,
ExpandableText,
FormLabel,
Icon,
@@ -75,7 +74,7 @@ export const ExecutionDetailPage: React.FC = () => {
if (isLoading)
return (
-
+
);
if (isError)
@@ -96,7 +95,7 @@ export const ExecutionDetailPage: React.FC = () => {
{status}
@@ -127,16 +126,16 @@ export const ExecutionDetailPage: React.FC = () => {
No steps found for this execution.
) : (
-
+
- Timeline
+ Timeline
),
},
@@ -145,7 +144,7 @@ export const ExecutionDetailPage: React.FC = () => {
label: (
- Details
+ Details
),
},
diff --git a/src/pages/main-page/monitoring-page/MonitoringPage.tsx b/src/pages/main-page/monitoring-page/MonitoringPage.tsx
index 8a6dcac..82e209c 100644
--- a/src/pages/main-page/monitoring-page/MonitoringPage.tsx
+++ b/src/pages/main-page/monitoring-page/MonitoringPage.tsx
@@ -97,7 +97,7 @@ export const MonitoringPage: React.FC = () => {
if (isLoading)
return (
-
+
);
if (isError)
@@ -133,7 +133,7 @@ export const MonitoringPage: React.FC = () => {
>
{r.status}
{r.hasActionRequired && (
-
+
)}
diff --git a/src/pages/main-page/monitoring-page/details-tab-view/DetailsTabView.styles.tsx b/src/pages/main-page/monitoring-page/details-tab-view/DetailsTabView.styles.tsx
index 77063d0..2bea185 100644
--- a/src/pages/main-page/monitoring-page/details-tab-view/DetailsTabView.styles.tsx
+++ b/src/pages/main-page/monitoring-page/details-tab-view/DetailsTabView.styles.tsx
@@ -6,13 +6,6 @@ export const DetailedContainer = styled.div`
gap: ${({ theme }) => theme.spacing.md};
`;
-export const DetailedItemHeaderRow = styled.div`
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: ${({ theme }) => theme.spacing.md};
-`;
-
export const DetailedItemSummary = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing.md};
@@ -23,8 +16,6 @@ export const DetailedItemSummary = styled.div`
export const CodeBlock = styled.pre`
width: 100%;
- box-sizing: border-box;
- margin: 0;
background: ${({ theme }) => theme.colors.background.secondary};
border: 1px solid ${({ theme }) => theme.colors.border.light};
border-radius: ${({ theme }) => theme.radius.md};
@@ -37,7 +28,6 @@ export const CodeBlock = styled.pre`
export const VariableListContainer = styled.div`
width: 100%;
- box-sizing: border-box;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing.sm};
@@ -45,7 +35,6 @@ export const VariableListContainer = styled.div`
export const VariableItemRow = styled.div`
width: 100%;
- box-sizing: border-box;
display: flex;
gap: ${({ theme }) => theme.spacing.md};
padding: ${({ theme }) => theme.spacing.sm} ${({ theme }) => theme.spacing.md};
@@ -58,7 +47,6 @@ export const VariableItemRow = styled.div`
export const VariableCellValue = styled.div`
width: 100%;
min-width: 0;
- box-sizing: border-box;
font: ${({ theme }) => theme.typography.body.font};
color: ${({ theme }) => theme.colors.text.primary};
word-break: break-word;
diff --git a/src/pages/main-page/monitoring-page/details-tab-view/DetailsTabView.tsx b/src/pages/main-page/monitoring-page/details-tab-view/DetailsTabView.tsx
index d21c868..179842c 100644
--- a/src/pages/main-page/monitoring-page/details-tab-view/DetailsTabView.tsx
+++ b/src/pages/main-page/monitoring-page/details-tab-view/DetailsTabView.tsx
@@ -45,7 +45,7 @@ const renderAutomatedIcon = (isAutomated: boolean | undefined) => {
return (
@@ -54,7 +54,7 @@ const renderAutomatedIcon = (isAutomated: boolean | undefined) => {
return (
@@ -109,7 +109,7 @@ export const DetailsTabView: React.FC = ({ steps }) => {
$icon={getIconFromStepStatus(status)}
$round
$variant={getBadgeVariantFromStepStatus(status)}
- $size={ThemeSize.Medium}
+ $size={ThemeSize.Small}
/>
}
@@ -125,7 +125,7 @@ export const DetailsTabView: React.FC = ({ steps }) => {
{status}
@@ -190,7 +190,7 @@ export const DetailsTabView: React.FC = ({ steps }) => {
{Array.isArray(step.commands_b64) &&
step.commands_b64.length > 0 && (
decodeBase64(c) || c)
.join("\n")}
title="Copy commands"
@@ -222,7 +222,7 @@ export const DetailsTabView: React.FC = ({ steps }) => {
{step.variables &&
Object.keys(step.variables).length > 0 && (
`${v?.name || k} (${v?.type || "unknown"}): ${v?.value ?? ""}`,
diff --git a/src/pages/main-page/monitoring-page/timeline-tab-view/ManualActionModal.styles.tsx b/src/pages/main-page/monitoring-page/timeline-tab-view/ManualActionModal.styles.tsx
index d225ccf..f78a0eb 100644
--- a/src/pages/main-page/monitoring-page/timeline-tab-view/ManualActionModal.styles.tsx
+++ b/src/pages/main-page/monitoring-page/timeline-tab-view/ManualActionModal.styles.tsx
@@ -1,20 +1,6 @@
import { IconInfoCircle } from "@tabler/icons-react";
import styled from "styled-components";
-import { ModalCloseButton } from "@/components";
-
-export const StyledCloseButton = styled(ModalCloseButton)`
- color: #ffffff;
- opacity: 0.9;
- font-size: 24px;
- font-weight: bold;
- transition: opacity 0.2s;
-
- &:hover {
- opacity: 1;
- }
-`;
-
export const StepSummary = styled.div`
margin-bottom: ${({ theme }) => theme.spacing.lg};
`;
@@ -28,7 +14,6 @@ export const StepName = styled.h3`
export const StepDescription = styled.p`
font: ${({ theme }) => theme.typography.body.font};
color: ${({ theme }) => theme.colors.text.secondary};
- margin: 0;
line-height: 1.5;
`;
@@ -37,9 +22,8 @@ export const Section = styled.div`
`;
export const SectionTitle = styled.h4`
- font: ${({ theme }) => theme.typography.bodyMedium.font};
+ font: ${({ theme }) => theme.typography.h4.font};
color: ${({ theme }) => theme.colors.text.primary};
- font-weight: 600;
margin: 0 0 ${({ theme }) => theme.spacing.md} 0;
`;
diff --git a/src/pages/main-page/monitoring-page/timeline-tab-view/ManualActionModal.tsx b/src/pages/main-page/monitoring-page/timeline-tab-view/ManualActionModal.tsx
index 8563c3a..5ea3135 100644
--- a/src/pages/main-page/monitoring-page/timeline-tab-view/ManualActionModal.tsx
+++ b/src/pages/main-page/monitoring-page/timeline-tab-view/ManualActionModal.tsx
@@ -134,7 +134,7 @@ export const ManualActionModal: React.FC = ({
aria-label="Manual action required"
>
- Manual action required
+ Manual action required
diff --git a/src/pages/main-page/monitoring-page/timeline-tab-view/TimelineTabView.styles.tsx b/src/pages/main-page/monitoring-page/timeline-tab-view/TimelineTabView.styles.tsx
index 42fc25b..8f7f92c 100644
--- a/src/pages/main-page/monitoring-page/timeline-tab-view/TimelineTabView.styles.tsx
+++ b/src/pages/main-page/monitoring-page/timeline-tab-view/TimelineTabView.styles.tsx
@@ -6,7 +6,6 @@ export const TimelineContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing.lg};
- padding: 0;
`;
export const TimelineRow = styled.div<{ $delay?: number }>`
@@ -37,26 +36,6 @@ export const TimelineIconContainer = styled.div`
z-index: 2;
`;
-export const SpinnerIconContainer = styled.div<{
- $variant: "primary" | "warning";
-}>`
- display: inline-flex;
- align-items: center;
- justify-content: center;
- height: 24px;
- width: 24px;
- border-radius: 50%;
- background-color: ${({ theme, $variant }) =>
- $variant === "warning"
- ? theme.colors.warning.solidBg
- : theme.colors.primary.main};
- border: 1px solid
- ${({ theme, $variant }) =>
- $variant === "warning"
- ? theme.colors.warning.border
- : theme.colors.primary.main};
-`;
-
export const TimelineLeftCell = styled.div`
display: flex;
justify-content: flex-end;
@@ -80,18 +59,12 @@ export const TimelineItemWrapper = styled.div<{ $side: "left" | "right" }>`
export const TimelineItem = styled.div`
position: relative;
width: fit-content;
- padding: 0;
- border: none;
- background: transparent;
box-shadow: none;
`;
export const UserReplyItem = styled.div`
position: relative;
width: fit-content;
- padding: 0;
- border: none;
- background: transparent;
box-shadow: none;
`;
@@ -115,25 +88,17 @@ export const TimelineHeader = styled.div`
align-items: center;
justify-content: space-between;
gap: ${({ theme }) => theme.spacing.md};
- padding: 0;
- border: none;
- background: transparent;
`;
export const TimelineTitle = styled.h3`
- margin: 0;
- font: ${({ theme }) => theme.typography.body.font};
+ font: ${({ theme }) => theme.typography.h3.font};
color: ${({ theme }) => theme.colors.text.primary};
- font-weight: 500;
`;
export const TimelineMetaBadge = styled.div`
display: inline-flex;
align-items: center;
gap: ${({ theme }) => theme.spacing.sm};
- padding: 0;
- background: transparent;
- border: none;
border-radius: 0;
font: ${({ theme }) => theme.typography.small.font};
color: ${({ theme }) => theme.colors.text.secondary};
diff --git a/src/pages/main-page/monitoring-page/timeline-tab-view/TimelineTabView.tsx b/src/pages/main-page/monitoring-page/timeline-tab-view/TimelineTabView.tsx
index 309aa9f..01861b4 100644
--- a/src/pages/main-page/monitoring-page/timeline-tab-view/TimelineTabView.tsx
+++ b/src/pages/main-page/monitoring-page/timeline-tab-view/TimelineTabView.tsx
@@ -131,7 +131,7 @@ export const TimelineTabView: React.FC = ({
{status === StepExecutionStatus.Running ? (
) : status === StepExecutionStatus.Executed ? (
@@ -157,7 +157,7 @@ export const TimelineTabView: React.FC = ({
@@ -290,7 +290,7 @@ const UserPendingActionCard: React.FC = ({ onClick }) => {
return (
-
+
User action required
diff --git a/src/pages/main-page/monitoring-page/timeline-tab-view/VariableInputs.tsx b/src/pages/main-page/monitoring-page/timeline-tab-view/VariableInputs.tsx
index b5ec3e9..54eb915 100644
--- a/src/pages/main-page/monitoring-page/timeline-tab-view/VariableInputs.tsx
+++ b/src/pages/main-page/monitoring-page/timeline-tab-view/VariableInputs.tsx
@@ -35,8 +35,8 @@ const DictionaryInput: React.FC<{
diff --git a/src/pages/main-page/playbooks-page/PlaybookCreatePage.styles.tsx b/src/pages/main-page/playbooks-page/PlaybookCreatePage.styles.tsx
index 9afec1f..ebcba65 100644
--- a/src/pages/main-page/playbooks-page/PlaybookCreatePage.styles.tsx
+++ b/src/pages/main-page/playbooks-page/PlaybookCreatePage.styles.tsx
@@ -21,7 +21,6 @@ export const MethodButton = styled.button<{ $active: boolean }>`
$active ? theme.colors.info.bg : theme.colors.background.primary};
color: ${({ theme }) => theme.colors.text.primary};
font: ${({ theme }) => theme.typography.bodyMedium.font};
- cursor: pointer;
transition: all ${({ theme }) => theme.transitions.base};
&:hover {
@@ -34,54 +33,3 @@ export const MethodButton = styled.button<{ $active: boolean }>`
cursor: not-allowed;
}
`;
-
-export const DropZone = styled.div<{
- $isDragActive: boolean;
- $hasError: boolean;
-}>`
- border: 2px dashed
- ${({ theme, $isDragActive, $hasError }) =>
- $hasError
- ? theme.colors.error.border
- : $isDragActive
- ? theme.colors.info.border
- : theme.colors.border.medium};
- border-radius: ${({ theme }) => theme.radius.md};
- padding: ${({ theme }) => theme.spacing["3xl"]};
- text-align: center;
- background-color: ${({ theme, $isDragActive }) =>
- $isDragActive ? theme.colors.info.bg : theme.colors.background.secondary};
- cursor: pointer;
- transition: all ${({ theme }) => theme.transitions.base};
- min-height: 300px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: ${({ theme }) => theme.spacing.md};
-
- &:hover {
- border-color: ${({ theme }) => theme.colors.info.border};
- background-color: ${({ theme }) => theme.colors.info.bg};
- }
-`;
-
-export const DropZoneText = styled.p`
- margin: 0;
- font: ${({ theme }) => theme.typography.body.font};
- color: ${({ theme }) => theme.colors.text.secondary};
-`;
-
-export const DropZoneHint = styled.span`
- font: ${({ theme }) => theme.typography.small.font};
- color: ${({ theme }) => theme.colors.text.tertiary};
-`;
-
-export const FileName = styled.div`
- font: ${({ theme }) => theme.typography.bodyMedium.font};
- color: ${({ theme }) => theme.colors.text.primary};
- margin-top: ${({ theme }) => theme.spacing.md};
- display: flex;
- align-items: center;
- gap: ${({ theme }) => theme.spacing.sm};
-`;
diff --git a/src/pages/main-page/playbooks-page/PlaybookCreatePage.tsx b/src/pages/main-page/playbooks-page/PlaybookCreatePage.tsx
index c350d90..8ec3d59 100644
--- a/src/pages/main-page/playbooks-page/PlaybookCreatePage.tsx
+++ b/src/pages/main-page/playbooks-page/PlaybookCreatePage.tsx
@@ -120,8 +120,8 @@ export const PlaybookCreatePage: React.FC = () => {
setError("");
}}
$accept=".json"
- $disabled={createMutation.isPending}
- $error={error}
+ disabled={createMutation.isPending}
+ $hasError={!!error}
/>
>
) : (
@@ -136,8 +136,8 @@ export const PlaybookCreatePage: React.FC = () => {
setError(validation.error || "Invalid JSON");
}
}}
- $placeholder="Paste or write your CACAO playbook JSON here..."
- $disabled={createMutation.isPending}
+ placeholder="Paste or write your CACAO playbook JSON here..."
+ disabled={createMutation.isPending}
$hasError={!!error}
$minHeight="400px"
/>
diff --git a/src/pages/main-page/playbooks-page/PlaybookEditPage.tsx b/src/pages/main-page/playbooks-page/PlaybookEditPage.tsx
index 3bf9a57..3671132 100644
--- a/src/pages/main-page/playbooks-page/PlaybookEditPage.tsx
+++ b/src/pages/main-page/playbooks-page/PlaybookEditPage.tsx
@@ -102,7 +102,7 @@ export const PlaybookEditPage: React.FC = () => {
{isLoading ? (
-
+
) : isError || !playbook ? (
@@ -128,8 +128,8 @@ export const PlaybookEditPage: React.FC = () => {
setError(validation.error || "Invalid JSON");
}
}}
- $placeholder="Edit your CACAO playbook JSON here..."
- $disabled={updateMutation.isPending}
+ placeholder="Edit your CACAO playbook JSON here..."
+ disabled={updateMutation.isPending}
$hasError={!!error}
$minHeight="500px"
/>
diff --git a/src/pages/main-page/playbooks-page/PlaybooksPage.styles.tsx b/src/pages/main-page/playbooks-page/PlaybooksPage.styles.tsx
index 8d2f5f1..7486651 100644
--- a/src/pages/main-page/playbooks-page/PlaybooksPage.styles.tsx
+++ b/src/pages/main-page/playbooks-page/PlaybooksPage.styles.tsx
@@ -1,6 +1,8 @@
import styled from "styled-components";
export const PlaybookListItem = styled.div`
+ position: relative;
+
display: flex;
align-items: center;
gap: ${({ theme }) => theme.spacing["2xl"]};
@@ -8,20 +10,30 @@ export const PlaybookListItem = styled.div`
padding-left: ${({ theme }) => theme.spacing.lg};
padding-right: ${({ theme }) => theme.spacing.lg};
- border: 2px solid ${({ theme }) => theme.colors.palette.primary};
- border-radius: ${({ theme }) => theme.radius.lg};
+ border: 1px solid ${({ theme }) => theme.colors.border.medium};
+ border-radius: ${({ theme }) => `0 ${theme.radius.sm} ${theme.radius.sm} 0`};
background-color: ${({ theme }) => theme.colors.background.primary};
+ &::before {
+ content: "";
+ position: absolute;
+ top: -1px;
+ bottom: -1px;
+ left: -1px;
+
+ width: 4px;
+
+ background-color: ${({ theme }) => theme.colors.palette.primary};
+ }
+
cursor: pointer;
transition:
background-color ${({ theme }) => theme.transitions.base},
- border-color ${({ theme }) => theme.transitions.base},
- box-shadow ${({ theme }) => theme.transitions.base};
+ border-color ${({ theme }) => theme.transitions.base};
&:hover {
border-color: ${({ theme }) => theme.colors.info.border};
background-color: ${({ theme }) => theme.colors.info.bg};
- box-shadow: ${({ theme }) => theme.shadows.md};
}
`;
@@ -34,9 +46,9 @@ export const PlaybookInfo = styled.div`
margin-right: ${({ theme }) => theme.spacing.lg};
`;
-export const PlaybookName = styled.h4`
+export const PlaybookName = styled.h3`
color: ${({ theme }) => theme.colors.text.primary};
- font: ${({ theme }) => theme.typography.h4.font};
+ font: ${({ theme }) => theme.typography.h3.font};
`;
export const PlaybookDescription = styled.p`
@@ -94,8 +106,8 @@ export const TimelineStep = styled.div<{ $position: "top" | "bottom" }>`
`;
export const TimelineDot = styled.div<{ $type: string }>`
- width: 12px;
- height: 12px;
+ width: 16px;
+ height: 16px;
border: 2px solid ${({ theme }) => theme.colors.background.primary};
border-radius: ${({ theme }) => theme.radius.full};
@@ -144,6 +156,15 @@ export const PlaybookActions = styled.div`
padding-left: ${({ theme }) => theme.spacing.md};
`;
+export const HeaderActions = styled.div`
+ display: flex;
+ align-items: center;
+ gap: ${({ theme }) => theme.spacing.md};
+
+ width: 100%;
+ max-width: 400px;
+`;
+
export const PlaybookList = styled.div`
display: flex;
flex-direction: column;
diff --git a/src/pages/main-page/playbooks-page/PlaybooksPage.tsx b/src/pages/main-page/playbooks-page/PlaybooksPage.tsx
index 447f549..45d9f16 100644
--- a/src/pages/main-page/playbooks-page/PlaybooksPage.tsx
+++ b/src/pages/main-page/playbooks-page/PlaybooksPage.tsx
@@ -1,6 +1,6 @@
import { ThemeSize, ThemeVariant } from "@/components";
-import { useQuery } from "@tanstack/react-query";
import { IconFilePlus } from "@tabler/icons-react";
+import { useQuery } from "@tanstack/react-query";
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router";
@@ -14,13 +14,16 @@ import {
CardTitle,
CenteredCardContent,
Icon,
+ Input,
Spinner,
Text,
} from "@/components";
import { Playbook, Step } from "@/types";
import { PATHS, sortByString } from "@/utils";
+import { PlaybookActionsMenu } from "./PlaybookActionsMenu";
import {
+ HeaderActions,
PlaybookActions,
PlaybookDescription,
PlaybookInfo,
@@ -34,8 +37,7 @@ import {
TimelineStep,
TimelineSteps,
} from "./PlaybooksPage.styles";
-import { getOrderedSteps } from "./utils";
-import { PlaybookActionsMenu } from "./PlaybookActionsMenu";
+import { filterPlaybooks, getOrderedSteps } from "./utils";
interface PlaybookTimelineProps {
workflow: Record;
@@ -135,6 +137,7 @@ const PlaybookItem: React.FC = ({ playbook, onClick }) => {
export const PlaybooksPage: React.FC = () => {
const navigate = useNavigate();
+ const [search, setSearch] = useState("");
const { data, isLoading, isError, error } = useQuery({
queryKey: ["playbooks"],
queryFn: getPlaybooks,
@@ -142,7 +145,7 @@ export const PlaybooksPage: React.FC = () => {
});
const sortedPlaybooks = sortByString(
- [...(data || [])],
+ [...filterPlaybooks(data || [], search)],
(p) => p.name || "",
true,
false,
@@ -158,13 +161,19 @@ export const PlaybooksPage: React.FC = () => {
if (isLoading)
return (
-
+
);
if (isError)
return {parsedError?.message || "Could not load playbooks"};
if (sortedPlaybooks.length === 0)
- return No playbooks available;
+ return (
+
+ {search.trim()
+ ? `No playbooks match "${search.trim()}"`
+ : "No playbooks available"}
+
+ );
return (
{sortedPlaybooks.map((playbook) => (
@@ -182,14 +191,24 @@ export const PlaybooksPage: React.FC = () => {
Playbooks
- navigate(PATHS.PLAYBOOKS.NEW)}
- >
-
- New
-
+
+ setSearch(e.target.value)}
+ />
+ navigate(PATHS.PLAYBOOKS.NEW)}
+ >
+
+ New
+
+
{renderBody()}
diff --git a/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookDetailCards.tsx b/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookDetailCards.tsx
index c97043d..ad0e935 100644
--- a/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookDetailCards.tsx
+++ b/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookDetailCards.tsx
@@ -51,7 +51,7 @@ export const PlaybookMainDetails: React.FC = ({
Type
{playbook_types?.map((type, idx) => (
-
+
{type}
)) || "N/A"}
@@ -121,7 +121,7 @@ export const PlaybookAdditionalDetails: React.FC<
Impact
- {impact}
+ {impact}
)}
@@ -144,7 +144,7 @@ export const PlaybookAdditionalDetails: React.FC<
{labels.map((label, idx) => (
{label}
diff --git a/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookDetailPage.tsx b/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookDetailPage.tsx
index ec2ba6a..c6f8150 100644
--- a/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookDetailPage.tsx
+++ b/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookDetailPage.tsx
@@ -15,7 +15,6 @@ import { triggerPlaybookById } from "@/api/trigger";
import { formatErrorForToast, getErrorFromApiResponse } from "@/api/utils";
import {
Button,
- ButtonWidth,
CardBody,
CardContainer,
CardHeader,
@@ -127,7 +126,7 @@ export const PlaybookDetailPage: React.FC = () => {
-
+
@@ -174,8 +173,7 @@ export const PlaybookDetailPage: React.FC = () => {
navigate(PATHS.PLAYBOOKS.EDIT.replace(":playbookId", playbookId!))
@@ -186,8 +184,7 @@ export const PlaybookDetailPage: React.FC = () => {
setOpenDeleteConfirm(true)}
disabled={deleteMutation.isPending}
@@ -197,8 +194,7 @@ export const PlaybookDetailPage: React.FC = () => {
hasExternalVars ? setOpenRunModal(true) : setOpenRunConfirm(true)
@@ -211,10 +207,10 @@ export const PlaybookDetailPage: React.FC = () => {
toast.success("Playbook exported")}
$onError={(err) =>
diff --git a/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookTimeline.styles.tsx b/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookTimeline.styles.tsx
index 5bf7ca2..8cfba9e 100644
--- a/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookTimeline.styles.tsx
+++ b/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookTimeline.styles.tsx
@@ -52,8 +52,8 @@ export const TimelineDotCenter = styled.div<{ $type: string }>`
left: 50%;
top: 24px;
transform: translate(-50%, -50%);
- width: 16px;
- height: 16px;
+ width: 22px;
+ height: 22px;
border-radius: ${({ theme }) => theme.radius.full};
background-color: ${({ theme, $type }) => {
switch ($type) {
@@ -104,9 +104,8 @@ export const StepCardHeader = styled.div<{ $expanded: boolean }>`
}
`;
-export const StepCardTitle = styled.h5`
- margin: 0;
- font: ${({ theme }) => theme.typography.bodyMedium.font};
+export const StepCardTitle = styled.h3`
+ font: ${({ theme }) => theme.typography.h3.font};
color: ${({ theme }) => theme.colors.text.primary};
`;
diff --git a/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookTimeline.tsx b/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookTimeline.tsx
index 0338313..c7efb27 100644
--- a/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookTimeline.tsx
+++ b/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookTimeline.tsx
@@ -52,7 +52,7 @@ export const PlaybookTimeline: React.FC = ({
{isExpanded && (
@@ -69,7 +69,7 @@ export const PlaybookTimeline: React.FC = ({
Type
-
+
{step.type}
diff --git a/src/pages/main-page/playbooks-page/playbook-detail-page/RunPlaybookModal.tsx b/src/pages/main-page/playbooks-page/playbook-detail-page/RunPlaybookModal.tsx
index ad82832..ca98c73 100644
--- a/src/pages/main-page/playbooks-page/playbook-detail-page/RunPlaybookModal.tsx
+++ b/src/pages/main-page/playbooks-page/playbook-detail-page/RunPlaybookModal.tsx
@@ -78,7 +78,7 @@ export const RunPlaybookModal: React.FC = ({
aria-label="Run playbook"
>
- Run playbook
+ Run playbook
!mutation.isPending && onClose()}
disabled={mutation.isPending}
diff --git a/src/pages/main-page/playbooks-page/shared.styles.tsx b/src/pages/main-page/playbooks-page/shared.styles.tsx
index 0373380..17f1866 100644
--- a/src/pages/main-page/playbooks-page/shared.styles.tsx
+++ b/src/pages/main-page/playbooks-page/shared.styles.tsx
@@ -21,7 +21,6 @@ export const EditorContainer = styled.div`
export const EditorTextarea = styled.textarea<{ $hasError: boolean }>`
width: 100%;
- box-sizing: border-box;
min-height: 500px;
padding: ${({ theme }) => theme.spacing.md};
border-radius: ${({ theme }) => theme.radius.md};
@@ -30,7 +29,7 @@ export const EditorTextarea = styled.textarea<{ $hasError: boolean }>`
$hasError ? theme.colors.error.border : theme.colors.border.medium};
background-color: ${({ theme }) => theme.colors.background.secondary};
color: ${({ theme }) => theme.colors.text.primary};
- font: ${({ theme }) => theme.typography.code_medium.font};
+ font: ${({ theme }) => theme.typography.code.font};
resize: vertical;
transition: all ${({ theme }) => theme.transitions.base};
diff --git a/src/pages/main-page/playbooks-page/utils.ts b/src/pages/main-page/playbooks-page/utils.ts
index 5af8a5a..d145401 100644
--- a/src/pages/main-page/playbooks-page/utils.ts
+++ b/src/pages/main-page/playbooks-page/utils.ts
@@ -101,6 +101,22 @@ export const getOrderedSteps = (
return steps;
};
+/**
+ * Case-insensitive search on name, id and playbook types.
+ */
+export const filterPlaybooks = (
+ playbooks: Playbook[],
+ query: string,
+): Playbook[] => {
+ const q = query.trim().toLowerCase();
+ if (!q) return playbooks;
+ return playbooks.filter((p) =>
+ [p.name, p.id, ...(p.playbook_types ?? [])].some((field) =>
+ field?.toLowerCase().includes(q),
+ ),
+ );
+};
+
/**
* Generates a safe filename for exporting a playbook.
* Format: {id}-{modified-date}.json
diff --git a/src/theme/GlobalStyles.ts b/src/theme/GlobalStyles.ts
index 9710227..948c4a7 100644
--- a/src/theme/GlobalStyles.ts
+++ b/src/theme/GlobalStyles.ts
@@ -2,10 +2,41 @@ import { createGlobalStyle } from "styled-components";
export const GlobalStyles = createGlobalStyle`
*, *::before, *::after {
+ box-sizing: border-box;
margin: 0;
padding: 0;
}
+ input, button, textarea, select {
+ font: inherit;
+ color: inherit;
+ }
+
+ button {
+ background: none;
+ border: none;
+ cursor: pointer;
+ }
+
+ /* Headings are chosen for document structure; their look comes from theme.typography. */
+ h1, h2, h3, h4, h5, h6 {
+ font-size: inherit;
+ font-weight: inherit;
+ }
+
+ p, h1, h2, h3, h4, h5, h6 {
+ overflow-wrap: break-word;
+ }
+
+ p {
+ text-wrap: pretty;
+ }
+
+ img {
+ display: block;
+ max-width: 100%;
+ }
+
html, body, #root {
height: 100%;
width: 100%;
diff --git a/src/theme/theme.ts b/src/theme/theme.ts
index 0616af3..6d0d9b4 100644
--- a/src/theme/theme.ts
+++ b/src/theme/theme.ts
@@ -9,10 +9,6 @@ const fonts = {
sm: "0.875rem", // 14px
base: "1rem", // 16px
lg: "1.125rem", // 18px
- xl: "1.25rem", // 20px
- "2xl": "1.5rem", // 24px
- "3xl": "1.875rem", // 30px
- "4xl": "2.25rem", // 36px
},
weight: {
normal: 400,
@@ -69,9 +65,9 @@ export const theme = {
},
warning: {
bg: "#fff7a3",
- border: "#facc15",
+ border: "#eab308",
text: "#ca8a04",
- solidBg: "#facc15",
+ solidBg: "#eab308",
solidText: "#ffffff",
},
accent: {
@@ -119,7 +115,7 @@ export const theme = {
border: "#e5e7eb",
},
palette: {
- primary: "#2d8cf0",
+ primary: "#3b82f6",
primaryDark: "#1f5fad",
secondary: "#27b3c7",
tertiary: "#8bd5ff",
@@ -139,25 +135,16 @@ export const theme = {
"2xl": "1.5rem", // 24px
"3xl": "2rem", // 32px
},
- size: {
- xs: "0.25rem", // 4px
- sm: "0.5rem", // 8px
- md: "0.75rem", // 12px
- lg: "1rem", // 16px
- xl: "1.25rem", // 20px
- "2xl": "1.5rem", // 24px
- "3xl": "2rem", // 32px
- "4xl": "3rem", // 48px
+ // Height of buttons, inputs and selects, so controls of the same size line up.
+ control_height: {
+ sm: "1.75rem", // 28px
+ md: "2.125rem", // 34px
+ lg: "2.625rem", // 42px
},
- size_pixels: {
- xs: 4,
- sm: 8,
- md: 12,
- lg: 16,
- xl: 20,
- "2xl": 24,
- "3xl": 32,
- "4xl": 48,
+ icon_size: {
+ sm: 12,
+ md: 16,
+ lg: 20,
},
radius: {
sm: "0.25rem",
@@ -197,17 +184,14 @@ export const theme = {
info: 3000,
},
typography: {
- h1: {
- font: `${fonts.weight.bold} ${fonts.size["4xl"]}/${fonts.lineHeight.tight} ${fonts.family.primary}`,
- },
h2: {
- font: `${fonts.weight.semibold} ${fonts.size["2xl"]}/${fonts.lineHeight.tight} ${fonts.family.primary}`,
+ font: `${fonts.weight.semibold} ${fonts.size.lg}/${fonts.lineHeight.tight} ${fonts.family.primary}`,
},
h3: {
- font: `${fonts.weight.semibold} ${fonts.size.xl}/${fonts.lineHeight.tight} ${fonts.family.primary}`,
+ font: `${fonts.weight.semibold} ${fonts.size.base}/${fonts.lineHeight.tight} ${fonts.family.primary}`,
},
h4: {
- font: `${fonts.weight.semibold} ${fonts.size.lg}/${fonts.lineHeight.tight} ${fonts.family.primary}`,
+ font: `${fonts.weight.semibold} ${fonts.size.sm}/${fonts.lineHeight.tight} ${fonts.family.primary}`,
},
body: {
font: `${fonts.weight.normal} ${fonts.size.base}/${fonts.lineHeight.normal} ${fonts.family.primary}`,
@@ -221,12 +205,9 @@ export const theme = {
small: {
font: `${fonts.weight.normal} ${fonts.size.xs}/${fonts.lineHeight.tight} ${fonts.family.primary}`,
},
- code_small: {
+ code: {
font: `${fonts.weight.normal} ${fonts.size.sm}/${fonts.lineHeight.relaxed} ${fonts.family.mono}`,
},
- code_medium: {
- font: `${fonts.weight.normal} ${fonts.size.base}/${fonts.lineHeight.relaxed} ${fonts.family.mono}`,
- },
},
interactions: {
subtleMix: "12%",
@@ -278,10 +259,10 @@ export const darkTheme: Theme = {
},
warning: {
bg: "#2e2a1a",
- border: "#facc15",
+ border: "#eab308",
text: "#fde68a",
- solidBg: "#facc15",
- solidText: "#1f2937",
+ solidBg: "#eab308",
+ solidText: "#ffffff",
},
accent: {
bg: "#2a1f3d",