Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions packages/demo/src/components/demo/segmented-controls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
Badge,
SegmentedControls,
type SegmentedControlOption,
type SegmentedControlsDisplay,
} from "@eqtylab/equality";
import { useState } from "react";

export const SegmentedControlsDemo = ({
options,
defaultValue,
display,
}: {
options: SegmentedControlOption[];
defaultValue: string;
display?: SegmentedControlsDisplay;
}) => {
const [value, setValue] = useState(defaultValue);

return (
<SegmentedControls
options={options}
value={value}
onValueChange={setValue}
display={display}
/>
);
};

const textOptions: SegmentedControlOption[] = [
{ value: "day", label: "Day" },
{ value: "week", label: "Week" },
{ value: "month", label: "Month" },
];

export const SegmentedControlsTextDemo = () => (
<SegmentedControlsDemo defaultValue="week" options={textOptions} />
);

const iconOptions: SegmentedControlOption[] = [
{ value: "list", label: "List", icon: "List" },
{ value: "board", label: "Board", icon: "Columns3" },
{ value: "calendar", label: "Calendar", icon: "Calendar" },
];

export const SegmentedControlsIconDemo = () => (
<SegmentedControlsDemo defaultValue="list" options={iconOptions} />
);

const suffixOptions: SegmentedControlOption[] = [
{
value: "all",
label: "All",
suffix: <Badge size="sm">128</Badge>,
},
{
value: "open",
label: "Open",
suffix: <Badge size="sm">12</Badge>,
},
{
value: "closed",
label: "Closed",
suffix: <Badge size="sm">116</Badge>,
},
];

export const SegmentedControlsSuffixDemo = () => (
<SegmentedControlsDemo defaultValue="all" options={suffixOptions} />
);

const iconOnlyOptions: SegmentedControlOption[] = [
{ value: "list", label: "List view", icon: "List" },
{ value: "table", label: "Table view", icon: "Grid3X3" },
];

export const SegmentedControlsIconOnlyDemo = () => (
<div style={{ marginBottom: "1rem" }}>
<SegmentedControlsDemo
display="icon-only"
defaultValue="table"
options={iconOnlyOptions}
/>
</div>
);
1 change: 1 addition & 0 deletions packages/demo/src/components/sidebar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const gettingStartedLinks = [
const componentsLinks = components.map((component: any) => ({
text: component.data.heading,
href: `/components/${component.id}`,
deprecated: component.data.deprecated,
}));
---

Expand Down
10 changes: 8 additions & 2 deletions packages/demo/src/components/ui/nav-list.astro
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
---
import { cn } from "@demo/lib/utils";
import { Badge } from "@eqtylab/equality";

interface Props {
label: string;
list: Array<{ text: string; href: string }>;
list: Array<{ text: string; href: string; deprecated?: boolean }>;
}

const { label, list } = Astro.props;
Expand Down Expand Up @@ -33,7 +34,12 @@ const isActive = (href: string) =>
: "text-text-secondary hover:border-text-primary hover:text-text-primary",
)}
>
{item.text}
<span class="inline-flex items-center gap-2">
{item.text}
{item.deprecated && (
<Badge variant="warning" className="shrink-0" display="icon-only">Deprecated</Badge>
)}
</span>
</a>
</li>
);
Expand Down
15 changes: 13 additions & 2 deletions packages/demo/src/components/ui/page-header.astro
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
---
import Heading from "@demo/components/ui/heading.astro";
import { Icon } from "@eqtylab/equality";
import { Alert, Icon } from "@eqtylab/equality";
import { getEntry } from "astro:content";
import { createElement } from "react";

const { heading, description } = Astro.props;
const { heading, description, deprecated, deprecatedMessage } = Astro.props;

const slug = Astro.url.pathname.replace(/\/$/, "").split("/").pop();
const componentEntry = slug ? await getEntry("components", slug) : undefined;
const markdownHref = componentEntry ? `/components/${slug}.md` : null;

const deprecatedDescription = createElement("span", {
className: "[&_a]:underline [&_a]:underline-offset-2",
dangerouslySetInnerHTML: { __html: deprecatedMessage ?? "This component is deprecated." },
});
---

<section class="space-y-3 border-b pb-4">
Expand Down Expand Up @@ -36,4 +42,9 @@ const markdownHref = componentEntry ? `/components/${slug}.md` : null;
)
}
</div>
{
deprecated && (
<Alert variant="warning" title="Deprecated" description={deprecatedDescription} />
)
}
</section>
7 changes: 7 additions & 0 deletions packages/demo/src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ const components = defineCollection({
schema: z.object({
heading: z.string(),
description: z.string().optional(),
// Marks a component as deprecated. Renders a warning callout on the docs
// page and a warning tag in the sidebar. Pair with the `@deprecated` JSDoc
// tag on the component itself in the `ui` package.
deprecated: z.boolean().default(false),
// Optional message shown in the deprecation callout. Supports inline HTML
// (e.g. a link to the replacement component).
deprecatedMessage: z.string().optional(),
}),
});

Expand Down
12 changes: 2 additions & 10 deletions packages/demo/src/content/components/list-or-grid-view-toggle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,21 @@
layout: "@demo/layouts/mdx-layout.astro"
heading: "List or Grid View Toggle"
description: "List or Grid View Toggle with sizes and variants"
deprecated: true
deprecatedMessage: 'Deprecated in favour of <a href="/components/segmented-controls">Segmented Controls</a>.'
---

import { ListOrGridViewToggle } from "@eqtylab/equality";
import { ListOrGridViewToggleDemo } from "@demo/components/demo/list-or-grid-view-toggle";

## Overview

---

The List or Grid View Toggle component is used to switch between list and grid view.

