mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Create primitive for IconPicker
This commit is contained in:
+6
-5
@@ -1,11 +1,11 @@
|
||||
.portal-icon-picker__grid {
|
||||
.sui-icon-picker__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 2.25rem);
|
||||
gap: 0.25rem;
|
||||
padding: 0.375rem;
|
||||
}
|
||||
|
||||
.portal-icon-picker__option {
|
||||
.sui-icon-picker__option {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -18,13 +18,14 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.portal-icon-picker__option:hover {
|
||||
.sui-icon-picker__option:hover {
|
||||
background: var(--c-hover);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.portal-icon-picker__option--selected {
|
||||
.sui-icon-picker__option--selected {
|
||||
border-color: var(--c-primary);
|
||||
background: var(--c-primary-subtle);
|
||||
color: var(--c-accent-fg, var(--c-primary));
|
||||
/* Accent tuned to clear the contrast floor on the tinted chip (light + dark). */
|
||||
color: var(--c-accent-text);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined";
|
||||
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
|
||||
import LabelOutlinedIcon from "@mui/icons-material/LabelOutlined";
|
||||
import LayersOutlinedIcon from "@mui/icons-material/LayersOutlined";
|
||||
import FolderOutlinedIcon from "@mui/icons-material/FolderOutlined";
|
||||
import BoltOutlinedIcon from "@mui/icons-material/BoltOutlined";
|
||||
import ScheduleOutlinedIcon from "@mui/icons-material/ScheduleOutlined";
|
||||
import AutoAwesomeOutlinedIcon from "@mui/icons-material/AutoAwesomeOutlined";
|
||||
import { IconPicker, type IconPickerOption } from "@app/ui/IconPicker";
|
||||
|
||||
const sx = { fontSize: "1.25rem" } as const;
|
||||
const OPTIONS: IconPickerOption[] = [
|
||||
{ key: "shield", label: "Shield", node: <ShieldOutlinedIcon sx={sx} /> },
|
||||
{ key: "lock", label: "Lock", node: <LockOutlinedIcon sx={sx} /> },
|
||||
{ key: "label", label: "Label", node: <LabelOutlinedIcon sx={sx} /> },
|
||||
{ key: "layers", label: "Layers", node: <LayersOutlinedIcon sx={sx} /> },
|
||||
{ key: "folder", label: "Folder", node: <FolderOutlinedIcon sx={sx} /> },
|
||||
{ key: "bolt", label: "Bolt", node: <BoltOutlinedIcon sx={sx} /> },
|
||||
{
|
||||
key: "schedule",
|
||||
label: "Schedule",
|
||||
node: <ScheduleOutlinedIcon sx={sx} />,
|
||||
},
|
||||
{
|
||||
key: "sparkle",
|
||||
label: "Sparkle",
|
||||
node: <AutoAwesomeOutlinedIcon sx={sx} />,
|
||||
},
|
||||
];
|
||||
|
||||
const meta: Meta<typeof IconPicker> = {
|
||||
title: "Primitives/IconPicker",
|
||||
component: IconPicker,
|
||||
tags: ["autodocs"],
|
||||
parameters: { layout: "centered" },
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof IconPicker>;
|
||||
|
||||
/** Click the glyph to open the grid and choose a new icon from the supplied set. */
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
const [value, setValue] = useState("shield");
|
||||
return (
|
||||
<IconPicker
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
options={OPTIONS}
|
||||
ariaLabel="Icon"
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { ActionIcon } from "@app/ui/ActionIcon";
|
||||
import { Dropdown } from "@app/ui/Dropdown";
|
||||
import "@app/ui/IconPicker.css";
|
||||
|
||||
export interface IconPickerOption {
|
||||
/** Stable identifier stored as the picked value. */
|
||||
key: string;
|
||||
/** The glyph to show, sized by the caller. */
|
||||
node: ReactNode;
|
||||
/** Accessible name for this option (falls back to the key). */
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface IconPickerProps {
|
||||
/** The picked option's key. */
|
||||
value: string;
|
||||
onChange: (key: string) => void;
|
||||
/** The icons to choose from, in display order. The caller supplies the set. */
|
||||
options: IconPickerOption[];
|
||||
/** Accessible name for the trigger (e.g. "Icon"). */
|
||||
ariaLabel: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick an icon from a caller-supplied set. The chosen glyph is the trigger; the menu is a grid. The
|
||||
* icon set is injected (via {@link options}) rather than baked in, so any surface - a pipeline, a
|
||||
* watched folder, an automation - passes its own vocabulary and shares the one control.
|
||||
*/
|
||||
export function IconPicker({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
ariaLabel,
|
||||
size = "sm",
|
||||
}: IconPickerProps) {
|
||||
// Controlled so a grid button (not a Dropdown.Item) can close the menu on pick.
|
||||
const [open, setOpen] = useState(false);
|
||||
const selected = options.find((option) => option.key === value) ?? options[0];
|
||||
|
||||
function pick(key: string) {
|
||||
onChange(key);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown.Root open={open} onOpenChange={setOpen} align="start">
|
||||
<Dropdown.Trigger>
|
||||
<ActionIcon variant="secondary" size={size} aria-label={ariaLabel}>
|
||||
{selected?.node}
|
||||
</ActionIcon>
|
||||
</Dropdown.Trigger>
|
||||
<Dropdown.Menu>
|
||||
<div className="sui-icon-picker__grid">
|
||||
{options.map((option) => {
|
||||
const isSelected = option.key === value;
|
||||
return (
|
||||
<button
|
||||
key={option.key}
|
||||
type="button"
|
||||
className={
|
||||
"sui-icon-picker__option" +
|
||||
(isSelected ? " sui-icon-picker__option--selected" : "")
|
||||
}
|
||||
aria-label={option.label ?? option.key}
|
||||
aria-pressed={isSelected}
|
||||
onClick={() => pick(option.key)}
|
||||
>
|
||||
{option.node}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown.Root>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export * from "@app/ui/ProgressBar";
|
||||
export * from "@app/ui/MetricCard";
|
||||
export * from "@app/ui/NodeCard";
|
||||
export * from "@app/ui/OptionCard";
|
||||
export * from "@app/ui/IconPicker";
|
||||
export * from "@app/ui/NavItem";
|
||||
export * from "@app/ui/NavSurface";
|
||||
export * from "@app/ui/Surface";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import { ActionIcon, Button, Input } from "@app/ui";
|
||||
import { ActionIcon, Button, IconPicker, Input } from "@app/ui";
|
||||
import { PipelineBlockerTooltip } from "@portal/components/pipelines/PipelineBlockerTooltip";
|
||||
import { PipelineIconPicker } from "@portal/components/pipelines/PipelineIconPicker";
|
||||
import { PIPELINE_ICON_OPTIONS } from "@portal/components/pipelines/pipelineIcon";
|
||||
import { PipelinePolicyControls } from "@portal/components/pipelines/PipelinePolicyControls";
|
||||
import "@portal/components/pipelines/PipelineCreateHeader.css";
|
||||
|
||||
@@ -63,7 +63,12 @@ export function PipelineCreateHeader({
|
||||
<ArrowBackRoundedIcon style={{ fontSize: "1.25rem" }} />
|
||||
</ActionIcon>
|
||||
|
||||
<PipelineIconPicker value={icon} onChange={onIconChange} />
|
||||
<IconPicker
|
||||
value={icon}
|
||||
onChange={onIconChange}
|
||||
options={PIPELINE_ICON_OPTIONS}
|
||||
ariaLabel={t("portal.pipelines.builder.icon.label")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="portal-pipeline-create-header__name"
|
||||
|
||||
@@ -8,9 +8,9 @@ import PowerSettingsNewRoundedIcon from "@mui/icons-material/PowerSettingsNewRou
|
||||
import ReplayRoundedIcon from "@mui/icons-material/ReplayRounded";
|
||||
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
|
||||
import MoreHorizRoundedIcon from "@mui/icons-material/MoreHorizRounded";
|
||||
import { ActionIcon, Button, Dropdown, Input } from "@app/ui";
|
||||
import { ActionIcon, Button, Dropdown, IconPicker, Input } from "@app/ui";
|
||||
import { PipelineBlockerTooltip } from "@portal/components/pipelines/PipelineBlockerTooltip";
|
||||
import { PipelineIconPicker } from "@portal/components/pipelines/PipelineIconPicker";
|
||||
import { PIPELINE_ICON_OPTIONS } from "@portal/components/pipelines/pipelineIcon";
|
||||
import { PipelinePolicyControls } from "@portal/components/pipelines/PipelinePolicyControls";
|
||||
import "@portal/components/pipelines/PipelineEditHeader.css";
|
||||
|
||||
@@ -130,7 +130,12 @@ export function PipelineEditHeader({
|
||||
<ArrowBackRoundedIcon style={{ fontSize: "1.25rem" }} />
|
||||
</ActionIcon>
|
||||
|
||||
<PipelineIconPicker value={icon} onChange={onIconChange} />
|
||||
<IconPicker
|
||||
value={icon}
|
||||
onChange={onIconChange}
|
||||
options={PIPELINE_ICON_OPTIONS}
|
||||
ariaLabel={t("portal.pipelines.builder.icon.label")}
|
||||
/>
|
||||
|
||||
{renaming ? (
|
||||
<Input
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { PipelineIconPicker } from "@portal/components/pipelines/PipelineIconPicker";
|
||||
|
||||
const meta: Meta<typeof PipelineIconPicker> = {
|
||||
title: "Portal/Pipelines/PipelineIconPicker",
|
||||
component: PipelineIconPicker,
|
||||
parameters: { layout: "centered" },
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof PipelineIconPicker>;
|
||||
|
||||
/** Live picker: click the glyph to open the grid and choose a new icon. */
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
const [icon, setIcon] = useState("route");
|
||||
return <PipelineIconPicker value={icon} onChange={setIcon} />;
|
||||
},
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ActionIcon, Dropdown } from "@app/ui";
|
||||
import {
|
||||
PIPELINE_ICON_KEYS,
|
||||
pipelineIcon,
|
||||
} from "@portal/components/pipelines/pipelineIcon";
|
||||
import "@portal/components/pipelines/PipelineIconPicker.css";
|
||||
|
||||
interface PipelineIconPickerProps {
|
||||
/** Current icon key (may be a category id or empty; resolved by pipelineIcon). */
|
||||
value: string;
|
||||
onChange: (key: string) => void;
|
||||
}
|
||||
|
||||
/** Picks the pipeline's icon from a small set. The chosen glyph is the trigger; the menu is a grid. */
|
||||
export function PipelineIconPicker({
|
||||
value,
|
||||
onChange,
|
||||
}: PipelineIconPickerProps) {
|
||||
const { t } = useTranslation();
|
||||
// Controlled so a grid button (not a Dropdown.Item) can close the menu on pick.
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
function pick(key: string) {
|
||||
onChange(key);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown.Root open={open} onOpenChange={setOpen} align="start">
|
||||
<Dropdown.Trigger>
|
||||
<ActionIcon
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
aria-label={t("portal.pipelines.builder.icon.label")}
|
||||
>
|
||||
{pipelineIcon(value, "1.125rem")}
|
||||
</ActionIcon>
|
||||
</Dropdown.Trigger>
|
||||
<Dropdown.Menu>
|
||||
<div className="portal-icon-picker__grid">
|
||||
{PIPELINE_ICON_KEYS.map((key) => {
|
||||
const selected = key === value;
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className={
|
||||
"portal-icon-picker__option" +
|
||||
(selected ? " portal-icon-picker__option--selected" : "")
|
||||
}
|
||||
aria-label={key}
|
||||
aria-pressed={selected}
|
||||
onClick={() => pick(key)}
|
||||
>
|
||||
{pipelineIcon(key, "1.25rem")}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown.Root>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
// pipeline that only stores its categoryId still resolves to the matching glyph.
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import type { IconPickerOption } from "@app/ui";
|
||||
import type { SxProps, Theme } from "@mui/material";
|
||||
import { PIPELINE_ROUTE_GLYPH } from "@portal/components/icons";
|
||||
import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined";
|
||||
@@ -100,3 +101,8 @@ export function pipelineIcon(
|
||||
const sx: SxProps<Theme> = { fontSize };
|
||||
return <Icon sx={sx} className={className} />;
|
||||
}
|
||||
|
||||
/** The pipeline's icon vocabulary as options for the shared SUI `IconPicker`. */
|
||||
export const PIPELINE_ICON_OPTIONS: IconPickerOption[] = PIPELINE_ICON_KEYS.map(
|
||||
(key) => ({ key, label: key, node: pipelineIcon(key, "1.25rem") }),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user