+ )
+};
diff --git a/frontend/src/components/designSystem/README.md b/frontend/src/components/designSystem/README.md
new file mode 100644
index 000000000..0bf06e6fa
--- /dev/null
+++ b/frontend/src/components/designSystem/README.md
@@ -0,0 +1,36 @@
+# Fileglancer Design System
+
+A library of UI-only, reusable components built on our existing Material Tailwind + Tailwind CSS stack. Components live in an atomic hierarchy:
+
+- **atoms/** -- Smallest building blocks (buttons, inputs, badges).
+- **molecules/** -- Compositions of atoms (search bars, form fields with labels).
+- **organisms/** -- Complex UI sections (dialogs, data tables, nav bars).
+- **templates/** -- Page-level layout skeletons that slot in organisms.
+
+Semantic color classes are defined in `tailwind.config.js`.
+
+All design-system components use the `Fg` prefix (e.g. `FgButton`, `FgDialog`) and are previewed in Storybook (`pixi run storybook`).
+
+---
+
+## Authoring Rules
+
+1. **UI-only.** No `useQuery`, no router hooks, no `fetch`, no context unless it's theme-related context.
+
+2. **Props are the contract.** Every variant and state is a prop. If a component needs data, the caller provides it.
+
+3. **Callbacks, not actions.** Emit `onClick`, `onChange`, `onSubmit`. Callers wire them to mutations/navigation.
+
+4. **Composition over configuration.** Prefer compound components (``, ``) over giant prop APIs.
+
+5. **Semantic color classes only, not raw hex.** Use Tailwind classes backed by `mtConfig` (`bg-primary`, `text-foreground`). No new hex values.
+
+6. **No `-default` in color class names.** Per existing convention: `bg-primary`, not `bg-primary-default`.
+
+7. **Every component has a Vitest test.** Minimum: renders with props, emits callbacks, renders correctly in dark mode.
+
+8. **Every component has a Storybook story.** Co-locate `ComponentName.stories.tsx` next to the component file.
+
+9. **Every interactive component has a `focus-visible` style.** Keyboard users must see focus.
+
+10. **`Fg` prefix for all design-system components.** Makes migration greppable and prevents collision with Material Tailwind.
diff --git a/frontend/src/components/designSystem/atoms/FgBadge.stories.tsx b/frontend/src/components/designSystem/atoms/FgBadge.stories.tsx
new file mode 100644
index 000000000..54157a7ff
--- /dev/null
+++ b/frontend/src/components/designSystem/atoms/FgBadge.stories.tsx
@@ -0,0 +1,117 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+
+import FgBadge from './FgBadge';
+
+const meta = {
+ title: 'Atoms/FgBadge',
+ component: FgBadge,
+ argTypes: {
+ variant: {
+ control: 'select',
+ options: ['default', 'pill']
+ },
+ color: {
+ control: 'select',
+ options: [
+ 'primary',
+ 'secondary',
+ 'success',
+ 'error',
+ 'warning',
+ 'info',
+ 'neutral'
+ ]
+ },
+ size: {
+ control: 'inline-radio',
+ options: ['sm', 'md']
+ }
+ },
+ args: {
+ children: 'Badge'
+ }
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const Colors: Story = {
+ render: args => (
+
diff --git a/frontend/src/components/ui/Dialogs/dataLinkUsage/tabsContent/NapariTab.tsx b/frontend/src/components/ui/Dialogs/dataLinkUsage/tabsContent/NapariTab.tsx
index abed34193..1b0c05155 100644
--- a/frontend/src/components/ui/Dialogs/dataLinkUsage/tabsContent/NapariTab.tsx
+++ b/frontend/src/components/ui/Dialogs/dataLinkUsage/tabsContent/NapariTab.tsx
@@ -2,7 +2,7 @@ import { Fragment } from 'react/jsx-runtime';
import CodeBlock from '@/components/ui/Dialogs/dataLinkUsage/CodeBlock';
import InstructionBlock from '@/components/ui/Dialogs/dataLinkUsage/InstructionBlock';
-import ExternalLink from '@/components/ui/Dialogs/dataLinkUsage/ExternalLink';
+import FgExternalLink from '@/components/designSystem/atoms/FgExternalLink';
import PrerequisitesBlock from '@/components/ui/Dialogs/dataLinkUsage/PrerequisitesBlock';
type NapariTabProps = {
@@ -46,9 +46,9 @@ export default function NapariTab({
language="bash"
tooltipTriggerClasses={tooltipTriggerClasses}
/>
-
+
Napari documentation
-
+
,
'In the pop-up, select the napari-ome-zarr plugin to open the image. Optionally, save this as the default choice for all files ending with .zarr.'
]}
diff --git a/frontend/src/components/ui/Dialogs/dataLinkUsage/tabsContent/VvdViewerTab.tsx b/frontend/src/components/ui/Dialogs/dataLinkUsage/tabsContent/VvdViewerTab.tsx
index d6c00f09c..9d2be3100 100644
--- a/frontend/src/components/ui/Dialogs/dataLinkUsage/tabsContent/VvdViewerTab.tsx
+++ b/frontend/src/components/ui/Dialogs/dataLinkUsage/tabsContent/VvdViewerTab.tsx
@@ -2,7 +2,7 @@ import { Fragment } from 'react/jsx-runtime';
import PrerequisitesBlock from '@/components/ui/Dialogs/dataLinkUsage/PrerequisitesBlock';
import InstructionBlock from '@/components/ui/Dialogs/dataLinkUsage/InstructionBlock';
-import ExternalLink from '@/components/ui/Dialogs/dataLinkUsage/ExternalLink';
+import FgExternalLink from '@/components/designSystem/atoms/FgExternalLink';
export default function VvdViewerTab() {
return (
@@ -19,9 +19,9 @@ export default function VvdViewerTab() {
steps={[
Launch VVDViewer.
-
+
MacOS users - see the known issues on GitHub
-
+
,
'In the VVDViewer tool bar, select File \u2192 Open URL.',
'Paste the data link in the dialog and click "Ok" to view the image.'
diff --git a/frontend/src/components/ui/FileSelector/FileSelectorBreadcrumbs.tsx b/frontend/src/components/ui/FileSelector/FileSelectorBreadcrumbs.tsx
index 93d58defb..23d4408a1 100644
--- a/frontend/src/components/ui/FileSelector/FileSelectorBreadcrumbs.tsx
+++ b/frontend/src/components/ui/FileSelector/FileSelectorBreadcrumbs.tsx
@@ -1,6 +1,7 @@
import { Breadcrumb } from '@material-tailwind/react';
import { HiChevronRight, HiOutlineSquares2X2 } from 'react-icons/hi2';
+import FgIcon from '@/components/designSystem/atoms/FgIcon';
import BreadcrumbSegment from '@/components/ui/widgets/BreadcrumbSegment';
import { usePreferencesContext } from '@/contexts/PreferencesContext';
import { makePathSegmentArray, joinPaths } from '@/utils/pathHandling';
@@ -88,8 +89,8 @@ export default function FileSelectorBreadcrumbs({
-
-
+
+
{segments.map((segment, index) => {
diff --git a/frontend/src/components/ui/FileSelector/FileSelectorButton.tsx b/frontend/src/components/ui/FileSelector/FileSelectorButton.tsx
index 9963bc565..32792e2b9 100644
--- a/frontend/src/components/ui/FileSelector/FileSelectorButton.tsx
+++ b/frontend/src/components/ui/FileSelector/FileSelectorButton.tsx
@@ -1,9 +1,11 @@
import { useState, useEffect } from 'react';
import type { MouseEvent } from 'react';
-import { Button, Input, Typography } from '@material-tailwind/react';
+import { Input, Typography } from '@material-tailwind/react';
import { HiOutlineFolder, HiOutlineFunnel, HiXMark } from 'react-icons/hi2';
import FgDialog from '@/components/ui/Dialogs/FgDialog';
+import FgButton from '@/components/designSystem/atoms/FgButton';
+import FgIcon from '@/components/designSystem/atoms/FgIcon';
import FileSelectorBreadcrumbs from './FileSelectorBreadcrumbs';
import FileSelectorTable from './FileSelectorTable';
import { Spinner } from '@/components/ui/widgets/Loaders';
@@ -103,19 +105,19 @@ export default function FileSelectorButton({
return (
<>
- ) => {
setShowDialog(true);
e.currentTarget.blur();
}}
size="sm"
type="button"
- variant="outline"
+ variant="ghost"
>
-
{label}
-
+
{showDialog ? (
-
+
{searchQuery ? (
@@ -160,7 +162,7 @@ export default function FileSelectorButton({
onClick={clearSearch}
type="button"
>
-
+
) : null}