Please adjust the order using the `order` parameter so that the default `viewMode` is always on the left side.

## Usage

---

Import the component:

```tsx
Expand All @@ -35,8 +33,6 @@ const [viewMode, setViewMode] = useState("grid");

## Examples

---

### Default

<ListOrGridViewToggleDemo viewMode="grid" client:only="react" />
Expand All @@ -51,8 +47,6 @@ const [viewMode, setViewMode] = useState("grid");

## Reordering

---

Reorder the order of the buttons using the `order` prop. The default `viewMode` should always be placed on the left side.

```tsx
Expand All @@ -65,8 +59,6 @@ Reorder the order of the buttons using the `order` prop. The default `viewMode`

## Props

---

| Prop | Type | Default | Description |
| ------------------ | ---------------------------------------- | ------------------ | ----------------------------------------------- |
| `viewMode` | `"grid"` \| `"list"` | Required | Sets the active state. |
Expand Down
110 changes: 110 additions & 0 deletions packages/demo/src/content/components/segmented-controls.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
layout: "@demo/layouts/mdx-layout.astro"
heading: "Segmented Controls"
description: "A linear set of two or more segments used to switch between mutually exclusive options or views."
---

import {
SegmentedControlsTextDemo,
SegmentedControlsIconDemo,
SegmentedControlsSuffixDemo,
SegmentedControlsIconOnlyDemo,
} from "@demo/components/demo/segmented-controls";

## Overview

Segmented Controls present a developer-defined set of two or more mutually exclusive
options in a single, connected control. They are commonly used to switch between views,
filters, or modes where a dropdown would be excessive.

Each option supports a prefix `icon`, a required text `label`, and an optional `suffix`
slot. The currently selected option is highlighted using the primary button colour.

## Usage

Import the component:

```tsx
import { SegmentedControls } from "@eqtylab/equality";
```

`options`, `value`, and `onValueChange` are required. `value` and `onValueChange` should be
controlled by the parent component.

```tsx
const [view, setView] = useState("day");

<SegmentedControls
value={view}
onValueChange={setView}
options={[
{ value: "day", label: "Day" },
{ value: "week", label: "Week" },
{ value: "month", label: "Month" },
]}
/>;
```

## Examples

### Text only

<SegmentedControlsTextDemo client:only="react" />

### With prefix icons

Provide an `icon` (a [Lucide](https://lucide.dev) icon name or a React element) to render a
prefix before the label.

<SegmentedControlsIconDemo client:only="react" />

### With a suffix slot

Use the `suffix` slot to render supplementary content, such as a count `Badge`, after the
label.

<SegmentedControlsSuffixDemo client:only="react" />

## Display modes

Like the [Badge](/components/badge) component, the `display` prop controls what is rendered
inside each segment: `both` (the default), `text-only`, or `icon-only`.

`icon-only` requires **every** option to define an `icon`, and it falls back to `both` if any
option is missing one. Because `label` is mandatory, it is used as the accessible label
(`aria-label`) and tooltip for each segment in `icon-only` mode.

### Icon only

<SegmentedControlsIconOnlyDemo client:only="react" />

```tsx
<SegmentedControls
display="icon-only"
value={view}
onValueChange={setView}
options={[
{ value: "grid", label: "Table view", icon: "Grid3X3" },
{ value: "list", label: "List view", icon: "List" },
]}
/>
```

## Props

| Name | Description | Type | Default | Required |
| --------------- | ----------------------------------------------------- | -------------------------------- | ------- | -------- |
| `options` | The set of segments to render. | `SegmentedControlOption[]` | - | ✅ |
| `value` | The value of the currently selected option. | `string` | - | ✅ |
| `onValueChange` | Called with the new value when a segment is selected. | `(value: string) => void` | - | ✅ |
| `display` | Controls what is rendered inside each segment. | `both`, `text-only`, `icon-only` | `both` | ❌ |
| `className` | Additional CSS classes to apply to the control. | `string` | - | ❌ |

### `SegmentedControlOption`

| Name | Description | Type | Default | Required |
| -------- | --------------------------------------------------------------- | ------------------------------ | ------- | -------- |
| `value` | Unique value used to identify the option. | `string` | - | ✅ |
| `label` | Text label. Also used as the accessible label when `icon-only`. | `string` | - | ✅ |
| `icon` | Prefix icon: a Lucide icon name or a React element. | `string`, `React.ReactElement` | - | ❌ |
| `suffix` | Content rendered after the label. | `React.ReactNode` | - | ❌ |
15 changes: 13 additions & 2 deletions packages/demo/src/layouts/mdx-layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,27 @@ interface Props {
frontmatter?: {
heading?: string;
description?: string;
deprecated?: boolean;
deprecatedMessage?: string;
};
}

const { frontmatter = {} } = Astro.props;
const { heading, description } = frontmatter;
const { heading, description, deprecated, deprecatedMessage } = frontmatter;
---

<PageWrapper>
<PageContentWrapper>
{heading && <PageHeader heading={heading} description={description} />}
{
heading && (
<PageHeader
heading={heading}
description={description}
deprecated={deprecated}
deprecatedMessage={deprecatedMessage}
/>
)
}
<section id="markdown-content">
<slot />
</section>
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export * from './radio-group/radio-group';
export * from './scroll-area/scroll-area';
export * from './search-bar/search-bar';
export * from './section-heading/section-heading';
export * from './segmented-controls/segmented-controls';
export * from './select/select';
export * from './separator/separator';
export * from './sheet/sheet';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ interface ListOrGridViewToggleProps {
className?: string;
}

/**
* @deprecated Use `SegmentedControls` instead.
*/
const ListOrGridViewToggle = ({
viewMode,
onViewModeChange,
Expand Down
Loading
Loading