docs(site): add ui-next component documentation and upgrade Docusaurus (#6102)
This commit is contained in:
parent
03b180f185
commit
ac45a9497f
@ -118,6 +118,7 @@ window.config = ({ servicesManager } = {}) => {
|
||||
|
||||
|
||||
Here are a list of some options available:
|
||||
- `customizationService`: An array of customization module references and inline overrides applied at the global scope. This is how deployers enable optional features like the [Appearance & Theming](./ui.md) system and configure UI components without modifying core code. See [Customization Service](../platform/services/customization-service/customizationService.md) for the full syntax reference.
|
||||
- `disableEditing`: If true, it disables editing in OHIF, hiding edit buttons in segmentation
|
||||
panel and locking already stored measurements.
|
||||
- `maxNumberOfWebWorkers`: The maximum number of web workers to use for
|
||||
|
||||
161
platform/docs/docs/configuration/ui.md
Normal file
161
platform/docs/docs/configuration/ui.md
Normal file
@ -0,0 +1,161 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
sidebar_label: UI & Appearance
|
||||
title: UI & Appearance
|
||||
summary: Overview of OHIF's UI system — the component library, theming and appearance customization, iconography, and the services that drive modals, dialogs, notifications, and viewport layout.
|
||||
---
|
||||
|
||||
# UI & Appearance
|
||||
|
||||
OHIF's user interface is built on **ui-next**, a component library purpose-built
|
||||
for medical imaging workflows. It provides a consistent set of React components,
|
||||
a token-based theming system, a curated icon set, and a collection of UI services
|
||||
that manage runtime behavior like modals, notifications, and viewport layout.
|
||||
|
||||
This page gives a high-level map of how these pieces fit together and where to
|
||||
find detailed documentation for each.
|
||||
|
||||
## Component Library
|
||||
|
||||
The [component library](/components) is the foundation of every panel, toolbar, dialog, and
|
||||
control in the viewer. Components are organized into three tiers:
|
||||
|
||||
- **Simple components** — atomic building blocks like Button, Checkbox, Input,
|
||||
Label, Slider, and Switch. These are styled with the design system's tokens and
|
||||
are the smallest interactive units.
|
||||
|
||||
- **Compound components** — composed from simple components to handle more
|
||||
complex interactions: Select, Dialog, DropdownMenu, Tabs, Table, Popover,
|
||||
Tooltip, Toast, and others. These manage their own state (open/close, selection,
|
||||
focus) via headless patterns.
|
||||
|
||||
- **OHIF-specific components** — built for medical imaging workflows and not
|
||||
found in general-purpose libraries: AllInOneMenu, CinePlayer, DataRow,
|
||||
DataTable, Numeric, PanelSection, SmartScrollbar, ToolButton, and
|
||||
ToolButtonList. These integrate directly with OHIF's services and extension
|
||||
system.
|
||||
|
||||
Every component renders live in the interactive
|
||||
[Components](/components) section of this documentation site, where you
|
||||
can see props, variants, and copy usage examples.
|
||||
|
||||
## Appearance Dialog
|
||||
|
||||
The Appearance dialog is an optional UI that lets end users switch presets,
|
||||
apply custom CSS overrides, and share themes via URL. It is not enabled by
|
||||
default, but deployers can
|
||||
[opt in via configuration](../platform/services/customization-service/appearance-theming.md#enabling-the-theme-module).
|
||||
See [Appearance & Theming](../platform/services/customization-service/appearance-theming.md)
|
||||
for the full setup and developer guide.
|
||||
|
||||
## Colors & Theming
|
||||
|
||||
OHIF uses a **token-based color system** built on CSS custom properties with HSL
|
||||
values. The base theme (dark mode) is defined in `:root` via Tailwind CSS, and
|
||||
theme presets override specific tokens by applying a CSS class to `document.body`.
|
||||
|
||||
The theming system covers:
|
||||
|
||||
- **[Design tokens](/colors-and-theming#color-tokens-and-roles)** — a set of semantic color variables (`--background`,
|
||||
`--primary`, `--card`, `--border`, etc.) that every component references. Tokens
|
||||
are defined as HSL triplets and consumed via Tailwind's `hsl(var(...))` pattern.
|
||||
|
||||
- **Theme presets** — six built-in themes (three tonal, three neutral) that shift
|
||||
the entire interface's color palette. Presets are defined as JSON files and
|
||||
applied via CSS classes. Also see:
|
||||
[Creating Themes](/colors-and-theming#creating-themes) and
|
||||
[Adding a Theme Preset](../platform/services/customization-service/appearance-theming.md#adding-a-theme-preset).
|
||||
|
||||
- **Accessibility** — foreground tokens are paired with every surface token to
|
||||
maintain contrast ratios across themes. When adding custom themes,
|
||||
[test colors to match accessibility standards](/colors-and-theming#accessibility).
|
||||
|
||||
Explore the live token swatches and theme picker in the interactive
|
||||
[Colors & Theming](/colors-and-theming) page, and see
|
||||
[Appearance & Theming](../platform/services/customization-service/appearance-theming.md)
|
||||
for the full configuration and developer guide.
|
||||
|
||||
## Iconography
|
||||
|
||||
OHIF ships a curated set of icons designed specifically for medical imaging interfaces.
|
||||
Icons are SVG-based, registered via `addIcon()`, and rendered through the `Icons`
|
||||
component. They follow the same token system as the rest of the UI, so icon colors
|
||||
respond to the active theme.
|
||||
|
||||
Browse the full set with search and click-to-copy in the
|
||||
[Iconography](/components/icons) page.
|
||||
|
||||
## White Labeling
|
||||
|
||||
OHIF supports replacing the header logo with custom branding via the
|
||||
`whiteLabeling` configuration key. This lets you rebrand the viewer without
|
||||
modifying source code.
|
||||
|
||||
Add a `whiteLabeling` entry to your configuration file with a
|
||||
`createLogoComponentFn` that returns a React element:
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
/** .. **/
|
||||
whiteLabeling: {
|
||||
createLogoComponentFn: function(React) {
|
||||
return React.createElement(
|
||||
'a',
|
||||
{
|
||||
target: '_self',
|
||||
rel: 'noopener noreferrer',
|
||||
className: 'text-purple-600 line-through',
|
||||
href: '/',
|
||||
},
|
||||
React.createElement('img', {
|
||||
src: './customLogo.svg',
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
/** .. **/
|
||||
};
|
||||
```
|
||||
|
||||

|
||||
|
||||
You can use Tailwind CSS utility classes in the `className` property to style
|
||||
your custom logo component.
|
||||
|
||||
## UI Services
|
||||
|
||||
While the component library handles what gets rendered, **UI services** handle
|
||||
the runtime behavior: opening modals, showing notifications, managing viewport
|
||||
layout, and coordinating dialogs. These services use a pub/sub architecture and
|
||||
are accessed through the `servicesManager`:
|
||||
|
||||
| Service | What it manages |
|
||||
|---------|----------------|
|
||||
| [Modal Service](../platform/services/ui/ui-modal-service.md) | Full-screen and contained modals (including the Appearance dialog) |
|
||||
| [Dialog Service](../platform/services/ui/ui-dialog-service.md) | Lightweight dialogs with form inputs and confirmations |
|
||||
| [Notification Service](../platform/services/ui/ui-notification-service.md) | Toast notifications (success, warning, error, info) |
|
||||
| [Viewport Grid Service](../platform/services/ui/viewport-grid-service.md) | Viewport layout, grid arrangement, and active viewport tracking |
|
||||
| [Viewport Dialog Service](../platform/services/ui/ui-viewport-dialog-service.md) | Dialogs anchored to specific viewports |
|
||||
| [Viewport Action Menu](../platform/services/ui/viewport-action-menu.md) | Context menus and action buttons on viewport corners |
|
||||
| [Cine Service](../platform/services/ui/cine-service.md) | Cine playback controls and frame rate |
|
||||
|
||||
UI services are documented in the [Services > UI](../platform/services/ui/index.md)
|
||||
section.
|
||||
|
||||
## Customization Service
|
||||
|
||||
The [Customization Service](../platform/services/customization-service/customizationService.md)
|
||||
is the mechanism that ties everything together. It allows deployers and
|
||||
extension authors to override, extend, or replace UI components and behaviors
|
||||
without modifying core code. Customizations can be scoped to a specific mode, set
|
||||
globally via configuration, or provided as defaults by extensions.
|
||||
|
||||
The Appearance dialog itself is delivered as a customization module. Deployers
|
||||
enable it by adding a single reference to their config's `customizationService`
|
||||
array. This pattern means that any part of the UI that supports customization
|
||||
can be configured the same way.
|
||||
|
||||
See the full customization guide at
|
||||
[Customization Service](../platform/services/customization-service/customizationService.md),
|
||||
including the syntax reference for `$set`, `$push`, `$merge`, `$filter`, and
|
||||
other operations.
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
sidebar_position: 4
|
||||
sidebar_label: URL
|
||||
title: URL Parameters
|
||||
summary: Reference for OHIF Viewer URL parameters that control WorkList filtering, study/series loading, and viewer behavior, including parameters for filtering by patient name, modality, data sources, specifying initial series/instances, and selecting hanging protocols.
|
||||
|
||||
@ -6,10 +6,9 @@ title: 3.10 to 3.11 Migration Guide
|
||||
---
|
||||
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
|
||||
|
||||
# 3.10 to 3.11 Migration Guide
|
||||
|
||||
Here you can find the migration guides for upgrading from OHIF version 3.10 to version 3.11.
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items.filter(item => item.docId !== 'migration-guide/3p10-to-3p11/index')}/>
|
||||
<DocCardList />
|
||||
|
||||
99
platform/docs/docs/migration-guide/3p12-to-3p13/colors.md
Normal file
99
platform/docs/docs/migration-guide/3p12-to-3p13/colors.md
Normal file
@ -0,0 +1,99 @@
|
||||
---
|
||||
sidebar_position: 7
|
||||
sidebar_label: Color System
|
||||
title: Color System Migration
|
||||
summary: 3.13 continues the migration from legacy color classes to the ui-next semantic color system. This guide maps legacy color names to their ui-next equivalents and shows common text, background, and border replacements.
|
||||
---
|
||||
|
||||
# Color System Migration
|
||||
|
||||
This guide covers migrating from legacy color classes to the new ui-next color system.
|
||||
|
||||
## Current State (3.13+)
|
||||
|
||||
The OHIF Viewer is now built entirely with ui-next components, which use the [new color system](/colors-and-theming). The legacy components in `platform/ui` remain on the old color system. For reference, the table below maps the legacy colors to their ui-next equivalents.
|
||||
|
||||
## Color Mapping Reference
|
||||
|
||||
| Legacy Color | Updated color (ui-next) |
|
||||
| ----------------- | ----------------------- |
|
||||
| `bkg-low` | `background` |
|
||||
| `common-bright` | `foreground` (various) |
|
||||
| `primary-light` | `highlight` |
|
||||
| `inputfield-main` | `input` |
|
||||
| `secondary-light` | `input` |
|
||||
| `primary-dark` | `muted` |
|
||||
| `bkg-med` | `muted` |
|
||||
| `aqua-pale` | `muted-foreground` |
|
||||
| `secondary-dark` | `popover` |
|
||||
| `primary-active` | `primary` |
|
||||
| `actions-primary` | `primary` |
|
||||
| `secondary-main` | |
|
||||
| `primary-main` | |
|
||||
| `common-dark` | |
|
||||
| `customblue` | |
|
||||
|
||||
> **Note:** The legacy variables left blank in the table above (`secondary-main`, `primary-main`, `common-dark`, and `customblue`) have no direct ui-next equivalent. If your code uses any of these, replace them case by case, choosing the semantic token that best fits how the color is used in each component.
|
||||
|
||||
## Generic Color Migrations
|
||||
|
||||
Avoid using generic color values. Use semantic colors instead:
|
||||
|
||||
| Generic Color | Semantic Replacement |
|
||||
| ---------------------------- | ----------------------- |
|
||||
| `white` | `foreground` |
|
||||
| `blue-*` (e.g., `blue-500`) | `primary` or `highlight`|
|
||||
| `gray-*` (e.g., `gray-800`) | `muted` or `popover` |
|
||||
| `red-*` | `destructive` |
|
||||
|
||||
> **Note:** `black` is still used in certain cases to replace default browser backgrounds and to match imaging backgrounds.
|
||||
|
||||
## Common Migrations
|
||||
|
||||
### Text Colors
|
||||
|
||||
```diff
|
||||
- text-white
|
||||
+ text-foreground
|
||||
|
||||
- text-primary-light
|
||||
+ text-highlight
|
||||
|
||||
- text-primary-active
|
||||
+ text-primary
|
||||
|
||||
- text-secondary-light
|
||||
+ text-muted-foreground
|
||||
```
|
||||
|
||||
### Background Colors
|
||||
|
||||
```diff
|
||||
- bg-primary-dark
|
||||
+ bg-muted
|
||||
|
||||
- bg-secondary-dark
|
||||
+ bg-popover
|
||||
|
||||
- bg-black
|
||||
+ bg-background
|
||||
```
|
||||
|
||||
### Border Colors
|
||||
|
||||
```diff
|
||||
- border-secondary-light
|
||||
+ border-input
|
||||
|
||||
- border-primary-light
|
||||
+ border-highlight
|
||||
|
||||
- border-primary-dark
|
||||
+ border-muted
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Legacy colors without a mapping (empty cells) should be evaluated on a case-by-case basis
|
||||
- The new color system uses CSS variables, enabling future theme support
|
||||
- When migrating, test components visually to ensure proper contrast and accessibility
|
||||
@ -6,7 +6,6 @@ title: 3.12 to 3.13 Migration Guide
|
||||
---
|
||||
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
|
||||
|
||||
# 3.12 to 3.13 Migration Guide
|
||||
|
||||
@ -25,4 +24,4 @@ The largest changes in 3.13 are infrastructure-level:
|
||||
- **[SegmentationService](./segmentation-service.md)** — the
|
||||
`removeSegmentationRepresentations` method was renamed.
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items.filter(item => item.docId !== 'migration-guide/3p12-to-3p13/index')} />
|
||||
<DocCardList />
|
||||
|
||||
@ -6,7 +6,6 @@ summary: Migration guide for segmentation architecture changes in OHIF 3.9, cove
|
||||
---
|
||||
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
|
||||
|
||||
:::info
|
||||
This migration involves significant architectural changes to the segmentation system. While we typically aim for incremental updates, the shift from a tool group-centric to a viewport-centric architecture was necessary to support OHIF 3.9's advanced visualization capabilities, and more flexible segmentation handling.
|
||||
@ -15,4 +14,4 @@ Don't worry - we'll guide you through each change step by step!
|
||||
:::
|
||||
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items}/>
|
||||
<DocCardList />
|
||||
|
||||
@ -7,8 +7,7 @@ summary: Migration guide for upgrading from OHIF 3.8 to 3.9, covering segmentati
|
||||
|
||||
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
|
||||
|
||||
## Migration Guide Sections
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items}/>
|
||||
<DocCardList />
|
||||
|
||||
@ -12,7 +12,7 @@ summary: Migration guide for OHIF 3.10's new color system, explaining the transi
|
||||
* **Component Abstraction:** Some styling, especially for interactive elements like buttons, has been abstracted into components (e.g., `ViewportActionButton`, UI library buttons) which use predefined variants (`default`, `secondary`, `ghost`) instead of manual style combinations.
|
||||
|
||||
:::note
|
||||
You can look at the set of colors in the [Color System](/colors-and-type)
|
||||
You can look at the set of colors in the [Color System](/colors-and-theming)
|
||||
:::
|
||||
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ This guide explains how to migrate from the existing `Input`, `InputNumber`, `In
|
||||
|
||||
## Why Migrate?
|
||||
|
||||
See the full list of components in the [Numeric Component Showcase](/components-list#numeric)
|
||||
See the full list of components in the [Numeric Component](/components/numeric)
|
||||
|
||||
|
||||
The old components relied heavily on props, making them complex and difficult to maintain and apply custom styles. The new `Numeric` component provides a structured approach with a context-based API, reducing prop clutter and improving reusability.
|
||||
|
||||
@ -6,10 +6,9 @@ summary: Introduction to OHIF migration guides covering the upgrade paths betwee
|
||||
|
||||
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
|
||||
|
||||
# Migration Guides
|
||||
|
||||
Based on the version you are migrating from, you can find the migration guide for the latest version of the platform.
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items}/>
|
||||
<DocCardList />
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
---
|
||||
sidebar_label: Measurements
|
||||
title: Measurements Customization
|
||||
summary: Documentation for customizing OHIF's measurement tools and displays, including how to modify display formats, annotation behaviors, and tracking interfaces for medical imaging measurements.
|
||||
sidebar_position: 4
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
---
|
||||
sidebar_label: Segmentation
|
||||
title: Segmentation Customization
|
||||
summary: Documentation for customizing OHIF's segmentation tools and visualization options, including configuration for segment rendering, color schemes, and interaction behaviors for medical image segmentation workflows.
|
||||
sidebar_position: 5
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
---
|
||||
sidebar_label: Study Browser
|
||||
title: Study Browser Customization
|
||||
summary: Documentation for customizing OHIF's Study Browser component, including configuration options for thumbnails, sorting functions, and display formats to enhance study navigation and selection workflows.
|
||||
sidebar_position: 6
|
||||
sidebar_position: 7
|
||||
---
|
||||
|
||||
# Study Browser
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
---
|
||||
sidebar_label: Viewport Scrollbar
|
||||
title: Viewport Scrollbar Customization
|
||||
summary: Documentation for configuring OHIF viewport scrollbar behavior, including progress vs legacy mode, loaded/viewed tracking visuals, loading pattern behavior, timing controls, and viewportScrollbar.indicator (size + optional custom indicator).
|
||||
sidebar_position: 7
|
||||
sidebar_position: 9
|
||||
---
|
||||
|
||||
# Viewport Scrollbar
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
---
|
||||
sidebar_label: Work List
|
||||
title: Work List Customization
|
||||
summary: Documentation for configuring the OHIF WorkList study-list route — selecting between the new (default) and legacy variants, the preview panel's series view (thumbnails, list, or both), and the columns shown in the study-list table.
|
||||
sidebar_position: 9
|
||||
sidebar_position: 10
|
||||
---
|
||||
|
||||
# Work List
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
---
|
||||
sidebar_label: Advanced
|
||||
title: Advanced Customization
|
||||
summary: Documentation for advanced OHIF customization techniques, including inheritance patterns, transform functions, and dynamic assembly of customizations to create sophisticated configurations across the platform.
|
||||
sidebar_position: 8
|
||||
sidebar_position: 11
|
||||
---
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,379 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
sidebar_label: Appearance & Theming
|
||||
title: Appearance & Theming
|
||||
summary: Documentation for OHIF's appearance and theming system, including theme presets, custom themes, URL-based theme selection, and how to enable the Appearance dialog for your deployment.
|
||||
---
|
||||
|
||||
# Appearance & Theming
|
||||
|
||||
OHIF includes an optional theming system that lets users switch between visual
|
||||
theme presets, apply custom CSS token overrides, and share themes via URL. The
|
||||
system is **not enabled by default** — deployers opt in by adding a single
|
||||
configuration line.
|
||||
|
||||
## Enabling the Theme Module
|
||||
|
||||
The theming system is delivered as a customization module in the default
|
||||
extension. To enable it, add the module reference to the `customizationService`
|
||||
array in your configuration file:
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
// ...
|
||||
customizationService: [
|
||||
'@ohif/extension-default.customizationModule.theme',
|
||||
],
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
This single line activates the entire theming system:
|
||||
|
||||
- The **Appearance** menu item appears in the viewer header and study list
|
||||
settings
|
||||
- Users can switch between theme presets via a dropdown
|
||||
- The active theme persists across sessions via `localStorage`
|
||||
- Themes can be applied via URL parameter (`?theme=orchid`)
|
||||
- Custom CSS token overrides can be pasted and applied
|
||||
|
||||
### What Happens Without It
|
||||
|
||||
When the theme module is not in the configuration:
|
||||
|
||||
- No Appearance menu item is shown anywhere in the UI
|
||||
- No theme provider is mounted in the component tree
|
||||
- URL parameters like `?theme=orchid` have no effect
|
||||
- No `localStorage` keys are read or written
|
||||
- Zero runtime footprint — the theming code does not execute
|
||||
|
||||
This opt-in design ensures that deployments that don't need theming pay no cost
|
||||
for it.
|
||||
|
||||
## Theme Presets
|
||||
|
||||
OHIF ships with six built-in theme presets organized into two families:
|
||||
|
||||
### Tonal Presets
|
||||
|
||||
Tonal presets introduce a color hue throughout the interface — backgrounds,
|
||||
borders, and surfaces all carry the theme's signature color.
|
||||
|
||||
| Preset | Description |
|
||||
|--------|-------------|
|
||||
| **Orchid** | Purple/violet tones with warm highlights |
|
||||
| **Arctic** | Cool blue tones with icy surfaces |
|
||||
| **Verdant** | Green tones with natural, earthy surfaces |
|
||||
|
||||
### Neutral Presets
|
||||
|
||||
Neutral presets keep surfaces achromatic (grays and blacks) and use color only
|
||||
for interactive elements and highlights.
|
||||
|
||||
| Preset | Description |
|
||||
|--------|-------------|
|
||||
| **Midnight** | Deep blacks with subtle gray layering |
|
||||
| **Slate** | Cool neutral grays with blue highlights |
|
||||
| **Deep** | Rich dark surfaces with minimal contrast |
|
||||
|
||||
### Selecting a Preset
|
||||
|
||||
Open the Appearance dialog from the header menu (or study list settings) and
|
||||
choose a preset from the Theme dropdown. The theme applies immediately and
|
||||
persists across sessions.
|
||||
|
||||
## URL-Based Theme Selection
|
||||
|
||||
Themes can be applied via URL parameter, which is useful for sharing links with a
|
||||
specific visual configuration:
|
||||
|
||||
```
|
||||
https://your-ohif-instance.com/viewer?theme=orchid
|
||||
```
|
||||
|
||||
Valid values are any preset name: `default`, `orchid`, `arctic`, `verdant`,
|
||||
`midnight`, `slate`, `deep`.
|
||||
|
||||
The URL parameter takes precedence over the stored theme. When a URL theme is
|
||||
applied, it is also saved to `localStorage` so the user's session continues with
|
||||
that theme after navigation.
|
||||
|
||||
:::note
|
||||
The `custom` theme value is deliberately not URL-addressable. Custom themes rely
|
||||
on CSS stored in the user's `localStorage`, so a `?theme=custom` link would land
|
||||
the app in a state with no CSS behind it.
|
||||
:::
|
||||
|
||||
## Custom Themes
|
||||
|
||||
The Appearance dialog includes a custom theme feature that allows pasting CSS
|
||||
variable overrides directly into the viewer.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. Open the Appearance dialog
|
||||
2. Click **Custom Theme** to expand the text area
|
||||
3. Paste CSS variable declarations (one per line):
|
||||
```css
|
||||
--background: 270 45% 6%;
|
||||
--foreground: 280 15% 96%;
|
||||
--card: 268 40% 10%;
|
||||
--primary: 270 85% 65%;
|
||||
--border: 268 30% 18%;
|
||||
```
|
||||
4. Click **Apply**
|
||||
|
||||
The viewer parses the variables and injects them as CSS overrides into both
|
||||
`:root` and `.dark` selectors. Invalid lines are silently skipped. If no valid
|
||||
variables are found, the Apply button shows an error message.
|
||||
|
||||
Click **Clear** to remove the custom theme and return to the default appearance.
|
||||
|
||||
### Token Format
|
||||
|
||||
Theme tokens use **HSL triplets without the `hsl()` wrapper** — just the three
|
||||
space-separated values:
|
||||
|
||||
```
|
||||
--token-name: hue saturation% lightness%;
|
||||
```
|
||||
|
||||
For example: `--primary: 270 85% 65%` represents a purple hue at 85% saturation
|
||||
and 65% lightness.
|
||||
|
||||
### Available Tokens
|
||||
|
||||
The theming system uses the same token set as the base OHIF design system. The
|
||||
most commonly customized tokens are:
|
||||
|
||||
| Token | Purpose |
|
||||
|-------|---------|
|
||||
| `--background` | Page/viewport background |
|
||||
| `--foreground` | Default text color |
|
||||
| `--card` | Surface color for cards and panels |
|
||||
| `--popover` | Elevated surface color (dropdowns, tooltips) |
|
||||
| `--primary` | Primary interactive color (buttons, links) |
|
||||
| `--secondary` | Secondary surface color |
|
||||
| `--muted` | Subdued background areas |
|
||||
| `--muted-foreground` | Subdued text color |
|
||||
| `--accent` | Accent surface color |
|
||||
| `--border` | Border color |
|
||||
| `--input` | Input field background |
|
||||
| `--ring` | Focus ring color |
|
||||
| `--highlight` | Highlight/selection color |
|
||||
| `--destructive` | Error/destructive action color |
|
||||
|
||||
Each token also has a corresponding `-foreground` variant for text on that
|
||||
surface (e.g., `--card-foreground`, `--primary-foreground`).
|
||||
|
||||
## Preset File Format
|
||||
|
||||
Theme presets are defined as JSON files in
|
||||
`platform/ui-next/src/themes/`. Each file follows this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "orchid",
|
||||
"label": "Tonal: Orchid",
|
||||
"cssVars": {
|
||||
"dark": {
|
||||
"highlight": "292 75% 62%",
|
||||
"background": "270 45% 6%",
|
||||
"foreground": "280 15% 96%",
|
||||
"card": "268 40% 10%",
|
||||
"primary": "270 85% 65%",
|
||||
"border": "268 30% 18%"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `name` | Internal identifier. Must be unique and URL-safe. Used as the CSS class name (`theme-{name}`) and `?theme=` value. |
|
||||
| `label` | Display label shown in the Appearance dialog dropdown. |
|
||||
| `cssVars.dark` | Object mapping token names to HSL triplet values. Only tokens that differ from the default theme need to be specified. |
|
||||
|
||||
The corresponding CSS class is defined in `platform/ui-next/src/themes/themes.css`
|
||||
and applies the token overrides when the class is added to `document.body`.
|
||||
|
||||
## Adding a Theme Preset
|
||||
|
||||
To add a new preset to the Appearance dialog dropdown, three files need to be
|
||||
updated:
|
||||
|
||||
### 1. Create the JSON file
|
||||
|
||||
Add a new JSON file in `platform/ui-next/src/themes/`. Use an existing preset as
|
||||
a template:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "ember",
|
||||
"label": "Tonal: Ember",
|
||||
"cssVars": {
|
||||
"dark": {
|
||||
"highlight": "15 85% 55%",
|
||||
"background": "12 40% 5%",
|
||||
"foreground": "20 15% 96%",
|
||||
"card": "10 35% 9%",
|
||||
"primary": "15 80% 50%",
|
||||
"border": "12 25% 16%"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `name` must be unique and URL-safe — it becomes the CSS class name
|
||||
(`theme-ember`) and the `?theme=` URL value. Only include tokens that differ from
|
||||
the default theme.
|
||||
|
||||
### 2. Add the CSS class to `themes.css`
|
||||
|
||||
Add a corresponding class in `platform/ui-next/src/themes/themes.css` with the
|
||||
same token values:
|
||||
|
||||
```css
|
||||
/* ─── Tonal: Ember ──────────────────────────────────────────────────── */
|
||||
|
||||
.theme-ember {
|
||||
--highlight: 15 85% 55%;
|
||||
--background: 12 40% 5%;
|
||||
--foreground: 20 15% 96%;
|
||||
--card: 10 35% 9%;
|
||||
--primary: 15 80% 50%;
|
||||
--border: 12 25% 16%;
|
||||
/* ... all tokens from the JSON */
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
The JSON and CSS must define the same tokens with the same values. The JSON is
|
||||
used for metadata (name, label) and validation. The CSS is what actually applies
|
||||
the theme at runtime via the body class.
|
||||
:::
|
||||
|
||||
### 3. Register in the barrel export
|
||||
|
||||
Import the JSON file and add it to the `themePresets` array in
|
||||
`platform/ui-next/src/themes/index.ts`:
|
||||
|
||||
```ts
|
||||
import orchid from './orchid.json';
|
||||
import arctic from './arctic.json';
|
||||
import verdant from './verdant.json';
|
||||
import midnight from './midnight.json';
|
||||
import slate from './slate.json';
|
||||
import deep from './deep.json';
|
||||
import ember from './ember.json'; // add import
|
||||
|
||||
export const themePresets: ThemePreset[] = [
|
||||
orchid, arctic, verdant, midnight, slate, deep, ember // add to array
|
||||
];
|
||||
```
|
||||
|
||||
The order of the array determines the order in the dropdown menu.
|
||||
|
||||
## Removing a Theme Preset
|
||||
|
||||
To remove a preset, reverse the three steps above:
|
||||
|
||||
1. **Remove from `index.ts`** — delete the import and remove it from the
|
||||
`themePresets` array
|
||||
2. **Remove from `themes.css`** — delete the `.theme-{name}` class block
|
||||
3. **Delete the JSON file** — remove the file from `platform/ui-next/src/themes/`
|
||||
|
||||
Users who had the removed theme active will fall back to the default theme on
|
||||
their next visit — the provider validates stored theme names against the current
|
||||
preset list and resets invalid values.
|
||||
|
||||
## Architecture
|
||||
|
||||
### How the Provider Registers
|
||||
|
||||
The theming system uses OHIF's `serviceProvidersManager` to register its React
|
||||
provider dynamically. During app initialization, the default extension's
|
||||
`preRegistration` hook checks whether the theme module is present in the
|
||||
configuration. If it is, `ActiveThemeProvider` is registered into the component
|
||||
tree:
|
||||
|
||||
```
|
||||
App initializes
|
||||
→ Default extension's preRegistration runs
|
||||
→ Checks config for 'customizationModule.theme'
|
||||
→ If present: registerProvider('activeTheme', ActiveThemeProvider)
|
||||
→ App.tsx builds the provider tree
|
||||
→ Picks up registered providers automatically
|
||||
```
|
||||
|
||||
App.tsx has no knowledge of theming. The extension that provides the theme module
|
||||
also decides whether to register its provider.
|
||||
|
||||
### How the Menu Item Appears
|
||||
|
||||
The Appearance menu item is gated by the customization service. Both the viewer
|
||||
header (`ViewerHeader.tsx`) and the study list settings
|
||||
(`StudyListSettingsPopover.tsx`) call:
|
||||
|
||||
```tsx
|
||||
const AppearanceModal = customizationService.getCustomization('ohif.appearanceModal');
|
||||
```
|
||||
|
||||
If the customization is not registered (because the theme module wasn't enabled),
|
||||
the call returns `undefined` and no menu item is rendered.
|
||||
|
||||
### State Management
|
||||
|
||||
`ActiveThemeProvider` manages theme state via React context:
|
||||
|
||||
- **Active theme**: stored in component state and `localStorage` (`ohif:theme`)
|
||||
- **Custom CSS**: stored in component state and `localStorage` (`ohif:custom-theme-css`)
|
||||
- **CSS classes**: applied to `document.body` (`theme-{name}`)
|
||||
- **Custom styles**: injected via a `<style>` element (`#ohif-custom-theme`)
|
||||
|
||||
The `useActiveTheme()` hook exposes the full API:
|
||||
|
||||
```tsx
|
||||
const {
|
||||
activeTheme, // Current theme name ('default', 'orchid', 'custom', etc.)
|
||||
setActiveTheme, // Switch to a named preset
|
||||
customCss, // Raw custom CSS text
|
||||
applyCustomTheme, // Parse and apply custom CSS, returns boolean
|
||||
clearCustomTheme, // Remove custom theme, revert to default
|
||||
} = useActiveTheme();
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### Minimal Setup
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
customizationService: [
|
||||
'@ohif/extension-default.customizationModule.theme',
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### With Other Customization Modules
|
||||
|
||||
The theme module can be combined with other customization modules:
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
customizationService: [
|
||||
'@ohif/extension-default.customizationModule.theme',
|
||||
'@ohif/extension-cornerstone-dicom-seg.customizationModule.dicom-seg-sorts',
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Setting a Default Theme via URL
|
||||
|
||||
To link directly to a themed viewer session:
|
||||
|
||||
```
|
||||
https://your-ohif-instance.com/viewer?StudyInstanceUIDs=1.2.3&theme=arctic
|
||||
```
|
||||
|
||||
The theme parameter works alongside all other URL parameters.
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
sidebar_label: Custom Routes
|
||||
sidebar_position: 2
|
||||
sidebar_position: 4
|
||||
title: Custom Routes
|
||||
summary: Documentation for creating custom routes in OHIF, allowing extensions to define new URL paths and React components to extend the application with additional pages and navigation options.
|
||||
---
|
||||
|
||||
@ -543,4 +543,9 @@ needless to say if you opted to choose `name: default` in the `getCustomizationM
|
||||
|
||||
Below we are providing the example configuration for global scenario (using the configuration file), however, you can also use the `setCustomizations` method to set the customizations.
|
||||
|
||||
In addition to the parts listed in the table below, the
|
||||
[Appearance & Theming](./appearance-theming.md) system is delivered as a
|
||||
customization module (`@ohif/extension-default.customizationModule.theme`) that
|
||||
adds theme preset switching, custom CSS overrides, and URL-based theme selection.
|
||||
|
||||
{TableGenerator(customizations)}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
sidebar_label: Viewport Overlay
|
||||
sidebar_position: 1
|
||||
sidebar_position: 8
|
||||
title: Viewport Overlay Customization
|
||||
summary: Documentation for customizing OHIF's viewport overlays, allowing configuration of the information displayed in each corner (top-right, top-left, bottom-left, bottom-right) of medical imaging viewports.
|
||||
---
|
||||
|
||||
@ -1,168 +0,0 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
sidebar_label: Theming
|
||||
title: Theming the OHIF Viewer
|
||||
summary: Documentation on customizing the visual appearance of the OHIF Viewer using Tailwind CSS and white-labeling capabilities, including how to modify the color scheme, add custom layouts via the LayoutTemplateModule, and replace the application logo with custom branding.
|
||||
---
|
||||
|
||||
# Viewer: Theming
|
||||
|
||||
`OHIF-v3` has introduced the
|
||||
[`LayoutTemplateModule`](./extensions/modules/layout-template.md) which enables
|
||||
addition of custom layouts. You can easily design your custom components inside
|
||||
an extension and consume it via the layoutTemplate module you write.
|
||||
|
||||
## Tailwind CSS
|
||||
|
||||
[Tailwind CSS](https://tailwindcss.com/) is a utility-first CSS framework for
|
||||
creating custom user interfaces.
|
||||
|
||||
Below you can see a compiled version of the tailwind configs. Each section can
|
||||
be edited accordingly. For instance screen size break points, primary and
|
||||
secondary colors, etc.
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
prefix: '',
|
||||
important: false,
|
||||
separator: ':',
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '640px',
|
||||
md: '768px',
|
||||
lg: '1024px',
|
||||
xl: '1280px',
|
||||
},
|
||||
colors: {
|
||||
overlay: 'rgba(0, 0, 0, 0.8)',
|
||||
transparent: 'transparent',
|
||||
black: '#000',
|
||||
white: '#fff',
|
||||
initial: 'initial',
|
||||
inherit: 'inherit',
|
||||
|
||||
indigo: {
|
||||
dark: '#0b1a42',
|
||||
},
|
||||
aqua: {
|
||||
pale: '#7bb2ce',
|
||||
},
|
||||
|
||||
primary: {
|
||||
light: '#5acce6',
|
||||
main: '#0944b3',
|
||||
dark: '#090c29',
|
||||
active: '#348cfd',
|
||||
},
|
||||
|
||||
secondary: {
|
||||
light: '#3a3f99',
|
||||
main: '#2b166b',
|
||||
dark: '#041c4a',
|
||||
active: '#1f1f27',
|
||||
},
|
||||
|
||||
common: {
|
||||
bright: '#e1e1e1',
|
||||
light: '#a19fad',
|
||||
main: '#fff',
|
||||
dark: '#726f7e',
|
||||
active: '#2c3074',
|
||||
},
|
||||
|
||||
customgreen: {
|
||||
100: '#05D97C',
|
||||
},
|
||||
|
||||
customblue: {
|
||||
100: '#c4fdff',
|
||||
200: '#38daff',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
You can also use the color variable like before. For instance:
|
||||
|
||||
```js
|
||||
primary: {
|
||||
default: ‘var(--default-color)‘,
|
||||
light: ‘#5ACCE6’,
|
||||
main: ‘#0944B3’,
|
||||
dark: ‘#090C29’,
|
||||
active: ‘#348CFD’,
|
||||
}
|
||||
```
|
||||
|
||||
## White Labeling
|
||||
|
||||
A white-label product is a product or service produced by one company (the
|
||||
producer) that other companies (the marketers) rebrand to make it appear as if
|
||||
they had made it -
|
||||
[Wikipedia: White-Label Product](https://en.wikipedia.org/wiki/White-label_product)
|
||||
|
||||
Current white-labeling options are limited. We expose the ability to replace the
|
||||
"Logo" section of the application with a custom "Logo" component. You can do
|
||||
this by adding a whiteLabeling key to your configuration file.
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
/** .. **/
|
||||
whiteLabeling: {
|
||||
createLogoComponentFn: function(React) {
|
||||
return React.createElement(
|
||||
'a',
|
||||
{
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
className: 'text-white underline',
|
||||
href: 'http://radicalimaging.com',
|
||||
},
|
||||
React.createElement('h5', {}, 'RADICAL IMAGING')
|
||||
);
|
||||
},
|
||||
},
|
||||
/** .. **/
|
||||
};
|
||||
```
|
||||
|
||||
> You can simply use the stylings from tailwind CSS in the whiteLabeling
|
||||
|
||||
In addition to text, you can also add your custom logo
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
/** .. **/
|
||||
whiteLabeling: {
|
||||
createLogoComponentFn: function(React) {
|
||||
return React.createElement(
|
||||
'a',
|
||||
{
|
||||
target: '_self',
|
||||
rel: 'noopener noreferrer',
|
||||
className: 'text-purple-600 line-through',
|
||||
href: '/',
|
||||
},
|
||||
React.createElement('img', {
|
||||
src: './customLogo.svg',
|
||||
// className: 'w-8 h-8',
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
/** .. **/
|
||||
};
|
||||
```
|
||||
|
||||
The output will look like
|
||||
|
||||

|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[wikipedia]: https://en.wikipedia.org/wiki/White-label_product
|
||||
<!-- prettier-ignore-end -->
|
||||
@ -1,6 +1,10 @@
|
||||
// read this text file
|
||||
const fs = require('fs');
|
||||
const versions = fs.readFileSync('../../version.txt', 'utf8').split('\n');
|
||||
const rawVersion = fs.readFileSync('../../version.txt', 'utf8').trim();
|
||||
const [major, minor] = rawVersion.split('.');
|
||||
const versionLabel = rawVersion.includes('beta')
|
||||
? `${major}.${minor} Beta`
|
||||
: `${major}.${minor}`;
|
||||
|
||||
const ArchivedVersionsDropdownItems = [
|
||||
{
|
||||
@ -35,7 +39,11 @@ const baseUrl = process.env.BASE_URL || '/';
|
||||
/** @type {import('@docusaurus/types').DocusaurusConfig} */
|
||||
module.exports = {
|
||||
future: {
|
||||
experimental_faster: true,
|
||||
faster: true,
|
||||
v4: {
|
||||
removeLegacyPostBuildHeadAttribute: true,
|
||||
useCssCascadeLayers: true,
|
||||
},
|
||||
},
|
||||
title: 'OHIF',
|
||||
tagline: 'Open-source web-based medical imaging platform',
|
||||
@ -49,7 +57,11 @@ module.exports = {
|
||||
locales: ['en'],
|
||||
},
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'throw',
|
||||
markdown: {
|
||||
hooks: {
|
||||
onBrokenMarkdownLinks: 'throw',
|
||||
},
|
||||
},
|
||||
favicon: 'img/favicon.ico',
|
||||
themes: ['@docusaurus/theme-live-codeblock'],
|
||||
plugins: [
|
||||
@ -97,7 +109,7 @@ module.exports = {
|
||||
// : undefined,
|
||||
versions: {
|
||||
current: {
|
||||
label: `${versions} (Latest)`,
|
||||
label: `${versionLabel} (Latest)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -181,12 +193,6 @@ module.exports = {
|
||||
label: 'Help',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
to: '/migration-guide/3p10-to-3p11/',
|
||||
//activeBaseRegex: '(^/help$)|(/help)',
|
||||
label: '3.11 Migration Guides',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
type: 'docsVersionDropdown',
|
||||
position: 'right',
|
||||
@ -229,78 +235,7 @@ module.exports = {
|
||||
},
|
||||
footer: {
|
||||
style: 'dark',
|
||||
links: [
|
||||
{
|
||||
title: ' ',
|
||||
items: [
|
||||
{
|
||||
// This doesn't show up on dev for some reason, but displays in build
|
||||
html: `
|
||||
<a href="https://www.massgeneral.org/" target="_blank" rel="noreferrer noopener">
|
||||
<img src="/img/mgh-logo.png" id="mgh-logo" alt="MGH" />
|
||||
</a>
|
||||
`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Learn',
|
||||
items: [
|
||||
{
|
||||
label: 'Introduction',
|
||||
to: '/',
|
||||
},
|
||||
{
|
||||
label: 'Getting Started',
|
||||
to: 'development/getting-started',
|
||||
},
|
||||
{
|
||||
label: 'FAQ',
|
||||
to: '/faq',
|
||||
},
|
||||
{
|
||||
label: 'Resources',
|
||||
to: '/resources',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Community',
|
||||
items: [
|
||||
{
|
||||
label: 'Discussion board',
|
||||
href: 'https://community.ohif.org/',
|
||||
},
|
||||
{
|
||||
label: 'Help',
|
||||
to: '/help',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'More',
|
||||
items: [
|
||||
{
|
||||
label: 'Donate',
|
||||
href: 'https://giving.massgeneral.org/ohif',
|
||||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/OHIF/Viewers',
|
||||
},
|
||||
{
|
||||
label: 'Twitter',
|
||||
href: 'https://twitter.com/OHIFviewer',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
logo: {
|
||||
alt: 'OHIF ',
|
||||
src: 'img/netlify-color-accent.svg',
|
||||
href: 'https://viewer.ohif.org/',
|
||||
},
|
||||
copyright: `OHIF is open source software released under the MIT license.`,
|
||||
links: [],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
@ -22,19 +22,19 @@
|
||||
"write-heading-ids": "docusaurus write-heading-ids"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.7.0",
|
||||
"@docusaurus/faster": "3.7.0",
|
||||
"@docusaurus/module-type-aliases": "3.7.0",
|
||||
"@docusaurus/plugin-client-redirects": "3.7.0",
|
||||
"@docusaurus/plugin-google-gtag": "3.7.0",
|
||||
"@docusaurus/plugin-ideal-image": "3.7.0",
|
||||
"@docusaurus/plugin-pwa": "3.7.0",
|
||||
"@docusaurus/preset-classic": "3.7.0",
|
||||
"@docusaurus/remark-plugin-npm2yarn": "3.7.0",
|
||||
"@docusaurus/theme-classic": "3.7.0",
|
||||
"@docusaurus/theme-live-codeblock": "3.7.0",
|
||||
"@docusaurus/tsconfig": "3.0.0",
|
||||
"@docusaurus/types": "3.0.0",
|
||||
"@docusaurus/core": "3.10.1",
|
||||
"@docusaurus/faster": "3.10.1",
|
||||
"@docusaurus/module-type-aliases": "3.10.1",
|
||||
"@docusaurus/plugin-client-redirects": "3.10.1",
|
||||
"@docusaurus/plugin-google-gtag": "3.10.1",
|
||||
"@docusaurus/plugin-ideal-image": "3.10.1",
|
||||
"@docusaurus/plugin-pwa": "3.10.1",
|
||||
"@docusaurus/preset-classic": "3.10.1",
|
||||
"@docusaurus/remark-plugin-npm2yarn": "3.10.1",
|
||||
"@docusaurus/theme-classic": "3.10.1",
|
||||
"@docusaurus/theme-live-codeblock": "3.10.1",
|
||||
"@docusaurus/tsconfig": "3.10.1",
|
||||
"@docusaurus/types": "3.10.1",
|
||||
"@mdx-js/react": "3.0.1",
|
||||
"@radix-ui/react-accordion": "1.2.11",
|
||||
"@radix-ui/react-checkbox": "1.3.2",
|
||||
@ -81,6 +81,7 @@
|
||||
"tailwind-merge": "2.6.0",
|
||||
"tailwindcss": "3.2.4",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"tailwindcss-scoped-preflight": "3.5.9",
|
||||
"typescript": "5.5.4",
|
||||
"url-loader": "4.1.1"
|
||||
},
|
||||
|
||||
@ -10,15 +10,17 @@
|
||||
:root {
|
||||
--highlight: 191 74% 63%;
|
||||
--neutral: 213 22% 59%;
|
||||
--background: 236 62% 5%;
|
||||
--neutral-light: 214 69% 81%;
|
||||
--neutral-dark: 214 16% 21%;
|
||||
--background: 0 0% 0%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 236 62% 5%;
|
||||
--card: 234 64% 10%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 219 90% 15%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 214 98% 60%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 214 66% 48%;
|
||||
--secondary: 214 65% 36%;
|
||||
--secondary-foreground: 200 50% 84%;
|
||||
--muted: 234 64% 10%;
|
||||
--muted-foreground: 200 46% 65%;
|
||||
@ -35,167 +37,24 @@
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--radius: 0.5rem;
|
||||
--badge-new-color: hsl(var(--primary-foreground));
|
||||
--badge-new-background: linear-gradient(135deg, hsl(var(--highlight)), hsl(var(--primary)));
|
||||
--badge-latest-stable-color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 214 98% 60%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 236 52% 30%;
|
||||
--ring: 214 98% 60%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--success-bg: hsl(143, 85%, 96%);
|
||||
--success-border: hsl(145, 92%, 91%);
|
||||
--success-text: hsl(140, 100%, 27%);
|
||||
--info-bg: hsl(208, 100%, 97%);
|
||||
--info-border: hsl(221, 91%, 91%);
|
||||
--info-text: hsl(210, 92%, 45%);
|
||||
--warning-bg: hsl(49, 100%, 97%);
|
||||
--warning-border: hsl(49, 91%, 91%);
|
||||
--warning-text: hsl(31, 92%, 45%);
|
||||
--error-bg: hsl(359, 100%, 97%);
|
||||
--error-border: hsl(359, 100%, 94%);
|
||||
--error-text: hsl(360, 100%, 45%);
|
||||
--badge-new-color: hsl(var(--primary-foreground));
|
||||
--badge-new-background: linear-gradient(135deg, hsl(var(--highlight)), hsl(var(--primary)));
|
||||
--badge-latest-stable-color: hsl(var(--primary));
|
||||
}
|
||||
}
|
||||
|
||||
/* ORIGINAL THEME for comparison and testing
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--destructive: 0 72.22% 50.59%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 5% 64.9%;
|
||||
--radius: 0.5rem;
|
||||
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 85.7% 97.3%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/* Theme Copy Example
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 224 71.4% 4.1%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 224 71.4% 4.1%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 224 71.4% 4.1%;
|
||||
--primary: 262.1 83.3% 57.8%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
--secondary: 220 14.3% 95.9%;
|
||||
--secondary-foreground: 220.9 39.3% 11%;
|
||||
--muted: 220 14.3% 95.9%;
|
||||
--muted-foreground: 220 8.9% 46.1%;
|
||||
--accent: 220 14.3% 95.9%;
|
||||
--accent-foreground: 220.9 39.3% 11%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--border: 220 13% 91%;
|
||||
--input: 220 13% 91%;
|
||||
--ring: 262.1 83.3% 57.8%;
|
||||
--radius: 0.5rem;
|
||||
--chart-1: ;
|
||||
--chart-2: ;
|
||||
--chart-3: ;
|
||||
--chart-4: ;
|
||||
--chart-5: ;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71.4% 4.1%;
|
||||
--foreground: 210 20% 98%;
|
||||
--card: 224 71.4% 4.1%;
|
||||
--card-foreground: 210 20% 98%;
|
||||
--popover: 224 71.4% 4.1%;
|
||||
--popover-foreground: 210 20% 98%;
|
||||
--primary: 263.4 70% 50.4%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
--secondary: 215 27.9% 16.9%;
|
||||
--secondary-foreground: 210 20% 98%;
|
||||
--muted: 215 27.9% 16.9%;
|
||||
--muted-foreground: 217.9 10.6% 64.9%;
|
||||
--accent: 215 27.9% 16.9%;
|
||||
--accent-foreground: 210 20% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--border: 215 27.9% 16.9%;
|
||||
--input: 215 27.9% 16.9%;
|
||||
--ring: 263.4 70% 50.4%;
|
||||
--chart-1: ;
|
||||
--chart-2: ;
|
||||
--chart-3: ;
|
||||
--chart-4: ;
|
||||
--chart-5: ;
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
h2.section-header {
|
||||
@apply py-4 text-2xl font-normal text-white;
|
||||
}
|
||||
@ -348,31 +207,40 @@ body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
.markdown h1,
|
||||
.markdown h2,
|
||||
.markdown h3,
|
||||
.markdown h4,
|
||||
.markdown h5,
|
||||
.theme-doc-markdown h1,
|
||||
.theme-doc-markdown h2,
|
||||
.theme-doc-markdown h3,
|
||||
.theme-doc-markdown h4,
|
||||
.theme-doc-markdown h5 {
|
||||
color: var(--ifm-color-primary);
|
||||
font-weight: 400;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
.markdown blockquote,
|
||||
.theme-doc-markdown blockquote {
|
||||
border-left: 3px solid #4042af;
|
||||
}
|
||||
|
||||
/* Temporary Type Size Changes */
|
||||
|
||||
article header h1 {
|
||||
.markdown article header h1,
|
||||
.theme-doc-markdown article header h1 {
|
||||
font-size: 2.6rem !important;
|
||||
}
|
||||
|
||||
article h2 {
|
||||
.markdown article h2,
|
||||
.theme-doc-markdown article h2 {
|
||||
font-size: 1.85rem !important;
|
||||
}
|
||||
|
||||
article header h3 {
|
||||
.markdown article header h3,
|
||||
.theme-doc-markdown article header h3 {
|
||||
font-size: 1.5rem !important;
|
||||
}
|
||||
|
||||
@ -398,6 +266,17 @@ article header h3 {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mit-license-link {
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.5);
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.mit-license-link:hover {
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
border-bottom-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.menu {
|
||||
font-weight: 400;
|
||||
font-size: 0.9rem;
|
||||
@ -483,21 +362,6 @@ html[data-theme='dark'] .docusaurus-highlight-code-line {
|
||||
display: none;
|
||||
} */
|
||||
|
||||
/* Footer logo MGH */
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
#mgh-logo {
|
||||
margin-right: 100px;
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
#mgh-logo {
|
||||
margin-right: 10px;
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-separator {
|
||||
margin: 0.3rem 0;
|
||||
@ -561,45 +425,38 @@ div[class^='announcementBar_'] {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* add proper ui link styling */
|
||||
|
||||
/* Bullet point styling */
|
||||
ul {
|
||||
/* List styling — scoped to doc content areas only */
|
||||
.markdown ul,
|
||||
.theme-doc-markdown ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
ul li {
|
||||
.markdown ul li,
|
||||
.theme-doc-markdown ul li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
ol {
|
||||
.markdown ol,
|
||||
.theme-doc-markdown ol {
|
||||
list-style-type: decimal;
|
||||
padding-left: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
ol li {
|
||||
.markdown ol li,
|
||||
.theme-doc-markdown ol li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Nested bullet points */
|
||||
ul ul {
|
||||
.markdown ul ul,
|
||||
.theme-doc-markdown ul ul {
|
||||
list-style-type: circle;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* For documentation bullet points specifically */
|
||||
.markdown ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.markdown ul li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Markdown link styling */
|
||||
.markdown a {
|
||||
color: #0066cc;
|
||||
@ -621,6 +478,12 @@ html[data-theme='dark'] .markdown a:hover {
|
||||
color: #99ccff;
|
||||
}
|
||||
|
||||
/* Component sidebar — reset preflight borders that leak from .showcase-isolated */
|
||||
.component-sidebar,
|
||||
.component-sidebar * {
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
/* Horizontal rule styling */
|
||||
.markdown hr {
|
||||
height: 1px;
|
||||
@ -723,6 +586,63 @@ a.dropdown__link[href='/3.9/migration-guide/3p8-to-3p9/']::after {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
li:first-child {
|
||||
margin-top: 0.5em; /* Adjust '1em' to your desired spacing */
|
||||
.markdown li:first-child,
|
||||
.theme-doc-markdown li:first-child {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
/* Isolate component showcases from Infima/Docusaurus styles.
|
||||
Requires v4.useCssCascadeLayers: true in docusaurus.config.js */
|
||||
.showcase-isolated:not(#a#b) {
|
||||
&,
|
||||
* {
|
||||
@layer docusaurus.infima {
|
||||
all: revert-layer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Extend isolation to the showcase demos' portal content. Radix and Sonner
|
||||
render floating UI (Dialog, Popover, Select, DropdownMenu, Tooltip, toasts)
|
||||
as direct body children — outside #__docusaurus and .showcase-isolated — so
|
||||
it misses both the revert-layer and the scoped preflight.
|
||||
|
||||
Scoped to the demos' OWN portal roots, NOT every body child:
|
||||
- [data-radix-popper-content-wrapper] Popover, Combobox, DropdownMenu,
|
||||
Select, Tooltip (all popper-based)
|
||||
- .showcase-portal explicit hook on Dialog demo content
|
||||
(Radix Dialog isn't popper-based)
|
||||
- [data-sonner-toaster] Sonner toast container
|
||||
This deliberately leaves third-party body portals (medium-zoom image overlay,
|
||||
Algolia DocSearch modal) untouched, which the previous catch-all reset broke.
|
||||
:where() keeps specificity at zero so Tailwind utility classes always win. */
|
||||
:where([data-radix-popper-content-wrapper], .showcase-portal, [data-sonner-toaster]) {
|
||||
font-family: Inter, sans-serif;
|
||||
|
||||
&,
|
||||
* {
|
||||
@layer docusaurus.infima {
|
||||
all: revert-layer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:where([data-radix-popper-content-wrapper], .showcase-portal, [data-sonner-toaster]),
|
||||
:where([data-radix-popper-content-wrapper], .showcase-portal, [data-sonner-toaster]) * {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
:where([data-radix-popper-content-wrapper], .showcase-portal, [data-sonner-toaster]) :where(button, input, select, textarea) {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
994
platform/docs/src/pages/colors-and-theming.tsx
Normal file
994
platform/docs/src/pages/colors-and-theming.tsx
Normal file
@ -0,0 +1,994 @@
|
||||
import React, { useState } from 'react';
|
||||
import '../css/custom.css';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
const themes: Record<string, string> = {
|
||||
default: `--highlight: 191 74% 63%;
|
||||
--neutral: 213 22% 59%;
|
||||
--neutral-light: 214 69% 81%;
|
||||
--neutral-dark: 214 16% 21%;
|
||||
--background: 0 0% 0%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 234 64% 10%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 219 90% 15%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 214 98% 60%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 214 65% 36%;
|
||||
--secondary-foreground: 200 50% 84%;
|
||||
--muted: 234 64% 10%;
|
||||
--muted-foreground: 200 46% 65%;
|
||||
--accent: 217 79% 24%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 236 52% 30%;
|
||||
--ring: 214 98% 60%;`,
|
||||
|
||||
orchid: `--highlight: 292 75% 62%;
|
||||
--neutral: 270 18% 55%;
|
||||
--neutral-light: 275 35% 75%;
|
||||
--neutral-dark: 268 20% 24%;
|
||||
--background: 270 45% 6%;
|
||||
--foreground: 280 15% 96%;
|
||||
--card: 268 40% 10%;
|
||||
--card-foreground: 280 15% 96%;
|
||||
--popover: 264 48% 13%;
|
||||
--popover-foreground: 280 15% 96%;
|
||||
--primary: 270 85% 65%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 268 45% 32%;
|
||||
--secondary-foreground: 275 45% 88%;
|
||||
--muted: 268 40% 10%;
|
||||
--muted-foreground: 272 30% 60%;
|
||||
--accent: 268 50% 20%;
|
||||
--accent-foreground: 280 15% 96%;
|
||||
--destructive: 0 65% 40%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 268 30% 18%;
|
||||
--input: 268 40% 25%;
|
||||
--ring: 270 80% 60%;`,
|
||||
|
||||
verdant: `--highlight: 152 79% 52%;
|
||||
--neutral: 150 15% 52%;
|
||||
--neutral-light: 145 30% 72%;
|
||||
--neutral-dark: 155 18% 24%;
|
||||
--background: 155 40% 5%;
|
||||
--foreground: 140 15% 95%;
|
||||
--card: 152 35% 9%;
|
||||
--card-foreground: 140 15% 95%;
|
||||
--popover: 167 65% 10%;
|
||||
--popover-foreground: 140 15% 95%;
|
||||
--primary: 152 75% 40%;
|
||||
--primary-foreground: 155 50% 8%;
|
||||
--secondary: 150 40% 26%;
|
||||
--secondary-foreground: 145 40% 85%;
|
||||
--muted: 152 35% 9%;
|
||||
--muted-foreground: 148 25% 55%;
|
||||
--accent: 150 45% 16%;
|
||||
--accent-foreground: 140 15% 95%;
|
||||
--destructive: 0 65% 38%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 150 25% 16%;
|
||||
--input: 150 35% 22%;
|
||||
--ring: 152 75% 40%;`,
|
||||
|
||||
arctic: `--highlight: 173 81% 52%;
|
||||
--neutral: 185 20% 55%;
|
||||
--neutral-light: 180 40% 78%;
|
||||
--neutral-dark: 190 18% 22%;
|
||||
--background: 0 0% 0%;
|
||||
--foreground: 180 10% 97%;
|
||||
--card: 203 39% 9%;
|
||||
--card-foreground: 180 10% 97%;
|
||||
--popover: 202 54% 11%;
|
||||
--popover-foreground: 180 10% 97%;
|
||||
--primary: 175 85% 42%;
|
||||
--primary-foreground: 185 50% 8%;
|
||||
--secondary: 180 45% 28%;
|
||||
--secondary-foreground: 175 50% 88%;
|
||||
--muted: 203 39% 9%;
|
||||
--muted-foreground: 185 30% 60%;
|
||||
--accent: 185 55% 18%;
|
||||
--accent-foreground: 180 10% 97%;
|
||||
--destructive: 0 65% 35%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 190 25% 17%;
|
||||
--input: 223 40% 24%;
|
||||
--ring: 175 85% 42%;`,
|
||||
|
||||
midnight: `--highlight: 188 90% 58%;
|
||||
--neutral: 213 22% 59%;
|
||||
--neutral-light: 214 69% 81%;
|
||||
--neutral-dark: 214 16% 21%;
|
||||
--background: 240 15% 3%;
|
||||
--foreground: 0 0% 99%;
|
||||
--card: 240 12% 8%;
|
||||
--card-foreground: 0 0% 99%;
|
||||
--popover: 225 45% 13%;
|
||||
--popover-foreground: 0 0% 99%;
|
||||
--primary: 210 100% 62%;
|
||||
--primary-foreground: 0 0% 99%;
|
||||
--secondary: 215 55% 32%;
|
||||
--secondary-foreground: 200 60% 88%;
|
||||
--muted: 240 12% 8%;
|
||||
--muted-foreground: 210 40% 70%;
|
||||
--accent: 220 65% 20%;
|
||||
--accent-foreground: 0 0% 99%;
|
||||
--destructive: 0 70% 35%;
|
||||
--destructive-foreground: 0 0% 99%;
|
||||
--border: 240 10% 18%;
|
||||
--input: 225 35% 24%;
|
||||
--ring: 210 100% 62%;`,
|
||||
|
||||
slate: `--highlight: 217 97% 52%;
|
||||
--neutral: 0 0% 55%;
|
||||
--neutral-light: 0 0% 75%;
|
||||
--neutral-dark: 0 0% 25%;
|
||||
--background: 0 0% 0%;
|
||||
--foreground: 0 0% 96%;
|
||||
--card: 0 0% 7%;
|
||||
--card-foreground: 0 0% 96%;
|
||||
--popover: 0 0% 9%;
|
||||
--popover-foreground: 0 0% 96%;
|
||||
--primary: 230 75% 55%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 25%;
|
||||
--secondary-foreground: 0 0% 85%;
|
||||
--muted: 0 0% 7%;
|
||||
--muted-foreground: 0 0% 60%;
|
||||
--accent: 0 0% 18%;
|
||||
--accent-foreground: 0 0% 96%;
|
||||
--destructive: 0 65% 40%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 18%;
|
||||
--input: 0 0% 22%;
|
||||
--ring: 215 75% 55%;`,
|
||||
|
||||
deep: `--highlight: 184 53% 54%;
|
||||
--neutral: 215 15% 50%;
|
||||
--neutral-light: 210 20% 68%;
|
||||
--neutral-dark: 220 18% 22%;
|
||||
--background: 0 0% 0%;
|
||||
--foreground: 215 15% 82%;
|
||||
--card: 218 25% 5%;
|
||||
--card-foreground: 215 15% 82%;
|
||||
--popover: 215 30% 8%;
|
||||
--popover-foreground: 215 15% 82%;
|
||||
--primary: 200 43% 48%;
|
||||
--primary-foreground: 210 20% 92%;
|
||||
--secondary: 218 30% 18%;
|
||||
--secondary-foreground: 215 25% 75%;
|
||||
--muted: 214 28% 5%;
|
||||
--muted-foreground: 215 18% 48%;
|
||||
--accent: 216 32% 12%;
|
||||
--accent-foreground: 215 15% 82%;
|
||||
--destructive: 0 50% 35%;
|
||||
--destructive-foreground: 0 15% 90%;
|
||||
--border: 218 22% 10%;
|
||||
--input: 216 28% 15%;
|
||||
--ring: 215 45% 42%;`,
|
||||
};
|
||||
|
||||
function Swatch({ color, size = 24 }: { color: string; size?: number }) {
|
||||
return (
|
||||
<span
|
||||
className="inline-block rounded-full"
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
background: color,
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
verticalAlign: 'middle',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SwatchSm({ color }: { color: string }) {
|
||||
return (
|
||||
<span
|
||||
className="mr-1.5 inline-block rounded-full align-middle"
|
||||
style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
background: color,
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TokenRow({
|
||||
color,
|
||||
token,
|
||||
children,
|
||||
}: {
|
||||
color: string;
|
||||
token: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-5 grid items-start gap-x-3" style={{ gridTemplateColumns: '24px 200px 1fr' }}>
|
||||
<Swatch color={color} />
|
||||
<code className="bg-transparent pl-0 pt-0.5 text-sm text-[#7cacf8]">{token}</code>
|
||||
<div className="text-secondary-foreground text-lg">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SwatchSet({ colors }: { colors: string[] }) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{colors.map((c, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-full"
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
background: c,
|
||||
border: '2px solid #232323',
|
||||
marginLeft: i > 0 ? -13 : 0,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyThemeLink({ themeName }: { themeName: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const tokens = themes[themeName];
|
||||
if (!tokens) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(tokens);
|
||||
} catch {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = tokens;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
onClick={handleCopy}
|
||||
aria-label={`Copy ${themeName} theme tokens`}
|
||||
className="whitespace-nowrap text-[13px] no-underline transition-colors"
|
||||
style={{ color: copied ? '#6ee7b7' : '#7cacf8' }}
|
||||
>
|
||||
{copied ? 'Copied!' : 'Copy Theme'}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function ThemeRow({
|
||||
backgrounds,
|
||||
text,
|
||||
interactive,
|
||||
name,
|
||||
previewUrl,
|
||||
themeName,
|
||||
}: {
|
||||
backgrounds: string[];
|
||||
text: string[];
|
||||
interactive: string[];
|
||||
name: string;
|
||||
previewUrl: string;
|
||||
themeName: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<SwatchSet colors={backgrounds} />
|
||||
<SwatchSet colors={text} />
|
||||
<SwatchSet colors={interactive} />
|
||||
<span className="pl-3.5 text-[15px] text-[#999]">{name}</span>
|
||||
<a
|
||||
href={previewUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`Preview ${name} theme in Viewer`}
|
||||
className="whitespace-nowrap text-[13px] text-primary no-underline hover:underline"
|
||||
>
|
||||
Preview in Viewer
|
||||
</a>
|
||||
<CopyThemeLink themeName={themeName} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ContrastTable({
|
||||
rows,
|
||||
}: {
|
||||
rows: Array<{
|
||||
fgLabel: string;
|
||||
fgColor: string;
|
||||
bgLabel: string;
|
||||
bgColor: string;
|
||||
result: string;
|
||||
requirement: string;
|
||||
}>;
|
||||
}) {
|
||||
return (
|
||||
<table className="my-4 mb-6 w-full border-collapse text-[15px]" style={{ tableLayout: 'fixed' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border-b border-[#333] px-3 py-2 text-left text-[13px] font-medium uppercase tracking-wider text-[#999]" style={{ width: '30%' }}>Foreground</th>
|
||||
<th className="border-b border-[#333] px-3 py-2 text-left text-[13px] font-medium uppercase tracking-wider text-[#999]" style={{ width: '30%' }}>Background</th>
|
||||
<th className="border-b border-[#333] px-3 py-2 text-left text-[13px] font-medium uppercase tracking-wider text-[#999]" style={{ width: '16%' }}>Result</th>
|
||||
<th className="border-b border-[#333] px-3 py-2 text-left text-[13px] font-medium uppercase tracking-wider text-[#999]" style={{ width: '24%' }}>Requirements</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r, i) => (
|
||||
<tr key={i}>
|
||||
<td className="border-b border-[#1a1a1a] px-3 py-2.5">
|
||||
<SwatchSm color={r.fgColor} />
|
||||
<code className="bg-transparent text-[13px] text-[#7cacf8]">{r.fgLabel}</code>
|
||||
</td>
|
||||
<td className="border-b border-[#1a1a1a] px-3 py-2.5">
|
||||
<SwatchSm color={r.bgColor} />
|
||||
<code className="bg-transparent text-[13px] text-[#7cacf8]">{r.bgLabel}</code>
|
||||
</td>
|
||||
<td className="border-b border-[#1a1a1a] px-3 py-2.5">{r.result}</td>
|
||||
<td className="border-b border-[#1a1a1a] px-3 py-2.5 text-[#666]">{r.requirement}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
function Accordion({ title, id, children }: { title: string; id?: string; children: React.ReactNode }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const buttonId = id ? `${id}-button` : undefined;
|
||||
const contentId = id ? `${id}-content` : undefined;
|
||||
return (
|
||||
<div id={id} className="my-2 mb-9 rounded-lg border border-[#222] bg-[#0a0a0a] px-5">
|
||||
<button
|
||||
id={buttonId}
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
aria-controls={contentId}
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex w-full items-center bg-transparent py-4 text-left text-lg font-normal text-white"
|
||||
style={{ border: 'none', cursor: 'pointer' }}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="mr-2 inline-block text-[#7cacf8] transition-transform"
|
||||
style={{ transform: open ? 'rotate(90deg)' : 'none' }}
|
||||
>
|
||||
▸
|
||||
</span>
|
||||
{title}
|
||||
</button>
|
||||
{open && (
|
||||
<div id={contentId} role="region" aria-labelledby={buttonId} className="mb-4 border-t border-[#222] pt-4">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BASE_VIEWER = 'https://ohif-theme-apply.netlify.app/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&theme=';
|
||||
|
||||
function ThemingPageContent() {
|
||||
const ComponentLayout = require('./components/_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./components/_layout/PageHeader').default;
|
||||
const Section = require('./components/_layout/Section').default;
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Colors & Theming"
|
||||
description="Color tokens, themes, and accessibility for the OHIF Viewer"
|
||||
>
|
||||
<PageHeader
|
||||
title="Colors & Theming"
|
||||
description="How color is used in the OHIF Viewer and how to apply new colors and themes for clinical use."
|
||||
/>
|
||||
|
||||
{/* ================================ */}
|
||||
{/* APPLYING THEMES */}
|
||||
{/* ================================ */}
|
||||
|
||||
<Section title="Applying Themes">
|
||||
<div className="text-secondary-foreground space-y-4 text-lg leading-relaxed">
|
||||
<p>
|
||||
The <strong className="text-foreground">Appearance</strong> dialog can be accessed from the settings menu in the
|
||||
main header. Open the <strong className="text-foreground">Theme</strong> dropdown and choose a preset. The
|
||||
interface will update immediately, and your selection is remembered the next time
|
||||
you open the Viewer. Selecting <strong className="text-foreground">Tonal: OHIF Blue</strong> returns to the default.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<em className="text-muted-foreground">Testing custom theme colors:</em> Use this dialog to test your custom themes in
|
||||
the viewer{' '}
|
||||
<a href="#testing-themes" className="text-primary no-underline hover:underline">(see more details)</a>
|
||||
</p>
|
||||
|
||||
<img
|
||||
src="/img/theming-dialog-01-select-theme.png"
|
||||
alt="Appearance modal with the Theme dropdown open, showing the preset options"
|
||||
className="my-4 mb-6 w-full rounded-lg border border-[#222]"
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ================================ */}
|
||||
{/* COLOR TOKENS AND ROLES */}
|
||||
{/* ================================ */}
|
||||
|
||||
<Section title="Color Tokens and Roles">
|
||||
<div className="text-secondary-foreground space-y-4 text-lg leading-relaxed">
|
||||
<p>
|
||||
While these token names are largely shared across many web applications, within the OHIF Viewer design,
|
||||
each one plays a specific role in the interface.
|
||||
</p>
|
||||
|
||||
<img
|
||||
src="/img/theming-color-roles.png"
|
||||
alt="Color roles overview"
|
||||
className="my-6 mb-8 w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Layering Model Accordion */}
|
||||
<Accordion title="Layering Model: colors used for hierarchy" id="layering-model">
|
||||
<p className="text-secondary-foreground mb-4 text-lg">
|
||||
A set of three background colors are used to create a layering system that shows visual depth
|
||||
and hierarchy in the product. Background color use should align with these details:
|
||||
</p>
|
||||
|
||||
<h3 className="mt-7 mb-3 text-lg font-medium text-foreground">Three Levels</h3>
|
||||
|
||||
<p className="text-secondary-foreground mb-4 text-lg">
|
||||
<strong className="text-foreground">Level 1: Surface</strong>{' '}
|
||||
<Swatch color="hsl(0, 0%, 0%)" />{' '}
|
||||
<code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">background</code>
|
||||
</p>
|
||||
<ul className="text-secondary-foreground mb-4 list-disc pl-6 text-lg">
|
||||
<li className="mb-2">The base layer of the entire interface</li>
|
||||
<li className="mb-2">Includes the app shell, panel backgrounds, and empty or negative space</li>
|
||||
<li className="mb-2">In the default OHIF theme, this is black to seamlessly match the viewport background</li>
|
||||
<li className="mb-2">Changing this from black can create more separation from viewports or add a full background color to panels</li>
|
||||
</ul>
|
||||
|
||||
<p className="text-secondary-foreground mb-4 text-lg">
|
||||
<strong className="text-foreground">Level 2: Working Space</strong>{' '}
|
||||
<Swatch color="hsl(234, 64%, 10%)" />{' '}
|
||||
<code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">muted</code> or{' '}
|
||||
<code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">card</code>
|
||||
</p>
|
||||
<ul className="text-secondary-foreground mb-4 list-disc pl-6 text-lg">
|
||||
<li className="mb-2">This is the base content layer of the interface</li>
|
||||
<li className="mb-2">Includes panel content areas, dialog backgrounds, rows of data, etc.</li>
|
||||
<li className="mb-2">This is the base of the working space separated from the viewer content</li>
|
||||
</ul>
|
||||
|
||||
<p className="text-secondary-foreground mb-4 text-lg">
|
||||
<strong className="text-foreground">Level 3: Elevated</strong>{' '}
|
||||
<Swatch color="hsl(219, 90%, 15%)" />{' '}
|
||||
<code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">popover</code>
|
||||
</p>
|
||||
<ul className="text-secondary-foreground mb-4 list-disc pl-6 text-lg">
|
||||
<li className="mb-2">The top-most layer, used for UI that floats above the surface</li>
|
||||
<li className="mb-2">Includes popovers, dropdown menus, floating components, grouping elements such as panel sections</li>
|
||||
<li className="mb-2">Use this for any component that needs to appear "above" the content layer</li>
|
||||
</ul>
|
||||
|
||||
<img
|
||||
src="/img/theming-layers.png"
|
||||
alt="Layering model overview"
|
||||
className="my-6 mb-8 w-full"
|
||||
/>
|
||||
|
||||
<h3 className="mt-7 mb-3 text-lg font-medium text-foreground">Creating Contrast</h3>
|
||||
<ul className="text-secondary-foreground mb-4 list-disc pl-6 text-lg">
|
||||
<li className="mb-2">
|
||||
The <code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">primary</code> color
|
||||
is used across these different layers to show what elements are interactable. Be sure to review any
|
||||
new colors for backgrounds work with <code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">primary</code> as the foreground at each of the three different levels.
|
||||
</li>
|
||||
<li className="mb-2">
|
||||
See the <a href="#creating-themes" className="text-primary no-underline hover:underline">Creating Themes</a> section
|
||||
for details on how different levels of contrast can be used to separate these layers.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="mt-7 mb-3 text-lg font-medium text-foreground">Alpha Colors</h3>
|
||||
<p className="text-secondary-foreground mb-4 text-lg">
|
||||
<code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">primary</code> also uses
|
||||
various alpha values for many components across the product. This allows components to adapt and work
|
||||
well with different background colors or various layers of the interface.
|
||||
</p>
|
||||
</Accordion>
|
||||
|
||||
{/* Token Sections */}
|
||||
<div className="ml-[248px]">
|
||||
<h3 className="mt-12 mb-4 text-[22px] font-normal text-foreground">Interactive or Currently Active</h3>
|
||||
</div>
|
||||
|
||||
<TokenRow color="hsl(214, 98%, 60%)" token="primary">
|
||||
The most important color in the system. Primary represents anything the user can interact with
|
||||
which includes: tool actions, toggles for showing and hiding content, links, navigation elements, etc.
|
||||
</TokenRow>
|
||||
|
||||
<TokenRow color="hsl(191, 74%, 63%)" token="highlight">
|
||||
The brightest color in the system. Highlight is used to show the user what is "currently active".
|
||||
It should not be used more than a few times in flows or screen views.
|
||||
<ul className="mt-2 list-disc pl-6">
|
||||
<li className="mb-1">Highlight border around the active viewport</li>
|
||||
<li className="mb-1">Highlight background to show the active tool</li>
|
||||
<li className="mb-1">Highlight color at the end of a selected data row</li>
|
||||
</ul>
|
||||
</TokenRow>
|
||||
|
||||
<div className="ml-[248px]">
|
||||
<h3 className="mt-12 mb-4 text-[22px] font-normal text-foreground">Text and Content</h3>
|
||||
</div>
|
||||
|
||||
<TokenRow color="hsl(0, 0%, 98%)" token="foreground">
|
||||
Used for standard text or for more important text to stand out. This includes headings,
|
||||
labels for controls, or anything critical for what the user is doing.
|
||||
</TokenRow>
|
||||
|
||||
<TokenRow color="hsl(200, 46%, 65%)" token="muted-foreground">
|
||||
Used for secondary text that is paired with standard text. This includes sub titles,
|
||||
help text, or anything that needs to play a reduced role for content.
|
||||
</TokenRow>
|
||||
|
||||
<div className="ml-[248px]">
|
||||
<h3 className="mt-12 mb-4 text-[22px] font-normal text-foreground">Background Colors</h3>
|
||||
<p className="text-secondary-foreground mb-4 text-lg">
|
||||
See the <a href="#layering-model" className="text-primary no-underline hover:underline">Layering Model</a> above for detailed notes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<TokenRow color="hsl(0, 0%, 0%)" token="background">
|
||||
The base background layer of the product (app shell, panels, negative spaces)
|
||||
</TokenRow>
|
||||
<TokenRow color="hsl(234, 64%, 10%)" token="muted">
|
||||
The second background layer where most content lives
|
||||
</TokenRow>
|
||||
<TokenRow color="hsl(219, 90%, 15%)" token="popover">
|
||||
The third background layer for any content that needs to be elevated
|
||||
</TokenRow>
|
||||
|
||||
<div className="ml-[248px]">
|
||||
<h3 className="mt-12 mb-4 text-[22px] font-normal text-foreground">Supporting Colors</h3>
|
||||
</div>
|
||||
|
||||
<TokenRow color="hsl(214, 65%, 36%)" token="secondary">
|
||||
Used for secondary buttons in the interface
|
||||
</TokenRow>
|
||||
<TokenRow color="hsl(200, 50%, 84%)" token="secondary-foreground">
|
||||
Text color used on secondary
|
||||
</TokenRow>
|
||||
<TokenRow color="hsl(217, 79%, 24%)" token="accent">
|
||||
Sometimes used for hover states or other interaction feedback
|
||||
</TokenRow>
|
||||
<TokenRow color="hsl(0, 0%, 98%)" token="accent-foreground">
|
||||
Text color used on accent
|
||||
</TokenRow>
|
||||
<TokenRow color="hsl(0, 62.8%, 30.6%)" token="destructive">
|
||||
Used for any destructive action or operations in the interface
|
||||
</TokenRow>
|
||||
<TokenRow color="hsl(0, 0%, 98%)" token="destructive-foreground">
|
||||
Text color used on destructive
|
||||
</TokenRow>
|
||||
|
||||
<div className="ml-[248px]">
|
||||
<h3 className="mt-12 mb-4 text-[22px] font-normal text-foreground">Inputs and Borders</h3>
|
||||
</div>
|
||||
|
||||
<TokenRow color="hsl(236, 52%, 30%)" token="input">
|
||||
Used on input fields and other interactive components
|
||||
</TokenRow>
|
||||
<TokenRow color="hsl(0, 0%, 14.9%)" token="border">
|
||||
A neutral color used minimally as separators
|
||||
</TokenRow>
|
||||
<TokenRow color="hsl(214, 98%, 60%)" token="ring">
|
||||
Used as focus rings for accessibility — indicates which components are currently selected
|
||||
(in use or keyboard highlighted)
|
||||
</TokenRow>
|
||||
|
||||
<div className="ml-[248px]">
|
||||
<h3 className="mt-12 mb-4 text-[22px] font-normal text-foreground">Neutral Colors</h3>
|
||||
<p className="text-secondary-foreground mb-4 text-lg">
|
||||
Neutral colors are used sparingly across the interface in areas such as viewports.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<TokenRow color="hsl(213, 22%, 59%)" token="neutral">
|
||||
Used for elements like scrollbars in viewports
|
||||
</TokenRow>
|
||||
<TokenRow color="hsl(214, 69%, 81%)" token="neutral-light">
|
||||
Lighter text that appears over dark background viewports
|
||||
</TokenRow>
|
||||
<TokenRow color="hsl(214, 16%, 21%)" token="neutral-dark">
|
||||
Darker text that appears over light background viewports
|
||||
</TokenRow>
|
||||
</Section>
|
||||
|
||||
{/* ================================ */}
|
||||
{/* CREATING THEMES */}
|
||||
{/* ================================ */}
|
||||
|
||||
<Section title="Creating Themes">
|
||||
<div className="text-secondary-foreground space-y-4 text-lg leading-relaxed">
|
||||
<img
|
||||
src="/img/theming-themes.png"
|
||||
alt="Creating themes overview"
|
||||
className="my-6 mb-8 w-full"
|
||||
/>
|
||||
|
||||
<p>
|
||||
Theming in the OHIF Viewer works by replacing the default color tokens and working with the product
|
||||
color system outlined in{' '}
|
||||
<a href="#color-tokens-and-roles" className="text-primary no-underline hover:underline">Color Tokens and Roles</a> and{' '}
|
||||
<a href="#layering-model" className="text-primary no-underline hover:underline">Layering Model</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
When updating any colors in the system, follow the{' '}
|
||||
<a href="#accessibility" className="text-primary no-underline hover:underline">Accessibility</a> section to ensure the
|
||||
product retains accessibility standards.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
OHIF can be themed with a few different approaches. Use the guidance below to match the desired
|
||||
product or brand feel.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 className="mt-12 mb-4 text-[22px] font-normal text-foreground">Approach 1: Tonal</h3>
|
||||
|
||||
<div className="text-secondary-foreground space-y-4 text-lg leading-relaxed">
|
||||
<p>
|
||||
A tonal theme applies a hue across the background layers of the product which gives the product a
|
||||
strong color identity. The default Viewer theme uses this approach with blue.
|
||||
</p>
|
||||
|
||||
<ul className="mb-4 list-disc pl-6">
|
||||
<li className="mb-2">All three background layers work within the theme's color hue</li>
|
||||
<li className="mb-2">Different hues can be used in place of the Viewer's default blue</li>
|
||||
<li className="mb-2">A tonal theme does not need to match the saturation of the default, see examples</li>
|
||||
<li className="mb-2">Use this approach if you like the general feel of the Viewer, but want it to be more unique</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Tonal theme grid */}
|
||||
<div className="my-9 mb-8 grid items-center gap-x-2.5 gap-y-2.5" style={{ gridTemplateColumns: '82px 59px 59px auto auto auto' }}>
|
||||
<span className="pb-1.5 text-[11px] font-normal text-[#555]">Backgrounds</span>
|
||||
<span className="pb-1.5 text-[11px] font-normal text-[#555]">Text</span>
|
||||
<span className="pb-1.5 text-[11px] font-normal text-[#555]">Interactive</span>
|
||||
<span className="pb-1.5 pl-3.5 text-[11px] font-normal text-[#555]">Theme name</span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
|
||||
<ThemeRow
|
||||
backgrounds={['hsl(0, 0%, 0%)', 'hsl(234, 64%, 10%)', 'hsl(219, 90%, 15%)']}
|
||||
text={['hsl(200, 46%, 65%)', 'hsl(0, 0%, 98%)']}
|
||||
interactive={['hsl(214, 98%, 60%)', 'hsl(191, 74%, 63%)']}
|
||||
name="Default Blue"
|
||||
previewUrl={BASE_VIEWER + 'default'}
|
||||
themeName="default"
|
||||
/>
|
||||
<ThemeRow
|
||||
backgrounds={['hsl(270, 45%, 6%)', 'hsl(268, 40%, 10%)', 'hsl(264, 48%, 13%)']}
|
||||
text={['hsl(272, 30%, 60%)', 'hsl(280, 15%, 96%)']}
|
||||
interactive={['hsl(270, 85%, 65%)', 'hsl(292, 75%, 62%)']}
|
||||
name="Orchid"
|
||||
previewUrl={BASE_VIEWER + 'orchid'}
|
||||
themeName="orchid"
|
||||
/>
|
||||
<ThemeRow
|
||||
backgrounds={['hsl(155, 40%, 5%)', 'hsl(152, 35%, 9%)', 'hsl(167, 65%, 10%)']}
|
||||
text={['hsl(148, 25%, 55%)', 'hsl(140, 15%, 95%)']}
|
||||
interactive={['hsl(152, 75%, 40%)', 'hsl(152, 79%, 52%)']}
|
||||
name="Verdant"
|
||||
previewUrl={BASE_VIEWER + 'verdant'}
|
||||
themeName="verdant"
|
||||
/>
|
||||
<ThemeRow
|
||||
backgrounds={['hsl(0, 0%, 0%)', 'hsl(203, 39%, 9%)', 'hsl(202, 54%, 11%)']}
|
||||
text={['hsl(185, 30%, 60%)', 'hsl(180, 10%, 97%)']}
|
||||
interactive={['hsl(175, 85%, 42%)', 'hsl(173, 81%, 52%)']}
|
||||
name="Arctic"
|
||||
previewUrl={BASE_VIEWER + 'arctic'}
|
||||
themeName="arctic"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="mt-12 mb-4 text-[22px] font-normal text-foreground">Approach 2: Neutral</h3>
|
||||
|
||||
<div className="text-secondary-foreground space-y-4 text-lg leading-relaxed">
|
||||
<p>
|
||||
A neutral theme still keeps the full color system in place, but reduces the reliance on color in backgrounds.
|
||||
</p>
|
||||
|
||||
<ul className="mb-4 list-disc pl-6">
|
||||
<li className="mb-2">All three background layers are more neutral and are more subtle in their differences</li>
|
||||
<li className="mb-2">A more unique primary can be chosen if background colors are more similar</li>
|
||||
<li className="mb-2">Brand color can be emphasized in detailed accents rather than color fills</li>
|
||||
<li className="mb-2">This approach reduces the focus on color to emphasize image content</li>
|
||||
<li className="mb-2">Accessibility can be simpler, but testing is still recommended</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Neutral theme grid */}
|
||||
<div className="my-9 mb-8 grid items-center gap-x-2.5 gap-y-2.5" style={{ gridTemplateColumns: '82px 59px 59px auto auto auto' }}>
|
||||
<span className="pb-1.5 text-[11px] font-normal text-[#555]">Backgrounds</span>
|
||||
<span className="pb-1.5 text-[11px] font-normal text-[#555]">Text</span>
|
||||
<span className="pb-1.5 text-[11px] font-normal text-[#555]">Interactive</span>
|
||||
<span className="pb-1.5 pl-3.5 text-[11px] font-normal text-[#555]">Theme name</span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
|
||||
<ThemeRow
|
||||
backgrounds={['hsl(240, 15%, 3%)', 'hsl(240, 12%, 8%)', 'hsl(225, 45%, 13%)']}
|
||||
text={['hsl(210, 40%, 70%)', 'hsl(0, 0%, 99%)']}
|
||||
interactive={['hsl(210, 100%, 62%)', 'hsl(188, 90%, 58%)']}
|
||||
name="Midnight"
|
||||
previewUrl={BASE_VIEWER + 'midnight'}
|
||||
themeName="midnight"
|
||||
/>
|
||||
<ThemeRow
|
||||
backgrounds={['hsl(0, 0%, 0%)', 'hsl(0, 0%, 7%)', 'hsl(0, 0%, 9%)']}
|
||||
text={['hsl(0, 0%, 60%)', 'hsl(0, 0%, 96%)']}
|
||||
interactive={['hsl(230, 75%, 55%)', 'hsl(217, 97%, 52%)']}
|
||||
name="Slate"
|
||||
previewUrl={BASE_VIEWER + 'slate'}
|
||||
themeName="slate"
|
||||
/>
|
||||
<ThemeRow
|
||||
backgrounds={['hsl(0, 0%, 0%)', 'hsl(214, 28%, 5%)', 'hsl(215, 30%, 8%)']}
|
||||
text={['hsl(215, 18%, 48%)', 'hsl(215, 15%, 82%)']}
|
||||
interactive={['hsl(200, 43%, 48%)', 'hsl(184, 53%, 54%)']}
|
||||
name="Deep"
|
||||
previewUrl={BASE_VIEWER + 'deep'}
|
||||
themeName="deep"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="mt-12 mb-4 text-[22px] font-normal text-foreground">Approach 3: Custom</h3>
|
||||
|
||||
<div className="text-secondary-foreground space-y-4 text-lg leading-relaxed">
|
||||
<p>
|
||||
Themes do not need to follow any of these models and more unique combinations can be
|
||||
considered by following the core color rules and product principles:
|
||||
</p>
|
||||
|
||||
<ul className="mb-4 list-disc pl-6">
|
||||
<li className="mb-2">Three levels of background are separated enough</li>
|
||||
<li className="mb-2">Primary serves its purpose to show to users what can be interacted with</li>
|
||||
<li className="mb-2">Colors are <a href="#accessibility" className="text-primary no-underline hover:underline">accessible</a> in all content scenarios</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Theming is flexible enough to support any direction, as long as the colors still work in the product.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 className="mt-12 mb-4 text-[22px] font-normal text-foreground">Experiment with the Color Tool</h3>
|
||||
|
||||
<div className="text-secondary-foreground space-y-4 text-lg leading-relaxed">
|
||||
<p>
|
||||
We built a small public web tool that edits the color tokens directly, so you can experiment with
|
||||
colors and see the results instantly. When a combination looks right, use its{' '}
|
||||
<strong className="text-foreground">Copy Theme</strong> button and paste the result into the
|
||||
Custom Theme field in the Viewer’s Appearance dialog (see{' '}
|
||||
<a href="#testing-themes" className="text-primary no-underline hover:underline">Testing Themes</a>) — an easy
|
||||
way to iterate quickly and find what works best. The tool is optional; you can also edit tokens by hand.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a
|
||||
href="https://ohif-theming-beta.netlify.app/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary no-underline hover:underline"
|
||||
>
|
||||
Open the color tool →
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 id="testing-themes" className="mt-12 mb-4 scroll-mt-20 text-[22px] font-normal text-foreground">
|
||||
Testing Themes in the Viewer
|
||||
</h3>
|
||||
|
||||
<div className="text-secondary-foreground space-y-4 text-lg leading-relaxed">
|
||||
<p>
|
||||
You can preview a theme without writing any code using the{' '}
|
||||
<strong className="text-foreground">Custom Theme</strong> option in the Appearance dialog
|
||||
(settings menu in the main header) — useful while designing a theme around the approaches above.
|
||||
</p>
|
||||
|
||||
<img
|
||||
src="/img/theming-dialog-02-default.png"
|
||||
alt="Appearance modal showing the Custom Theme button below the Theme dropdown"
|
||||
className="my-4 mb-6 w-full rounded-lg border border-[#222]"
|
||||
/>
|
||||
|
||||
<p>
|
||||
The <strong className="text-foreground">Custom Theme</strong> option opens a text field where
|
||||
you paste CSS color tokens and press <strong className="text-foreground">Apply</strong> to see
|
||||
them right away. <strong className="text-foreground">Clear</strong> removes them and returns to
|
||||
the default. A pasted theme is remembered across page reloads.
|
||||
</p>
|
||||
|
||||
<img
|
||||
src="/img/theming-dialog-03-custom.png"
|
||||
alt="Custom theme text field for pasting color tokens, with Apply and Clear buttons"
|
||||
className="my-4 mb-6 w-full rounded-lg border border-[#222]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="mt-12 mb-4 text-[22px] font-normal text-foreground">For Developers: Adding a New Preset</h3>
|
||||
|
||||
<div className="text-secondary-foreground space-y-4 text-lg leading-relaxed">
|
||||
<p>
|
||||
A preset lives in two places. The <strong className="text-foreground">CSS block is what actually renders</strong>;
|
||||
the JSON only registers the preset so it appears in the dropdown.
|
||||
</p>
|
||||
|
||||
<ul className="mb-4 list-disc pl-6">
|
||||
<li className="mb-2">
|
||||
<strong className="text-foreground">themes.css</strong> — add a{' '}
|
||||
<code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">.theme-{name}</code>{' '}
|
||||
block to <code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">platform/ui-next/src/themes/themes.css</code>{' '}
|
||||
with the full token set. This is the source of truth for the theme’s colors.
|
||||
</li>
|
||||
<li className="mb-2">
|
||||
<strong className="text-foreground">{name}.json</strong> — create{' '}
|
||||
<code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">platform/ui-next/src/themes/{name}.json</code>{' '}
|
||||
providing <code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">name</code> and{' '}
|
||||
<code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">label</code> (the dropdown entry).
|
||||
Its <code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">cssVars.dark</code> values
|
||||
are <strong className="text-foreground">not read at runtime</strong> — only{' '}
|
||||
<code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">name</code> and{' '}
|
||||
<code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">label</code> are used —
|
||||
so the <code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">.theme-{name}</code>{' '}
|
||||
CSS block above is what takes effect.
|
||||
</li>
|
||||
<li className="mb-2">
|
||||
<strong className="text-foreground">index.ts</strong> — import the JSON in{' '}
|
||||
<code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">platform/ui-next/src/themes/index.ts</code>{' '}
|
||||
and add it to the <code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">themePresets</code> array.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
The dropdown, persistence, and theme switching then work automatically. The JSON{' '}
|
||||
<code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">name</code> field must match the
|
||||
CSS class suffix (e.g. <code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">"name": "orchid"</code>{' '}
|
||||
↔ <code className="rounded bg-[#111] px-1.5 py-0.5 text-sm text-[#7cacf8]">.theme-orchid</code>).
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ================================ */}
|
||||
{/* ACCESSIBILITY */}
|
||||
{/* ================================ */}
|
||||
|
||||
<Section title="Accessibility">
|
||||
<div className="text-secondary-foreground space-y-4 text-lg leading-relaxed">
|
||||
<p>
|
||||
The OHIF Viewer is a medical imaging product. The interface supports clinicians and other
|
||||
professionals working and reading images. Colors in theming should never get in the way.
|
||||
A theme needs to remain accessible with color contrast and other accessibility standards.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The default OHIF theme has been tested and works out of the box to meet these standards.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Color contrast must meet{' '}
|
||||
<a
|
||||
href="https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary no-underline hover:underline"
|
||||
>
|
||||
WCAG 2.1 Success Criterion 1.4.3 Contrast (Minimum), Level AA
|
||||
</a>
|
||||
: text needs a contrast ratio of at least <strong className="text-foreground">4.5:1</strong> against
|
||||
its background (<strong className="text-foreground">3:1</strong> for large text — 24px, or 18.66px bold).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The pairs below show the <strong className="text-foreground">default OHIF theme</strong> and how its
|
||||
colors meet the contrast requirements. The <strong className="text-foreground">Result</strong> column
|
||||
is each pair's actual contrast ratio, and the <strong className="text-foreground">Guidance</strong>{' '}
|
||||
column is the minimum it must meet. When creating your own theme, check every foreground against each
|
||||
background layer it can appear on.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 className="mt-12 mb-4 text-[22px] font-normal text-foreground">
|
||||
Test Primary Across All Three Background Colors
|
||||
</h3>
|
||||
|
||||
<p className="text-secondary-foreground mb-4 text-lg">
|
||||
The most important color that shows content that can be interactive needs to be tested across
|
||||
all three background layers.
|
||||
</p>
|
||||
|
||||
<ContrastTable rows={[
|
||||
{ fgLabel: 'primary', fgColor: 'hsl(214, 98%, 60%)', bgLabel: 'background', bgColor: 'hsl(0, 0%, 0%)', result: '6.3:1', requirement: '≥ 4.5:1' },
|
||||
{ fgLabel: 'primary', fgColor: 'hsl(214, 98%, 60%)', bgLabel: 'muted', bgColor: 'hsl(234, 64%, 10%)', result: '5.7:1', requirement: '≥ 4.5:1' },
|
||||
{ fgLabel: 'primary', fgColor: 'hsl(214, 98%, 60%)', bgLabel: 'popover', bgColor: 'hsl(219, 90%, 15%)', result: '5.0:1', requirement: '≥ 4.5:1' },
|
||||
]} />
|
||||
|
||||
<h3 className="mt-12 mb-4 text-[22px] font-normal text-foreground">
|
||||
Test Text Content Across All Three Background Colors
|
||||
</h3>
|
||||
|
||||
<p className="text-secondary-foreground mb-4 text-lg">
|
||||
Text appears on each background layer. Be sure to test each to ensure readability.
|
||||
</p>
|
||||
|
||||
<ContrastTable rows={[
|
||||
{ fgLabel: 'foreground', fgColor: 'hsl(0, 0%, 98%)', bgLabel: 'background', bgColor: 'hsl(0, 0%, 0%)', result: '20.1:1', requirement: '≥ 4.5:1' },
|
||||
{ fgLabel: 'foreground', fgColor: 'hsl(0, 0%, 98%)', bgLabel: 'muted', bgColor: 'hsl(234, 64%, 10%)', result: '18.3:1', requirement: '≥ 4.5:1' },
|
||||
{ fgLabel: 'foreground', fgColor: 'hsl(0, 0%, 98%)', bgLabel: 'popover', bgColor: 'hsl(219, 90%, 15%)', result: '15.9:1', requirement: '≥ 4.5:1' },
|
||||
]} />
|
||||
|
||||
<ContrastTable rows={[
|
||||
{ fgLabel: 'muted-foreground', fgColor: 'hsl(200, 46%, 65%)', bgLabel: 'background', bgColor: 'hsl(0, 0%, 0%)', result: '9.2:1', requirement: '≥ 4.5:1' },
|
||||
{ fgLabel: 'muted-foreground', fgColor: 'hsl(200, 46%, 65%)', bgLabel: 'muted', bgColor: 'hsl(234, 64%, 10%)', result: '8.4:1', requirement: '≥ 4.5:1' },
|
||||
{ fgLabel: 'muted-foreground', fgColor: 'hsl(200, 46%, 65%)', bgLabel: 'popover', bgColor: 'hsl(219, 90%, 15%)', result: '7.3:1', requirement: '≥ 4.5:1' },
|
||||
]} />
|
||||
|
||||
<h3 className="mt-12 mb-4 text-[22px] font-normal text-foreground">
|
||||
Test Viewport Text If You Change It
|
||||
</h3>
|
||||
|
||||
<p className="text-secondary-foreground mb-4 text-lg">
|
||||
Be sure to test the viewport neutral text over standard viewport backgrounds, but increase
|
||||
the contrast here as much as possible since text needs to remain readable over various image content.
|
||||
</p>
|
||||
|
||||
<ContrastTable rows={[
|
||||
{ fgLabel: 'neutral-light', fgColor: 'hsl(214, 69%, 81%)', bgLabel: '#000000', bgColor: '#000000', result: '12.5:1', requirement: '≥ 4.5:1 (aim higher)' },
|
||||
{ fgLabel: 'neutral-dark', fgColor: 'hsl(214, 16%, 21%)', bgLabel: '#FFFFFF', bgColor: '#ffffff', result: '12.5:1', requirement: '≥ 4.5:1 (aim higher)' },
|
||||
]} />
|
||||
|
||||
<h3 className="mt-12 mb-4 text-[22px] font-normal text-foreground">
|
||||
Test Foreground Colors Over Their Backgrounds
|
||||
</h3>
|
||||
|
||||
<ContrastTable rows={[
|
||||
{ fgLabel: 'foreground', fgColor: 'hsl(0, 0%, 98%)', bgLabel: 'primary/80', bgColor: 'hsla(214, 98%, 60%, 0.8)', result: '4.7:1', requirement: '≥ 4.5:1' },
|
||||
{ fgLabel: 'secondary-foreground', fgColor: 'hsl(200, 50%, 84%)', bgLabel: 'secondary', bgColor: 'hsl(214, 65%, 36%)', result: '5.3:1', requirement: '≥ 4.5:1' },
|
||||
{ fgLabel: 'accent-foreground', fgColor: 'hsl(0, 0%, 98%)', bgLabel: 'accent', bgColor: 'hsl(217, 79%, 24%)', result: '11.9:1', requirement: '≥ 4.5:1' },
|
||||
{ fgLabel: 'destructive-foreground', fgColor: 'hsl(0, 0%, 98%)', bgLabel: 'destructive', bgColor: 'hsl(0, 62.8%, 30.6%)', result: '9.6:1', requirement: '≥ 4.5:1' },
|
||||
]} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ThemingPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <ThemingPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
@ -1,434 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import '../css/custom.css';
|
||||
|
||||
import Layout from '@theme/Layout';
|
||||
import { Label } from '../../../ui-next/src/components/Label';
|
||||
import { Input } from '../../../ui-next/src/components/Input';
|
||||
import { Separator } from '../../../ui-next/src/components/Separator';
|
||||
import { Tabs, TabsList, TabsTrigger } from '../../../ui-next/src/components/Tabs';
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from '../../../ui-next/src/components/Select';
|
||||
import { Button } from '../../../ui-next/src/components/Button';
|
||||
import { Switch } from '../../../ui-next/src/components/Switch';
|
||||
import { Checkbox } from '../../../ui-next/src/components/Checkbox';
|
||||
import { Toggle } from '../../../ui-next/src/components/Toggle';
|
||||
import { Slider } from '../../../ui-next/src/components/Slider';
|
||||
import { ScrollArea } from '../../../ui-next/src/components/ScrollArea';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from '../../../ui-next/src/components/DropdownMenu';
|
||||
import { Icons } from '../../../ui-next/src/components/Icons';
|
||||
import { Toaster, toast } from '../../../ui-next/src/components/Sonner';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from '../../../ui-next/src/components/Card';
|
||||
|
||||
interface ShowcaseRowProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export default function ComponentShowcase() {
|
||||
// Handlers to trigger different types of toasts
|
||||
const triggerSuccess = () => {
|
||||
toast.success('This is a success toast!');
|
||||
};
|
||||
|
||||
const triggerError = () => {
|
||||
toast.error('This is an error toast!');
|
||||
};
|
||||
|
||||
const triggerInfo = () => {
|
||||
toast.info('This is an info toast!');
|
||||
};
|
||||
|
||||
const triggerWarning = () => {
|
||||
toast.warning('This is a warning toast!');
|
||||
};
|
||||
|
||||
// Handler to trigger a toast.promise example
|
||||
const triggerPromiseToast = () => {
|
||||
const promise = () =>
|
||||
new Promise<{ name: string }>(resolve =>
|
||||
setTimeout(() => resolve({ name: 'Segmentation 1' }), 3000)
|
||||
);
|
||||
|
||||
toast.promise(promise(), {
|
||||
loading: 'Loading Segmentation...',
|
||||
success: data => `${data.name} has been added`,
|
||||
error: 'Error',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with description
|
||||
const triggerDescriptionToast = () => {
|
||||
toast.success('Success heading', {
|
||||
description: 'This is a detailed description of the success message.',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with an action button
|
||||
const triggerActionButtonToast = () => {
|
||||
toast.info('No active segmentation detected', {
|
||||
description: 'Create a segmentation before using the Brush',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with a cancel button
|
||||
const triggerCancelButtonToast = () => {
|
||||
toast.error('No active segmentation detected', {
|
||||
description: 'Create a segmentation before using the Brush',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with both action and cancel buttons
|
||||
const triggerCombinedToast = () => {
|
||||
toast.warning('Warning!', {
|
||||
description: 'This is a warning with both action and cancel buttons.',
|
||||
action: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => alert('Retry action clicked')}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
),
|
||||
cancel: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => toast.dismiss()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a loading toast using Toaster's default loading icon
|
||||
const showLoadingToast = () => {
|
||||
toast.loading('Loading your data...');
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
title="Colors and Typography"
|
||||
description="Colors and Typography"
|
||||
>
|
||||
<div className="text-foreground min-h-screen bg-background">
|
||||
<div className="mx-auto my-4 max-w-5xl pt-4 pb-6">
|
||||
{/* Navigation cards */}
|
||||
<div className="mb-8 grid grid-cols-1 gap-5 md:grid-cols-3">
|
||||
<a
|
||||
href="/colors-and-type"
|
||||
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
|
||||
>
|
||||
<Card className="hover:bg-primary/30 w-full transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground text-xl">
|
||||
<Icons.ColorChange className="h-12 w-12" />
|
||||
Colors & Typography
|
||||
</CardTitle>
|
||||
<CardDescription className="text-lg">
|
||||
Color Palette and Typography Guidelines
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</a>
|
||||
<a
|
||||
href="/components-list"
|
||||
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
|
||||
>
|
||||
<Card className="hover:bg-primary/30 w-full transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground text-xl">
|
||||
<Icons.ColorChange className="h-12 w-12" />
|
||||
Components
|
||||
</CardTitle>
|
||||
<CardDescription className="text-lg">
|
||||
Essential UI Components with Variants
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</a>
|
||||
<a
|
||||
href="/patterns"
|
||||
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
|
||||
>
|
||||
<Card className="hover:bg-primary/30 w-full transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground text-xl">
|
||||
<Icons.ColorChange className="h-12 w-12" />
|
||||
Patterns
|
||||
</CardTitle>
|
||||
<CardDescription className="text-lg">
|
||||
Component-Based Layout Examples
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h1 className="text-foreground ml-6 mb-6 text-5xl">Colors & Typography</h1>
|
||||
|
||||
<ShowcaseRow
|
||||
title="Colors"
|
||||
description="Updated color variables for new components."
|
||||
code={`Code Example Coming Soon`}
|
||||
>
|
||||
<div className="mb-6 grid grid-cols-[28%,1fr] items-start gap-x-8">
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-highlight h-[30px] w-[30px] rounded"></div>
|
||||
<span className="text-foreground text-lg">highlight</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3"></div>
|
||||
</div>
|
||||
<div className="text-secondary-foreground flex items-center pt-1.5 text-lg">
|
||||
Used for active or selected elements in the Viewer.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 grid grid-cols-[28%,1fr] items-start gap-x-8">
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-primary h-[30px] w-[30px] rounded"></div>
|
||||
<span className="text-foreground text-lg">primary</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3"></div>
|
||||
</div>
|
||||
<div className="text-secondary-foreground flex items-center pt-1.5 text-lg">
|
||||
Used for Actions. Icons use 'primary' at 100% opacity while various components will
|
||||
use a reduced opacity. Hover and other states increase the opacity.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 grid grid-cols-[28%,1fr] items-start gap-x-8">
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-popover h-[30px] w-[30px] rounded"></div>
|
||||
<span className="text-foreground text-lg">popover</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-muted h-[30px] w-[30px] rounded"></div>
|
||||
<span className="text-foreground text-lg">muted</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-background border-input h-[30px] w-[30px] rounded border"></div>
|
||||
<span className="text-foreground text-lg">background</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-secondary-foreground flex items-center pt-1.5 text-lg">
|
||||
These three colors are used as background colors. For the lowest level above black
|
||||
use 'background'. For normal panel backgrounds and other interactive components, use
|
||||
'muted'. For elements such as menus and popovers, use 'popover'.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 grid grid-cols-[28%,1fr] items-start gap-x-8">
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-foreground h-[30px] w-[30px] rounded"></div>
|
||||
<span className="text-foreground text-lg">foreground</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-muted-foreground h-[30px] w-[30px] rounded"></div>
|
||||
<span className="text-foreground text-lg">muted-foreground</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-secondary-foreground flex items-center pt-1.5 text-lg">
|
||||
For primary and important text, use 'foreground'. When secondary text is available,
|
||||
use 'muted-foreground' to create separation and readability.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 grid grid-cols-[28%,1fr] items-start gap-x-8">
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-input h-[30px] w-[30px] rounded"></div>
|
||||
<span className="text-foreground text-lg">input</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-neutral h-[30px] w-[30px] rounded"></div>
|
||||
<span className="text-foreground text-lg">neutral</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-secondary-foreground flex items-center pt-1.5 text-lg">
|
||||
Used for borders and UI elements. 'neutral' is typically used at 50% opacity for
|
||||
elements such as scrollbars and will work over light and dark backgrounds
|
||||
</div>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
|
||||
<ShowcaseRow
|
||||
title="Typography"
|
||||
description="Type variables and guidelines"
|
||||
code={`
|
||||
Code example coming soon
|
||||
`}
|
||||
>
|
||||
<div className="mb-6 grid grid-cols-[28%,1fr] items-start gap-x-8">
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center space-x-3 text-base">
|
||||
<span className="text-foreground">text-base</span>
|
||||
<span className="text-muted-foreground">13px</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3"></div>
|
||||
</div>
|
||||
<div className="text-secondary-foreground flex items-center pt-0 text-lg">
|
||||
text-base is used as the base font size of the Viewer interface. Use when putting
|
||||
text in panels or other interface elements next to medical images.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 grid grid-cols-[28%,1fr] items-start gap-x-8">
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center space-x-3 text-lg">
|
||||
<span className="text-foreground">text-lg</span>
|
||||
<span className="text-muted-foreground">14px</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3"></div>
|
||||
</div>
|
||||
<div className="text-secondary-foreground flex items-center pt-0 text-lg">
|
||||
text-lg can be used for dialog text or important messaging text within the Viewer.
|
||||
Use this font size for easier reading on other standard text pages.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 grid grid-cols-[28%,1fr] items-start gap-x-8">
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center space-x-3 text-xl">
|
||||
<span className="text-foreground">text-xl</span>
|
||||
<span className="text-muted-foreground">16px</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3"></div>
|
||||
</div>
|
||||
<div className="text-secondary-foreground flex items-center pt-0 text-lg">
|
||||
text-xl can be used as headings within dialogs or messaging.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 grid grid-cols-[28%,1fr] items-start gap-x-8">
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center space-x-3 text-2xl">
|
||||
<span className="text-foreground">text-2xl</span>
|
||||
<span className="text-muted-foreground">18px</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3"></div>
|
||||
</div>
|
||||
<div className="text-secondary-foreground flex items-center pt-0 text-lg">
|
||||
text-2xl can be used for page headers in the Viewer application or as dialog titles.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 grid grid-cols-[28%,1fr] items-start gap-x-8">
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center space-x-3 text-3xl">
|
||||
<span className="text-foreground">text-3xl</span>
|
||||
<span className="text-muted-foreground">20px</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3"></div>
|
||||
</div>
|
||||
<div className="text-secondary-foreground flex items-center pt-0 text-lg">
|
||||
text-3xl can be used for extra large text size in the application.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 grid grid-cols-[28%,1fr] items-start gap-x-8">
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center space-x-3 text-sm">
|
||||
<span className="text-foreground">text-sm</span>
|
||||
<span className="text-muted-foreground">12px</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3"></div>
|
||||
</div>
|
||||
<div className="text-secondary-foreground flex items-center pt-0 text-lg">
|
||||
text-sm can be used for details that do not need to be standard sizes in the Viewer.
|
||||
</div>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) {
|
||||
const [showCode, setShowCode] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="bg-background mb-8 rounded-lg p-6">
|
||||
<div className="mb-4 flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="text-highlight text-2xl">{title}</h2>
|
||||
</div>
|
||||
<Button
|
||||
className="text-primary"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowCode(!showCode)}
|
||||
>
|
||||
{showCode ? 'Hide Code' : 'Show Code'} <Icons.Code className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-3">
|
||||
<div className="text-lg md:col-span-1">
|
||||
{description && <p className="text-secondary-foreground mt-2">{description}</p>}
|
||||
</div>
|
||||
<div className="flex min-h-[120px] items-center md:col-span-2">
|
||||
<div className="showcase-content">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
{showCode && (
|
||||
<pre className="border-input mt-4 overflow-x-auto rounded-md border bg-background p-4 text-sm">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) {
|
||||
// const [showCode, setShowCode] = useState(false);
|
||||
|
||||
// return (
|
||||
// <div className="bg-background mb-8 rounded-lg p-6">
|
||||
// <div className="mb-4 flex items-start justify-between">
|
||||
// <div>
|
||||
// <h2 className="text-2xl font-bold">{title}</h2>
|
||||
// {description && <p className="text-secondary-foreground mt-1">{description}</p>}
|
||||
// </div>
|
||||
// <Button
|
||||
// className="text-primary"
|
||||
// variant="outline"
|
||||
// size="sm"
|
||||
// onClick={() => setShowCode(!showCode)}
|
||||
// >
|
||||
// {showCode ? 'Hide Code' : 'Show Code'} <Icons.Code className="ml-2 h-4 w-4" />
|
||||
// </Button>
|
||||
// </div>
|
||||
// <div className="showcase-content mb-4">{children}</div>
|
||||
// {showCode && (
|
||||
// <pre className="mt-4 overflow-x-auto rounded-md bg-background p-4 text-sm">
|
||||
// <code>{code}</code>
|
||||
// </pre>
|
||||
// )}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
@ -1,142 +0,0 @@
|
||||
import React from 'react';
|
||||
import '../css/custom.css';
|
||||
import Layout from '@theme/Layout';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
/**
|
||||
* Components List page that displays all available UI components
|
||||
*/
|
||||
export default function ComponentsList() {
|
||||
return (
|
||||
<Layout title="Components" description="OHIF Viewer Components">
|
||||
<BrowserOnly fallback={<></>}>
|
||||
{() => {
|
||||
// Dynamically require all sub-components to avoid SSR issues
|
||||
const { TooltipProvider } =
|
||||
require('../../../ui-next/src/components/Tooltip');
|
||||
|
||||
const {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} = require('../../../ui-next/src/components/Card');
|
||||
const { Icons } = require('../../../ui-next/src/components/Icons');
|
||||
|
||||
// Showcase modules
|
||||
const AllinOneMenuShowcase = require('./components/AllinOneMenuShowcase').default;
|
||||
const ButtonShowcase = require('./components/ButtonShowcase').default;
|
||||
const CheckboxShowcase = require('./components/CheckboxShowcase').default;
|
||||
const CinePlayerShowcase = require('./components/CinePlayerShowcase').default;
|
||||
const ComboboxShowcase = require('./components/ComboboxShowcase').default;
|
||||
const DataRowShowcase = require('./components/DataRowShowcase').default;
|
||||
const DialogShowcase = require('./components/DialogShowcase').default;
|
||||
const DropdownMenuShowcase = require('./components/DropdownMenuShowcase').default;
|
||||
const HoverCardShowcase = require('./components/HoverCardShowcase').default;
|
||||
const InputShowcase = require('./components/InputShowcase').default;
|
||||
const LabelShowcase = require('./components/LabelShowcase').default;
|
||||
const NumericMetaShowcase = require('./components/NumericMetaShowcase').default;
|
||||
const PanelSectionShowcase = require('./components/PanelSectionShowcase').default;
|
||||
const PopoverShowcase = require('./components/PopoverShowcase').default;
|
||||
const ScrollAreaShowcase = require('./components/ScrollAreaShowcase').default;
|
||||
const SelectShowcase = require('./components/SelectShowcase').default;
|
||||
const SliderShowcase = require('./components/SliderShowcase').default;
|
||||
const SwitchShowcase = require('./components/SwitchShowcase').default;
|
||||
const TabsShowcase = require('./components/TabsShowcase').default;
|
||||
const ToastShowcase = require('./components/ToastShowcase').default;
|
||||
const ToolButtonShowcase = require('./components/ToolButtonShowcase').default;
|
||||
const ToolButtonListShowcase = require('./components/ToolButtonListShowcase').default;
|
||||
const TooltipShowcase = require('./components/TooltipShowcase').default;
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="text-foreground min-h-screen bg-background">
|
||||
<div className="mx-auto my-4 max-w-5xl pt-4 pb-6">
|
||||
{/* Navigation cards */}
|
||||
<div className="mb-8 grid grid-cols-1 gap-5 md:grid-cols-3">
|
||||
<a
|
||||
href="/colors-and-type"
|
||||
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
|
||||
>
|
||||
<Card className="hover:bg-primary/30 w-full transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground text-xl">
|
||||
<Icons.ColorChange className="h-12 w-12" />
|
||||
Colors & Typography
|
||||
</CardTitle>
|
||||
<CardDescription className="text-lg">
|
||||
Color Palette and Typography Guidelines
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</a>
|
||||
<a
|
||||
href="/components-list"
|
||||
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
|
||||
>
|
||||
<Card className="hover:bg-primary/30 w-full transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground text-xl">
|
||||
<Icons.ColorChange className="h-12 w-12" />
|
||||
Components
|
||||
</CardTitle>
|
||||
<CardDescription className="text-lg">
|
||||
Essential UI Components with Variants
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</a>
|
||||
<a
|
||||
href="/patterns"
|
||||
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
|
||||
>
|
||||
<Card className="hover:bg-primary/30 w-full transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground text-xl">
|
||||
<Icons.ColorChange className="h-12 w-12" />
|
||||
Patterns
|
||||
</CardTitle>
|
||||
<CardDescription className="text-lg">
|
||||
Component‑Based Layout Examples
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h1 className="text-foreground ml-6 mb-6 text-5xl">
|
||||
Components
|
||||
</h1>
|
||||
|
||||
<AllinOneMenuShowcase />
|
||||
<ButtonShowcase />
|
||||
<CheckboxShowcase />
|
||||
<CinePlayerShowcase />
|
||||
<ComboboxShowcase />
|
||||
<DataRowShowcase />
|
||||
<DialogShowcase />
|
||||
<DropdownMenuShowcase />
|
||||
<HoverCardShowcase />
|
||||
<InputShowcase />
|
||||
<LabelShowcase />
|
||||
<NumericMetaShowcase />
|
||||
<PanelSectionShowcase />
|
||||
<PopoverShowcase />
|
||||
<ScrollAreaShowcase />
|
||||
<SelectShowcase />
|
||||
<SliderShowcase />
|
||||
<SwitchShowcase />
|
||||
<TabsShowcase />
|
||||
<ToastShowcase />
|
||||
<ToolButtonShowcase />
|
||||
<ToolButtonListShowcase />
|
||||
<TooltipShowcase />
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}}
|
||||
</BrowserOnly>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@ -1,265 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import '../css/custom.css';
|
||||
|
||||
import Layout from '@theme/Layout';
|
||||
import { Label } from '../../../ui-next/src/components/Label';
|
||||
import { Input } from '../../../ui-next/src/components/Input';
|
||||
import { Separator } from '../../../ui-next/src/components/Separator';
|
||||
import { Tabs, TabsList, TabsTrigger } from '../../../ui-next/src/components/Tabs';
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from '../../../ui-next/src/components/Select';
|
||||
import { Button } from '../../../ui-next/src/components/Button';
|
||||
import { Switch } from '../../../ui-next/src/components/Switch';
|
||||
import { Checkbox } from '../../../ui-next/src/components/Checkbox';
|
||||
import { Toggle } from '../../../ui-next/src/components/Toggle';
|
||||
import { Slider } from '../../../ui-next/src/components/Slider';
|
||||
import { ScrollArea } from '../../../ui-next/src/components/ScrollArea';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from '../../../ui-next/src/components/DropdownMenu';
|
||||
import { Icons } from '../../../ui-next/src/components/Icons';
|
||||
import { Toaster, toast } from '../../../ui-next/src/components/Sonner';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from '../../../ui-next/src/components/Card';
|
||||
|
||||
interface ShowcaseRowProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export default function ComponentShowcase() {
|
||||
// Handlers to trigger different types of toasts
|
||||
const triggerSuccess = () => {
|
||||
toast.success('This is a success toast!');
|
||||
};
|
||||
|
||||
const triggerError = () => {
|
||||
toast.error('This is an error toast!');
|
||||
};
|
||||
|
||||
const triggerInfo = () => {
|
||||
toast.info('This is an info toast!');
|
||||
};
|
||||
|
||||
const triggerWarning = () => {
|
||||
toast.warning('This is a warning toast!');
|
||||
};
|
||||
|
||||
// Handler to trigger a toast.promise example
|
||||
const triggerPromiseToast = () => {
|
||||
const promise = () =>
|
||||
new Promise<{ name: string }>(resolve =>
|
||||
setTimeout(() => resolve({ name: 'Segmentation 1' }), 3000)
|
||||
);
|
||||
|
||||
toast.promise(promise(), {
|
||||
loading: 'Loading Segmentation...',
|
||||
success: data => `${data.name} has been added`,
|
||||
error: 'Error',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with description
|
||||
const triggerDescriptionToast = () => {
|
||||
toast.success('Success heading', {
|
||||
description: 'This is a detailed description of the success message.',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with an action button
|
||||
const triggerActionButtonToast = () => {
|
||||
toast.info('No active segmentation detected', {
|
||||
description: 'Create a segmentation before using the Brush',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with a cancel button
|
||||
const triggerCancelButtonToast = () => {
|
||||
toast.error('No active segmentation detected', {
|
||||
description: 'Create a segmentation before using the Brush',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with both action and cancel buttons
|
||||
const triggerCombinedToast = () => {
|
||||
toast.warning('Warning!', {
|
||||
description: 'This is a warning with both action and cancel buttons.',
|
||||
action: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => alert('Retry action clicked')}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
),
|
||||
cancel: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => toast.dismiss()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a loading toast using Toaster's default loading icon
|
||||
const showLoadingToast = () => {
|
||||
toast.loading('Loading your data...');
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
title="OHIF Viewer Design System"
|
||||
description="OHIF Viewer Design System"
|
||||
>
|
||||
<div className="text-foreground min-h-screen bg-background">
|
||||
<div className="mx-auto my-4 max-w-5xl pt-6 pb-3">
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-3">
|
||||
<a
|
||||
href="/colors-and-type"
|
||||
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
|
||||
>
|
||||
<Card className="hover:bg-primary/30 w-full transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground text-xl">
|
||||
<Icons.ColorChange className="h-12 w-12" />
|
||||
Colors & Typography
|
||||
</CardTitle>
|
||||
<CardDescription className="text-lg">
|
||||
Color Palette and Typography Guidelines
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</a>
|
||||
<a
|
||||
href="/components-list"
|
||||
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
|
||||
>
|
||||
<Card className="hover:bg-primary/30 w-full transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground text-xl">
|
||||
<Icons.ColorChange className="h-12 w-12" />
|
||||
Components
|
||||
</CardTitle>
|
||||
<CardDescription className="text-lg">
|
||||
Essential UI Components with Variants
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</a>
|
||||
<a
|
||||
href="/patterns"
|
||||
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
|
||||
>
|
||||
<Card className="hover:bg-primary/30 w-full transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground text-xl">
|
||||
<Icons.ColorChange className="h-12 w-12" />
|
||||
Patterns
|
||||
</CardTitle>
|
||||
<CardDescription className="text-lg">
|
||||
Component-Based Layout Examples
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-12">
|
||||
<div className="text-md mx-auto max-w-2xl"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) {
|
||||
const [showCode, setShowCode] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="bg-background mb-8 rounded-lg p-6">
|
||||
{/* Header Section */}
|
||||
<div className="mb-4 flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="text-foreground text-2xl">{title}</h2>
|
||||
</div>
|
||||
<Button
|
||||
className="text-primary"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowCode(!showCode)}
|
||||
>
|
||||
{showCode ? 'Hide Code' : 'Show Code'} <Icons.Code className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content Section: 1/3 Left, 2/3 Right */}
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-3">
|
||||
{/* Left Side: Title and Description */}
|
||||
<div className="text-lg md:col-span-1">
|
||||
{description && <p className="text-muted-foreground mt-2">{description}</p>}
|
||||
</div>
|
||||
|
||||
{/* Right Side: Example */}
|
||||
<div className="flex min-h-[120px] items-center md:col-span-2">
|
||||
<div className="showcase-content">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Code Section */}
|
||||
{showCode && (
|
||||
<pre className="mt-4 overflow-x-auto rounded-md bg-background p-4 text-sm">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) {
|
||||
// const [showCode, setShowCode] = useState(false);
|
||||
|
||||
// return (
|
||||
// <div className="bg-background mb-8 rounded-lg p-6">
|
||||
// <div className="mb-4 flex items-start justify-between">
|
||||
// <div>
|
||||
// <h2 className="text-2xl font-bold">{title}</h2>
|
||||
// {description && <p className="text-muted-foreground mt-1">{description}</p>}
|
||||
// </div>
|
||||
// <Button
|
||||
// className="text-primary"
|
||||
// variant="outline"
|
||||
// size="sm"
|
||||
// onClick={() => setShowCode(!showCode)}
|
||||
// >
|
||||
// {showCode ? 'Hide Code' : 'Show Code'} <Icons.Code className="ml-2 h-4 w-4" />
|
||||
// </Button>
|
||||
// </div>
|
||||
// <div className="showcase-content mb-4">{children}</div>
|
||||
// {showCode && (
|
||||
// <pre className="mt-4 overflow-x-auto rounded-md bg-background p-4 text-sm">
|
||||
// <code>{code}</code>
|
||||
// </pre>
|
||||
// )}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
@ -1,119 +0,0 @@
|
||||
import React from 'react';
|
||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* Pure‑UI mock of the in‑app Window/Level menu.
|
||||
* Clickable, but all actions are inert.
|
||||
*/
|
||||
export default function AllinOneMenuShowcase() {
|
||||
const isBrowser = useIsBrowser();
|
||||
|
||||
if (!isBrowser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
default: AllInOneMenu,
|
||||
IconMenu,
|
||||
SubMenu,
|
||||
ItemPanel,
|
||||
Item,
|
||||
DividerItem,
|
||||
HorizontalDirection,
|
||||
VerticalDirection,
|
||||
} = require('../../../../ui-next/src/components/AllInOneMenu');
|
||||
const { Switch } = require('../../../../ui-next/src/components/Switch');
|
||||
|
||||
const renderColorLUTItems = () =>
|
||||
[
|
||||
'Grayscale',
|
||||
'X Ray',
|
||||
'HSV',
|
||||
'Hot Iron',
|
||||
'Red Hot',
|
||||
'S PET',
|
||||
'Perfusion',
|
||||
'Rainbow',
|
||||
'SUV',
|
||||
'GE 256',
|
||||
'GE',
|
||||
'Siemens',
|
||||
].map(name => <Item key={name} label={name} />);
|
||||
|
||||
const renderWindowPresetItems = () =>
|
||||
[
|
||||
{ desc: 'Soft tissue', wl: '400 / 40' },
|
||||
{ desc: 'Lung', wl: '1500 / -600' },
|
||||
{ desc: 'Liver', wl: '150 / 90' },
|
||||
{ desc: 'Bone', wl: '2500 / 480' },
|
||||
{ desc: 'Brain', wl: '80 / 40' },
|
||||
].map(p => (
|
||||
<Item key={p.desc} label={p.desc} secondaryLabel={p.wl} />
|
||||
));
|
||||
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="All In One Menu"
|
||||
description="A structured menu that consolidates controls and actions."
|
||||
code={`<IconMenu icon="viewport-window-level" menuStyle={{ width: 212, maxHeight: 500 }}>…</IconMenu>`}
|
||||
>
|
||||
<div className="border-input/70 relative flex h-12 items-center rounded border bg-background px-4">
|
||||
<IconMenu
|
||||
icon="viewport-window-level"
|
||||
iconClassName="text-xl text-highlight hover:bg-primary/30 cursor-pointer rounded"
|
||||
horizontalDirection={HorizontalDirection.LeftToRight}
|
||||
verticalDirection={VerticalDirection.TopToBottom}
|
||||
menuStyle={{ width: 212, maxHeight: 500 }}
|
||||
>
|
||||
<ItemPanel label="Display">
|
||||
<Item
|
||||
label="Display Color bar"
|
||||
rightIcon={
|
||||
<Switch
|
||||
checked={false}
|
||||
disabled
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
}
|
||||
useIconSpace={false}
|
||||
/>
|
||||
<DividerItem />
|
||||
<SubMenu itemLabel="Color LUT" itemIcon="icon-color-lut">
|
||||
<ItemPanel
|
||||
label="Color LUTs"
|
||||
maxHeight="calc(100vh - 250px)"
|
||||
className="flex flex-col"
|
||||
>
|
||||
<Item
|
||||
label="Preview in viewport"
|
||||
rightIcon={
|
||||
<Switch
|
||||
checked={false}
|
||||
disabled
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<DividerItem />
|
||||
{renderColorLUTItems()}
|
||||
</ItemPanel>
|
||||
</SubMenu>
|
||||
<SubMenu
|
||||
itemLabel="Window Presets"
|
||||
itemIcon="viewport-window-level"
|
||||
>
|
||||
<ItemPanel label="CT Presets">
|
||||
{renderWindowPresetItems()}
|
||||
</ItemPanel>
|
||||
</SubMenu>
|
||||
</ItemPanel>
|
||||
</IconMenu>
|
||||
|
||||
<span className="text-muted-foreground ml-3 text-sm">
|
||||
Click the icon to explore an example
|
||||
</span>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* ButtonShowcase component displays button variants and examples
|
||||
*/
|
||||
export default function ButtonShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Buttons"
|
||||
description="Button components and size variants. Use the primary and secondary buttons in dialogs or screens where one action is required. In the Viewer application, use ghost button in panels where many different actions are available."
|
||||
code={`
|
||||
<Button variant="default">Primary Button</Button>
|
||||
|
||||
<Button variant="secondary">Secondary Button</Button>
|
||||
|
||||
<Button variant="ghost">Ghost Button</Button>
|
||||
|
||||
<Button variant="ghost" size="icon">?</Button>
|
||||
|
||||
<Button variant="link">Link</Button>
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Button variant="default">Primary Button</Button>
|
||||
<Button variant="secondary">Secondary Button</Button>
|
||||
<Button variant="ghost">Ghost Button</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
?
|
||||
</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-wrap gap-4">
|
||||
<Button
|
||||
variant="default"
|
||||
size="lg"
|
||||
className="w-[107px]"
|
||||
>
|
||||
Large Button
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
Small Button
|
||||
</Button>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Checkbox } from '../../../../ui-next/src/components/Checkbox';
|
||||
import { Label } from '../../../../ui-next/src/components/Label';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* CheckboxShowcase component displays checkbox variants and examples
|
||||
*/
|
||||
export default function CheckboxShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Checkbox"
|
||||
description="When possible use Switch in place of checkbox. If necessary, Checkbox provides a smaller component to change between two states or options."
|
||||
code={`
|
||||
<div className="items-top flex space-x-2">
|
||||
<Checkbox id="terms1" />
|
||||
<div className="grid gap-1.5 pt-0.5 leading-none">
|
||||
<Label>Display inactive segmentations</Label>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<div className="items-top flex space-x-2">
|
||||
<Checkbox id="terms1" />
|
||||
<div className="grid gap-1.5 pt-0.5 leading-none">
|
||||
<Label>Display inactive segmentations</Label>
|
||||
</div>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* CinePlayerShowcase displays a playable/pausable cine player with FPS control.
|
||||
*/
|
||||
export default function CinePlayerShowcase() {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [fps, setFps] = useState(24);
|
||||
const isBrowser = useIsBrowser();
|
||||
|
||||
// If not browser, return null to avoid SSR parse errors
|
||||
if (!isBrowser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { default: CinePlayer } = require('../../../../ui-next/src/components/CinePlayer/CinePlayer');
|
||||
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Cine Player"
|
||||
description="Play, pause, scrub through dynamic image series and adjust FPS."
|
||||
code={`
|
||||
<CinePlayer
|
||||
className="w-[300px]"
|
||||
isPlaying={isPlaying}
|
||||
frameRate={fps}
|
||||
onPlayPauseChange={setIsPlaying}
|
||||
onFrameRateChange={setFps}
|
||||
onClose={() => console.log('close clicked')}
|
||||
/>
|
||||
`}
|
||||
>
|
||||
<CinePlayer
|
||||
className="w-[300px]"
|
||||
isPlaying={isPlaying}
|
||||
frameRate={fps}
|
||||
onPlayPauseChange={setIsPlaying}
|
||||
onFrameRateChange={setFps}
|
||||
onClose={() => console.log('close clicked')}
|
||||
/>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,105 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Combobox } from '../../../../ui-next/src/components/Combobox/Combobox';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* ComboboxShowcase demonstrates the searchable Combobox component with a
|
||||
* comprehensive “Modality” list.
|
||||
*/
|
||||
export default function ComboboxShowcase() {
|
||||
const modalities = [
|
||||
'AR',
|
||||
'ASMT',
|
||||
'AU',
|
||||
'BDUS',
|
||||
'BI',
|
||||
'BMD',
|
||||
'CR',
|
||||
'CT',
|
||||
'CTPROTOCOL',
|
||||
'DG',
|
||||
'DOC',
|
||||
'DX',
|
||||
'ECG',
|
||||
'EPS',
|
||||
'ES',
|
||||
'FID',
|
||||
'GM',
|
||||
'HC',
|
||||
'HD',
|
||||
'IO',
|
||||
'IOL',
|
||||
'IVOCT',
|
||||
'IVUS',
|
||||
'KER',
|
||||
'KO',
|
||||
'LEN',
|
||||
'LS',
|
||||
'MG',
|
||||
'MR',
|
||||
'M3D',
|
||||
'NM',
|
||||
'OAM',
|
||||
'OCT',
|
||||
'OP',
|
||||
'OPM',
|
||||
'OPT',
|
||||
'OPTBSV',
|
||||
'OPTENF',
|
||||
'OPV',
|
||||
'OSS',
|
||||
'OT',
|
||||
'PLAN',
|
||||
'PR',
|
||||
'PT',
|
||||
'PX',
|
||||
'REG',
|
||||
'RESP',
|
||||
'RF',
|
||||
'RG',
|
||||
'RTDOSE',
|
||||
'RTIMAGE',
|
||||
'RTINTENT',
|
||||
'RTPLAN',
|
||||
'RTRAD',
|
||||
'RTRECORD',
|
||||
'RTSEGANN',
|
||||
'RTSTRUCT',
|
||||
'RWV',
|
||||
'SEG',
|
||||
'SM',
|
||||
'SMR',
|
||||
'SR',
|
||||
'SRF',
|
||||
'STAIN',
|
||||
'TEXTUREMAP',
|
||||
'TG',
|
||||
'US',
|
||||
'VA',
|
||||
'XA',
|
||||
'XC',
|
||||
].map(m => ({ value: m, label: m }));
|
||||
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Combobox"
|
||||
description="Searchable dropdown built with Command + Popover primitives."
|
||||
code={`
|
||||
const modalities = [
|
||||
'AR','ASMT','AU','BDUS','BI','BMD','CR','CT','CTPROTOCOL','DG','DOC','DX','ECG','EPS','ES','FID',
|
||||
'GM','HC','HD','IO','IOL','IVOCT','IVUS','KER','KO','LEN','LS','MG','MR','M3D','NM','OAM','OCT',
|
||||
'OP','OPM','OPT','OPTBSV','OPTENF','OPV','OSS','OT','PLAN','PR','PT','PX','REG','RESP','RF','RG',
|
||||
'RTDOSE','RTIMAGE','RTINTENT','RTPLAN','RTRAD','RTRECORD','RTSEGANN','RTSTRUCT','RWV','SEG','SM',
|
||||
'SMR','SR','SRF','STAIN','TEXTUREMAP','TG','US','VA','XA','XC',
|
||||
].map(m => ({ value: m, label: m }));
|
||||
|
||||
<Combobox data={modalities} placeholder="Modality" />
|
||||
`}
|
||||
>
|
||||
<Combobox
|
||||
data={modalities}
|
||||
placeholder="Modality"
|
||||
/>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import DataRowExample from '../patterns/DataRowExample';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* DataRowShowcase component displays DataRow variants and examples
|
||||
*/
|
||||
export default function DataRowShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Data Row"
|
||||
description="A selectable row with action menu options and visibility toggle. Color, Secondary details, and Image Series are optional to display."
|
||||
code={`
|
||||
Example code coming soon.
|
||||
`}
|
||||
>
|
||||
<DataRowExample />
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogClose,
|
||||
} from '../../../../ui-next/src/components/Dialog/Dialog';
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* DialogShowcase demonstrates a simple Radix‑based dialog.
|
||||
*/
|
||||
export default function DialogShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Dialog"
|
||||
description="Modal dialog with header, content, and footer actions."
|
||||
code={`
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="default">Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Example Dialog</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
This is a short message inside the dialog.
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary" className="min-w-[60px]">Cancel</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button variant="default" className="min-w-[60px]">OK</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
`}
|
||||
>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="default">Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Example Dialog</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogDescription>This is a short message inside the dialog.</DialogDescription>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="min-w-[60px]"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="min-w-[60px]"
|
||||
>
|
||||
OK
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,81 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from '../../../../ui-next/src/components/DropdownMenu';
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* DropdownMenuShowcase component displays DropdownMenu variants and examples
|
||||
*/
|
||||
export default function DropdownMenuShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Dropdown Menu"
|
||||
description="Dropdown menu provides a flexible list of options that can open from buttons or other elements"
|
||||
code={`
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Open Basic</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button>Open Basic</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Open Align Start</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Open Align End</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Open Align Top</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
>
|
||||
<DropdownMenuItem onSelect={() => console.debug('Item 1')}>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => console.debug('Item 2')}>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => console.debug('Item 3')}>
|
||||
Long name Item 3
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardTrigger,
|
||||
HoverCardContent,
|
||||
} from '../../../../ui-next/src/components/HoverCard/HoverCard';
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* HoverCardShowcase demonstrates a Hover Card that appears on pointer hover.
|
||||
*/
|
||||
export default function HoverCardShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Hover Card"
|
||||
description="Lightweight, non‑modal surface that appears on hover or focus."
|
||||
code={`
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button variant="link">Hover me</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<p className="text-sm">Hello there! I'm a hover card.</p>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
`}
|
||||
>
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button variant="link">Hover me</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<p className="text-sm">Hello there! I'm a hover card.</p>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Input } from '../../../../ui-next/src/components/Input';
|
||||
import { Label } from '../../../../ui-next/src/components/Label';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* InputShowcase component displays Input variants and examples
|
||||
*/
|
||||
export default function InputShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Input"
|
||||
description="Input fields can be used with or without example text"
|
||||
code={`
|
||||
<div className="inline-block">
|
||||
<div className="mr-4 inline-block">
|
||||
<Label>Patient Weight</Label>
|
||||
</div>
|
||||
<div className="inline-block">
|
||||
<Input placeholder="(kg)" />
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<div className="inline-block">
|
||||
<div className="mr-4 inline-block">
|
||||
<Label>Patient Weight</Label>
|
||||
</div>
|
||||
<div className="inline-block">
|
||||
<Input placeholder="(kg)" />
|
||||
</div>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Label } from '../../../../ui-next/src/components/Label/Label';
|
||||
import { Switch } from '../../../../ui-next/src/components/Switch';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* LabelShowcase pairs a Label with a Switch inline.
|
||||
*/
|
||||
export default function LabelShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Label"
|
||||
description="Labels clarify the purpose of controls and improve accessibility."
|
||||
code={`
|
||||
<Switch defaultChecked id="preview-switch" />
|
||||
<Label htmlFor="preview-switch" className="ml-2">
|
||||
Preview edits before creating
|
||||
</Label>
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch defaultChecked id="preview-switch" />
|
||||
<Label htmlFor="preview-switch">Preview edits before creating</Label>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,463 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import Numeric from '../../../../ui-next/src/components/Numeric';
|
||||
import Icons from '../../../../ui-next/src/components/Icons';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* NumericShowcase component displays Numeric variants and examples
|
||||
*/
|
||||
export default function NumericShowcase() {
|
||||
const [controlledValue, setControlledValue] = useState(0);
|
||||
const [controlledValues, setControlledValues] = useState([0, 100] as [number, number]);
|
||||
const [dimensionGroupNumber, setDimensionGroupNumber] = useState(1);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Basic Number Input */}
|
||||
<ShowcaseRow
|
||||
title="Numeric - Number Input"
|
||||
description="Basic number input with min, max and a label"
|
||||
code={`
|
||||
<Numeric.Container mode="number" min={0} max={10} onChange={val => console.debug('Value changed:', val)}>
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<Numeric.Label>Width</Numeric.Label>
|
||||
<Numeric.NumberInput />
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container mode="number" className="space-y-1" min={0} max={100} onChange={val => console.debug('Value changed:', val)}>
|
||||
<Numeric.Label className="text-muted-foreground text-sm font-bold">Bolder</Numeric.Label>
|
||||
<Numeric.NumberInput className="w-12" />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="number"
|
||||
className="flex flex-row items-center justify-between"
|
||||
onChange={val => console.debug('Value changed:', val)}
|
||||
min={0}
|
||||
value={123465789}
|
||||
max={10000000000000}
|
||||
>
|
||||
<Numeric.Label className="flex flex-row items-center">
|
||||
<Icons.Add />
|
||||
With Icon
|
||||
</Numeric.Label>
|
||||
<Numeric.NumberInput className="w-32 text-center" />
|
||||
</Numeric.Container>`}
|
||||
>
|
||||
<div className="bg-muted flex w-[300px] flex-col space-y-4 rounded p-4">
|
||||
<Numeric.Container
|
||||
mode="number"
|
||||
min={0}
|
||||
max={10}
|
||||
onChange={val => console.debug('Value changed:', val)}
|
||||
>
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<Numeric.Label>Width</Numeric.Label>
|
||||
<Numeric.NumberInput />
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="number"
|
||||
className="space-y-1"
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={val => console.debug('Value changed:', val)}
|
||||
>
|
||||
<Numeric.Label className="text-muted-foreground text-sm font-bold">
|
||||
Bolder
|
||||
</Numeric.Label>
|
||||
<Numeric.NumberInput className="w-12" />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="number"
|
||||
className="flex flex-row items-center justify-between"
|
||||
onChange={val => console.debug('Value changed:', val)}
|
||||
min={0}
|
||||
value={123465789}
|
||||
max={10000000000000}
|
||||
>
|
||||
<Numeric.Label className="flex flex-row items-center">
|
||||
<Icons.Add />
|
||||
With Icon
|
||||
</Numeric.Label>
|
||||
<Numeric.NumberInput className="w-32 text-center" />
|
||||
</Numeric.Container>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
|
||||
{/* Stepper Controls */}
|
||||
<ShowcaseRow
|
||||
title="Numeric - Stepper Controls"
|
||||
description="Number input with increment/decrement controls in horizontal or vertical layout"
|
||||
code={`
|
||||
// For controlled component
|
||||
const [dimensionGroupNumber, setDimensionGroupNumber] = useState(1);
|
||||
|
||||
<Numeric.Container
|
||||
mode="stepper"
|
||||
value={dimensionGroupNumber}
|
||||
onChange={val => setDimensionGroupNumber(val as number)}
|
||||
min={1}
|
||||
max={5}
|
||||
step={1}
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<Numeric.NumberStepper className="w-[58px]" direction="horizontal"/>
|
||||
<Numeric.Label className="mt-1">Frame</Numeric.Label>
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="stepper"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
defaultValue={50}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Numeric.Label>Opacity</Numeric.Label>
|
||||
<Numeric.NumberStepper className="w-[58px]" direction="horizontal"/>
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="stepper"
|
||||
min={-10}
|
||||
max={10}
|
||||
step={0.1}
|
||||
defaultValue={0}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Numeric.Label>Zoom:</Numeric.Label>
|
||||
<Numeric.NumberStepper className="w-[65px]" direction="vertical"/>
|
||||
</div>
|
||||
</Numeric.Container>`}
|
||||
>
|
||||
<div className="bg-muted flex w-[300px] flex-col space-y-4 rounded p-4">
|
||||
<Numeric.Container
|
||||
mode="stepper"
|
||||
value={dimensionGroupNumber}
|
||||
onChange={val => setDimensionGroupNumber(val as number)}
|
||||
min={1}
|
||||
max={5}
|
||||
step={1}
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<Numeric.NumberStepper
|
||||
className="flex w-[78px]"
|
||||
direction="horizontal"
|
||||
>
|
||||
<span className="text-muted-foreground text-xs">FPS</span>
|
||||
</Numeric.NumberStepper>
|
||||
<Numeric.Label className="mt-1">Frame</Numeric.Label>
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="stepper"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
defaultValue={50}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Numeric.Label>Opacity</Numeric.Label>
|
||||
<Numeric.NumberStepper
|
||||
className="w-[58px]"
|
||||
direction="horizontal"
|
||||
/>
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="stepper"
|
||||
min={-10}
|
||||
max={10}
|
||||
step={1}
|
||||
defaultValue={0}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Numeric.NumberStepper
|
||||
className="w-[53px]"
|
||||
direction="vertical"
|
||||
/>
|
||||
<Numeric.Label>Zoom</Numeric.Label>
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
|
||||
{/* Single Range Slider */}
|
||||
<ShowcaseRow
|
||||
title="Numeric - Single Range"
|
||||
description="Single range slider with optional number input"
|
||||
code={`
|
||||
// For controlled component
|
||||
const [controlledValue, setControlledValue] = useState(0);
|
||||
|
||||
<Numeric.Container mode="singleRange" min={0} max={100} onChange={val => console.debug('Value changed:', val)}>
|
||||
<Numeric.Label>Brightness</Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
min={-50}
|
||||
max={50}
|
||||
step={1}
|
||||
defaultValue={0}
|
||||
className="flex flex-row items-center"
|
||||
onChange={val => console.debug('Value changed:', val)}
|
||||
>
|
||||
<Numeric.Label showValue>Contrast</Numeric.Label>
|
||||
<Numeric.SingleRange />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={controlledValue}
|
||||
className="flex flex-row items-center space-x-2"
|
||||
onChange={val => setControlledValue(val as number)}
|
||||
>
|
||||
<Numeric.Label>Controlled State (Parent) </Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>`}
|
||||
>
|
||||
<div className="bg-muted flex w-[300px] flex-col space-y-4 rounded p-4">
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={val => console.debug('Value changed:', val)}
|
||||
>
|
||||
<Numeric.Label>Brightness</Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
min={-50}
|
||||
max={50}
|
||||
step={1}
|
||||
defaultValue={0}
|
||||
className="flex flex-row items-center"
|
||||
onChange={val => console.debug('Value changed:', val)}
|
||||
>
|
||||
<Numeric.Label showValue>Contrast</Numeric.Label>
|
||||
<Numeric.SingleRange />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={controlledValue}
|
||||
className="flex flex-row items-center space-x-2"
|
||||
onChange={val => setControlledValue(val as number)}
|
||||
>
|
||||
<Numeric.Label>Controlled State (Parent) </Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
|
||||
{/* Double Range Slider */}
|
||||
<ShowcaseRow
|
||||
title="Numeric - Double Range"
|
||||
description="Double range slider for selecting a range of values"
|
||||
code={`
|
||||
// For controlled component
|
||||
const [controlledValues, setControlledValues] = useState<[number, number]>([0, 100]);
|
||||
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
className="space-y-1"
|
||||
onChange={vals => console.debug('Values changed:', vals)}
|
||||
>
|
||||
<Numeric.Label showValue>Window Width/Level</Numeric.Label>
|
||||
<Numeric.DoubleRange />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
defaultValues={[30, 70]}
|
||||
className="space-y-1"
|
||||
onChange={vals => console.debug('Values changed:', vals)}
|
||||
>
|
||||
<Numeric.Label>Window Width/Level</Numeric.Label>
|
||||
<Numeric.DoubleRange showNumberInputs />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
values={controlledValues}
|
||||
className="flex flex-row items-center space-x-2"
|
||||
onChange={vals => setControlledValues(vals as [number, number])}
|
||||
>
|
||||
<Numeric.Label>Controlled State (Parent) </Numeric.Label>
|
||||
<Numeric.DoubleRange />
|
||||
</Numeric.Container>`}
|
||||
>
|
||||
<div className="bg-muted flex w-[300px] flex-col space-y-4 rounded p-4">
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
className="space-y-1"
|
||||
onChange={vals => console.debug('Values changed:', vals)}
|
||||
>
|
||||
<Numeric.Label showValue>Window Width/Level</Numeric.Label>
|
||||
<Numeric.DoubleRange />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
defaultValues={[30, 70]}
|
||||
className="space-y-1"
|
||||
onChange={vals => console.debug('Values changed:', vals)}
|
||||
>
|
||||
<Numeric.Label>Window Width/Level</Numeric.Label>
|
||||
<Numeric.DoubleRange showNumberInputs />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
values={controlledValues}
|
||||
className="flex flex-row items-center space-x-2"
|
||||
onChange={vals => setControlledValues(vals as [number, number])}
|
||||
>
|
||||
<Numeric.Label>Controlled State (Parent) </Numeric.Label>
|
||||
<Numeric.DoubleRange />
|
||||
</Numeric.Container>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
|
||||
{/* Combined Examples */}
|
||||
<ShowcaseRow
|
||||
title="Numeric - Combined Examples"
|
||||
description="Different modes and configurations working together"
|
||||
code={`
|
||||
<Numeric.Container
|
||||
mode="number"
|
||||
min={0}
|
||||
max={10}
|
||||
step={0.1}
|
||||
className="space-y-1"
|
||||
>
|
||||
<Numeric.Label>Zoom Factor</Numeric.Label>
|
||||
<Numeric.NumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="stepper"
|
||||
min={-5}
|
||||
max={5}
|
||||
step={0.5}
|
||||
defaultValue={0}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Numeric.Label>Offset</Numeric.Label>
|
||||
<Numeric.NumberStepper className="w-[58px]">
|
||||
</Numeric.NumberStepper>
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
min={0}
|
||||
max={360}
|
||||
step={1}
|
||||
className="space-y-1"
|
||||
>
|
||||
<Numeric.Label showValue>Rotation</Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
min={-1000}
|
||||
max={3000}
|
||||
step={10}
|
||||
className="space-y-1"
|
||||
>
|
||||
<Numeric.Label showValue>CT Window</Numeric.Label>
|
||||
<Numeric.DoubleRange showNumberInputs />
|
||||
</Numeric.Container>`}
|
||||
>
|
||||
<div className="bg-muted flex w-[300px] flex-col space-y-4 rounded p-4">
|
||||
<Numeric.Container
|
||||
mode="number"
|
||||
min={0}
|
||||
max={10}
|
||||
step={0.1}
|
||||
className="space-y-1"
|
||||
>
|
||||
<Numeric.Label>Zoom Factor</Numeric.Label>
|
||||
<Numeric.NumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="stepper"
|
||||
min={-5}
|
||||
max={5}
|
||||
step={0.5}
|
||||
defaultValue={0}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Numeric.Label>Offset</Numeric.Label>
|
||||
<Numeric.NumberStepper
|
||||
className="w-[58px]"
|
||||
direction="horizontal"
|
||||
/>
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
min={0}
|
||||
max={360}
|
||||
step={1}
|
||||
className="space-y-1"
|
||||
>
|
||||
<Numeric.Label showValue>Rotation</Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
min={-1000}
|
||||
max={3000}
|
||||
step={10}
|
||||
className="space-y-1"
|
||||
>
|
||||
<Numeric.Label showValue>CT Window</Numeric.Label>
|
||||
<Numeric.DoubleRange showNumberInputs />
|
||||
</Numeric.Container>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
// docs/src/pages/components/PanelSectionShowcase.tsx
|
||||
import React from 'react';
|
||||
import { PanelSection } from '../../../../ui-next/src/components/PanelSection/PanelSection';
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* PanelSectionShowcase displays an expandable/collapsible panel section.
|
||||
*/
|
||||
export default function PanelSectionShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Panel Section"
|
||||
description="Collapsible panels for grouping controls or metadata inside side‑panels."
|
||||
code={`
|
||||
<PanelSection defaultOpen>
|
||||
<PanelSection.Header>Series Information</PanelSection.Header>
|
||||
<PanelSection.Content>
|
||||
<div className="p-2 space-y-1 text-sm text-muted-foreground">
|
||||
<div>Images: 120</div>
|
||||
<div>Modality: MR</div>
|
||||
<div>Body Part: Brain</div>
|
||||
<Button variant="link" size="xs">Load another series</Button>
|
||||
</div>
|
||||
</PanelSection.Content>
|
||||
</PanelSection>
|
||||
`}
|
||||
>
|
||||
<PanelSection
|
||||
defaultOpen
|
||||
className="bg-muted w-[280px]"
|
||||
>
|
||||
<PanelSection.Header className="bg-popover">Series Information</PanelSection.Header>
|
||||
<PanelSection.Content>
|
||||
<div className="text-muted-foreground space-y-1 p-2 text-sm">
|
||||
<div className="pl-2">
|
||||
<div>Images: 120</div>
|
||||
<div>Modality: MR</div>
|
||||
<div>Body Part: Brain</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
Load more information
|
||||
</Button>
|
||||
</div>
|
||||
</PanelSection.Content>
|
||||
</PanelSection>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from '../../../../ui-next/src/components/Popover/Popover';
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* PopoverShowcase presents a simple Popover that appears after clicking a button.
|
||||
*/
|
||||
export default function PopoverShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Popover"
|
||||
description="Transient panel that appears over content—great for small forms, extra details, or actions."
|
||||
code={`
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="default">Open Popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-60">
|
||||
<p className="text-sm">
|
||||
Hello! I’m a Popover. Click outside or press Esc to close me.
|
||||
</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
`}
|
||||
>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="default">Open Popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-60">
|
||||
<p className="text-sm">
|
||||
Hello! I’m a Popover. Click outside or press Esc to close me.
|
||||
</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ScrollArea } from '../../../../ui-next/src/components/ScrollArea';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* ScrollAreaShowcase component displays ScrollArea variants and examples
|
||||
*/
|
||||
export default function ScrollAreaShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Scroll Area"
|
||||
description="Displays a scroll indicator when hovering within an element."
|
||||
code={`
|
||||
<ScrollArea className="border-input bg-background h-[150px] w-[350px] rounded-md border p-2 text-sm text-white">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
|
||||
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
|
||||
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua.
|
||||
</ScrollArea>
|
||||
`}
|
||||
>
|
||||
<ScrollArea className="border-input bg-background h-[150px] w-[350px] rounded-md border p-2 text-sm text-white">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
|
||||
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
|
||||
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
|
||||
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
|
||||
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem
|
||||
ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
|
||||
labore et dolore magna aliqua.
|
||||
</ScrollArea>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from '../../../../ui-next/src/components/Select';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* SelectShowcase component displays Select variants and examples
|
||||
*/
|
||||
export default function SelectShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Select"
|
||||
description="Switch between a list of options"
|
||||
code={`
|
||||
<Select>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
`}
|
||||
>
|
||||
<Select>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
import { Icons } from '../../../../ui-next/src/components/Icons';
|
||||
|
||||
interface ShowcaseRowProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ShowcaseRow component displays a UI component example with title, description,
|
||||
* and optional code snippet that can be toggled.
|
||||
*/
|
||||
export default function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) {
|
||||
const [showCode, setShowCode] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="bg-background mb-8 rounded-lg p-6">
|
||||
<div className="mb-4 flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="text-highlight text-2xl">{title}</h2>
|
||||
</div>
|
||||
<Button
|
||||
className="text-primary"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowCode(!showCode)}
|
||||
>
|
||||
{showCode ? 'Hide Code' : 'Show Code'} <Icons.Code className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-3">
|
||||
<div className="text-base md:col-span-1">
|
||||
{description && <p className="text-secondary-foreground mt-2">{description}</p>}
|
||||
</div>
|
||||
<div className="flex min-h-[120px] items-center md:col-span-2">
|
||||
<div className="showcase-content">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
{showCode && (
|
||||
<pre className="border-input mt-4 overflow-x-auto rounded-md border bg-background p-4 text-sm">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Slider } from '../../../../ui-next/src/components/Slider';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* SliderShowcase component displays Slider variants and examples
|
||||
*/
|
||||
export default function SliderShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Slider"
|
||||
description="Used to select a value in a predefined range."
|
||||
code={`
|
||||
<div className="w-40 px-5">
|
||||
<Slider
|
||||
className="w-full"
|
||||
defaultValue={[50]}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<div className="w-40 px-5">
|
||||
<Slider
|
||||
className="w-full"
|
||||
defaultValue={[50]}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Switch } from '../../../../ui-next/src/components/Switch';
|
||||
import { Label } from '../../../../ui-next/src/components/Label';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* SwitchShowcase component displays Switch variants and examples
|
||||
*/
|
||||
export default function SwitchShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Switch"
|
||||
description="A toggle Switch is used to change between two different states. Use descriptive labels next to Switches that are understandable before interacting."
|
||||
code={`
|
||||
<Switch />
|
||||
`}
|
||||
>
|
||||
<Switch defaultChecked />
|
||||
<Label className="text-foreground mx-2 w-14 flex-none whitespace-nowrap text-sm">
|
||||
Sync changes in all viewports
|
||||
</Label>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs';
|
||||
import { Separator } from '../../../../ui-next/src/components/Separator';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* TabsShowcase component displays Tabs variants and examples
|
||||
*/
|
||||
export default function TabsShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Tabs"
|
||||
description="Tabs (or segmented controls) can be used to provide navigation options or allow users to switch between multiple options (e.g., tool settings) "
|
||||
code={`
|
||||
<Tabs defaultValue="circle" className="w-[400px]" onValueChange={newValue => console.log(newValue)}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="circle">Circle</TabsTrigger>
|
||||
<Separator orientation="vertical" />
|
||||
<TabsTrigger value="sphere">Sphere</TabsTrigger>
|
||||
<Separator orientation="vertical" />
|
||||
<TabsTrigger value="square">Square</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
`}
|
||||
>
|
||||
<Tabs
|
||||
defaultValue="circle"
|
||||
className="w-[400px]"
|
||||
onValueChange={newValue => console.log(newValue)}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="circle">Circle</TabsTrigger>
|
||||
<Separator orientation="vertical" />
|
||||
<TabsTrigger value="sphere">Sphere</TabsTrigger>
|
||||
<Separator orientation="vertical" />
|
||||
<TabsTrigger value="square">Square</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,158 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
import { Toaster, toast } from '../../../../ui-next/src/components/Sonner';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* ToastShowcase component displays Toast variants and examples
|
||||
*/
|
||||
export default function ToastShowcase() {
|
||||
// Handlers to trigger different types of toasts
|
||||
const triggerSuccess = () => {
|
||||
toast.success('This is a success toast!');
|
||||
};
|
||||
|
||||
const triggerError = () => {
|
||||
toast.error('This is an error toast!');
|
||||
};
|
||||
|
||||
const triggerInfo = () => {
|
||||
toast.info('This is an info toast!');
|
||||
};
|
||||
|
||||
const triggerWarning = () => {
|
||||
toast.warning('This is a warning toast!');
|
||||
};
|
||||
|
||||
// Handler to trigger a toast.promise example
|
||||
const triggerPromiseToast = () => {
|
||||
const promise = () =>
|
||||
new Promise<{ name: string }>(resolve =>
|
||||
setTimeout(() => resolve({ name: 'Segmentation 1' }), 3000)
|
||||
);
|
||||
|
||||
toast.promise(promise(), {
|
||||
loading: 'Loading Segmentation...',
|
||||
success: data => `${data.name} has been added`,
|
||||
error: 'Error',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with description
|
||||
const triggerDescriptionToast = () => {
|
||||
toast.success('Completed', {
|
||||
description: 'This is a detailed description of the success message.',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with an action button
|
||||
const triggerActionButtonToast = () => {
|
||||
toast.info('No active segmentation detected', {
|
||||
description: 'Create a segmentation before using the Brush',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with a cancel button
|
||||
const triggerCancelButtonToast = () => {
|
||||
toast.error('No active segmentation detected', {
|
||||
description: 'Create a segmentation before using the Brush',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with both action and cancel buttons
|
||||
const triggerCombinedToast = () => {
|
||||
toast.warning('Warning!', {
|
||||
description: 'This is a warning with both action and cancel buttons.',
|
||||
action: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => console.debug('Retry action clicked')}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
),
|
||||
cancel: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => toast.dismiss()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Toast"
|
||||
description="A toast notification displays temporary feedback messages to users above the current UI. Notifications stack into one unit after multiple cascading notifications."
|
||||
code={`
|
||||
Example code coming soon.
|
||||
`}
|
||||
>
|
||||
Simple message:
|
||||
<div className="mt-2 mb-7 space-x-2">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerPromiseToast}
|
||||
>
|
||||
Loading & Success Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerSuccess}
|
||||
>
|
||||
Success Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerError}
|
||||
>
|
||||
Error Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerInfo}
|
||||
>
|
||||
Info Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerWarning}
|
||||
>
|
||||
Warning Toast
|
||||
</Button>
|
||||
</div>
|
||||
Message with details:
|
||||
<div className="mt-2 space-x-2">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerDescriptionToast}
|
||||
>
|
||||
Success Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerActionButtonToast}
|
||||
>
|
||||
Info Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerCancelButtonToast}
|
||||
>
|
||||
Error Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerCombinedToast}
|
||||
>
|
||||
Toast with Buttons
|
||||
</Button>
|
||||
</div>
|
||||
<Toaster />
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
ToolButtonList,
|
||||
ToolButton,
|
||||
ToolButtonListDefault,
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
} from '../../../../ui-next/src/components/ToolButton';
|
||||
import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip';
|
||||
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* ToolButtonListShowcase component displays ToolButtonList variants and examples
|
||||
*/
|
||||
export default function ToolButtonListShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="ToolButtonList"
|
||||
description="A compound component that combines a primary tool button with a dropdown menu of additional related tools"
|
||||
code={`
|
||||
// Example usage:
|
||||
<ToolButtonList>
|
||||
<ToolButtonListDefault>
|
||||
<ToolButton
|
||||
id="Length"
|
||||
icon="tool-length"
|
||||
label="Length"
|
||||
tooltip="Length Tool"
|
||||
onInteraction={({ itemId }) => console.debug(\`Clicked \${itemId}\`)}
|
||||
/>
|
||||
</ToolButtonListDefault>
|
||||
<ToolButtonListDivider />
|
||||
<ToolButtonListDropDown>
|
||||
<ToolButtonListItem
|
||||
icon="tool-length"
|
||||
onSelect={() => console.debug('Selected Length')}
|
||||
>
|
||||
<span className="pl-1">Length</span>
|
||||
</ToolButtonListItem>
|
||||
<ToolButtonListItem
|
||||
icon="tool-bidirectional"
|
||||
onSelect={() => console.debug('Selected Bidirectional')}
|
||||
>
|
||||
<span className="pl-1">Bidirectional</span>
|
||||
</ToolButtonListItem>
|
||||
</ToolButtonListDropDown>
|
||||
</ToolButtonList>
|
||||
`}
|
||||
>
|
||||
<div className="bg-popover flex h-11 w-[450px] items-center justify-start rounded p-2">
|
||||
<TooltipProvider>
|
||||
<ToolButtonList>
|
||||
<ToolButtonListDefault>
|
||||
<ToolButton
|
||||
id="Length"
|
||||
icon="ToolLength"
|
||||
label="Length"
|
||||
tooltip="Length Tool"
|
||||
onInteraction={({ itemId }) => console.debug(`Clicked ${itemId}`)}
|
||||
/>
|
||||
</ToolButtonListDefault>
|
||||
<ToolButtonListDivider />
|
||||
<ToolButtonListDropDown>
|
||||
<ToolButtonListItem
|
||||
icon="ToolLength"
|
||||
onSelect={() => console.debug('Selected Length')}
|
||||
>
|
||||
<span className="pl-1">Length</span>
|
||||
</ToolButtonListItem>
|
||||
<ToolButtonListItem
|
||||
icon="ToolBidirectional"
|
||||
onSelect={() => console.debug('Selected Bidirectional')}
|
||||
>
|
||||
<span className="pl-1">Bidirectional</span>
|
||||
</ToolButtonListItem>
|
||||
<ToolButtonListItem
|
||||
icon="ToolAnnotate"
|
||||
onSelect={() => console.debug('Selected Annotation')}
|
||||
>
|
||||
<span className="pl-1">Annotation</span>
|
||||
</ToolButtonListItem>
|
||||
</ToolButtonListDropDown>
|
||||
</ToolButtonList>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
import React from 'react';
|
||||
import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip';
|
||||
import ToolButton from '../../../../ui-next/src/components/ToolButton/ToolButton';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* ToolButtonShowcase component displays ToolButton variants and examples
|
||||
*/
|
||||
export default function ToolButtonShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="ToolButton"
|
||||
description="Used to activate tools or perform single actions"
|
||||
code={`
|
||||
// Example usage:
|
||||
<ToolButton
|
||||
id="Zoom"
|
||||
icon="zoom" // must exist in your Icons or fallback to 'MissingIcon'
|
||||
label="Zoom"
|
||||
tooltip="Zoom Tool"
|
||||
isActive={false}
|
||||
onInteraction={({ itemId }) => console.debug(\`Clicked \${itemId}\`)}
|
||||
/>
|
||||
`}
|
||||
>
|
||||
<div className="bg-popover flex h-11 w-[450px] items-center justify-center rounded">
|
||||
<TooltipProvider>
|
||||
<ToolButton
|
||||
id="Zoom"
|
||||
icon="ToolZoom"
|
||||
isActive={true}
|
||||
label="Zoom"
|
||||
tooltip="Zoom"
|
||||
onInteraction={({ itemId }) => console.debug(`Clicked ${itemId}`)}
|
||||
/>
|
||||
<ToolButton
|
||||
id="Zoom"
|
||||
icon="ToolMove"
|
||||
label="Pan"
|
||||
tooltip="Pan"
|
||||
onInteraction={({ itemId }) => console.debug(`Clicked ${itemId}`)}
|
||||
/>
|
||||
<ToolButton
|
||||
id="Zoom"
|
||||
icon="ToolWindowLevel"
|
||||
label="Window Level"
|
||||
tooltip="Window Level"
|
||||
onInteraction={({ itemId }) => console.debug(`Clicked ${itemId}`)}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
|
||||
/**
|
||||
* TooltipShowcase component displays Tooltip variants and examples
|
||||
*/
|
||||
export default function TooltipShowcase() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>
|
||||
{() => {
|
||||
const {
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
} = require('../../../../ui-next/src/components/Tooltip');
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<ShowcaseRow
|
||||
title="Tooltip"
|
||||
description="Tooltips reveal helper text when users hover, focus, or tap an element."
|
||||
code={`
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">?</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Tooltip content
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
`}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
?
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Tooltip content</TooltipContent>
|
||||
</Tooltip>
|
||||
</ShowcaseRow>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
45
platform/docs/src/pages/components/_layout/CodeBlock.tsx
Normal file
45
platform/docs/src/pages/components/_layout/CodeBlock.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
|
||||
interface CodeBlockProps {
|
||||
code: string;
|
||||
}
|
||||
|
||||
export default function CodeBlock({ code }: CodeBlockProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
useEffect(() => () => clearTimeout(timerRef.current), []);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code.trim());
|
||||
} catch {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = code.trim();
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
setCopied(true);
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative rounded-lg border border-input/50 bg-muted">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
aria-label={copied ? 'Copied' : 'Copy code'}
|
||||
className="absolute top-3 right-3 rounded-md bg-popover px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-popover/80 hover:text-foreground"
|
||||
>
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
<pre className="overflow-x-auto p-4 text-sm leading-relaxed text-muted-foreground">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
120
platform/docs/src/pages/components/_layout/ComponentLayout.tsx
Normal file
120
platform/docs/src/pages/components/_layout/ComponentLayout.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import Layout from '@theme/Layout';
|
||||
import Link from '@docusaurus/Link';
|
||||
import { useLocation } from '@docusaurus/router';
|
||||
import { sidebarSections } from './sidebar-config';
|
||||
import TableOfContents from './TableOfContents';
|
||||
|
||||
interface ComponentLayoutProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function ComponentLayout({ title, description, children }: ComponentLayoutProps) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const contentRef = useRef<HTMLElement>(null);
|
||||
const { pathname: currentPath } = useLocation();
|
||||
const { ScrollArea } = require('../../../../../ui-next/src/components/ScrollArea');
|
||||
|
||||
return (
|
||||
<Layout
|
||||
title={title}
|
||||
description={description || `OHIF ${title} component documentation`}
|
||||
>
|
||||
<div className="showcase-isolated flex min-h-screen font-['Inter',sans-serif] bg-background">
|
||||
{/* Mobile sidebar toggle */}
|
||||
<button
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
className="fixed top-[60px] left-0 z-50 flex h-8 w-8 items-center justify-center rounded-r-md bg-muted text-foreground lg:hidden"
|
||||
aria-label="Toggle sidebar"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
{sidebarOpen ? (
|
||||
<path d="M4 4l8 8M12 4l-8 8" />
|
||||
) : (
|
||||
<path d="M2 4h12M2 8h12M2 12h12" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Mobile overlay */}
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-30 bg-black/50 lg:hidden"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={`
|
||||
fixed top-[60px] left-0 z-40 h-[calc(100vh-60px)] w-60 shrink-0
|
||||
component-sidebar bg-background
|
||||
transition-transform duration-200
|
||||
lg:sticky lg:translate-x-0
|
||||
${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
|
||||
`}
|
||||
>
|
||||
<ScrollArea className="h-full">
|
||||
<nav className="px-3 pt-4 pb-8">
|
||||
{sidebarSections.map(section => (
|
||||
<div
|
||||
key={section.title}
|
||||
className="mb-4"
|
||||
>
|
||||
<h4 className="mb-1 px-2 text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
||||
{section.title}
|
||||
</h4>
|
||||
<ul className="space-y-0.5">
|
||||
{section.items.map(item => {
|
||||
const isActive =
|
||||
currentPath === item.href ||
|
||||
currentPath === item.href + '/';
|
||||
return (
|
||||
<li key={item.href + item.label}>
|
||||
<Link
|
||||
to={item.href}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className={`
|
||||
block rounded-md px-2 py-1.5 text-base font-semibold no-underline transition-colors
|
||||
${
|
||||
isActive
|
||||
? 'bg-primary/15 text-primary'
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-highlight'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</ScrollArea>
|
||||
</aside>
|
||||
|
||||
{/* Content */}
|
||||
<main
|
||||
ref={contentRef}
|
||||
className="min-w-0 flex-1 px-6 py-8 lg:px-12"
|
||||
>
|
||||
<div className="mx-auto max-w-4xl pb-36">{children}</div>
|
||||
</main>
|
||||
|
||||
{/* Right-hand table of contents */}
|
||||
<TableOfContents contentRef={contentRef} />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
16
platform/docs/src/pages/components/_layout/ExampleBlock.tsx
Normal file
16
platform/docs/src/pages/components/_layout/ExampleBlock.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ExampleBlockProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
last?: boolean;
|
||||
}
|
||||
|
||||
export default function ExampleBlock({ title, children, last }: ExampleBlockProps) {
|
||||
return (
|
||||
<div className={last ? '' : 'mb-6'}>
|
||||
<h3 className="text-foreground mb-3 text-lg font-medium">{title}</h3>
|
||||
<div className="rounded-lg border border-input/50 bg-muted/30 p-6">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export interface PickerOption {
|
||||
value: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface InteractivePickerProps {
|
||||
options: PickerOption[];
|
||||
defaultValue?: string;
|
||||
renderPreview: (activeValue: string) => React.ReactNode;
|
||||
}
|
||||
|
||||
export default function InteractivePicker({
|
||||
options,
|
||||
defaultValue,
|
||||
renderPreview,
|
||||
}: InteractivePickerProps) {
|
||||
const [active, setActive] = useState(defaultValue || options[0]?.value || '');
|
||||
const activeOption = options.find(o => o.value === active);
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border border-input/50">
|
||||
<div
|
||||
role="tablist"
|
||||
className="flex flex-wrap gap-1 border-b border-input/50 bg-muted/40 px-3 py-2"
|
||||
>
|
||||
{options.map(o => (
|
||||
<button
|
||||
key={o.value}
|
||||
role="tab"
|
||||
aria-selected={active === o.value}
|
||||
onClick={() => setActive(o.value)}
|
||||
className={`rounded-md px-3 py-1.5 text-sm transition-colors ${
|
||||
active === o.value
|
||||
? 'bg-primary/15 text-primary font-medium'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
{o.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
className="flex min-h-[160px] items-center justify-center bg-muted/10 p-8"
|
||||
>
|
||||
{renderPreview(active)}
|
||||
</div>
|
||||
<div className="border-t border-input/50 bg-muted/20 px-4 py-3">
|
||||
<p className="text-lg text-muted-foreground">
|
||||
<span className="font-mono text-xs text-highlight">{active}</span>
|
||||
{' — '}
|
||||
{activeOption?.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
platform/docs/src/pages/components/_layout/PageHeader.tsx
Normal file
18
platform/docs/src/pages/components/_layout/PageHeader.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
interface PageHeaderProps {
|
||||
title: React.ReactNode;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export default function PageHeader({ title, description }: PageHeaderProps) {
|
||||
return (
|
||||
<div
|
||||
id="overview"
|
||||
className="mb-8 scroll-mt-20"
|
||||
>
|
||||
<h1 className="text-highlight mb-2 text-4xl font-medium">{title}</h1>
|
||||
<p className="text-muted-foreground text-xl">{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
platform/docs/src/pages/components/_layout/PropsTable.tsx
Normal file
46
platform/docs/src/pages/components/_layout/PropsTable.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface PropDef {
|
||||
name: string;
|
||||
type: string;
|
||||
default: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface PropsTableProps {
|
||||
props: PropDef[];
|
||||
}
|
||||
|
||||
export default function PropsTable({ props }: PropsTableProps) {
|
||||
const {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
} = require('../../../../../ui-next/src/components/Table');
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-foreground font-medium">Prop</TableHead>
|
||||
<TableHead className="text-foreground font-medium">Type</TableHead>
|
||||
<TableHead className="text-foreground font-medium">Default</TableHead>
|
||||
<TableHead className="text-foreground font-medium">Description</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{props.map(prop => (
|
||||
<TableRow key={prop.name}>
|
||||
<TableCell className="font-mono text-base text-foreground">{prop.name}</TableCell>
|
||||
<TableCell className="font-mono text-base">{prop.type}</TableCell>
|
||||
<TableCell className="font-mono text-base">{prop.default}</TableCell>
|
||||
<TableCell>{prop.description}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
29
platform/docs/src/pages/components/_layout/Section.tsx
Normal file
29
platform/docs/src/pages/components/_layout/Section.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
function slugify(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
interface SectionProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function Section({ title, children }: SectionProps) {
|
||||
const id = slugify(title);
|
||||
|
||||
return (
|
||||
<div className="mb-10">
|
||||
<h2
|
||||
id={id}
|
||||
className="text-highlight mb-4 scroll-mt-20 text-2xl font-medium"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
101
platform/docs/src/pages/components/_layout/TableOfContents.tsx
Normal file
101
platform/docs/src/pages/components/_layout/TableOfContents.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface TocItem {
|
||||
id: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface TableOfContentsProps {
|
||||
contentRef: React.RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
export default function TableOfContents({ contentRef }: TableOfContentsProps) {
|
||||
const [headings, setHeadings] = useState<TocItem[]>([]);
|
||||
const [activeId, setActiveId] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const container = contentRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const discoverHeadings = () => {
|
||||
const h2s = container.querySelectorAll('h2[id]');
|
||||
if (h2s.length === 0) return false;
|
||||
const sections = Array.from(h2s).map(h2 => ({
|
||||
id: h2.id,
|
||||
text: h2.textContent || '',
|
||||
}));
|
||||
setHeadings([{ id: 'overview', text: 'Overview' }, ...sections]);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (discoverHeadings()) return;
|
||||
|
||||
// BrowserOnly content renders after mount — watch for it
|
||||
const observer = new MutationObserver(() => {
|
||||
if (discoverHeadings()) observer.disconnect();
|
||||
});
|
||||
observer.observe(container, { childList: true, subtree: true });
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (headings.length === 0) return;
|
||||
|
||||
let rafId = 0;
|
||||
const handleScroll = () => {
|
||||
cancelAnimationFrame(rafId);
|
||||
rafId = requestAnimationFrame(() => {
|
||||
const scrollTop = window.scrollY + 80;
|
||||
let current = headings[0]?.id || '';
|
||||
|
||||
for (const { id } of headings) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
const top = el.getBoundingClientRect().top + window.scrollY;
|
||||
if (top <= scrollTop) {
|
||||
current = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setActiveId(current);
|
||||
});
|
||||
};
|
||||
|
||||
handleScroll();
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
cancelAnimationFrame(rafId);
|
||||
};
|
||||
}, [headings]);
|
||||
|
||||
if (headings.length === 0) return null;
|
||||
|
||||
return (
|
||||
<nav className="hidden w-44 shrink-0 xl:block">
|
||||
<div className="sticky top-[60px] overflow-y-auto py-8 pl-4">
|
||||
<ul className="space-y-0.5">
|
||||
{headings.map(({ id, text }) => (
|
||||
<li key={id}>
|
||||
<a
|
||||
href={`#${id}`}
|
||||
className={`
|
||||
block py-1 pl-3 text-base font-semibold no-underline transition-colors
|
||||
${
|
||||
activeId === id
|
||||
? 'text-primary'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
51
platform/docs/src/pages/components/_layout/sidebar-config.ts
Normal file
51
platform/docs/src/pages/components/_layout/sidebar-config.ts
Normal file
@ -0,0 +1,51 @@
|
||||
export interface SidebarItem {
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export interface SidebarSection {
|
||||
title: string;
|
||||
items: SidebarItem[];
|
||||
}
|
||||
|
||||
export const sidebarSections: SidebarSection[] = [
|
||||
{
|
||||
title: 'Foundations',
|
||||
items: [
|
||||
{ label: 'Overview', href: '/components' },
|
||||
{ label: 'Colors & Theming', href: '/colors-and-theming' },
|
||||
{ label: 'Iconography', href: '/components/icons' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Components',
|
||||
items: [
|
||||
{ label: 'AllInOneMenu', href: '/components/all-in-one-menu' },
|
||||
{ label: 'Button', href: '/components/button' },
|
||||
{ label: 'Checkbox', href: '/components/checkbox' },
|
||||
{ label: 'CinePlayer', href: '/components/cine-player' },
|
||||
{ label: 'Combobox', href: '/components/combobox' },
|
||||
{ label: 'DataRow', href: '/components/data-row' },
|
||||
{ label: 'DataTable', href: '/components/data-table' },
|
||||
{ label: 'Dialog', href: '/components/dialog' },
|
||||
{ label: 'DropdownMenu', href: '/components/dropdown-menu' },
|
||||
{ label: 'HoverCard', href: '/components/hover-card' },
|
||||
{ label: 'Input', href: '/components/input' },
|
||||
{ label: 'Label', href: '/components/label' },
|
||||
{ label: 'Numeric', href: '/components/numeric' },
|
||||
{ label: 'PanelSection', href: '/components/panel-section' },
|
||||
{ label: 'Popover', href: '/components/popover' },
|
||||
{ label: 'ScrollArea', href: '/components/scroll-area' },
|
||||
{ label: 'Select', href: '/components/select' },
|
||||
{ label: 'Slider', href: '/components/slider' },
|
||||
{ label: 'SmartScrollbar', href: '/components/smart-scrollbar' },
|
||||
{ label: 'Switch', href: '/components/switch-toggle' },
|
||||
{ label: 'Table', href: '/components/table' },
|
||||
{ label: 'Tabs', href: '/components/tabs' },
|
||||
{ label: 'Toast', href: '/components/toast' },
|
||||
{ label: 'ToolButton', href: '/components/tool-button' },
|
||||
{ label: 'ToolButtonList', href: '/components/tool-button-list' },
|
||||
{ label: 'Tooltip', href: '/components/tooltip' },
|
||||
],
|
||||
},
|
||||
];
|
||||
410
platform/docs/src/pages/components/all-in-one-menu.tsx
Normal file
410
platform/docs/src/pages/components/all-in-one-menu.tsx
Normal file
@ -0,0 +1,410 @@
|
||||
import React, { useState } from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function AllInOneMenuPageContent() {
|
||||
// Load from the barrel to avoid circular dependency TDZ crash.
|
||||
// IconMenu, SubMenu, BackItem, and ItemPanel all import from '@ohif/ui-next'.
|
||||
const {
|
||||
AllInOneMenu,
|
||||
Switch,
|
||||
} = require('../../../../ui-next/src/components');
|
||||
|
||||
const {
|
||||
IconMenu,
|
||||
SubMenu,
|
||||
ItemPanel,
|
||||
Item,
|
||||
DividerItem,
|
||||
Menu,
|
||||
HorizontalDirection,
|
||||
VerticalDirection,
|
||||
} = AllInOneMenu;
|
||||
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const colorLUTs = [
|
||||
'Grayscale',
|
||||
'X Ray',
|
||||
'HSV',
|
||||
'Hot Iron',
|
||||
'Red Hot',
|
||||
'S PET',
|
||||
'Perfusion',
|
||||
'Rainbow',
|
||||
'SUV',
|
||||
'GE 256',
|
||||
'GE',
|
||||
'Siemens',
|
||||
];
|
||||
|
||||
const windowPresets = [
|
||||
{ desc: 'Soft tissue', wl: '400 / 40' },
|
||||
{ desc: 'Lung', wl: '1500 / -600' },
|
||||
{ desc: 'Liver', wl: '150 / 90' },
|
||||
{ desc: 'Bone', wl: '2500 / 480' },
|
||||
{ desc: 'Brain', wl: '80 / 40' },
|
||||
];
|
||||
|
||||
const menuProps = [
|
||||
{ name: 'isVisible', type: 'boolean', default: 'false', description: 'Controls menu visibility' },
|
||||
{ name: 'menuStyle', type: 'CSSProperties', default: '—', description: 'Inline styles on the menu container (e.g. width, maxHeight)' },
|
||||
{ name: 'menuClassName', type: 'string', default: '—', description: 'Additional CSS classes on the menu container' },
|
||||
{ name: 'backLabel', type: 'string', default: '"Back"', description: 'Label shown on the back button when inside a SubMenu' },
|
||||
{ name: 'headerComponent', type: 'ReactNode', default: '—', description: 'Content rendered above the menu items (e.g. a search input)' },
|
||||
{ name: 'showHeaderDivider', type: 'boolean', default: 'false', description: 'Show a divider below the header component' },
|
||||
{ name: 'activePanelIndex', type: 'number', default: '0', description: 'Which ItemPanel tab is active (when multiple panels exist)' },
|
||||
{ name: 'preventHideMenu', type: 'boolean', default: 'false', description: 'Prevent the menu from closing on item click' },
|
||||
{ name: 'onVisibilityChange', type: '(visible: boolean) => void', default: '—', description: 'Called when visibility changes' },
|
||||
{ name: 'children', type: 'ReactNode', default: '—', description: 'Menu content (ItemPanel, SubMenu, Item, etc.)' },
|
||||
];
|
||||
|
||||
const iconMenuProps = [
|
||||
{ name: 'icon', type: 'string', default: '—', description: 'Icon name from the OHIF icon registry' },
|
||||
{ name: 'iconClassName', type: 'string', default: '—', description: 'CSS classes on the icon wrapper' },
|
||||
{ name: 'horizontalDirection', type: 'HorizontalDirection', default: 'LeftToRight', description: 'Which edge of the icon the menu aligns to' },
|
||||
{ name: 'verticalDirection', type: 'VerticalDirection', default: 'BottomToTop', description: 'Whether the menu opens above or below the icon' },
|
||||
{ name: 'menuStyle', type: 'CSSProperties', default: '—', description: 'Inline styles passed to the inner Menu' },
|
||||
{ name: 'menuKey', type: 'string | number', default: '—', description: 'React key for the inner Menu (forces remount on change)' },
|
||||
];
|
||||
|
||||
const itemProps = [
|
||||
{ name: 'label', type: 'string', default: '—', description: 'Primary text for the item' },
|
||||
{ name: 'secondaryLabel', type: 'string', default: '—', description: 'Right-aligned secondary text (e.g. a keyboard shortcut or value)' },
|
||||
{ name: 'icon', type: 'ReactNode', default: '—', description: 'Icon rendered in the left gutter' },
|
||||
{ name: 'rightIcon', type: 'ReactNode', default: '—', description: 'Content rendered on the far right (e.g. a Switch toggle)' },
|
||||
{ name: 'useIconSpace', type: 'boolean', default: 'false', description: 'Reserve left gutter space even when no icon is provided' },
|
||||
{ name: 'onClick', type: '() => void', default: '—', description: 'Called on click. The menu auto-hides after.' },
|
||||
];
|
||||
|
||||
const subMenuProps = [
|
||||
{ name: 'itemLabel', type: 'string', default: '—', description: 'Text shown in the parent menu for this submenu entry' },
|
||||
{ name: 'itemIcon', type: 'string', default: '—', description: 'Icon name shown next to the submenu label' },
|
||||
{ name: 'onClick', type: '() => void', default: '—', description: 'Called when the submenu entry is clicked (in addition to navigating)' },
|
||||
{ name: 'children', type: 'ReactNode', default: '—', description: 'The submenu content (ItemPanel with Items)' },
|
||||
];
|
||||
|
||||
const itemPanelProps = [
|
||||
{ name: 'label', type: 'string', default: '—', description: 'Tab label shown in the PanelSelector when multiple panels exist' },
|
||||
{ name: 'index', type: 'number', default: '0', description: 'Panel index for tab ordering' },
|
||||
{ name: 'maxHeight', type: 'string', default: '"250px"', description: 'Max height before scrolling (CSS value)' },
|
||||
{ name: 'className', type: 'string', default: '—', description: 'Additional CSS classes' },
|
||||
{ name: 'children', type: 'ReactNode', default: '—', description: 'Panel content (Items, SubMenus, DividerItems, etc.)' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="AllInOneMenu"
|
||||
description="Hierarchical menu system"
|
||||
>
|
||||
<PageHeader
|
||||
title="AllInOneMenu"
|
||||
description="A structured, navigable menu that consolidates controls, actions, and submenus into a single component."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
AllInOneMenu is a <strong className="text-foreground">compound component system</strong> for
|
||||
building hierarchical menus with stack-based navigation. Clicking a SubMenu pushes a
|
||||
new level onto the menu stack; a Back button at the top pops back to the previous level.
|
||||
This lets users drill into nested options without losing context.
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, AllInOneMenu powers the{' '}
|
||||
<strong className="text-foreground">viewport action corner menus</strong> — Window/Level
|
||||
presets, color LUT selection, orientation controls, and display options. It is one of the
|
||||
most heavily used UI patterns in the application.
|
||||
</p>
|
||||
<p>
|
||||
The system consists of several composable parts:{' '}
|
||||
<strong className="text-foreground">Menu</strong> (root container),{' '}
|
||||
<strong className="text-foreground">IconMenu</strong> (icon trigger + menu),{' '}
|
||||
<strong className="text-foreground">ItemPanel</strong> (scrollable panel with optional tabs),{' '}
|
||||
<strong className="text-foreground">SubMenu</strong> (drills deeper),{' '}
|
||||
<strong className="text-foreground">Item</strong> (leaf action),{' '}
|
||||
<strong className="text-foreground">DividerItem</strong> (separator), and{' '}
|
||||
<strong className="text-foreground">HeaderItem</strong> (section label).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Window / Level menu (viewport action corner)">
|
||||
<div className="relative z-10 flex items-center gap-3">
|
||||
<div className="relative flex h-12 items-center px-4">
|
||||
<IconMenu
|
||||
icon="viewport-window-level"
|
||||
iconClassName="text-xl text-primary hover:bg-primary/30 cursor-pointer rounded"
|
||||
horizontalDirection={HorizontalDirection.LeftToRight}
|
||||
verticalDirection={VerticalDirection.TopToBottom}
|
||||
menuStyle={{ width: 212, maxHeight: 500 }}
|
||||
>
|
||||
<ItemPanel label="Display">
|
||||
<Item
|
||||
label="Display Color bar"
|
||||
rightIcon={
|
||||
<Switch
|
||||
checked={false}
|
||||
disabled
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
}
|
||||
useIconSpace={false}
|
||||
/>
|
||||
<DividerItem />
|
||||
<SubMenu itemLabel="Color LUT" itemIcon="icon-color-lut">
|
||||
<ItemPanel
|
||||
label="Color LUTs"
|
||||
maxHeight="calc(100vh - 250px)"
|
||||
className="flex flex-col"
|
||||
>
|
||||
<Item
|
||||
label="Preview in viewport"
|
||||
rightIcon={
|
||||
<Switch
|
||||
checked={false}
|
||||
disabled
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<DividerItem />
|
||||
{colorLUTs.map(name => (
|
||||
<Item key={name} label={name} />
|
||||
))}
|
||||
</ItemPanel>
|
||||
</SubMenu>
|
||||
<SubMenu itemLabel="Window Presets" itemIcon="viewport-window-level">
|
||||
<ItemPanel label="CT Presets">
|
||||
{windowPresets.map(p => (
|
||||
<Item key={p.desc} label={p.desc} secondaryLabel={p.wl} />
|
||||
))}
|
||||
</ItemPanel>
|
||||
</SubMenu>
|
||||
</ItemPanel>
|
||||
</IconMenu>
|
||||
<span className="text-muted-foreground ml-3 text-sm">
|
||||
Click to explore the menu hierarchy
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Simple flat menu">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative flex h-12 items-center px-4">
|
||||
<IconMenu
|
||||
icon="icon-settings"
|
||||
iconClassName="flex h-6 w-6 items-center justify-center text-primary hover:bg-primary/30 cursor-pointer rounded"
|
||||
horizontalDirection={HorizontalDirection.LeftToRight}
|
||||
verticalDirection={VerticalDirection.TopToBottom}
|
||||
menuStyle={{ width: 180 }}
|
||||
>
|
||||
<ItemPanel>
|
||||
<Item label="Reset viewport" />
|
||||
<Item label="Rotate left" />
|
||||
<Item label="Rotate right" />
|
||||
<DividerItem />
|
||||
<Item label="Flip horizontal" />
|
||||
<Item label="Flip vertical" />
|
||||
</ItemPanel>
|
||||
</IconMenu>
|
||||
<span className="text-muted-foreground ml-3 text-sm">
|
||||
Simple action list
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Controlled Menu (inline, always visible)" last>
|
||||
<div className="relative w-[220px]">
|
||||
<Menu
|
||||
isVisible={true}
|
||||
preventHideMenu={true}
|
||||
menuStyle={{ width: 212 }}
|
||||
>
|
||||
<ItemPanel>
|
||||
<Item label="Axial" secondaryLabel="A" />
|
||||
<Item label="Sagittal" secondaryLabel="S" />
|
||||
<Item label="Coronal" secondaryLabel="C" />
|
||||
<DividerItem />
|
||||
<Item label="3D" secondaryLabel="3" />
|
||||
</ItemPanel>
|
||||
</Menu>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Anatomy">
|
||||
<div className="text-secondary-foreground space-y-4 text-lg leading-relaxed">
|
||||
<p>
|
||||
A typical AllInOneMenu is structured as an <strong className="text-foreground">IconMenu</strong> wrapping
|
||||
one or more <strong className="text-foreground">ItemPanels</strong>, each containing{' '}
|
||||
<strong className="text-foreground">Items</strong> and{' '}
|
||||
<strong className="text-foreground">SubMenus</strong>.
|
||||
</p>
|
||||
<pre className="overflow-x-auto rounded-md border border-input/50 bg-muted/50 p-4 text-sm leading-relaxed text-muted-foreground">
|
||||
{`IconMenu (icon trigger + positioned Menu)
|
||||
├── ItemPanel (scrollable, labeled for tabs)
|
||||
│ ├── Item (leaf action — label, icon, secondaryLabel)
|
||||
│ ├── Item + rightIcon (e.g. Switch toggle)
|
||||
│ ├── DividerItem (separator)
|
||||
│ ├── SubMenu → pushes a new level
|
||||
│ │ └── ItemPanel
|
||||
│ │ ├── Item
|
||||
│ │ └── Item
|
||||
│ └── SubMenu → pushes a new level
|
||||
│ └── ItemPanel
|
||||
│ └── Item
|
||||
└── (optional) second ItemPanel → creates tabbed panels`}
|
||||
</pre>
|
||||
<p>
|
||||
When a <strong className="text-foreground">SubMenu</strong> is clicked, its children replace the
|
||||
current view and a <strong className="text-foreground">Back</strong> button appears at the top.
|
||||
When multiple <strong className="text-foreground">ItemPanels</strong> exist at the same level,
|
||||
a tab bar appears to switch between them.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import AllInOneMenu from '@ohif/ui-next/AllInOneMenu';
|
||||
|
||||
const { IconMenu, SubMenu, ItemPanel, Item, DividerItem,
|
||||
HorizontalDirection, VerticalDirection } = AllInOneMenu;
|
||||
|
||||
// Icon-triggered menu with submenus
|
||||
<IconMenu
|
||||
icon="viewport-window-level"
|
||||
iconClassName="text-xl text-highlight cursor-pointer"
|
||||
horizontalDirection={HorizontalDirection.LeftToRight}
|
||||
verticalDirection={VerticalDirection.TopToBottom}
|
||||
menuStyle={{ width: 212, maxHeight: 500 }}
|
||||
>
|
||||
<ItemPanel label="Display">
|
||||
<Item label="Reset" onClick={handleReset} />
|
||||
<DividerItem />
|
||||
<SubMenu itemLabel="Window Presets" itemIcon="viewport-window-level">
|
||||
<ItemPanel label="CT Presets">
|
||||
<Item label="Soft tissue" secondaryLabel="400 / 40" onClick={...} />
|
||||
<Item label="Lung" secondaryLabel="1500 / -600" onClick={...} />
|
||||
</ItemPanel>
|
||||
</SubMenu>
|
||||
</ItemPanel>
|
||||
</IconMenu>
|
||||
|
||||
// Simple flat menu
|
||||
<IconMenu
|
||||
icon="icon-settings"
|
||||
verticalDirection={VerticalDirection.TopToBottom}
|
||||
menuStyle={{ width: 180 }}
|
||||
>
|
||||
<ItemPanel>
|
||||
<Item label="Rotate left" onClick={handleRotateLeft} />
|
||||
<Item label="Rotate right" onClick={handleRotateRight} />
|
||||
</ItemPanel>
|
||||
</IconMenu>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Direction & Positioning">
|
||||
<div className="text-secondary-foreground space-y-4 text-lg leading-relaxed">
|
||||
<p>
|
||||
<strong className="text-foreground">HorizontalDirection</strong> controls which edge the menu aligns to:
|
||||
</p>
|
||||
<ul className="list-disc space-y-1 pl-6">
|
||||
<li><code className="text-xs text-highlight">LeftToRight</code> — menu left edge aligns with icon left edge. Use when the icon is near the left side of its container.</li>
|
||||
<li><code className="text-xs text-highlight">RightToLeft</code> — menu right edge aligns with icon right edge. Use when the icon is near the right side.</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong className="text-foreground">VerticalDirection</strong> controls whether the menu opens above or below:
|
||||
</p>
|
||||
<ul className="list-disc space-y-1 pl-6">
|
||||
<li><code className="text-xs text-highlight">TopToBottom</code> — menu appears below the icon. Use when the icon is near the top.</li>
|
||||
<li><code className="text-xs text-highlight">BottomToTop</code> — menu appears above the icon. Use when the icon is near the bottom (default).</li>
|
||||
</ul>
|
||||
<p>
|
||||
In the OHIF Viewer, viewport action corners typically use BottomToTop + RightToLeft
|
||||
(top-right corner) or TopToBottom + LeftToRight (bottom-left corner) depending on
|
||||
where the trigger lives.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">Menu</h3>
|
||||
<p className="text-muted-foreground mb-3 text-sm">
|
||||
Root container. Manages visibility, menu path stack, and panel selection.
|
||||
Usually used indirectly via IconMenu.
|
||||
</p>
|
||||
<PropsTable props={menuProps} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">IconMenu</h3>
|
||||
<p className="text-muted-foreground mb-3 text-sm">
|
||||
Wraps Menu with a clickable icon trigger and outside-click dismissal.
|
||||
Inherits all Menu props in addition to the ones below.
|
||||
</p>
|
||||
<PropsTable props={iconMenuProps} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">Item</h3>
|
||||
<p className="text-muted-foreground mb-3 text-sm">
|
||||
A single actionable row. Clicking an Item auto-closes the menu.
|
||||
</p>
|
||||
<PropsTable props={itemProps} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">SubMenu</h3>
|
||||
<p className="text-muted-foreground mb-3 text-sm">
|
||||
Pushes a new level onto the menu stack when clicked. Shows a chevron indicator.
|
||||
Its children define the content of the new level.
|
||||
</p>
|
||||
<PropsTable props={subMenuProps} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">ItemPanel</h3>
|
||||
<p className="text-muted-foreground mb-3 text-sm">
|
||||
Scrollable container for Items. When multiple ItemPanels exist at the same menu level,
|
||||
a tab bar appears to switch between them.
|
||||
</p>
|
||||
<PropsTable props={itemPanelProps} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">DividerItem</h3>
|
||||
<p className="text-muted-foreground mb-3 text-sm">
|
||||
A thin horizontal line separating groups of items. No props.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">HeaderItem</h3>
|
||||
<p className="text-muted-foreground mb-3 text-sm">
|
||||
A compact section label rendered in muted text. Accepts <code className="text-xs">children: ReactNode</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AllInOneMenuPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <AllInOneMenuPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
147
platform/docs/src/pages/components/button.tsx
Normal file
147
platform/docs/src/pages/components/button.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function ButtonPageContent() {
|
||||
const { Button } = require('../../../../ui-next/src/components/Button');
|
||||
const { Icons } = require('../../../../ui-next/src/components/Icons');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const InteractivePicker = require('./_layout/InteractivePicker').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const variants = [
|
||||
{ value: 'default', label: 'Default', description: 'Primary action. Solid blue background.' },
|
||||
{ value: 'secondary', label: 'Secondary', description: 'Secondary action. Muted blue background.' },
|
||||
{ value: 'ghost', label: 'Ghost', description: 'Minimal emphasis. Transparent until hovered. Used in toolbars and panels.' },
|
||||
{ value: 'outline', label: 'Outline', description: 'Border-only with transparent fill. Presence without weight.' },
|
||||
{ value: 'link', label: 'Link', description: 'Inline text with underline on hover. For navigation-style actions.' },
|
||||
{ value: 'destructive', label: 'Destructive', description: 'Red background for dangerous or irreversible actions.' },
|
||||
];
|
||||
|
||||
const sizes = [
|
||||
{ value: 'sm', label: 'Small', description: 'Compact. Height: 24px (h-6).' },
|
||||
{ value: 'default', label: 'Default', description: 'Standard. Height: 28px (h-7).' },
|
||||
{ value: 'lg', label: 'Large', description: 'Height: 36px (h-9). For prominent dialog actions.' },
|
||||
{ value: 'icon', label: 'Icon', description: 'Square icon button. 24×24px (h-6 w-6).' },
|
||||
];
|
||||
|
||||
const props = [
|
||||
{ name: 'variant', type: '"default" | "secondary" | "ghost" | "outline" | "link" | "destructive"', default: '"default"', description: 'Visual style variant' },
|
||||
{ name: 'size', type: '"default" | "sm" | "lg" | "icon"', default: '"default"', description: 'Button size' },
|
||||
{ name: 'asChild', type: 'boolean', default: 'false', description: 'Merge props onto child element instead of rendering a button' },
|
||||
{ name: 'disabled', type: 'boolean', default: 'false', description: 'Disables interaction and reduces opacity' },
|
||||
{ name: 'className', type: 'string', default: '—', description: 'Additional CSS classes merged via cn()' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Button"
|
||||
description="Primary action component with multiple variants and sizes"
|
||||
>
|
||||
<PageHeader
|
||||
title="Button"
|
||||
description="Triggers an action or event. Use variants to communicate hierarchy and intent."
|
||||
/>
|
||||
|
||||
{/* Description */}
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
The Button component is the primary interactive element for triggering actions.
|
||||
It supports six variants to express different levels of emphasis and intent.
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, <strong className="text-foreground">default</strong> (primary)
|
||||
buttons appear in dialogs and confirmation screens where a single action is required.{' '}
|
||||
<strong className="text-foreground">Ghost</strong> buttons are used throughout panels
|
||||
and toolbars where many actions compete for attention.{' '}
|
||||
<strong className="text-foreground">Secondary</strong> buttons pair with primary buttons
|
||||
when a less prominent alternative is needed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Variants">
|
||||
<InteractivePicker
|
||||
options={variants}
|
||||
defaultValue="default"
|
||||
renderPreview={(active) => (
|
||||
<Button variant={active as any}>
|
||||
{active === 'destructive' ? 'Delete' : `${variants.find(v => v.value === active)?.label} Button`}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Sizes">
|
||||
<InteractivePicker
|
||||
options={sizes}
|
||||
defaultValue="default"
|
||||
renderPreview={(active) =>
|
||||
active === 'icon' ? (
|
||||
<Button variant="ghost" size="icon">
|
||||
<Icons.More />
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="default" size={active as any}>
|
||||
{sizes.find(s => s.value === active)?.label} Button
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Dialog footer">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="ghost">Cancel</Button>
|
||||
<Button variant="default">Confirm</Button>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Button with icon">
|
||||
<div className="flex gap-3">
|
||||
<Button variant="default">
|
||||
<Icons.Add className="mr-1.5 h-4 w-4" />
|
||||
Add Segment
|
||||
</Button>
|
||||
<Button variant="ghost">
|
||||
<Icons.Settings className="mr-1.5 h-4 w-4" />
|
||||
Settings
|
||||
</Button>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Disabled state" last>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="default" disabled>Disabled</Button>
|
||||
<Button variant="ghost" disabled>Disabled Ghost</Button>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { Button } from '@ohif/ui-next';
|
||||
|
||||
<Button variant="default">Click me</Button>
|
||||
<Button variant="ghost" size="sm">Settings</Button>
|
||||
<Button variant="destructive">Delete</Button>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ButtonPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <ButtonPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
129
platform/docs/src/pages/components/checkbox.tsx
Normal file
129
platform/docs/src/pages/components/checkbox.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function CheckboxPageContent() {
|
||||
const { Checkbox } = require('../../../../ui-next/src/components/Checkbox');
|
||||
const { Label } = require('../../../../ui-next/src/components/Label');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const InteractivePicker = require('./_layout/InteractivePicker').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const states = [
|
||||
{ value: 'unchecked', label: 'Unchecked', description: 'Default state. Empty border.' },
|
||||
{ value: 'checked', label: 'Checked', description: 'Active state. Filled with primary color and check icon.' },
|
||||
{ value: 'disabled', label: 'Disabled', description: 'Non-interactive. Reduced opacity.' },
|
||||
];
|
||||
|
||||
const props = [
|
||||
{ name: 'checked', type: 'boolean', default: '—', description: 'Controlled checked state' },
|
||||
{ name: 'defaultChecked', type: 'boolean', default: 'false', description: 'Initial checked state (uncontrolled)' },
|
||||
{ name: 'onCheckedChange', type: '(checked: boolean) => void', default: '—', description: 'Called when the checked state changes' },
|
||||
{ name: 'disabled', type: 'boolean', default: 'false', description: 'Disables interaction and reduces opacity' },
|
||||
{ name: 'className', type: 'string', default: '—', description: 'Additional CSS classes merged via cn()' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Checkbox"
|
||||
description="Boolean toggle for form controls"
|
||||
>
|
||||
<PageHeader
|
||||
title="Checkbox"
|
||||
description="A small control for toggling between two states. Prefer Switch when space allows."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
Checkbox provides a compact boolean toggle built on Radix UI primitives.
|
||||
When possible, prefer <strong className="text-foreground">Switch</strong> for
|
||||
better visibility and touch targets. Use Checkbox when space is constrained
|
||||
or when multiple options appear in a list.
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, checkboxes appear in settings panels and segmentation
|
||||
controls where multiple independent options need toggling.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="States">
|
||||
<InteractivePicker
|
||||
options={states}
|
||||
defaultValue="unchecked"
|
||||
renderPreview={(active) => (
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox
|
||||
id="demo"
|
||||
checked={active === 'checked'}
|
||||
disabled={active === 'disabled'}
|
||||
/>
|
||||
<Label htmlFor="demo" className={active === 'disabled' ? 'opacity-50' : ''}>
|
||||
Display inactive segmentations
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="With label">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox id="ex1" defaultChecked />
|
||||
<Label htmlFor="ex1">Display inactive segmentations</Label>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Multiple options">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox id="opt1" defaultChecked />
|
||||
<Label htmlFor="opt1">Show annotations</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox id="opt2" />
|
||||
<Label htmlFor="opt2">Show measurements</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox id="opt3" defaultChecked />
|
||||
<Label htmlFor="opt3">Show segmentations</Label>
|
||||
</div>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Disabled" last>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox id="dis1" disabled />
|
||||
<Label htmlFor="dis1" className="opacity-50">Unavailable option</Label>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { Checkbox } from '@ohif/ui-next';
|
||||
import { Label } from '@ohif/ui-next';
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="option" />
|
||||
<Label htmlFor="option">Enable feature</Label>
|
||||
</div>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CheckboxPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <CheckboxPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
163
platform/docs/src/pages/components/cine-player.tsx
Normal file
163
platform/docs/src/pages/components/cine-player.tsx
Normal file
@ -0,0 +1,163 @@
|
||||
import React, { useState } from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function CinePlayerPageContent() {
|
||||
// Load from the barrel — CinePlayer.tsx imports { Icons } from '@ohif/ui-next',
|
||||
// which re-exports CinePlayer, creating a circular dependency TDZ crash if the barrel
|
||||
// isn't already cached.
|
||||
const { CinePlayer } = require('../../../../ui-next/src/components');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const [isPlaying1, setIsPlaying1] = useState(false);
|
||||
const [fps1, setFps1] = useState(24);
|
||||
|
||||
const [isPlaying2, setIsPlaying2] = useState(false);
|
||||
const [fps2, setFps2] = useState(10);
|
||||
|
||||
const [isPlaying3, setIsPlaying3] = useState(false);
|
||||
const [fps3, setFps3] = useState(24);
|
||||
const [dynamicGroup, setDynamicGroup] = useState(3);
|
||||
|
||||
const props = [
|
||||
{ name: 'isPlaying', type: 'boolean', default: 'false', description: 'Whether cine playback is active. Controls the play/pause icon.' },
|
||||
{ name: 'frameRate', type: 'number', default: '24', description: 'Current frames per second value' },
|
||||
{ name: 'minFrameRate', type: 'number', default: '1', description: 'Minimum FPS allowed' },
|
||||
{ name: 'maxFrameRate', type: 'number', default: '90', description: 'Maximum FPS allowed' },
|
||||
{ name: 'stepFrameRate', type: 'number', default: '1', description: 'FPS increment step for the stepper and slider' },
|
||||
{ name: 'onPlayPauseChange', type: '(playing: boolean) => void', default: '—', description: 'Called when play/pause is toggled' },
|
||||
{ name: 'onFrameRateChange', type: '(fps: number) => void', default: '—', description: 'Called when FPS changes (debounced 100ms)' },
|
||||
{ name: 'onClose', type: '() => void', default: '—', description: 'Called when the close button is clicked' },
|
||||
{ name: 'className', type: 'string', default: '—', description: 'Additional CSS classes on the root container' },
|
||||
{ name: 'dynamicInfo', type: '{ dimensionGroupNumber, numDimensionGroups, label? }', default: '—', description: 'Dynamic volume info for 4D series. Shows group counter and dimension slider.' },
|
||||
{ name: 'updateDynamicInfo', type: '(info) => void', default: '—', description: 'Called when the dynamic dimension slider changes' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="CinePlayer"
|
||||
description="Video playback controls"
|
||||
>
|
||||
<PageHeader
|
||||
title="CinePlayer"
|
||||
description="Floating playback controls for scrubbing through image series and adjusting frame rate."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
CinePlayer provides <strong className="text-foreground">play/pause</strong>,{' '}
|
||||
<strong className="text-foreground">FPS control</strong> (stepper + popover slider),
|
||||
and a <strong className="text-foreground">close button</strong> in a compact floating bar.
|
||||
It composes Button, Numeric, and Popover internally.
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, CinePlayer appears as a{' '}
|
||||
<strong className="text-foreground">floating overlay</strong> on viewports when cine
|
||||
mode is activated. For <strong className="text-foreground">4D dynamic volumes</strong>{' '}
|
||||
(e.g. cardiac time series), it additionally shows a dimension group counter and
|
||||
a scrub slider below the main controls.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Basic playback controls">
|
||||
<div className="relative h-[80px] w-[320px]">
|
||||
<CinePlayer
|
||||
className="w-[300px]"
|
||||
isPlaying={isPlaying1}
|
||||
frameRate={fps1}
|
||||
onPlayPauseChange={setIsPlaying1}
|
||||
onFrameRateChange={setFps1}
|
||||
onClose={() => setIsPlaying1(false)}
|
||||
/>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Custom FPS range (1–30, step 2)">
|
||||
<div className="relative h-[80px] w-[320px]">
|
||||
<CinePlayer
|
||||
className="w-[300px]"
|
||||
isPlaying={isPlaying2}
|
||||
frameRate={fps2}
|
||||
minFrameRate={1}
|
||||
maxFrameRate={30}
|
||||
stepFrameRate={2}
|
||||
onPlayPauseChange={setIsPlaying2}
|
||||
onFrameRateChange={setFps2}
|
||||
onClose={() => setIsPlaying2(false)}
|
||||
/>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="With dynamic volume info (4D series)" last>
|
||||
<div className="relative h-[120px] w-[320px]">
|
||||
<CinePlayer
|
||||
className="w-[300px]"
|
||||
isPlaying={isPlaying3}
|
||||
frameRate={fps3}
|
||||
onPlayPauseChange={setIsPlaying3}
|
||||
onFrameRateChange={setFps3}
|
||||
onClose={() => setIsPlaying3(false)}
|
||||
dynamicInfo={{
|
||||
dimensionGroupNumber: dynamicGroup,
|
||||
numDimensionGroups: 12,
|
||||
label: 'timepoint',
|
||||
}}
|
||||
updateDynamicInfo={(info) => setDynamicGroup(info.dimensionGroupNumber)}
|
||||
/>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import CinePlayer from '@ohif/ui-next/CinePlayer';
|
||||
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [fps, setFps] = useState(24);
|
||||
|
||||
<CinePlayer
|
||||
className="w-[300px]"
|
||||
isPlaying={isPlaying}
|
||||
frameRate={fps}
|
||||
onPlayPauseChange={setIsPlaying}
|
||||
onFrameRateChange={setFps}
|
||||
onClose={() => setCineEnabled(false)}
|
||||
/>
|
||||
|
||||
// With 4D dynamic volume
|
||||
<CinePlayer
|
||||
className="w-[300px]"
|
||||
isPlaying={isPlaying}
|
||||
frameRate={fps}
|
||||
onPlayPauseChange={setIsPlaying}
|
||||
onFrameRateChange={setFps}
|
||||
onClose={handleClose}
|
||||
dynamicInfo={{
|
||||
dimensionGroupNumber: currentGroup,
|
||||
numDimensionGroups: totalGroups,
|
||||
label: 'timepoint',
|
||||
}}
|
||||
updateDynamicInfo={handleDynamicChange}
|
||||
/>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CinePlayerPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <CinePlayerPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
102
platform/docs/src/pages/components/combobox.tsx
Normal file
102
platform/docs/src/pages/components/combobox.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function ComboboxPageContent() {
|
||||
const { Combobox, Label } = require('../../../../ui-next/src/components');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const modalities = [
|
||||
'AR', 'AU', 'BI', 'CR', 'CT', 'DG', 'DOC', 'DX', 'ECG', 'ES',
|
||||
'GM', 'IO', 'KO', 'MG', 'MR', 'NM', 'OCT', 'OP', 'OT', 'PR',
|
||||
'PT', 'REG', 'RF', 'RG', 'RTDOSE', 'RTIMAGE', 'RTPLAN', 'RTSTRUCT',
|
||||
'SEG', 'SM', 'SR', 'US', 'XA', 'XC',
|
||||
].map(m => ({ value: m, label: m }));
|
||||
|
||||
const tools = [
|
||||
{ value: 'length', label: 'Length' },
|
||||
{ value: 'bidirectional', label: 'Bidirectional' },
|
||||
{ value: 'elliptical-roi', label: 'Elliptical ROI' },
|
||||
{ value: 'rectangle-roi', label: 'Rectangle ROI' },
|
||||
{ value: 'angle', label: 'Angle' },
|
||||
{ value: 'cobb-angle', label: 'Cobb Angle' },
|
||||
];
|
||||
|
||||
const props = [
|
||||
{ name: 'data', type: '{ value: string; label: string }[]', default: '[]', description: 'Array of selectable options' },
|
||||
{ name: 'placeholder', type: 'string', default: '"Select item..."', description: 'Placeholder text shown when no value is selected' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Combobox"
|
||||
description="Searchable dropdown for filtering and selecting"
|
||||
>
|
||||
<PageHeader
|
||||
title="Combobox"
|
||||
description="A searchable dropdown built with Command and Popover primitives."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
Combobox combines a text search input with a dropdown list, allowing users to
|
||||
filter through large option sets. It's built on top of{' '}
|
||||
<strong className="text-foreground">Command</strong> (cmdk) and{' '}
|
||||
<strong className="text-foreground">Popover</strong> components.
|
||||
</p>
|
||||
<p>
|
||||
Use Combobox instead of Select when the option list is long (10+ items) or
|
||||
when users benefit from type-to-filter. In the OHIF Viewer, it's used for
|
||||
modality selection and other searchable lists.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Modality list">
|
||||
<Combobox data={modalities} placeholder="Modality" />
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Tool selection">
|
||||
<Combobox data={tools} placeholder="Measurement tool" />
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="With label" last>
|
||||
<div className="flex items-center gap-4">
|
||||
<Label>Filter by</Label>
|
||||
<Combobox data={modalities} placeholder="Modality" />
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { Combobox } from '@ohif/ui-next';
|
||||
|
||||
const items = [
|
||||
{ value: 'ct', label: 'CT' },
|
||||
{ value: 'mr', label: 'MR' },
|
||||
{ value: 'us', label: 'US' },
|
||||
];
|
||||
|
||||
<Combobox data={items} placeholder="Modality" />`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ComboboxPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <ComboboxPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
289
platform/docs/src/pages/components/data-row.tsx
Normal file
289
platform/docs/src/pages/components/data-row.tsx
Normal file
@ -0,0 +1,289 @@
|
||||
import React, { useState } from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function DataRowPageContent() {
|
||||
const { DataRow } = require('../../../../ui-next/src/components/DataRow');
|
||||
const { TooltipProvider } = require('../../../../ui-next/src/components/Tooltip');
|
||||
const { dataList } = require('../../../../ui-next/assets/data');
|
||||
|
||||
const roiToolsGroup = dataList.find(g => g.type === 'ROI Tools');
|
||||
const measurementItems = roiToolsGroup ? roiToolsGroup.items.slice(0, 5) : [];
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const InteractivePicker = require('./_layout/InteractivePicker').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const [selectedId, setSelectedId] = useState('seg-1');
|
||||
const [visibility, setVisibility] = useState({ 'seg-1': true, 'seg-2': true, 'seg-3': true });
|
||||
const [locked, setLocked] = useState({ 'seg-1': false, 'seg-2': true, 'seg-3': false });
|
||||
|
||||
const noop = (e) => e?.stopPropagation?.();
|
||||
|
||||
const states = [
|
||||
{ value: 'default', label: 'Default', description: 'Idle state. Muted background, foreground text.' },
|
||||
{ value: 'selected', label: 'Selected', description: 'Primary selection. Popover background, highlight title and number box.' },
|
||||
{ value: 'secondary', label: 'Secondary', description: 'Secondary selection. Primary tint overlay, used for inactive segmentation.' },
|
||||
{ value: 'hidden', label: 'Hidden', description: 'Visibility off. Entire row at 60% opacity.' },
|
||||
];
|
||||
|
||||
const props = [
|
||||
{ name: 'number', type: 'number | null', default: '—', description: 'Display index. Renders a colored number box when provided.' },
|
||||
{ name: 'description', type: 'string', default: '—', description: 'Secondary text shown below the title (e.g. volume, stats).' },
|
||||
{ name: 'title', type: 'string', default: '—', description: 'Primary text label. Long titles (>25 chars) get a tooltip.' },
|
||||
{ name: 'colorHex', type: 'string', default: '—', description: 'Hex color for the dot indicator (e.g. segmentation color)' },
|
||||
{ name: 'isSelected', type: 'boolean', default: 'false', description: 'Primary selection state (highlighted background + title)' },
|
||||
{ name: 'isSecondarySelected', type: 'boolean', default: 'false', description: 'Secondary selection (primary tint, for inactive segmentation)' },
|
||||
{ name: 'onSelect', type: '(e) => void', default: '—', description: 'Called when the row is clicked' },
|
||||
{ name: 'isVisible', type: 'boolean', default: 'true', description: 'Visibility state. Row renders at 60% opacity when false.' },
|
||||
{ name: 'onToggleVisibility', type: '(e) => void', default: '—', description: 'Called when the eye icon is clicked' },
|
||||
{ name: 'isLocked', type: 'boolean', default: 'false', description: 'Shows a lock icon when true' },
|
||||
{ name: 'onToggleLocked', type: '(e) => void', default: '—', description: 'Called when lock is toggled via the action menu' },
|
||||
{ name: 'disableEditing', type: 'boolean', default: 'false', description: 'Hides the action menu (rename, delete, color, lock)' },
|
||||
{ name: 'onRename', type: '(e) => void', default: '—', description: 'Called from the action menu' },
|
||||
{ name: 'onDelete', type: '(e) => void', default: '—', description: 'Called from the action menu' },
|
||||
{ name: 'onColor', type: '(e) => void', default: '—', description: 'Called from the action menu' },
|
||||
{ name: 'onCopy', type: '(e) => void', default: '—', description: 'Optional. Adds a "Duplicate" item to the action menu.' },
|
||||
{ name: 'details', type: '{ primary: string[], secondary: string[] }', default: '—', description: 'Expandable detail lines below the row (max 4 visible)' },
|
||||
{ name: 'children', type: 'ReactNode', default: '—', description: 'Status indicators via DataRow.Status.Warning/Success/Error/Info' },
|
||||
];
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<ComponentLayout
|
||||
title="DataRow"
|
||||
description="Interactive list row for panels"
|
||||
>
|
||||
<PageHeader
|
||||
title="DataRow"
|
||||
description="A selectable, numbered row with visibility toggle, color indicator, and contextual action menu."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
DataRow is the primary list item component in OHIF side panels. It displays a
|
||||
numbered entry with a title, optional{' '}
|
||||
<strong className="text-foreground">color dot</strong>,{' '}
|
||||
<strong className="text-foreground">visibility toggle</strong>,{' '}
|
||||
<strong className="text-foreground">lock indicator</strong>, and a{' '}
|
||||
<strong className="text-foreground">three-dot action menu</strong> with
|
||||
rename, delete, color change, duplicate, and lock/unlock options.
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, DataRow is used for{' '}
|
||||
<strong className="text-foreground">segmentation lists</strong>,{' '}
|
||||
<strong className="text-foreground">measurement lists</strong>, and any panel
|
||||
where items need selection, visibility control, and contextual actions. It supports
|
||||
primary selection (active segmentation) and secondary selection (inactive segmentation)
|
||||
states.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="States">
|
||||
<InteractivePicker
|
||||
options={states}
|
||||
defaultValue="default"
|
||||
renderPreview={(active) => (
|
||||
<div className="w-[280px] space-y-px">
|
||||
<DataRow
|
||||
number={1}
|
||||
title="Liver"
|
||||
description=""
|
||||
colorHex="#E2B93B"
|
||||
isSelected={active === 'selected'}
|
||||
isSecondarySelected={active === 'secondary'}
|
||||
isVisible={active !== 'hidden'}
|
||||
isLocked={false}
|
||||
disableEditing={false}
|
||||
onToggleVisibility={noop}
|
||||
onToggleLocked={noop}
|
||||
onRename={noop}
|
||||
onDelete={noop}
|
||||
onColor={noop}
|
||||
onSelect={noop}
|
||||
/>
|
||||
<DataRow
|
||||
number={2}
|
||||
title="Spleen"
|
||||
description=""
|
||||
colorHex="#68B9FF"
|
||||
isSelected={false}
|
||||
isVisible={true}
|
||||
isLocked={false}
|
||||
disableEditing={false}
|
||||
onToggleVisibility={noop}
|
||||
onToggleLocked={noop}
|
||||
onRename={noop}
|
||||
onDelete={noop}
|
||||
onColor={noop}
|
||||
onSelect={noop}
|
||||
/>
|
||||
<DataRow
|
||||
number={3}
|
||||
title="Kidney L"
|
||||
description=""
|
||||
colorHex="#FF5733"
|
||||
isSelected={false}
|
||||
isVisible={true}
|
||||
isLocked={false}
|
||||
disableEditing={false}
|
||||
onToggleVisibility={noop}
|
||||
onToggleLocked={noop}
|
||||
onRename={noop}
|
||||
onDelete={noop}
|
||||
onColor={noop}
|
||||
onSelect={noop}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Segmentation list with interactive selection">
|
||||
<div className="w-[280px] space-y-px">
|
||||
{[
|
||||
{ id: 'seg-1', num: 1, title: 'Liver', color: '#E2B93B' },
|
||||
{ id: 'seg-2', num: 2, title: 'Spleen', color: '#68B9FF' },
|
||||
{ id: 'seg-3', num: 3, title: 'Kidney Left', color: '#FF5733' },
|
||||
].map((seg) => (
|
||||
<DataRow
|
||||
key={seg.id}
|
||||
number={seg.num}
|
||||
title={seg.title}
|
||||
description=""
|
||||
colorHex={seg.color}
|
||||
isSelected={selectedId === seg.id}
|
||||
isVisible={visibility[seg.id]}
|
||||
isLocked={locked[seg.id]}
|
||||
disableEditing={false}
|
||||
onSelect={() => setSelectedId(seg.id === selectedId ? null : seg.id)}
|
||||
onToggleVisibility={(e) => {
|
||||
e.stopPropagation();
|
||||
setVisibility(v => ({ ...v, [seg.id]: !v[seg.id] }));
|
||||
}}
|
||||
onToggleLocked={(e) => {
|
||||
e.stopPropagation();
|
||||
setLocked(l => ({ ...l, [seg.id]: !l[seg.id] }));
|
||||
}}
|
||||
onRename={noop}
|
||||
onDelete={noop}
|
||||
onColor={noop}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Measurement list with details">
|
||||
<div className="w-[280px] space-y-px">
|
||||
{measurementItems.map((item, index) => (
|
||||
<DataRow
|
||||
key={item.id}
|
||||
number={index + 1}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
details={item.details}
|
||||
isSelected={index === 0}
|
||||
isVisible={true}
|
||||
isLocked={false}
|
||||
disableEditing={false}
|
||||
onSelect={noop}
|
||||
onToggleVisibility={noop}
|
||||
onToggleLocked={noop}
|
||||
onRename={noop}
|
||||
onDelete={noop}
|
||||
onColor={noop}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Read-only (editing disabled)" last>
|
||||
<div className="w-[280px] space-y-px">
|
||||
<DataRow
|
||||
number={1}
|
||||
title="CT Axial 2.0mm"
|
||||
description=""
|
||||
isSelected={true}
|
||||
isVisible={true}
|
||||
isLocked={false}
|
||||
disableEditing={true}
|
||||
onToggleVisibility={noop}
|
||||
onToggleLocked={noop}
|
||||
onRename={noop}
|
||||
onDelete={noop}
|
||||
onColor={noop}
|
||||
/>
|
||||
<DataRow
|
||||
number={2}
|
||||
title="PET Coronal"
|
||||
description=""
|
||||
isSelected={false}
|
||||
isVisible={true}
|
||||
isLocked={false}
|
||||
disableEditing={true}
|
||||
onToggleVisibility={noop}
|
||||
onToggleLocked={noop}
|
||||
onRename={noop}
|
||||
onDelete={noop}
|
||||
onColor={noop}
|
||||
/>
|
||||
<DataRow
|
||||
number={3}
|
||||
title="Segmentation Overlay"
|
||||
description=""
|
||||
isSelected={false}
|
||||
isVisible={false}
|
||||
isLocked={false}
|
||||
disableEditing={true}
|
||||
onToggleVisibility={noop}
|
||||
onToggleLocked={noop}
|
||||
onRename={noop}
|
||||
onDelete={noop}
|
||||
onColor={noop}
|
||||
/>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { DataRow } from '@ohif/ui-next';
|
||||
|
||||
<DataRow
|
||||
number={1}
|
||||
title="Liver"
|
||||
colorHex="#E2B93B"
|
||||
isSelected={selectedId === 'liver'}
|
||||
isVisible={true}
|
||||
isLocked={false}
|
||||
disableEditing={false}
|
||||
onSelect={() => setSelectedId('liver')}
|
||||
onToggleVisibility={handleToggleVisibility}
|
||||
onToggleLocked={handleToggleLocked}
|
||||
onRename={handleRename}
|
||||
onDelete={handleDelete}
|
||||
onColor={handleColor}
|
||||
>
|
||||
<DataRow.Status.Success tooltip="Tracked" />
|
||||
</DataRow>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DataRowPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <DataRowPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
352
platform/docs/src/pages/components/data-table.tsx
Normal file
352
platform/docs/src/pages/components/data-table.tsx
Normal file
@ -0,0 +1,352 @@
|
||||
import React, { useState } from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function DataTablePageContent() {
|
||||
const { DataTable } = require('../../../../ui-next/src/components/DataTable');
|
||||
const {
|
||||
Table: BasicTable,
|
||||
TableHeader: BasicTableHeader,
|
||||
TableBody: BasicTableBody,
|
||||
TableHead: BasicTableHead,
|
||||
TableRow: BasicTableRow,
|
||||
TableCell: BasicTableCell,
|
||||
} = require('../../../../ui-next/src/components/Table');
|
||||
const { TooltipProvider } = require('../../../../ui-next/src/components/Tooltip');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const [sorting, setSorting] = useState([]);
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 5 });
|
||||
const [filters, setFilters] = useState([]);
|
||||
|
||||
const studies = [
|
||||
{ studyInstanceUid: '1.2.840.1', patientName: 'Smith, John', mrn: '12345678', modality: 'CT', date: 'Mar 15, 2024', description: 'CT Chest with Contrast', instances: 245 },
|
||||
{ studyInstanceUid: '1.2.840.2', patientName: 'Doe, Jane', mrn: '23456789', modality: 'MR', date: 'Mar 14, 2024', description: 'MR Brain without Contrast', instances: 186 },
|
||||
{ studyInstanceUid: '1.2.840.3', patientName: 'Lee, Alex', mrn: '34567890', modality: 'CT', date: 'Mar 14, 2024', description: 'CT Abdomen/Pelvis', instances: 312 },
|
||||
{ studyInstanceUid: '1.2.840.4', patientName: 'Garcia, Maria', mrn: '45678901', modality: 'PET/CT', date: 'Mar 13, 2024', description: 'PET/CT Whole Body', instances: 1024 },
|
||||
{ studyInstanceUid: '1.2.840.5', patientName: 'Brown, Robert', mrn: '56789012', modality: 'MR', date: 'Mar 12, 2024', description: 'MR Knee Left', instances: 92 },
|
||||
{ studyInstanceUid: '1.2.840.6', patientName: 'Wilson, Emily', mrn: '67890123', modality: 'CT', date: 'Mar 11, 2024', description: 'CT Head without Contrast', instances: 156 },
|
||||
{ studyInstanceUid: '1.2.840.7', patientName: 'Chen, Wei', mrn: '78901234', modality: 'US', date: 'Mar 10, 2024', description: 'US Abdomen Complete', instances: 48 },
|
||||
{ studyInstanceUid: '1.2.840.8', patientName: 'Johnson, Sarah', mrn: '89012345', modality: 'MR', date: 'Mar 09, 2024', description: 'MR Lumbar Spine', instances: 220 },
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
id: 'patientName',
|
||||
accessorFn: row => row.patientName,
|
||||
header: ({ column }) => <DataTable.ColumnHeader column={column} />,
|
||||
cell: ({ row }) => <div className="truncate">{row.getValue('patientName')}</div>,
|
||||
meta: { label: 'Patient', minWidth: 160, priority: 100 },
|
||||
},
|
||||
{
|
||||
id: 'mrn',
|
||||
accessorFn: row => row.mrn,
|
||||
header: ({ column }) => <DataTable.ColumnHeader column={column} />,
|
||||
cell: ({ row }) => <div className="truncate">{row.getValue('mrn')}</div>,
|
||||
meta: { label: 'MRN', minWidth: 110, priority: 20 },
|
||||
},
|
||||
{
|
||||
id: 'date',
|
||||
accessorFn: row => row.date,
|
||||
header: ({ column }) => <DataTable.ColumnHeader column={column} />,
|
||||
cell: ({ row }) => <div className="truncate">{row.getValue('date')}</div>,
|
||||
meta: { label: 'Study Date', minWidth: 130, priority: 70 },
|
||||
},
|
||||
{
|
||||
id: 'modality',
|
||||
accessorFn: row => row.modality,
|
||||
header: ({ column }) => <DataTable.ColumnHeader column={column} />,
|
||||
cell: ({ row }) => <div className="truncate">{row.getValue('modality')}</div>,
|
||||
meta: { label: 'Modality', minWidth: 90, priority: 60 },
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
accessorFn: row => row.description,
|
||||
header: ({ column }) => <DataTable.ColumnHeader column={column} />,
|
||||
cell: ({ row }) => {
|
||||
const desc = row.getValue('description');
|
||||
return (
|
||||
<div className={!desc ? 'text-muted-foreground/40' : 'truncate'}>
|
||||
{desc || 'No Description'}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: { label: 'Description', minWidth: 200, priority: 90 },
|
||||
},
|
||||
{
|
||||
id: 'instances',
|
||||
accessorFn: row => Number(row.instances),
|
||||
header: ({ column }) => <DataTable.ColumnHeader column={column} />,
|
||||
cell: ({ row }) => <div className="text-right">{row.getValue('instances')}</div>,
|
||||
sortingFn: (a, b) => a.getValue('instances') - b.getValue('instances'),
|
||||
meta: { label: 'Instances', align: 'right', minWidth: 80, priority: 50 },
|
||||
},
|
||||
];
|
||||
|
||||
const simpleColumns = [
|
||||
{
|
||||
accessorKey: 'patientName',
|
||||
header: 'Patient',
|
||||
meta: { label: 'Patient' },
|
||||
},
|
||||
{
|
||||
accessorKey: 'modality',
|
||||
header: 'Modality',
|
||||
meta: { label: 'Modality' },
|
||||
},
|
||||
{
|
||||
accessorKey: 'date',
|
||||
header: 'Date',
|
||||
meta: { label: 'Date' },
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Description',
|
||||
meta: { label: 'Description' },
|
||||
},
|
||||
];
|
||||
|
||||
const dataTableProps = [
|
||||
{ name: 'data', type: 'TData[]', default: '—', description: 'Array of data objects to display in the table' },
|
||||
{ name: 'columns', type: 'ColumnDef<TData>[]', default: '—', description: 'TanStack React Table column definitions' },
|
||||
{ name: 'getRowId', type: '(row, index) => string', default: '—', description: 'Custom row ID resolver (defaults to row index)' },
|
||||
{ name: 'initialVisibility', type: 'VisibilityState', default: '{}', description: 'Initial column visibility state' },
|
||||
{ name: 'sorting', type: 'SortingState', default: '[]', description: 'Controlled sorting state' },
|
||||
{ name: 'pagination', type: 'PaginationState', default: '{pageIndex: 0, pageSize: 50}', description: 'Controlled pagination state' },
|
||||
{ name: 'filters', type: 'ColumnFiltersState', default: '[]', description: 'Controlled column filter state' },
|
||||
{ name: 'onSortingChange', type: '(updater) => void', default: '—', description: 'Called when sorting changes' },
|
||||
{ name: 'onPaginationChange', type: '(updater) => void', default: '—', description: 'Called when page or page size changes' },
|
||||
{ name: 'onFiltersChange', type: '(updater) => void', default: '—', description: 'Called when column filters change' },
|
||||
{ name: 'manualFiltering', type: 'boolean', default: 'false', description: 'Disables automatic client-side filtering (use for server-side filtering)' },
|
||||
{ name: 'enforceSingleSelection', type: 'boolean', default: 'true', description: 'Restricts row selection to a single row at a time' },
|
||||
{ name: 'onSelectionChange', type: '(rows: TData[]) => void', default: '—', description: 'Called when the set of selected rows changes' },
|
||||
];
|
||||
|
||||
const columnMetaProps = [
|
||||
{ name: 'label', type: 'string', default: '—', description: 'Column label shown in the ViewOptions dropdown and as the default header text' },
|
||||
{ name: 'headerContent', type: 'ReactNode', default: '—', description: 'Custom header content that replaces the label in the column header' },
|
||||
{ name: 'align', type: '"left" | "center" | "right"', default: '"left"', description: 'Content alignment for both header and body cells' },
|
||||
{ name: 'headerClassName', type: 'string', default: '—', description: 'CSS class applied to the header cell' },
|
||||
{ name: 'cellClassName', type: 'string', default: '—', description: 'CSS class applied to body cells in this column' },
|
||||
{ name: 'minWidth', type: 'number | string', default: '—', description: 'Fixed column width (used by colgroup)' },
|
||||
{ name: 'priority', type: 'number', default: '—', description: 'Responsive drop priority — columns with lower values are hidden first when the table is narrow. Columns without a priority are never auto-hidden.' },
|
||||
];
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<ComponentLayout
|
||||
title="DataTable"
|
||||
description="Compound data table with sorting, filtering, and pagination"
|
||||
>
|
||||
<PageHeader
|
||||
title="DataTable"
|
||||
description="A compound data table built on TanStack React Table with sorting, filtering, pagination, and column management."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
DataTable is a compound component built on{' '}
|
||||
<strong className="text-foreground">@tanstack/react-table</strong>. The root provider
|
||||
manages table state and exposes it via context to composable child components:{' '}
|
||||
<strong className="text-foreground">Toolbar</strong>,{' '}
|
||||
<strong className="text-foreground">Title</strong>,{' '}
|
||||
<strong className="text-foreground">Pagination</strong>,{' '}
|
||||
<strong className="text-foreground">ViewOptions</strong>,{' '}
|
||||
<strong className="text-foreground">Table</strong>,{' '}
|
||||
<strong className="text-foreground">Header</strong>,{' '}
|
||||
<strong className="text-foreground">FilterRow</strong>,{' '}
|
||||
<strong className="text-foreground">Body</strong>,{' '}
|
||||
<strong className="text-foreground">ColumnHeader</strong>, and{' '}
|
||||
<strong className="text-foreground">ActionOverlayCell</strong>.
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, DataTable powers the{' '}
|
||||
<strong className="text-foreground">Study List</strong> — the landing page where
|
||||
users browse, filter, and open studies. It supports sortable columns, per-column
|
||||
text filters, pagination, column visibility toggling, responsive column dropping,
|
||||
and row selection with action overlays. Built on top of the{' '}
|
||||
<a href="/components/table" className="text-primary hover:underline">Table</a>{' '}
|
||||
primitives.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Study browser">
|
||||
<div className="flex flex-col">
|
||||
<DataTable
|
||||
data={studies}
|
||||
columns={columns}
|
||||
getRowId={row => row.studyInstanceUid}
|
||||
sorting={sorting}
|
||||
pagination={pagination}
|
||||
filters={filters}
|
||||
onSortingChange={setSorting}
|
||||
onPaginationChange={setPagination}
|
||||
onFiltersChange={setFilters}
|
||||
onSelectionChange={rows => {}}
|
||||
>
|
||||
<DataTable.Toolbar>
|
||||
<DataTable.Title>Study List</DataTable.Title>
|
||||
<div className="flex-1" />
|
||||
<DataTable.Pagination />
|
||||
<DataTable.ViewOptions />
|
||||
</DataTable.Toolbar>
|
||||
<DataTable.Table tableClassName="text-sm">
|
||||
<DataTable.Header />
|
||||
<DataTable.FilterRow excludeColumnIds={['instances']} />
|
||||
<DataTable.Body
|
||||
rowProps={{
|
||||
className: 'group cursor-pointer',
|
||||
onClick: row => row.toggleSelected(),
|
||||
}}
|
||||
/>
|
||||
</DataTable.Table>
|
||||
</DataTable>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Minimal table" last>
|
||||
<DataTable
|
||||
data={studies.slice(0, 5)}
|
||||
columns={simpleColumns}
|
||||
>
|
||||
<DataTable.Table>
|
||||
<DataTable.Header />
|
||||
<DataTable.Body />
|
||||
</DataTable.Table>
|
||||
</DataTable>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Composition">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
DataTable uses a compound component pattern. The root{' '}
|
||||
<strong className="text-foreground">{'<DataTable>'}</strong> creates the TanStack table
|
||||
instance and provides it via context. Child components consume that context to render
|
||||
their piece of the UI.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<BasicTable>
|
||||
<BasicTableHeader>
|
||||
<BasicTableRow>
|
||||
<BasicTableHead className="text-foreground font-medium">Sub-component</BasicTableHead>
|
||||
<BasicTableHead className="text-foreground font-medium">Purpose</BasicTableHead>
|
||||
</BasicTableRow>
|
||||
</BasicTableHeader>
|
||||
<BasicTableBody>
|
||||
{[
|
||||
['DataTable.Toolbar', 'Flex container for title, pagination, and view options', 0],
|
||||
['DataTable.Title', 'Table heading text', 1],
|
||||
['DataTable.Pagination', 'Page navigation with configurable page sizes (25, 50, 100)', 1],
|
||||
['DataTable.ViewOptions', 'Column visibility toggle dropdown', 1],
|
||||
['DataTable.Table', 'Layout shell — fixed header table + scrollable body table', 0],
|
||||
['DataTable.Header', 'Renders column headers with sort indicators', 1],
|
||||
['DataTable.FilterRow', 'Row of per-column text filter inputs', 1],
|
||||
['DataTable.Body', 'Table rows with selection state and empty/loading states', 1],
|
||||
['DataTable.ColumnHeader', 'Individual column header with sort toggle button', 0],
|
||||
['DataTable.ActionOverlayCell', 'Cell with a value that swaps to an action overlay on hover or selection', 0],
|
||||
].map(([name, purpose, indent]) => (
|
||||
<BasicTableRow key={name}>
|
||||
<BasicTableCell className="font-mono text-base text-foreground">
|
||||
{indent ? <span className="pl-4 text-muted-foreground/60">↳ </span> : null}
|
||||
{name}
|
||||
</BasicTableCell>
|
||||
<BasicTableCell>{purpose}</BasicTableCell>
|
||||
</BasicTableRow>
|
||||
))}
|
||||
</BasicTableBody>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { DataTable } from '@ohif/ui-next';
|
||||
|
||||
// 1. Define your data type and mock data
|
||||
const studies = [
|
||||
{ uid: '1.2.840.1', patient: 'Smith, John', modality: 'CT', date: 'Mar 15, 2024' },
|
||||
// ...
|
||||
];
|
||||
|
||||
// 2. Define columns with TanStack ColumnDef
|
||||
const columns = [
|
||||
{
|
||||
id: 'patient',
|
||||
accessorFn: row => row.patient,
|
||||
header: ({ column }) => <DataTable.ColumnHeader column={column} />,
|
||||
cell: ({ row }) => <div className="truncate">{row.getValue('patient')}</div>,
|
||||
meta: { label: 'Patient', minWidth: 160, priority: 100 },
|
||||
},
|
||||
// ...more columns
|
||||
];
|
||||
|
||||
// 3. Manage state
|
||||
const [sorting, setSorting] = useState([]);
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 });
|
||||
const [filters, setFilters] = useState([]);
|
||||
|
||||
// 4. Compose the table
|
||||
<DataTable
|
||||
data={studies}
|
||||
columns={columns}
|
||||
getRowId={row => row.uid}
|
||||
sorting={sorting}
|
||||
pagination={pagination}
|
||||
filters={filters}
|
||||
onSortingChange={setSorting}
|
||||
onPaginationChange={setPagination}
|
||||
onFiltersChange={setFilters}
|
||||
>
|
||||
<DataTable.Toolbar>
|
||||
<DataTable.Title>Studies</DataTable.Title>
|
||||
<div className="flex-1" />
|
||||
<DataTable.Pagination />
|
||||
<DataTable.ViewOptions />
|
||||
</DataTable.Toolbar>
|
||||
<DataTable.Table>
|
||||
<DataTable.Header />
|
||||
<DataTable.FilterRow />
|
||||
<DataTable.Body
|
||||
rowProps={{
|
||||
className: 'group cursor-pointer',
|
||||
onClick: row => row.toggleSelected(),
|
||||
}}
|
||||
/>
|
||||
</DataTable.Table>
|
||||
</DataTable>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="DataTable Props">
|
||||
<PropsTable props={dataTableProps} />
|
||||
</Section>
|
||||
|
||||
<Section title="ColumnMeta">
|
||||
<div className="mb-4">
|
||||
<p className="text-secondary-foreground text-lg leading-relaxed">
|
||||
Column metadata is set via the <strong className="text-foreground">meta</strong> property
|
||||
on each TanStack column definition. It controls header labels, alignment, sizing, and
|
||||
responsive behavior.
|
||||
</p>
|
||||
</div>
|
||||
<PropsTable props={columnMetaProps} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DataTablePage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <DataTablePageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
189
platform/docs/src/pages/components/dialog.tsx
Normal file
189
platform/docs/src/pages/components/dialog.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function DialogPageContent() {
|
||||
const {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogClose,
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
} = require('../../../../ui-next/src/components');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const props = [
|
||||
{ name: 'isDraggable', type: 'boolean', default: 'false', description: 'Enables dragging the dialog by its header' },
|
||||
{ name: 'shouldCloseOnEsc', type: 'boolean', default: 'true', description: 'Whether pressing Escape closes the dialog' },
|
||||
{ name: 'shouldCloseOnOverlayClick', type: 'boolean', default: 'true', description: 'Whether clicking the overlay closes the dialog' },
|
||||
{ name: 'showOverlay', type: 'boolean', default: 'true', description: 'Shows a dimmed backdrop behind the dialog' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Dialog"
|
||||
description="Modal overlay for focused interactions"
|
||||
>
|
||||
<PageHeader
|
||||
title="Dialog"
|
||||
description="A modal window that overlays the page for confirmations, forms, and focused tasks."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
Dialog is a multi-part component built on Radix UI primitives:{' '}
|
||||
<strong className="text-foreground">Dialog</strong> (root),{' '}
|
||||
<strong className="text-foreground">DialogTrigger</strong>,{' '}
|
||||
<strong className="text-foreground">DialogContent</strong>,{' '}
|
||||
<strong className="text-foreground">DialogHeader</strong>,{' '}
|
||||
<strong className="text-foreground">DialogFooter</strong>,{' '}
|
||||
<strong className="text-foreground">DialogTitle</strong>,{' '}
|
||||
and <strong className="text-foreground">DialogDescription</strong>.
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, dialogs are used for{' '}
|
||||
<strong className="text-foreground">confirmation prompts</strong>,{' '}
|
||||
<strong className="text-foreground">measurement labels</strong>,{' '}
|
||||
<strong className="text-foreground">export options</strong>, and{' '}
|
||||
<strong className="text-foreground">settings panels</strong>. The draggable variant
|
||||
lets users reposition the dialog to see underlying content.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Basic confirmation">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="default">Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="showcase-portal">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Confirm Action</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
Are you sure you want to proceed? This action cannot be undone.
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="ghost">Cancel</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button variant="default">Confirm</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Form dialog">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary">Edit Label</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="showcase-portal">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Measurement Label</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter a label for this measurement annotation.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="dlg-label" className="mb-1 block">Label</Label>
|
||||
<Input id="dlg-label" placeholder="e.g. Lesion 1" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="dlg-desc" className="mb-1 block">Description</Label>
|
||||
<Input id="dlg-desc" placeholder="Optional description..." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="ghost">Cancel</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button variant="default">Save</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Draggable dialog" last>
|
||||
<Dialog isDraggable showOverlay={false}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Open Draggable</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="showcase-portal">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Draggable Dialog</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
Drag the header to reposition this dialog over the viewport.
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="default">Done</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import {
|
||||
Dialog, DialogTrigger, DialogContent,
|
||||
DialogHeader, DialogFooter,
|
||||
DialogTitle, DialogDescription, DialogClose,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Title</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>Description text.</DialogDescription>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="ghost">Cancel</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button>Confirm</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DialogPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <DialogPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
195
platform/docs/src/pages/components/dropdown-menu.tsx
Normal file
195
platform/docs/src/pages/components/dropdown-menu.tsx
Normal file
@ -0,0 +1,195 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function DropdownMenuPageContent() {
|
||||
const {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuShortcut,
|
||||
} = require('../../../../ui-next/src/components/DropdownMenu');
|
||||
const { Button } = require('../../../../ui-next/src/components/Button');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const [showMeasurements, setShowMeasurements] = React.useState(true);
|
||||
const [showSegmentations, setShowSegmentations] = React.useState(false);
|
||||
|
||||
const props = [
|
||||
{ name: 'align', type: '"start" | "center" | "end"', default: '"center"', description: 'Horizontal alignment of the menu relative to the trigger (on DropdownMenuContent)' },
|
||||
{ name: 'side', type: '"top" | "right" | "bottom" | "left"', default: '"bottom"', description: 'Which side of the trigger the menu opens on (on DropdownMenuContent)' },
|
||||
{ name: 'sideOffset', type: 'number', default: '4', description: 'Distance in pixels between the trigger and the menu (on DropdownMenuContent)' },
|
||||
{ name: 'onSelect', type: '() => void', default: '—', description: 'Called when a menu item is selected (on DropdownMenuItem)' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="DropdownMenu"
|
||||
description="Context menu triggered from a button"
|
||||
>
|
||||
<PageHeader
|
||||
title="DropdownMenu"
|
||||
description="A menu of actions or options that opens from a trigger button."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
DropdownMenu is a multi-part component built on Radix UI primitives:{' '}
|
||||
<strong className="text-foreground">DropdownMenu</strong> (root),{' '}
|
||||
<strong className="text-foreground">DropdownMenuTrigger</strong>,{' '}
|
||||
<strong className="text-foreground">DropdownMenuContent</strong>,{' '}
|
||||
and <strong className="text-foreground">DropdownMenuItem</strong>. It also supports{' '}
|
||||
<strong className="text-foreground">labels</strong>,{' '}
|
||||
<strong className="text-foreground">separators</strong>,{' '}
|
||||
<strong className="text-foreground">checkbox items</strong>, and{' '}
|
||||
<strong className="text-foreground">keyboard shortcuts</strong>.
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, dropdown menus appear in toolbar overflow menus, viewport
|
||||
action corners, layout selectors, and context menus for measurements and segments.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Alignment">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">Default</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">Align Start</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">Align End</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">Side Top</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="start">
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="With labels and shortcuts">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">Layout</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-48">
|
||||
<DropdownMenuLabel>Viewport Layout</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
1×1
|
||||
<DropdownMenuShortcut>⌘1</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
1×2
|
||||
<DropdownMenuShortcut>⌘2</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
2×2
|
||||
<DropdownMenuShortcut>⌘3</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Custom...</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Checkbox items" last>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">Overlays</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-52">
|
||||
<DropdownMenuLabel>Visible Overlays</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={showMeasurements}
|
||||
onCheckedChange={setShowMeasurements}
|
||||
>
|
||||
Measurements
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={showSegmentations}
|
||||
onCheckedChange={setShowSegmentations}
|
||||
>
|
||||
Segmentations
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import {
|
||||
Button,
|
||||
DropdownMenu, DropdownMenuTrigger,
|
||||
DropdownMenuContent, DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Actions</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onSelect={() => handleExport()}>
|
||||
Export
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DropdownMenuPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <DropdownMenuPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
304
platform/docs/src/pages/components/hover-card.tsx
Normal file
304
platform/docs/src/pages/components/hover-card.tsx
Normal file
@ -0,0 +1,304 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function HoverCardPageContent() {
|
||||
const {
|
||||
HoverCard,
|
||||
HoverCardTrigger,
|
||||
HoverCardContent,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
Button,
|
||||
Icons,
|
||||
DataRow,
|
||||
TooltipProvider,
|
||||
} = require('../../../../ui-next/src/components');
|
||||
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const segments = [
|
||||
{ title: 'Liver', color: '#E2B93B', stats: { mean: '72.4', std: '18.2', min: '31.0', max: '128.5', volume: '1,847' } },
|
||||
{ title: 'Spleen', color: '#68B9FF', stats: { mean: '54.1', std: '12.8', min: '22.0', max: '91.3', volume: '423' } },
|
||||
{ title: 'Left Kidney', color: '#FF6B6B', stats: { mean: '38.7', std: '9.4', min: '15.2', max: '72.1', volume: '312' } },
|
||||
{ title: 'Right Kidney', color: '#4ECDC4', stats: { mean: '41.2', std: '10.1', min: '18.0', max: '76.8', volume: '298' } },
|
||||
{ title: 'Aorta', color: '#C084FC', stats: { mean: '62.9', std: '15.6', min: '28.4', max: '105.2', volume: '186' } },
|
||||
];
|
||||
|
||||
const hoverCardProps = [
|
||||
{ name: 'openDelay', type: 'number', default: '700', description: 'Delay in ms before the card opens' },
|
||||
{ name: 'closeDelay', type: 'number', default: '300', description: 'Delay in ms before the card closes' },
|
||||
{ name: 'children', type: 'ReactNode', default: '—', description: 'Must contain HoverCardTrigger and HoverCardContent' },
|
||||
];
|
||||
|
||||
const contentProps = [
|
||||
{ name: 'side', type: '"top" | "right" | "bottom" | "left"', default: '"bottom"', description: 'Which side of the trigger the card appears on' },
|
||||
{ name: 'align', type: '"start" | "center" | "end"', default: '"center"', description: 'Alignment relative to the trigger along the side axis' },
|
||||
{ name: 'sideOffset', type: 'number', default: '4', description: 'Distance in pixels from the trigger' },
|
||||
{ name: 'className', type: 'string', default: '—', description: 'Additional CSS classes on the content container' },
|
||||
];
|
||||
|
||||
const triggerProps = [
|
||||
{ name: 'asChild', type: 'boolean', default: 'false', description: 'Merge props onto the child element instead of rendering a span' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="HoverCard"
|
||||
description="Rich preview surface on hover"
|
||||
>
|
||||
<PageHeader
|
||||
title="HoverCard"
|
||||
description="A non-modal floating card that appears on hover to show rich, structured content."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
HoverCard is built on Radix UI primitives with three parts:{' '}
|
||||
<strong className="text-foreground">HoverCard</strong> (root with open/close delay),{' '}
|
||||
<strong className="text-foreground">HoverCardTrigger</strong> (the hover target), and{' '}
|
||||
<strong className="text-foreground">HoverCardContent</strong> (the floating surface).
|
||||
</p>
|
||||
<p>
|
||||
Unlike tooltips, hover cards can contain structured layouts — metadata grids,
|
||||
statistics, and interactive elements. In the OHIF Viewer, hover cards are used for{' '}
|
||||
<strong className="text-foreground">data source configuration</strong> previews,{' '}
|
||||
<strong className="text-foreground">segment statistics</strong> in the segmentation panel,
|
||||
and <strong className="text-foreground">study/measurement metadata</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Data source configuration">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Hover the Source button to see the configuration card
|
||||
</span>
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="gap-1 text-sm"
|
||||
>
|
||||
<Icons.CloudSettings className="h-5 w-5" />
|
||||
Source
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
align="center"
|
||||
className="w-72 p-0"
|
||||
>
|
||||
<Card className="border-0 shadow-none">
|
||||
<CardHeader className="p-3 pb-1">
|
||||
<CardDescription className="text-sm">
|
||||
<span className="text-foreground font-semibold">Data Source:</span>{' '}
|
||||
Configure the server connection and storage settings
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 p-3 pt-0 text-sm">
|
||||
<div className="bg-input col-span-2 my-2 h-px" />
|
||||
<span className="text-muted-foreground">Project</span>
|
||||
<span>ohif-cloud-healthcare</span>
|
||||
<span className="text-muted-foreground">Location</span>
|
||||
<span>us-east1</span>
|
||||
<span className="text-muted-foreground">Data set</span>
|
||||
<span>radiology-primary</span>
|
||||
<span className="text-muted-foreground">DICOM store</span>
|
||||
<span>dicom-store-prod</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Segment statistics (segmentation list pattern)">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Hover a segment to see its statistics card
|
||||
</span>
|
||||
<TooltipProvider>
|
||||
<div className="bg-muted w-[280px] rounded-md">
|
||||
{segments.map((seg, i) => (
|
||||
<HoverCard key={seg.title} openDelay={300} closeDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<div>
|
||||
<DataRow
|
||||
number={i + 1}
|
||||
title={seg.title}
|
||||
description=""
|
||||
colorHex={seg.color}
|
||||
isSelected={i === 0}
|
||||
isVisible={true}
|
||||
isLocked={false}
|
||||
disableEditing={false}
|
||||
onSelect={() => {}}
|
||||
onToggleVisibility={() => {}}
|
||||
onToggleLocked={() => {}}
|
||||
onRename={() => {}}
|
||||
onDelete={() => {}}
|
||||
onColor={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
side="left"
|
||||
align="start"
|
||||
className="w-64 p-0"
|
||||
>
|
||||
<Card className="border-0 shadow-none">
|
||||
<CardHeader className="p-3 pb-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div
|
||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
||||
style={{ backgroundColor: seg.color }}
|
||||
/>
|
||||
<CardTitle className="text-muted-foreground text-base">{seg.title}</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1 p-3 pt-0 text-sm">
|
||||
<div className="bg-input my-2 h-px" />
|
||||
{[
|
||||
['Mean', seg.stats.mean],
|
||||
['Std Dev', seg.stats.std],
|
||||
['Min', seg.stats.min],
|
||||
['Max', seg.stats.max],
|
||||
['Volume', seg.stats.volume],
|
||||
].map(([label, value]) => (
|
||||
<div key={label} className="flex justify-between">
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<span>
|
||||
<span className="text-foreground">{value}</span>{' '}
|
||||
{label === 'Volume' ? 'mm³' : 'HU'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
))}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Study metadata preview" last>
|
||||
<div className="flex justify-center">
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button variant="link">CT Chest w/ Contrast</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-72">
|
||||
<div className="space-y-2">
|
||||
<p className="text-foreground text-sm font-medium">CT Chest w/ Contrast</p>
|
||||
<div className="text-secondary-foreground space-y-1 text-xs">
|
||||
<p><span className="text-muted-foreground">Patient:</span> DOE^JOHN</p>
|
||||
<p><span className="text-muted-foreground">MRN:</span> 123456</p>
|
||||
<p><span className="text-muted-foreground">Date:</span> 2024-03-15</p>
|
||||
<p><span className="text-muted-foreground">Series:</span> 4 · <span className="text-muted-foreground">Images:</span> 512</p>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import {
|
||||
HoverCard, HoverCardTrigger, HoverCardContent,
|
||||
Card, CardHeader, CardTitle, CardContent, CardDescription,
|
||||
Button, Icons,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
// Basic hover card
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button variant="link">Hover me</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<p>Rich preview content here.</p>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
|
||||
// With Card inside (data source pattern)
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Icons.CloudSettings className="h-5 w-5" />
|
||||
Source
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent align="center" className="w-72 p-0">
|
||||
<Card className="border-0 shadow-none">
|
||||
<CardHeader className="p-3 pb-1">
|
||||
<CardDescription>Server configuration details</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 p-3 pt-0 text-sm">
|
||||
<span className="text-muted-foreground">Project</span>
|
||||
<span>my-project</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
|
||||
// Side positioning (segmentation list pattern)
|
||||
<HoverCard openDelay={300} closeDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<div>{/* DataRow or other trigger */}</div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent side="left" align="start" className="w-64 p-0">
|
||||
{/* Statistics card */}
|
||||
</HoverCardContent>
|
||||
</HoverCard>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">HoverCard</h3>
|
||||
<p className="text-muted-foreground mb-3 text-sm">
|
||||
Root component. Controls open/close timing.
|
||||
</p>
|
||||
<PropsTable props={hoverCardProps} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">HoverCardTrigger</h3>
|
||||
<p className="text-muted-foreground mb-3 text-sm">
|
||||
The element that activates the hover card. Use asChild to merge onto your own element.
|
||||
</p>
|
||||
<PropsTable props={triggerProps} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">HoverCardContent</h3>
|
||||
<p className="text-muted-foreground mb-3 text-sm">
|
||||
The floating surface. Positioned relative to the trigger via side, align, and sideOffset.
|
||||
</p>
|
||||
<PropsTable props={contentProps} />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HoverCardPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <HoverCardPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
327
platform/docs/src/pages/components/icons.tsx
Normal file
327
platform/docs/src/pages/components/icons.tsx
Normal file
@ -0,0 +1,327 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
const FINAL_ICONS = [
|
||||
'ActionNewDialog',
|
||||
'Actions',
|
||||
'ActionsBidirectional',
|
||||
'ActionsCombine',
|
||||
'ActionsCombineIntersect',
|
||||
'ActionsCombineMerge',
|
||||
'ActionsCombineSubtract',
|
||||
'ActionsInterpolate',
|
||||
'ActionsSetting',
|
||||
'ActionsSimplify',
|
||||
'ActionsSmooth',
|
||||
'Add',
|
||||
'Close',
|
||||
'CloudSettings',
|
||||
'ColorChange',
|
||||
'Controls',
|
||||
'Copy',
|
||||
'Delete',
|
||||
'DicomTagBrowser',
|
||||
'DisplayFillAndOutline',
|
||||
'DisplayFillOnly',
|
||||
'DisplayOutlineOnly',
|
||||
'Download',
|
||||
'Export',
|
||||
'ExternalLink',
|
||||
'EyeVisible',
|
||||
'FeedbackComplete',
|
||||
'GearSettings',
|
||||
'GroupLayers',
|
||||
'Hide',
|
||||
'IconColorLUT',
|
||||
'IconMPR',
|
||||
'Info',
|
||||
'InfoLink',
|
||||
'InfoSeries',
|
||||
'JumpToSlice',
|
||||
'LayerBackground',
|
||||
'LayerForeground',
|
||||
'LayerSegmentation',
|
||||
'LayoutAdvanced3DFourUp',
|
||||
'LayoutAdvanced3DMain',
|
||||
'LayoutAdvanced3DOnly',
|
||||
'LayoutAdvanced3DPrimary',
|
||||
'LayoutAdvancedAxialPrimary',
|
||||
'LayoutCommon1x1',
|
||||
'LayoutCommon1x2',
|
||||
'LayoutCommon2x2',
|
||||
'LayoutCommon2x3',
|
||||
'ListView',
|
||||
'LoadingSpinner',
|
||||
'Lock',
|
||||
'More',
|
||||
'MultiplePatients',
|
||||
'Opacity',
|
||||
'OrientationSwitch',
|
||||
'OrientationSwitchA',
|
||||
'OrientationSwitchC',
|
||||
'OrientationSwitchR',
|
||||
'OrientationSwitchS',
|
||||
'Patient',
|
||||
'Pause',
|
||||
'Pin',
|
||||
'PinFill',
|
||||
'Play',
|
||||
'Redo',
|
||||
'Rename',
|
||||
'Series',
|
||||
'Settings',
|
||||
'Show',
|
||||
'SidePanelCloseLeft',
|
||||
'SidePanelCloseRight',
|
||||
'SocialGithub',
|
||||
'SortingNew',
|
||||
'SortingNewAscending',
|
||||
'SortingNewDescending',
|
||||
'StatusError',
|
||||
'StatusSuccess',
|
||||
'StatusWarning',
|
||||
'Tab4D',
|
||||
'TabContours',
|
||||
'TabLinear',
|
||||
'TabPatientInfo',
|
||||
'TabRoiThreshold',
|
||||
'TabSegmentation',
|
||||
'TabStudies',
|
||||
'Threshold',
|
||||
'ThumbnailView',
|
||||
'Tool3DRotate',
|
||||
'ToolAngle',
|
||||
'ToolAnnotate',
|
||||
'ToolBidirectional',
|
||||
'ToolBidirectionalSegment',
|
||||
'ToolBrush',
|
||||
'ToolCalibrate',
|
||||
'ToolCapture',
|
||||
'ToolCine',
|
||||
'ToolCircle',
|
||||
'ToolCobbAngle',
|
||||
'ToolContract',
|
||||
'ToolCreateThreshold',
|
||||
'ToolCrosshair',
|
||||
'ToolCrosshairChecked',
|
||||
'ToolDicomTagBrowser',
|
||||
'ToolEraser',
|
||||
'ToolExpand',
|
||||
'ToolFlipHorizontal',
|
||||
'ToolFreehandRoi',
|
||||
'ToolInterpolation',
|
||||
'ToolInvert',
|
||||
'ToolLabelmapAssist',
|
||||
'ToolLayout',
|
||||
'ToolLength',
|
||||
'ToolMagneticRoi',
|
||||
'ToolMagnify',
|
||||
'ToolMeasureEllipse',
|
||||
'ToolMove',
|
||||
'ToolPETSegment',
|
||||
'ToolRectangle',
|
||||
'ToolReferenceLines',
|
||||
'ToolReset',
|
||||
'ToolRotateRight',
|
||||
'ToolSegBrush',
|
||||
'ToolSegEraser',
|
||||
'ToolSegmentAnything',
|
||||
'ToolShape',
|
||||
'ToolSplineRoi',
|
||||
'ToolStackScroll',
|
||||
'ToolThreshold',
|
||||
'ToolToggleDicomOverlay',
|
||||
'ToolUltrasoundBidirectional',
|
||||
'ToolWindowLevel',
|
||||
'ToolWindowRegion',
|
||||
'ToolZoom',
|
||||
'Undo',
|
||||
'ViewportViews',
|
||||
'ViewportWindowLevel',
|
||||
'WindowLevelAdvanced',
|
||||
];
|
||||
|
||||
const SIZE_OPTIONS = [16, 20, 24, 32];
|
||||
|
||||
function IconsPageContent() {
|
||||
const { Icons } = require('../../../../ui-next/src/components/Icons');
|
||||
const {
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
} = require('../../../../ui-next/src/components/Tooltip');
|
||||
const { Input } = require('../../../../ui-next/src/components/Input');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [iconSize, setIconSize] = useState(24);
|
||||
const [copiedIcon, setCopiedIcon] = useState<string | null>(null);
|
||||
|
||||
const handleCopyIcon = useCallback(async (name: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(name);
|
||||
} catch {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = name;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
setCopiedIcon(name);
|
||||
setTimeout(() => setCopiedIcon(null), 1500);
|
||||
}, []);
|
||||
|
||||
const filteredIcons = searchQuery.trim()
|
||||
? FINAL_ICONS.filter(name => name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
: FINAL_ICONS;
|
||||
|
||||
function IconTile({ name }: { name: string }) {
|
||||
const IconComponent = (Icons as any)[name];
|
||||
if (!IconComponent || typeof IconComponent !== 'function') return null;
|
||||
|
||||
const isCopied = copiedIcon === name;
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => handleCopyIcon(name)}
|
||||
className={`group flex flex-col items-center gap-2 rounded-lg border p-3 transition-colors ${
|
||||
isCopied
|
||||
? 'border-primary bg-primary/10'
|
||||
: 'border-transparent hover:border-input/50 hover:bg-muted/30'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-center text-foreground"
|
||||
style={{ minHeight: iconSize + 8, minWidth: iconSize + 8 }}
|
||||
>
|
||||
<IconComponent width={iconSize} height={iconSize} />
|
||||
</div>
|
||||
<span
|
||||
className={`max-w-full truncate text-center text-xs leading-tight ${
|
||||
isCopied
|
||||
? 'font-medium text-primary'
|
||||
: 'text-muted-foreground group-hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{isCopied ? 'Copied!' : name}
|
||||
</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{name}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Iconography"
|
||||
description="Icons for tools and actions in the viewer"
|
||||
>
|
||||
<PageHeader
|
||||
title="Iconography"
|
||||
description="Icons for tools and actions in the viewer"
|
||||
/>
|
||||
|
||||
<div className="mb-8">
|
||||
<p className="text-secondary-foreground text-lg leading-relaxed">
|
||||
Click any icon to copy its name. Use the search to filter by name, and
|
||||
the size controls to preview at different dimensions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Controls: search + size toggles */}
|
||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="relative max-w-sm flex-1">
|
||||
<Input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
placeholder="Search icons..."
|
||||
className="h-8 w-full px-3"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery('')}
|
||||
aria-label="Clear search"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-muted-foreground">Size</span>
|
||||
<div className="flex items-center gap-1 rounded-lg border border-input/50 p-1">
|
||||
{SIZE_OPTIONS.map(size => (
|
||||
<button
|
||||
key={size}
|
||||
onClick={() => setIconSize(size)}
|
||||
className={`rounded-md px-2.5 py-1 text-sm transition-colors ${
|
||||
iconSize === size
|
||||
? 'bg-primary/15 font-medium text-primary'
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{size}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Icon count */}
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
{filteredIcons.length === FINAL_ICONS.length
|
||||
? `${FINAL_ICONS.length} icons`
|
||||
: `${filteredIcons.length} of ${FINAL_ICONS.length} icons`}
|
||||
</p>
|
||||
|
||||
{/* Icon grid */}
|
||||
<TooltipProvider delayDuration={300}>
|
||||
{filteredIcons.length > 0 ? (
|
||||
<div className="mb-10 grid grid-cols-[repeat(auto-fill,minmax(100px,1fr))] gap-3">
|
||||
{filteredIcons.map(name => (
|
||||
<IconTile key={name} name={name} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-10 flex h-48 items-center justify-center text-muted-foreground">
|
||||
No icons found matching “{searchQuery}”
|
||||
</div>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { Icons } from '@ohif/ui-next';
|
||||
|
||||
// Use as a React component
|
||||
<Icons.ToolLength />
|
||||
<Icons.Add className="h-4 w-4" />
|
||||
|
||||
// Dynamic lookup by name
|
||||
<Icons.ByName name="ToolLength" />
|
||||
|
||||
// With custom size
|
||||
<Icons.ToolCapture width={32} height={32} />`}
|
||||
/>
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function IconsPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <IconsPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
156
platform/docs/src/pages/components/index.tsx
Normal file
156
platform/docs/src/pages/components/index.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
const foundations = [
|
||||
{
|
||||
label: 'Colors & Theming',
|
||||
href: '/colors-and-theming',
|
||||
description: 'Color tokens, theme presets, and accessibility guidance.',
|
||||
},
|
||||
{
|
||||
label: 'Iconography',
|
||||
href: '/components/icons',
|
||||
description: '137 curated icons, searchable, with click-to-copy names.',
|
||||
},
|
||||
];
|
||||
|
||||
const componentGroups = [
|
||||
{
|
||||
category: 'Simple',
|
||||
items: [
|
||||
{ label: 'Button', href: '/components/button' },
|
||||
{ label: 'Checkbox', href: '/components/checkbox' },
|
||||
{ label: 'Input', href: '/components/input' },
|
||||
{ label: 'Label', href: '/components/label' },
|
||||
{ label: 'Slider', href: '/components/slider' },
|
||||
{ label: 'Switch', href: '/components/switch-toggle' },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Compound',
|
||||
items: [
|
||||
{ label: 'Combobox', href: '/components/combobox' },
|
||||
{ label: 'Dialog', href: '/components/dialog' },
|
||||
{ label: 'DropdownMenu', href: '/components/dropdown-menu' },
|
||||
{ label: 'HoverCard', href: '/components/hover-card' },
|
||||
{ label: 'Popover', href: '/components/popover' },
|
||||
{ label: 'ScrollArea', href: '/components/scroll-area' },
|
||||
{ label: 'Select', href: '/components/select' },
|
||||
{ label: 'Table', href: '/components/table' },
|
||||
{ label: 'Tabs', href: '/components/tabs' },
|
||||
{ label: 'Toast / Sonner', href: '/components/toast' },
|
||||
{ label: 'Tooltip', href: '/components/tooltip' },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'OHIF-specific',
|
||||
items: [
|
||||
{ label: 'AllInOneMenu', href: '/components/all-in-one-menu' },
|
||||
{ label: 'CinePlayer', href: '/components/cine-player' },
|
||||
{ label: 'DataRow', href: '/components/data-row' },
|
||||
{ label: 'DataTable', href: '/components/data-table' },
|
||||
{ label: 'Numeric', href: '/components/numeric' },
|
||||
{ label: 'PanelSection', href: '/components/panel-section' },
|
||||
{ label: 'SmartScrollbar', href: '/components/smart-scrollbar' },
|
||||
{ label: 'ToolButton', href: '/components/tool-button' },
|
||||
{ label: 'ToolButtonList', href: '/components/tool-button-list' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function OverviewContent() {
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const Link = require('@docusaurus/Link').default;
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Components / ui-next"
|
||||
description="Documentation for the OHIF Viewer design system"
|
||||
>
|
||||
<PageHeader
|
||||
title={
|
||||
<>
|
||||
Components <span className="opacity-50">/</span> ui-next
|
||||
</>
|
||||
}
|
||||
description="Documentation for the OHIF Viewer design system"
|
||||
/>
|
||||
|
||||
<div className="text-secondary-foreground space-y-4 text-lg leading-relaxed">
|
||||
<p>
|
||||
Welcome to the component documentation for the OHIF Viewer.{' '}
|
||||
<strong className="text-foreground">@ohif/ui-next</strong> is the
|
||||
design system that shapes the viewer's interface, from the smallest
|
||||
toggle to the panels and toolbars that frame a study. It builds on the
|
||||
shadcn/ui and Radix foundation many developers already know, styled
|
||||
with Tailwind and shaped for the realities of medical imaging: dense
|
||||
layouts, dark viewports, and long clinical sessions where clarity
|
||||
matters.
|
||||
</p>
|
||||
<p>
|
||||
Whether you're building the viewer, extending it with a new mode, or
|
||||
crafting a theme, these docs are here to help you move quickly and
|
||||
stay consistent. Every component is live and interactive, so you're
|
||||
looking at the real thing, not a picture of it. Pick a component from
|
||||
the sidebar to see its variants and props, or open the{' '}
|
||||
<strong className="text-foreground">Foundations</strong> section to
|
||||
explore the dark-first theming and color roles that tie the whole
|
||||
system together.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-12">
|
||||
<Section title="Foundations">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{foundations.map(item => (
|
||||
<Link
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
className="group border-input/50 bg-muted/40 hover:border-primary/50 hover:bg-muted block rounded-lg border p-5 no-underline transition-colors"
|
||||
>
|
||||
<div className="text-foreground group-hover:text-highlight mb-1 text-lg font-semibold">
|
||||
{item.label}
|
||||
</div>
|
||||
<p className="text-muted-foreground mb-0 text-base">
|
||||
{item.description}
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Components">
|
||||
<div className="grid grid-cols-1 gap-x-8 gap-y-6 sm:grid-cols-2 md:grid-cols-3">
|
||||
{componentGroups.map(group => (
|
||||
<div key={group.category}>
|
||||
<h3 className="border-input/50 text-foreground mb-3 border-b pb-2 text-lg font-semibold tracking-wide">
|
||||
{group.category}
|
||||
</h3>
|
||||
<ul className="space-y-1">
|
||||
{group.items.map(item => (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
to={item.href}
|
||||
className="text-muted-foreground hover:text-highlight inline-block py-0.5 text-lg no-underline transition-colors"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OverviewPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <OverviewContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
121
platform/docs/src/pages/components/input.tsx
Normal file
121
platform/docs/src/pages/components/input.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function InputPageContent() {
|
||||
const { Input } = require('../../../../ui-next/src/components/Input');
|
||||
const { Label } = require('../../../../ui-next/src/components/Label');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const InteractivePicker = require('./_layout/InteractivePicker').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const types = [
|
||||
{ value: 'text', label: 'Text', description: 'Default text input.' },
|
||||
{ value: 'number', label: 'Number', description: 'Numeric input with stepper controls.' },
|
||||
{ value: 'password', label: 'Password', description: 'Masked input for sensitive values.' },
|
||||
{ value: 'search', label: 'Search', description: 'Search input with clear affordance in some browsers.' },
|
||||
];
|
||||
|
||||
const props = [
|
||||
{ name: 'type', type: 'string', default: '"text"', description: 'HTML input type (text, number, password, search, etc.)' },
|
||||
{ name: 'placeholder', type: 'string', default: '—', description: 'Placeholder text shown when empty' },
|
||||
{ name: 'value', type: 'string', default: '—', description: 'Controlled input value' },
|
||||
{ name: 'onChange', type: '(e: ChangeEvent) => void', default: '—', description: 'Called when the input value changes' },
|
||||
{ name: 'disabled', type: 'boolean', default: 'false', description: 'Disables interaction and reduces opacity' },
|
||||
{ name: 'className', type: 'string', default: '—', description: 'Additional CSS classes merged via cn()' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Input"
|
||||
description="Text input field for form data"
|
||||
>
|
||||
<PageHeader
|
||||
title="Input"
|
||||
description="A single-line text field for entering and editing values."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
The Input component is a styled text field that supports all standard HTML input types.
|
||||
It features a border on focus, hover highlight, and placeholder text styling.
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, inputs appear in dialogs for patient weight, window/level values,
|
||||
measurement labels, and search fields throughout panels.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Types">
|
||||
<InteractivePicker
|
||||
options={types}
|
||||
defaultValue="text"
|
||||
renderPreview={(active) => (
|
||||
<div className="w-64">
|
||||
<Input
|
||||
type={active}
|
||||
placeholder={
|
||||
active === 'number' ? '0'
|
||||
: active === 'password' ? 'Enter password'
|
||||
: active === 'search' ? 'Search...'
|
||||
: 'Enter value'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="With label">
|
||||
<div className="flex items-center gap-4">
|
||||
<Label htmlFor="ex1">Patient Weight</Label>
|
||||
<Input id="ex1" placeholder="(kg)" className="w-32" />
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Form layout">
|
||||
<div className="max-w-xs space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="f1" className="mb-1 block">Label</Label>
|
||||
<Input id="f1" placeholder="Enter label..." />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="f2" className="mb-1 block">Description</Label>
|
||||
<Input id="f2" placeholder="Optional description..." />
|
||||
</div>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Disabled" last>
|
||||
<Input placeholder="Disabled input" disabled className="w-64" />
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { Input } from '@ohif/ui-next';
|
||||
import { Label } from '@ohif/ui-next';
|
||||
|
||||
<Label htmlFor="weight">Patient Weight</Label>
|
||||
<Input id="weight" placeholder="(kg)" />`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InputPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <InputPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
95
platform/docs/src/pages/components/label.tsx
Normal file
95
platform/docs/src/pages/components/label.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function LabelPageContent() {
|
||||
const { Label } = require('../../../../ui-next/src/components/Label');
|
||||
const { Switch } = require('../../../../ui-next/src/components/Switch');
|
||||
const { Checkbox } = require('../../../../ui-next/src/components/Checkbox');
|
||||
const { Input } = require('../../../../ui-next/src/components/Input');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const props = [
|
||||
{ name: 'htmlFor', type: 'string', default: '—', description: 'ID of the associated form control' },
|
||||
{ name: 'className', type: 'string', default: '—', description: 'Additional CSS classes merged via cn()' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Label"
|
||||
description="Accessible text label for form controls"
|
||||
>
|
||||
<PageHeader
|
||||
title="Label"
|
||||
description="Associates descriptive text with a form control for clarity and accessibility."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
Label renders accessible text linked to a form control via the{' '}
|
||||
<strong className="text-foreground">htmlFor</strong> prop. Built on Radix UI's Label
|
||||
primitive, it automatically handles click-to-focus and screen reader association.
|
||||
</p>
|
||||
<p>
|
||||
Always pair labels with interactive controls — inputs, switches, checkboxes, and selects.
|
||||
Labels improve usability by expanding the clickable area and providing context.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="With Switch">
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch id="l-sw" defaultChecked />
|
||||
<Label htmlFor="l-sw">Preview edits before creating</Label>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="With Checkbox">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox id="l-cb" defaultChecked />
|
||||
<Label htmlFor="l-cb">Display inactive segmentations</Label>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="With Input">
|
||||
<div className="max-w-xs">
|
||||
<Label htmlFor="l-in" className="mb-1 block">Patient Weight</Label>
|
||||
<Input id="l-in" placeholder="(kg)" />
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Disabled state" last>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch id="l-dis" disabled />
|
||||
<Label htmlFor="l-dis" className="opacity-50">Unavailable setting</Label>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { Label, Input } from '@ohif/ui-next';
|
||||
|
||||
<Label htmlFor="name">Patient Name</Label>
|
||||
<Input id="name" placeholder="Enter name..." />`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LabelPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <LabelPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
294
platform/docs/src/pages/components/numeric.tsx
Normal file
294
platform/docs/src/pages/components/numeric.tsx
Normal file
@ -0,0 +1,294 @@
|
||||
import React, { useState } from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function NumericPageContent() {
|
||||
const Numeric = require('../../../../ui-next/src/components/Numeric').default;
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const InteractivePicker = require('./_layout/InteractivePicker').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const [controlledValue, setControlledValue] = useState(50);
|
||||
const [controlledValues, setControlledValues] = useState([30, 70] as [number, number]);
|
||||
const [frameNumber, setFrameNumber] = useState(1);
|
||||
|
||||
const modes = [
|
||||
{ value: 'number', label: 'Number', description: 'Basic number input field with min/max bounds.' },
|
||||
{ value: 'stepper', label: 'Stepper', description: 'Number input with increment/decrement buttons. Horizontal or vertical layout.' },
|
||||
{ value: 'singleRange', label: 'Single Range', description: 'Slider for selecting a single value in a range. Optional number input.' },
|
||||
{ value: 'doubleRange', label: 'Double Range', description: 'Dual-thumb slider for selecting a value range (e.g. window width/level).' },
|
||||
];
|
||||
|
||||
const containerProps = [
|
||||
{ name: 'mode', type: '"number" | "singleRange" | "doubleRange" | "stepper"', default: '—', description: 'Which input mode to render' },
|
||||
{ name: 'value', type: 'number', default: '—', description: 'Controlled single value (number, singleRange, stepper modes)' },
|
||||
{ name: 'defaultValue', type: 'number', default: 'midpoint', description: 'Initial uncontrolled single value' },
|
||||
{ name: 'values', type: '[number, number]', default: '—', description: 'Controlled range values (doubleRange mode)' },
|
||||
{ name: 'defaultValues', type: '[number, number]', default: '[30%, 70%]', description: 'Initial uncontrolled range values' },
|
||||
{ name: 'onChange', type: '(val: number | [number, number]) => void', default: '—', description: 'Called when any value changes' },
|
||||
{ name: 'min', type: 'number', default: '0', description: 'Minimum allowed value' },
|
||||
{ name: 'max', type: 'number', default: '100', description: 'Maximum allowed value' },
|
||||
{ name: 'step', type: 'number', default: '1', description: 'Step increment' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Numeric"
|
||||
description="Compound numeric input with multiple modes"
|
||||
>
|
||||
<PageHeader
|
||||
title="Numeric"
|
||||
description="A compound component for numeric input — number fields, steppers, single sliders, and dual-range sliders."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
Numeric is a compound component with 6 sub-components that share state through
|
||||
context: <strong className="text-foreground">Numeric.Container</strong> (root + mode),{' '}
|
||||
<strong className="text-foreground">Numeric.Label</strong>,{' '}
|
||||
<strong className="text-foreground">Numeric.NumberInput</strong>,{' '}
|
||||
<strong className="text-foreground">Numeric.NumberStepper</strong>,{' '}
|
||||
<strong className="text-foreground">Numeric.SingleRange</strong>, and{' '}
|
||||
<strong className="text-foreground">Numeric.DoubleRange</strong>.
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, Numeric controls appear in{' '}
|
||||
<strong className="text-foreground">segmentation tool settings</strong> (brush size, threshold),{' '}
|
||||
<strong className="text-foreground">window/level adjustment</strong>,{' '}
|
||||
<strong className="text-foreground">opacity controls</strong>, and{' '}
|
||||
<strong className="text-foreground">cine playback frame rate</strong>. The{' '}
|
||||
<strong className="text-foreground">mode</strong> prop on the Container determines which
|
||||
input type renders.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Modes">
|
||||
<InteractivePicker
|
||||
options={modes}
|
||||
defaultValue="number"
|
||||
renderPreview={(active) => (
|
||||
<div className="bg-muted w-[280px] rounded p-4">
|
||||
{active === 'number' && (
|
||||
<Numeric.Container mode="number" min={0} max={10}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Numeric.Label>Width</Numeric.Label>
|
||||
<Numeric.NumberInput />
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
)}
|
||||
{active === 'stepper' && (
|
||||
<Numeric.Container mode="stepper" min={0} max={100} step={1} defaultValue={50}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Numeric.Label>Opacity</Numeric.Label>
|
||||
<Numeric.NumberStepper className="w-[58px]" direction="horizontal" />
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
)}
|
||||
{active === 'singleRange' && (
|
||||
<Numeric.Container mode="singleRange" min={0} max={100} className="space-y-1">
|
||||
<Numeric.Label>Brightness</Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
)}
|
||||
{active === 'doubleRange' && (
|
||||
<Numeric.Container mode="doubleRange" min={-1000} max={3000} step={10} className="space-y-1">
|
||||
<Numeric.Label showValue>CT Window</Numeric.Label>
|
||||
<Numeric.DoubleRange />
|
||||
</Numeric.Container>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Number inputs">
|
||||
<div className="bg-muted flex w-[280px] flex-col space-y-4 rounded p-4">
|
||||
<Numeric.Container mode="number" min={0} max={10}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Numeric.Label>Width</Numeric.Label>
|
||||
<Numeric.NumberInput />
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="number"
|
||||
className="space-y-1"
|
||||
min={0}
|
||||
max={100}
|
||||
>
|
||||
<Numeric.Label className="text-muted-foreground text-sm font-bold">
|
||||
Threshold
|
||||
</Numeric.Label>
|
||||
<Numeric.NumberInput className="w-12" />
|
||||
</Numeric.Container>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Stepper controls">
|
||||
<div className="bg-muted flex w-[280px] flex-col space-y-4 rounded p-4">
|
||||
<Numeric.Container
|
||||
mode="stepper"
|
||||
value={frameNumber}
|
||||
onChange={val => setFrameNumber(val as number)}
|
||||
min={1}
|
||||
max={5}
|
||||
step={1}
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<Numeric.NumberStepper className="flex w-[78px]" direction="horizontal">
|
||||
<span className="text-muted-foreground text-xs">FPS</span>
|
||||
</Numeric.NumberStepper>
|
||||
<Numeric.Label className="mt-1">Frame</Numeric.Label>
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container mode="stepper" min={0} max={100} step={1} defaultValue={50}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Numeric.Label>Opacity</Numeric.Label>
|
||||
<Numeric.NumberStepper className="w-[58px]" direction="horizontal" />
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container mode="stepper" min={-10} max={10} step={1} defaultValue={0}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Numeric.NumberStepper className="w-[53px]" direction="vertical" />
|
||||
<Numeric.Label>Zoom</Numeric.Label>
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Single range sliders">
|
||||
<div className="bg-muted flex w-[280px] flex-col space-y-4 rounded p-4">
|
||||
<Numeric.Container mode="singleRange" min={0} max={100}>
|
||||
<Numeric.Label>Brightness</Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
min={-50}
|
||||
max={50}
|
||||
step={1}
|
||||
defaultValue={0}
|
||||
>
|
||||
<Numeric.Label showValue>Contrast</Numeric.Label>
|
||||
<Numeric.SingleRange />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={controlledValue}
|
||||
onChange={val => setControlledValue(val as number)}
|
||||
>
|
||||
<Numeric.Label>Controlled</Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Double range sliders">
|
||||
<div className="bg-muted flex w-[280px] flex-col space-y-4 rounded p-4">
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
className="space-y-1"
|
||||
>
|
||||
<Numeric.Label showValue>Window Width/Level</Numeric.Label>
|
||||
<Numeric.DoubleRange />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
defaultValues={[30, 70]}
|
||||
className="space-y-1"
|
||||
>
|
||||
<Numeric.Label>With number inputs</Numeric.Label>
|
||||
<Numeric.DoubleRange showNumberInputs />
|
||||
</Numeric.Container>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Combined panel" last>
|
||||
<div className="bg-muted flex w-[280px] flex-col space-y-4 rounded p-4">
|
||||
<Numeric.Container mode="number" min={0} max={10} step={0.1} className="space-y-1">
|
||||
<Numeric.Label>Zoom Factor</Numeric.Label>
|
||||
<Numeric.NumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container mode="stepper" min={-5} max={5} step={0.5} defaultValue={0}>
|
||||
<div className="flex items-center justify-between">
|
||||
<Numeric.Label>Offset</Numeric.Label>
|
||||
<Numeric.NumberStepper className="w-[58px]" direction="horizontal" />
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container mode="singleRange" min={0} max={360} step={1} className="space-y-1">
|
||||
<Numeric.Label showValue>Rotation</Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container mode="doubleRange" min={-1000} max={3000} step={10} className="space-y-1">
|
||||
<Numeric.Label showValue>CT Window</Numeric.Label>
|
||||
<Numeric.DoubleRange showNumberInputs />
|
||||
</Numeric.Container>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { Numeric } from '@ohif/ui-next';
|
||||
|
||||
// Number input
|
||||
<Numeric.Container mode="number" min={0} max={10}>
|
||||
<Numeric.Label>Width</Numeric.Label>
|
||||
<Numeric.NumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
// Stepper
|
||||
<Numeric.Container mode="stepper" min={1} max={30} defaultValue={1}>
|
||||
<Numeric.Label>Frame</Numeric.Label>
|
||||
<Numeric.NumberStepper direction="horizontal" />
|
||||
</Numeric.Container>
|
||||
|
||||
// Single range slider
|
||||
<Numeric.Container mode="singleRange" min={0} max={100}>
|
||||
<Numeric.Label>Opacity</Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
// Double range slider
|
||||
<Numeric.Container mode="doubleRange" min={-1000} max={3000} step={10}>
|
||||
<Numeric.Label showValue>CT Window</Numeric.Label>
|
||||
<Numeric.DoubleRange showNumberInputs />
|
||||
</Numeric.Container>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={containerProps} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NumericPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <NumericPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
255
platform/docs/src/pages/components/panel-section.tsx
Normal file
255
platform/docs/src/pages/components/panel-section.tsx
Normal file
@ -0,0 +1,255 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function PanelSectionPageContent() {
|
||||
const { PanelSection, Button, DataRow, TooltipProvider } = require('../../../../ui-next/src/components');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const InteractivePicker = require('./_layout/InteractivePicker').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const noop = (e) => e?.stopPropagation?.();
|
||||
|
||||
const states = [
|
||||
{ value: 'open', label: 'Open', description: 'Expanded state. Header shows a downward chevron, content is visible.' },
|
||||
{ value: 'closed', label: 'Closed', description: 'Collapsed state. Header shows a rightward chevron, content is hidden.' },
|
||||
];
|
||||
|
||||
const props = [
|
||||
{ name: 'defaultOpen', type: 'boolean', default: 'true', description: 'Whether the section is expanded on first render' },
|
||||
{ name: 'className', type: 'string', default: '—', description: 'Additional classes on the root accordion container' },
|
||||
];
|
||||
|
||||
const headerProps = [
|
||||
{ name: 'children', type: 'ReactNode', default: '—', description: 'Header text or content displayed in the trigger bar' },
|
||||
{ name: 'className', type: 'string', default: '—', description: 'Additional classes on the trigger element' },
|
||||
];
|
||||
|
||||
const contentProps = [
|
||||
{ name: 'children', type: 'ReactNode', default: '—', description: 'Content shown when the section is expanded' },
|
||||
{ name: 'className', type: 'string', default: '—', description: 'Additional classes on the content wrapper' },
|
||||
];
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<ComponentLayout
|
||||
title="PanelSection"
|
||||
description="Collapsible panel section"
|
||||
>
|
||||
<PageHeader
|
||||
title="PanelSection"
|
||||
description="A collapsible section for grouping controls and metadata inside side panels."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
PanelSection is a compound component built on Radix Accordion with three
|
||||
parts: <strong className="text-foreground">PanelSection</strong> (root),{' '}
|
||||
<strong className="text-foreground">PanelSection.Header</strong> (clickable trigger
|
||||
with auto-rotating chevron), and{' '}
|
||||
<strong className="text-foreground">PanelSection.Content</strong> (collapsible body).
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, PanelSection is used throughout{' '}
|
||||
<strong className="text-foreground">side panels</strong> to organize{' '}
|
||||
<strong className="text-foreground">segmentation lists</strong>,{' '}
|
||||
<strong className="text-foreground">measurement groups</strong>,{' '}
|
||||
<strong className="text-foreground">series metadata</strong>, and{' '}
|
||||
<strong className="text-foreground">tool configuration</strong> into
|
||||
collapsible groups. Sections default to open.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="States">
|
||||
<InteractivePicker
|
||||
options={states}
|
||||
defaultValue="open"
|
||||
renderPreview={(active) => (
|
||||
<div className="w-[280px]">
|
||||
<PanelSection
|
||||
defaultOpen={active === 'open'}
|
||||
key={active}
|
||||
className="bg-muted"
|
||||
>
|
||||
<PanelSection.Header>Series Information</PanelSection.Header>
|
||||
<PanelSection.Content>
|
||||
<div className="text-muted-foreground space-y-1 p-2 pl-4 text-sm">
|
||||
<div>Images: 120</div>
|
||||
<div>Modality: MR</div>
|
||||
<div>Body Part: Brain</div>
|
||||
</div>
|
||||
</PanelSection.Content>
|
||||
</PanelSection>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Series metadata">
|
||||
<div className="w-[280px]">
|
||||
<PanelSection defaultOpen className="bg-muted">
|
||||
<PanelSection.Header>Series Information</PanelSection.Header>
|
||||
<PanelSection.Content>
|
||||
<div className="text-muted-foreground space-y-1 p-2 pl-4 text-sm">
|
||||
<div>Images: 120</div>
|
||||
<div>Modality: MR</div>
|
||||
<div>Body Part: Brain</div>
|
||||
<div>Slice Thickness: 2.0mm</div>
|
||||
</div>
|
||||
</PanelSection.Content>
|
||||
</PanelSection>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Multiple stacked sections">
|
||||
<div className="w-[280px] space-y-0.5">
|
||||
<PanelSection defaultOpen className="bg-muted">
|
||||
<PanelSection.Header>Segmentations</PanelSection.Header>
|
||||
<PanelSection.Content>
|
||||
<div className="space-y-px">
|
||||
<DataRow
|
||||
number={1}
|
||||
title="Liver"
|
||||
description=""
|
||||
colorHex="#E2B93B"
|
||||
isSelected={true}
|
||||
isVisible={true}
|
||||
isLocked={false}
|
||||
disableEditing={false}
|
||||
onSelect={noop}
|
||||
onToggleVisibility={noop}
|
||||
onToggleLocked={noop}
|
||||
onRename={noop}
|
||||
onDelete={noop}
|
||||
onColor={noop}
|
||||
/>
|
||||
<DataRow
|
||||
number={2}
|
||||
title="Spleen"
|
||||
description=""
|
||||
colorHex="#68B9FF"
|
||||
isSelected={false}
|
||||
isVisible={true}
|
||||
isLocked={false}
|
||||
disableEditing={false}
|
||||
onSelect={noop}
|
||||
onToggleVisibility={noop}
|
||||
onToggleLocked={noop}
|
||||
onRename={noop}
|
||||
onDelete={noop}
|
||||
onColor={noop}
|
||||
/>
|
||||
</div>
|
||||
</PanelSection.Content>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection defaultOpen={false} className="bg-muted">
|
||||
<PanelSection.Header>Measurements</PanelSection.Header>
|
||||
<PanelSection.Content>
|
||||
<div className="space-y-px">
|
||||
<DataRow
|
||||
number={1}
|
||||
title="Length"
|
||||
description=""
|
||||
isSelected={false}
|
||||
isVisible={true}
|
||||
isLocked={false}
|
||||
disableEditing={true}
|
||||
onSelect={noop}
|
||||
onToggleVisibility={noop}
|
||||
onToggleLocked={noop}
|
||||
onRename={noop}
|
||||
onDelete={noop}
|
||||
onColor={noop}
|
||||
/>
|
||||
</div>
|
||||
</PanelSection.Content>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection defaultOpen={false} className="bg-muted">
|
||||
<PanelSection.Header>Display Sets</PanelSection.Header>
|
||||
<PanelSection.Content>
|
||||
<div className="text-muted-foreground p-2 pl-4 text-sm">
|
||||
CT Axial 2.0mm
|
||||
</div>
|
||||
</PanelSection.Content>
|
||||
</PanelSection>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="With action button in content" last>
|
||||
<div className="w-[280px]">
|
||||
<PanelSection defaultOpen className="bg-muted">
|
||||
<PanelSection.Header>Patient Information</PanelSection.Header>
|
||||
<PanelSection.Content>
|
||||
<div className="text-muted-foreground space-y-1 p-2 pl-4 text-sm">
|
||||
<div>Name: DOE^JOHN</div>
|
||||
<div>MRN: 12345678</div>
|
||||
<div>DOB: 1990-01-15</div>
|
||||
<div>Sex: M</div>
|
||||
</div>
|
||||
<div className="px-2 pb-2">
|
||||
<Button variant="ghost" size="sm">
|
||||
Load more information
|
||||
</Button>
|
||||
</div>
|
||||
</PanelSection.Content>
|
||||
</PanelSection>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { PanelSection } from '@ohif/ui-next';
|
||||
|
||||
<PanelSection defaultOpen>
|
||||
<PanelSection.Header>Series Information</PanelSection.Header>
|
||||
<PanelSection.Content>
|
||||
<div className="p-2 text-sm text-muted-foreground">
|
||||
<div>Images: 120</div>
|
||||
<div>Modality: MR</div>
|
||||
</div>
|
||||
</PanelSection.Content>
|
||||
</PanelSection>
|
||||
|
||||
// Collapsed by default
|
||||
<PanelSection defaultOpen={false}>
|
||||
<PanelSection.Header>Advanced Settings</PanelSection.Header>
|
||||
<PanelSection.Content>
|
||||
{/* Tool configuration controls */}
|
||||
</PanelSection.Content>
|
||||
</PanelSection>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<div className="mb-6">
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">PanelSection</h3>
|
||||
<PropsTable props={props} />
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">PanelSection.Header</h3>
|
||||
<PropsTable props={headerProps} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">PanelSection.Content</h3>
|
||||
<PropsTable props={contentProps} />
|
||||
</div>
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PanelSectionPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <PanelSectionPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
144
platform/docs/src/pages/components/popover.tsx
Normal file
144
platform/docs/src/pages/components/popover.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function PopoverPageContent() {
|
||||
const {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
} = require('../../../../ui-next/src/components');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const props = [
|
||||
{ name: 'align', type: '"start" | "center" | "end"', default: '"center"', description: 'Horizontal alignment relative to the trigger (on PopoverContent)' },
|
||||
{ name: 'side', type: '"top" | "right" | "bottom" | "left"', default: '"bottom"', description: 'Which side of the trigger the popover opens on (on PopoverContent)' },
|
||||
{ name: 'sideOffset', type: 'number', default: '4', description: 'Distance in pixels from the trigger (on PopoverContent)' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Popover"
|
||||
description="Floating panel for forms and extra details"
|
||||
>
|
||||
<PageHeader
|
||||
title="Popover"
|
||||
description="A floating panel that appears on click for small forms, actions, or additional details."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
Popover is a multi-part component built on Radix UI primitives:{' '}
|
||||
<strong className="text-foreground">Popover</strong> (root),{' '}
|
||||
<strong className="text-foreground">PopoverTrigger</strong>, and{' '}
|
||||
<strong className="text-foreground">PopoverContent</strong>. It also exports{' '}
|
||||
<strong className="text-foreground">PopoverAnchor</strong> for custom positioning.
|
||||
</p>
|
||||
<p>
|
||||
Unlike tooltips which show on hover, popovers require a click and can contain
|
||||
interactive content like inputs and buttons. In the OHIF Viewer, popovers are
|
||||
used for inline settings, color pickers, and compact option panels.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Basic popover">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">Open Popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-60">
|
||||
<p className="text-secondary-foreground text-sm">
|
||||
Click outside or press Escape to close.
|
||||
</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="With form content">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="secondary">Edit Window</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-64">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="pop-wc" className="mb-1 block text-sm">Window Center</Label>
|
||||
<Input id="pop-wc" type="number" placeholder="40" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="pop-ww" className="mb-1 block text-sm">Window Width</Label>
|
||||
<Input id="pop-ww" type="number" placeholder="400" />
|
||||
</div>
|
||||
<Button variant="default" size="sm" className="w-full">Apply</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Alignment" last>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">Align Start</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" className="w-48">
|
||||
<p className="text-secondary-foreground text-sm">Aligned to start</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">Side Right</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="right" className="w-48">
|
||||
<p className="text-secondary-foreground text-sm">Opens to the right</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">Side Top</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="top" className="w-48">
|
||||
<p className="text-secondary-foreground text-sm">Opens above</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { Popover, PopoverTrigger, PopoverContent, Button } from '@ohif/ui-next';
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button>Open</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<p>Popover content here.</p>
|
||||
</PopoverContent>
|
||||
</Popover>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PopoverPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <PopoverPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
129
platform/docs/src/pages/components/scroll-area.tsx
Normal file
129
platform/docs/src/pages/components/scroll-area.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function ScrollAreaPageContent() {
|
||||
const { ScrollArea } = require('../../../../ui-next/src/components/ScrollArea');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const tags = [
|
||||
'CT Axial 2.0mm', 'CT Coronal 2.0mm', 'CT Sagittal 2.0mm',
|
||||
'PET Axial', 'PET Coronal', 'PET MIP',
|
||||
'Segmentation — Liver', 'Segmentation — Spleen', 'Segmentation — Kidney L',
|
||||
'Segmentation — Kidney R', 'Segmentation — Aorta', 'RTSTRUCT',
|
||||
'Key Images', 'Prior CT 2023-01-15', 'Prior CT 2022-06-20',
|
||||
];
|
||||
|
||||
const props = [
|
||||
{ name: 'showArrows', type: 'boolean', default: 'false', description: 'Shows gradient arrow indicators at top/bottom when content is scrollable' },
|
||||
{ name: 'type', type: '"auto" | "always" | "scroll"', default: '"auto"', description: 'Scrollbar visibility behavior' },
|
||||
{ name: 'className', type: 'string', default: '—', description: 'Additional CSS classes on the root element' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="ScrollArea"
|
||||
description="Custom scrollbar container for overflow content"
|
||||
>
|
||||
<PageHeader
|
||||
title="ScrollArea"
|
||||
description="A container with custom-styled scrollbars that replace native browser scrollbars."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
ScrollArea provides consistent, theme-aware scrollbars across all platforms.
|
||||
Built on Radix UI's ScrollArea primitive, it replaces native scrollbars with
|
||||
a slim track and thumb styled for the OHIF dark theme.
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, scroll areas wrap{' '}
|
||||
<strong className="text-foreground">panel content</strong>,{' '}
|
||||
<strong className="text-foreground">measurement lists</strong>,{' '}
|
||||
<strong className="text-foreground">segmentation lists</strong>, and any
|
||||
container where content may exceed the visible area. The optional{' '}
|
||||
<strong className="text-foreground">showArrows</strong> prop adds gradient
|
||||
indicators at the top and bottom edges.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Text content">
|
||||
<ScrollArea className="border-border bg-muted/30 h-[150px] w-[350px] rounded-md border p-3">
|
||||
<p className="text-secondary-foreground text-sm leading-relaxed">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
|
||||
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
|
||||
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
|
||||
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
|
||||
culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet,
|
||||
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua.
|
||||
</p>
|
||||
</ScrollArea>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="List of items">
|
||||
<ScrollArea className="border-border h-[200px] w-[280px] rounded-md border">
|
||||
<div className="p-2">
|
||||
{tags.map((tag) => (
|
||||
<div
|
||||
key={tag}
|
||||
className="text-secondary-foreground border-border border-b px-2 py-2 text-sm last:border-0"
|
||||
>
|
||||
{tag}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="With scroll arrows" last>
|
||||
<ScrollArea showArrows className="border-border h-[200px] w-[280px] rounded-md border">
|
||||
<div className="p-2">
|
||||
{tags.map((tag) => (
|
||||
<div
|
||||
key={tag}
|
||||
className="text-secondary-foreground border-border border-b px-2 py-2 text-sm last:border-0"
|
||||
>
|
||||
{tag}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { ScrollArea } from '@ohif/ui-next';
|
||||
|
||||
<ScrollArea className="h-[300px]">
|
||||
{/* Long content here */}
|
||||
</ScrollArea>
|
||||
|
||||
// With scroll arrows
|
||||
<ScrollArea showArrows className="h-[300px]">
|
||||
{/* Long content here */}
|
||||
</ScrollArea>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ScrollAreaPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <ScrollAreaPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
152
platform/docs/src/pages/components/select.tsx
Normal file
152
platform/docs/src/pages/components/select.tsx
Normal file
@ -0,0 +1,152 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function SelectPageContent() {
|
||||
const {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
SelectGroup,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
} = require('../../../../ui-next/src/components/Select');
|
||||
const { Label } = require('../../../../ui-next/src/components/Label');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const props = [
|
||||
{ name: 'value', type: 'string', default: '—', description: 'Controlled selected value' },
|
||||
{ name: 'defaultValue', type: 'string', default: '—', description: 'Initial value (uncontrolled)' },
|
||||
{ name: 'onValueChange', type: '(value: string) => void', default: '—', description: 'Called when selection changes' },
|
||||
{ name: 'disabled', type: 'boolean', default: 'false', description: 'Disables the entire select' },
|
||||
{ name: 'placeholder', type: 'string', default: '—', description: 'Text shown when no value is selected (on SelectValue)' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Select"
|
||||
description="Dropdown for choosing from a list of options"
|
||||
>
|
||||
<PageHeader
|
||||
title="Select"
|
||||
description="A dropdown control for selecting a single value from a predefined list."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
Select is a multi-part component built on Radix UI primitives:{' '}
|
||||
<strong className="text-foreground">Select</strong> (root),{' '}
|
||||
<strong className="text-foreground">SelectTrigger</strong> (button),{' '}
|
||||
<strong className="text-foreground">SelectContent</strong> (dropdown),{' '}
|
||||
and <strong className="text-foreground">SelectItem</strong> (options).
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, selects appear in segmentation panels for choosing the active
|
||||
segmentation, in settings for display presets, and throughout dialogs for configuration options.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Basic">
|
||||
<Select>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="With label">
|
||||
<div className="flex items-center gap-4">
|
||||
<Label htmlFor="display-set-select">Display Set</Label>
|
||||
<Select defaultValue="ct">
|
||||
<SelectTrigger id="display-set-select" className="w-[200px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ct">CT Axial</SelectItem>
|
||||
<SelectItem value="pet">PET Coronal</SelectItem>
|
||||
<SelectItem value="seg">Segmentation</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="With groups and separator">
|
||||
<Select>
|
||||
<SelectTrigger className="w-[220px]">
|
||||
<SelectValue placeholder="Select modality" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Common</SelectLabel>
|
||||
<SelectItem value="ct">CT</SelectItem>
|
||||
<SelectItem value="mr">MR</SelectItem>
|
||||
<SelectItem value="us">US</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectSeparator />
|
||||
<SelectGroup>
|
||||
<SelectLabel>Nuclear</SelectLabel>
|
||||
<SelectItem value="pt">PT</SelectItem>
|
||||
<SelectItem value="nm">NM</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Disabled" last>
|
||||
<Select disabled>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Disabled" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="a">Option A</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import {
|
||||
Select, SelectTrigger, SelectContent,
|
||||
SelectItem, SelectValue,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
<Select>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SelectPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <SelectPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
121
platform/docs/src/pages/components/slider.tsx
Normal file
121
platform/docs/src/pages/components/slider.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import React, { useState } from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function SliderPageContent() {
|
||||
const { Slider } = require('../../../../ui-next/src/components/Slider');
|
||||
const { Label } = require('../../../../ui-next/src/components/Label');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const props = [
|
||||
{ name: 'defaultValue', type: 'number[]', default: '—', description: 'Initial value (uncontrolled). Array with one element for single thumb.' },
|
||||
{ name: 'value', type: 'number[]', default: '—', description: 'Controlled value' },
|
||||
{ name: 'onValueChange', type: '(value: number[]) => void', default: '—', description: 'Called on every value change during drag' },
|
||||
{ name: 'onValueCommit', type: '(value: number[]) => void', default: '—', description: 'Called when the user releases the thumb' },
|
||||
{ name: 'min', type: 'number', default: '0', description: 'Minimum value' },
|
||||
{ name: 'max', type: 'number', default: '100', description: 'Maximum value' },
|
||||
{ name: 'step', type: 'number', default: '1', description: 'Step increment between values' },
|
||||
{ name: 'disabled', type: 'boolean', default: 'false', description: 'Disables interaction and reduces opacity' },
|
||||
];
|
||||
|
||||
function SliderWithValue({ defaultValue = [50], min = 0, max = 100, step = 1, label = '' }) {
|
||||
const [val, setVal] = useState(defaultValue);
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
{label && <Label className="w-28 shrink-0">{label}</Label>}
|
||||
<Slider
|
||||
className="w-48"
|
||||
value={val}
|
||||
onValueChange={setVal}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
/>
|
||||
<span className="text-muted-foreground w-10 text-right font-mono text-sm">{val[0]}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Slider"
|
||||
description="Range control for selecting a numeric value"
|
||||
>
|
||||
<PageHeader
|
||||
title="Slider"
|
||||
description="A draggable thumb control for selecting a value within a range."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
Slider provides a visual way to select a numeric value by dragging a thumb along
|
||||
a track. It's built on Radix UI's Slider primitive with custom styling for the
|
||||
OHIF dark theme.
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, sliders appear in{' '}
|
||||
<strong className="text-foreground">window/level adjustments</strong>,{' '}
|
||||
<strong className="text-foreground">opacity controls</strong> for overlays
|
||||
and segmentations, and <strong className="text-foreground">threshold settings</strong>{' '}
|
||||
in segmentation tools.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Basic slider">
|
||||
<div className="w-64 px-2">
|
||||
<Slider defaultValue={[50]} max={100} step={1} />
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="With live value display">
|
||||
<SliderWithValue defaultValue={[75]} />
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Labeled controls">
|
||||
<div className="space-y-4">
|
||||
<SliderWithValue defaultValue={[80]} label="Opacity" />
|
||||
<SliderWithValue defaultValue={[40]} min={-1000} max={1000} step={10} label="Window Center" />
|
||||
<SliderWithValue defaultValue={[400]} min={1} max={2000} step={10} label="Window Width" />
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Disabled" last>
|
||||
<div className="w-64 px-2">
|
||||
<Slider defaultValue={[30]} max={100} step={1} disabled />
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { Slider } from '@ohif/ui-next';
|
||||
|
||||
<Slider
|
||||
defaultValue={[50]}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
onValueChange={(value) => console.log(value[0])}
|
||||
/>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SliderPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <SliderPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
474
platform/docs/src/pages/components/smart-scrollbar.tsx
Normal file
474
platform/docs/src/pages/components/smart-scrollbar.tsx
Normal file
@ -0,0 +1,474 @@
|
||||
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Loading simulation — hardcoded to from-indicator pattern, normal speed
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LOAD_INTERVAL_MS = 50;
|
||||
const TOTAL_SLICES = 100;
|
||||
const START_INDEX = Math.round((TOTAL_SLICES - 1) * 0.2);
|
||||
|
||||
type SimState = 'idle' | 'loading' | 'complete';
|
||||
|
||||
function buildFromIndicatorQueue(startIndex: number, total: number): number[] {
|
||||
const queue: number[] = [startIndex];
|
||||
let lo = startIndex - 1;
|
||||
let hi = startIndex + 1;
|
||||
while (lo >= 0 || hi < total) {
|
||||
if (hi < total) queue.push(hi++);
|
||||
if (lo >= 0) queue.push(lo--);
|
||||
}
|
||||
return queue;
|
||||
}
|
||||
|
||||
function useLoadingSimulation(currentIndex: number, useByteArray: any) {
|
||||
const [simState, setSimState] = useState<SimState>('idle');
|
||||
const loaded = useByteArray(TOTAL_SLICES);
|
||||
|
||||
const queueRef = useRef<number[]>([]);
|
||||
const queueIndexRef = useRef(0);
|
||||
const intervalRef = useRef<number | null>(null);
|
||||
const simStateRef = useRef<SimState>('idle');
|
||||
const currentIndexRef = useRef(currentIndex);
|
||||
const loadedRef = useRef(loaded);
|
||||
|
||||
simStateRef.current = simState;
|
||||
currentIndexRef.current = currentIndex;
|
||||
loadedRef.current = loaded;
|
||||
|
||||
const clearTimer = useCallback(() => {
|
||||
if (intervalRef.current !== null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const play = useCallback(() => {
|
||||
queueRef.current = buildFromIndicatorQueue(currentIndexRef.current, TOTAL_SLICES);
|
||||
queueIndexRef.current = 0;
|
||||
loadedRef.current.resetWith(() => {});
|
||||
setSimState('loading');
|
||||
simStateRef.current = 'loading';
|
||||
|
||||
clearTimer();
|
||||
intervalRef.current = window.setInterval(() => {
|
||||
if (queueIndexRef.current >= TOTAL_SLICES) return;
|
||||
loadedRef.current.setByte(queueRef.current[queueIndexRef.current]);
|
||||
queueIndexRef.current++;
|
||||
if (queueIndexRef.current >= TOTAL_SLICES) {
|
||||
clearTimer();
|
||||
setSimState('complete');
|
||||
simStateRef.current = 'complete';
|
||||
}
|
||||
}, LOAD_INTERVAL_MS);
|
||||
}, [clearTimer]);
|
||||
|
||||
useEffect(() => clearTimer, [clearTimer]);
|
||||
|
||||
return { loaded, simState, play };
|
||||
}
|
||||
|
||||
function useViewedTracking(currentIndex: number, loadedBytes: Uint8Array, useByteArray: any) {
|
||||
const viewed = useByteArray(TOTAL_SLICES);
|
||||
|
||||
useEffect(() => {
|
||||
if (loadedBytes[currentIndex] && !viewed.bytes[currentIndex]) {
|
||||
viewed.setByte(currentIndex);
|
||||
}
|
||||
}, [currentIndex, loadedBytes, viewed]);
|
||||
|
||||
return viewed;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Demo viewport with play/replay button
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const VP_SIZE = 400;
|
||||
const VP_PAD = 8;
|
||||
|
||||
function DemoViewport({
|
||||
children,
|
||||
simState,
|
||||
onPlay,
|
||||
onWheel,
|
||||
Button,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
simState: SimState;
|
||||
onPlay: () => void;
|
||||
onWheel: (deltaY: number) => void;
|
||||
Button: any;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleWheel = useCallback(
|
||||
(e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
onWheel(e.deltaY);
|
||||
},
|
||||
[onWheel]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
el.addEventListener('wheel', handleWheel, { passive: false });
|
||||
return () => el.removeEventListener('wheel', handleWheel);
|
||||
}, [handleWheel]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative" style={{ width: VP_SIZE, height: VP_SIZE }}>
|
||||
<div className="absolute inset-0 border border-input" />
|
||||
<div className="absolute border border-highlight" style={{ inset: 1, borderRadius: 8 }} />
|
||||
<div className="absolute bg-background" style={{ inset: 2, borderRadius: 7 }} />
|
||||
|
||||
{/* Center prompt */}
|
||||
<div className="absolute inset-0 flex items-center justify-center text-muted-foreground text-sm select-none pointer-events-none">
|
||||
Scroll here
|
||||
</div>
|
||||
|
||||
{/* Play / Replay button — top-left corner */}
|
||||
<div className="absolute z-10" style={{ top: 12, left: 12 }}>
|
||||
<Button
|
||||
onClick={onPlay}
|
||||
disabled={simState === 'loading'}
|
||||
className="gap-[5px]"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
||||
<path d="M3 1.5v11l9-5.5L3 1.5z" />
|
||||
</svg>
|
||||
{simState === 'complete' ? 'Replay' : simState === 'loading' ? 'Loading…' : 'Play'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Scrollbar area */}
|
||||
<div className="absolute" style={{ right: VP_PAD, top: VP_PAD, bottom: VP_PAD }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SmartScrollbarDemo — minimal: viewport + play button, that's it
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function SmartScrollbarDemo({
|
||||
SmartScrollbar,
|
||||
SmartScrollbarTrack,
|
||||
SmartScrollbarFill,
|
||||
SmartScrollbarIndicator,
|
||||
SmartScrollbarEndpoints,
|
||||
useByteArray,
|
||||
Button,
|
||||
}: {
|
||||
SmartScrollbar: any;
|
||||
SmartScrollbarTrack: any;
|
||||
SmartScrollbarFill: any;
|
||||
SmartScrollbarIndicator: any;
|
||||
SmartScrollbarEndpoints: any;
|
||||
useByteArray: any;
|
||||
Button: any;
|
||||
}) {
|
||||
const [currentIndex, setCurrentIndex] = useState(START_INDEX);
|
||||
const [resetKey, setResetKey] = useState(0);
|
||||
|
||||
const { loaded, simState, play } = useLoadingSimulation(currentIndex, useByteArray);
|
||||
const viewed = useViewedTracking(currentIndex, loaded.bytes, useByteArray);
|
||||
|
||||
const handleWheel = useCallback((deltaY: number) => {
|
||||
setCurrentIndex(prev => Math.max(0, Math.min(TOTAL_SLICES - 1, prev + (deltaY > 0 ? 1 : -1))));
|
||||
}, []);
|
||||
|
||||
const handlePlay = useCallback(() => {
|
||||
setResetKey(k => k + 1);
|
||||
viewed.resetWith(() => {});
|
||||
play();
|
||||
}, [play, viewed]);
|
||||
|
||||
return (
|
||||
<DemoViewport simState={simState} onPlay={handlePlay} onWheel={handleWheel} Button={Button}>
|
||||
<SmartScrollbar
|
||||
key={resetKey}
|
||||
value={currentIndex}
|
||||
total={TOTAL_SLICES}
|
||||
onValueChange={setCurrentIndex}
|
||||
isLoading={simState === 'loading'}
|
||||
>
|
||||
<SmartScrollbarTrack>
|
||||
<SmartScrollbarFill
|
||||
marked={loaded.bytes}
|
||||
version={loaded.version}
|
||||
className="bg-neutral/25"
|
||||
loadingClassName="bg-neutral/50"
|
||||
/>
|
||||
<SmartScrollbarFill
|
||||
marked={viewed.bytes}
|
||||
version={viewed.version}
|
||||
className="bg-primary/35"
|
||||
/>
|
||||
</SmartScrollbarTrack>
|
||||
<SmartScrollbarIndicator />
|
||||
<SmartScrollbarEndpoints marked={loaded.bytes} version={loaded.version} />
|
||||
</SmartScrollbar>
|
||||
</DemoViewport>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Page Content
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function SmartScrollbarPageContent() {
|
||||
const {
|
||||
SmartScrollbar,
|
||||
SmartScrollbarTrack,
|
||||
SmartScrollbarFill,
|
||||
SmartScrollbarIndicator,
|
||||
SmartScrollbarEndpoints,
|
||||
useByteArray,
|
||||
Button,
|
||||
} = require('../../../../ui-next/src/components');
|
||||
const {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
} = require('../../../../ui-next/src/components/Table');
|
||||
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const scrollbarProps = [
|
||||
{ name: 'value', type: 'number', default: '—', description: 'Current scroll index (0 to total - 1)' },
|
||||
{ name: 'total', type: 'number', default: '—', description: 'Total number of items' },
|
||||
{ name: 'onValueChange', type: '(index: number) => void', default: '—', description: 'Called when scroll position changes via click, drag, or keyboard' },
|
||||
{ name: 'isLoading', type: 'boolean', default: 'false', description: 'Shows dot-grid loading pattern and expands track width' },
|
||||
{ name: 'enableKeyboardNavigation', type: 'boolean', default: 'false', description: 'Enables Arrow, Page Up/Down, Home, End key navigation' },
|
||||
{ name: 'indicator', type: '{ totalWidth, totalHeight, renderIndicator }', default: '—', description: 'Custom indicator configuration to replace the default pill SVG' },
|
||||
];
|
||||
|
||||
const fillProps = [
|
||||
{ name: 'marked', type: 'Uint8Array', default: '—', description: 'Byte array where 1 = marked position, 0 = unmarked' },
|
||||
{ name: 'version', type: 'number', default: '—', description: 'Change token — bump when the array mutates in-place' },
|
||||
{ name: 'className', type: 'string', default: 'bg-neutral/25', description: 'Fill color class for normal state' },
|
||||
{ name: 'loadingClassName', type: 'string', default: 'bg-neutral/50', description: 'Fill color class while parent isLoading is true' },
|
||||
];
|
||||
|
||||
const endpointsProps = [
|
||||
{ name: 'marked', type: 'Uint8Array', default: '—', description: 'Byte array marking loaded positions' },
|
||||
{ name: 'version', type: 'number', default: '—', description: 'Change token — bump when the array mutates in-place' },
|
||||
];
|
||||
|
||||
const byteArrayFields = [
|
||||
{ name: 'bytes', type: 'Uint8Array', default: '—', description: 'Mutable array — safe for in-place writes' },
|
||||
{ name: 'version', type: 'number', default: '—', description: 'Invalidation token for React memo dependencies' },
|
||||
{ name: 'isFull', type: 'boolean', default: '—', description: 'True when all bytes are set to 1' },
|
||||
{ name: 'setByte(index)', type: 'function', default: '—', description: 'Mark a position as loaded or viewed' },
|
||||
{ name: 'clearByte(index)', type: 'function', default: '—', description: 'Unmark a position' },
|
||||
{ name: 'resetWith(fn)', type: 'function', default: '—', description: 'Clear array and optionally bulk-populate via callback' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout title="SmartScrollbar" description="Viewport scrollbar with loading progress">
|
||||
<PageHeader
|
||||
title="SmartScrollbar"
|
||||
description="Viewport scrollbar with loading progress and viewed-slice tracking."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
SmartScrollbar is a{' '}
|
||||
<strong className="text-foreground">compound component</strong> that composes
|
||||
Track, Fill, Indicator, and Endpoints into a scrollbar that shows{' '}
|
||||
<strong className="text-foreground">what's loaded</strong>,{' '}
|
||||
<strong className="text-foreground">what's been viewed</strong>, and{' '}
|
||||
<strong className="text-foreground">where you are</strong> in a series.
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, it sits on the{' '}
|
||||
<strong className="text-foreground">right edge of each viewport</strong> and
|
||||
is driven by Cornerstone image cache events. The{' '}
|
||||
<strong className="text-foreground">neutral fill</strong> grows as images load
|
||||
from the server; the <strong className="text-foreground">primary fill</strong>{' '}
|
||||
tracks which slices the user has scrolled through. A dot-grid pattern animates
|
||||
behind the track while loading is in progress.
|
||||
</p>
|
||||
<p>
|
||||
The track expands from <strong className="text-foreground">4px to 8px</strong>{' '}
|
||||
on hover, drag, or during loading, then contracts 600ms after loading completes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Interactive Demo">
|
||||
<div className="mb-3 text-lg text-secondary-foreground leading-relaxed">
|
||||
<p>
|
||||
Press <strong className="text-foreground">Play</strong> to simulate image loading.
|
||||
Scroll the viewport with the <strong className="text-foreground">mouse wheel</strong>{' '}
|
||||
or click and drag the scrollbar. Slices you scroll to while loaded are marked as{' '}
|
||||
<strong className="text-foreground">viewed</strong> (blue fill).
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-input/50 bg-muted/30 p-6">
|
||||
<SmartScrollbarDemo
|
||||
SmartScrollbar={SmartScrollbar}
|
||||
SmartScrollbarTrack={SmartScrollbarTrack}
|
||||
SmartScrollbarFill={SmartScrollbarFill}
|
||||
SmartScrollbarIndicator={SmartScrollbarIndicator}
|
||||
SmartScrollbarEndpoints={SmartScrollbarEndpoints}
|
||||
useByteArray={useByteArray}
|
||||
Button={Button}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Customization">
|
||||
<div className="mb-4 text-lg text-secondary-foreground leading-relaxed">
|
||||
<p>
|
||||
Scrollbar behavior is configured via the{' '}
|
||||
<strong className="text-foreground">Customization Service</strong>.
|
||||
</p>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-foreground font-medium">Key</TableHead>
|
||||
<TableHead className="text-foreground font-medium">Default</TableHead>
|
||||
<TableHead className="text-foreground font-medium">Description</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{[
|
||||
['variant', "'progress'", 'Progress scrollbar or legacy range input'],
|
||||
['showLoadedFill', 'true', 'Show the neutral loaded/cached fill'],
|
||||
['showViewedFill', 'true', 'Show the primary viewed-slice fill'],
|
||||
['showLoadedEndpoints', 'true', 'Show endpoint caps at loaded range boundaries'],
|
||||
['showLoadingPattern', 'true', 'Show dot-grid pattern while loading'],
|
||||
['viewedDwellMs', '0', 'Delay before marking a slice as viewed (ms)'],
|
||||
['loadedBatchIntervalMs', '200', 'Coalesce loaded-state updates for performance (ms)'],
|
||||
['indicator', '{}', 'Custom indicator SVG (totalWidth, totalHeight, renderIndicator)'],
|
||||
].map(([key, defaultVal, desc]) => (
|
||||
<TableRow key={key}>
|
||||
<TableCell className="font-mono text-base text-foreground">{key}</TableCell>
|
||||
<TableCell className="font-mono text-base">{defaultVal}</TableCell>
|
||||
<TableCell>{desc}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="mt-4 text-lg text-secondary-foreground leading-relaxed">
|
||||
<p>
|
||||
All keys are prefixed with{' '}
|
||||
<code className="text-highlight text-sm">viewportScrollbar.</code> in the
|
||||
customization service. For full configuration examples, screenshots, and the
|
||||
advanced indicator API, see the{' '}
|
||||
<a
|
||||
href="/platform/services/customization-service/ViewportScrollbar"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Viewport Scrollbar Customization
|
||||
</a>{' '}
|
||||
reference.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<div className="mb-4 text-lg text-secondary-foreground leading-relaxed">
|
||||
<p>
|
||||
SmartScrollbar uses a compound component pattern — the root provides layout
|
||||
context and children render into specific layers of the scrollbar.
|
||||
</p>
|
||||
</div>
|
||||
<CodeBlock
|
||||
code={`import {
|
||||
SmartScrollbar,
|
||||
SmartScrollbarTrack,
|
||||
SmartScrollbarFill,
|
||||
SmartScrollbarIndicator,
|
||||
SmartScrollbarEndpoints,
|
||||
useByteArray,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
// Create byte arrays for loaded and viewed tracking
|
||||
const loaded = useByteArray(numberOfSlices);
|
||||
const viewed = useByteArray(numberOfSlices);
|
||||
|
||||
// Mark slices as loaded (e.g. from image cache events)
|
||||
loaded.setByte(sliceIndex);
|
||||
|
||||
// Mark slices as viewed (e.g. on scroll)
|
||||
viewed.setByte(currentIndex);
|
||||
|
||||
<SmartScrollbar
|
||||
value={imageIndex}
|
||||
total={numberOfSlices}
|
||||
onValueChange={handleJumpToSlice}
|
||||
isLoading={!isFullyLoaded}
|
||||
>
|
||||
<SmartScrollbarTrack>
|
||||
<SmartScrollbarFill
|
||||
marked={loaded.bytes}
|
||||
version={loaded.version}
|
||||
className="bg-neutral/25"
|
||||
loadingClassName="bg-neutral/50"
|
||||
/>
|
||||
<SmartScrollbarFill
|
||||
marked={viewed.bytes}
|
||||
version={viewed.version}
|
||||
className="bg-primary/35"
|
||||
/>
|
||||
</SmartScrollbarTrack>
|
||||
<SmartScrollbarIndicator />
|
||||
<SmartScrollbarEndpoints
|
||||
marked={loaded.bytes}
|
||||
version={loaded.version}
|
||||
/>
|
||||
</SmartScrollbar>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">
|
||||
SmartScrollbar
|
||||
</h3>
|
||||
<PropsTable props={scrollbarProps} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">
|
||||
SmartScrollbarFill
|
||||
</h3>
|
||||
<PropsTable props={fillProps} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">
|
||||
SmartScrollbarEndpoints
|
||||
</h3>
|
||||
<PropsTable props={endpointsProps} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-highlight mb-3 text-base font-semibold">
|
||||
useByteArray(length) → ByteArrayHandle
|
||||
</h3>
|
||||
<PropsTable props={byteArrayFields} />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SmartScrollbarSimplePage() {
|
||||
return <BrowserOnly fallback={<></>}>{() => <SmartScrollbarPageContent />}</BrowserOnly>;
|
||||
}
|
||||
129
platform/docs/src/pages/components/switch-toggle.tsx
Normal file
129
platform/docs/src/pages/components/switch-toggle.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function SwitchPageContent() {
|
||||
const { Switch } = require('../../../../ui-next/src/components/Switch');
|
||||
const { Label } = require('../../../../ui-next/src/components/Label');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const InteractivePicker = require('./_layout/InteractivePicker').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const states = [
|
||||
{ value: 'off', label: 'Off', description: 'Default state. Muted primary background.' },
|
||||
{ value: 'on', label: 'On', description: 'Active state. Solid primary background with thumb shifted right.' },
|
||||
{ value: 'disabled', label: 'Disabled', description: 'Non-interactive. Reduced opacity.' },
|
||||
];
|
||||
|
||||
const props = [
|
||||
{ name: 'checked', type: 'boolean', default: '—', description: 'Controlled checked state' },
|
||||
{ name: 'defaultChecked', type: 'boolean', default: 'false', description: 'Initial checked state (uncontrolled)' },
|
||||
{ name: 'onCheckedChange', type: '(checked: boolean) => void', default: '—', description: 'Called when the checked state changes' },
|
||||
{ name: 'disabled', type: 'boolean', default: 'false', description: 'Disables interaction and reduces opacity' },
|
||||
{ name: 'className', type: 'string', default: '—', description: 'Additional CSS classes merged via cn()' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Switch"
|
||||
description="Toggle control for binary settings"
|
||||
>
|
||||
<PageHeader
|
||||
title="Switch"
|
||||
description="A toggle for changing between two states. The preferred control for on/off settings."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
Switch is the preferred toggle control in OHIF. It provides a larger, more visible
|
||||
target than Checkbox and communicates its current state more clearly through its
|
||||
sliding thumb animation.
|
||||
</p>
|
||||
<p>
|
||||
Use descriptive labels next to switches that are understandable before interacting.
|
||||
In the OHIF Viewer, switches control viewport sync, overlay visibility, and
|
||||
panel-level settings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="States">
|
||||
<InteractivePicker
|
||||
options={states}
|
||||
defaultValue="off"
|
||||
renderPreview={(active) => (
|
||||
<div className="flex items-center space-x-3">
|
||||
<Switch
|
||||
id="demo"
|
||||
checked={active === 'on'}
|
||||
disabled={active === 'disabled'}
|
||||
/>
|
||||
<Label htmlFor="demo" className={active === 'disabled' ? 'opacity-50' : ''}>
|
||||
Sync changes in all viewports
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="With label">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Switch id="ex1" defaultChecked />
|
||||
<Label htmlFor="ex1">Sync changes in all viewports</Label>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Settings list">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="s1">Show overlay</Label>
|
||||
<Switch id="s1" defaultChecked />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="s2">Show annotations</Label>
|
||||
<Switch id="s2" defaultChecked />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="s3">Invert colors</Label>
|
||||
<Switch id="s3" />
|
||||
</div>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Disabled" last>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Switch id="dis1" disabled />
|
||||
<Label htmlFor="dis1" className="opacity-50">Unavailable setting</Label>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { Switch } from '@ohif/ui-next';
|
||||
import { Label } from '@ohif/ui-next';
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="sync" defaultChecked />
|
||||
<Label htmlFor="sync">Sync viewports</Label>
|
||||
</div>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SwitchPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <SwitchPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
216
platform/docs/src/pages/components/table.tsx
Normal file
216
platform/docs/src/pages/components/table.tsx
Normal file
@ -0,0 +1,216 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function TablePageContent() {
|
||||
const {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
} = require('../../../../ui-next/src/components/Table');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const studies = [
|
||||
{ id: 'ST-001', patient: 'Smith, John', modality: 'CT', date: '2024-03-15', series: 4, status: 'Complete' },
|
||||
{ id: 'ST-002', patient: 'Doe, Jane', modality: 'MR', date: '2024-03-14', series: 6, status: 'Complete' },
|
||||
{ id: 'ST-003', patient: 'Lee, Alex', modality: 'CT', date: '2024-03-14', series: 3, status: 'In Progress' },
|
||||
{ id: 'ST-004', patient: 'Garcia, Maria', modality: 'PET/CT', date: '2024-03-13', series: 8, status: 'Complete' },
|
||||
{ id: 'ST-005', patient: 'Brown, Robert', modality: 'MR', date: '2024-03-12', series: 5, status: 'Failed' },
|
||||
];
|
||||
|
||||
const measurements = [
|
||||
{ label: 'Lesion 1', tool: 'Bidirectional', value: '24.3 × 18.1 mm', location: 'Liver' },
|
||||
{ label: 'Lesion 2', tool: 'Length', value: '15.7 mm', location: 'Lung RUL' },
|
||||
{ label: 'Lesion 3', tool: 'EllipticalROI', value: '42.1 mm²', location: 'Kidney L' },
|
||||
];
|
||||
|
||||
const tableProps = [
|
||||
{ name: 'containerClassName', type: 'string', default: '—', description: 'CSS classes applied to the outer scroll container' },
|
||||
{ name: 'noScroll', type: 'boolean', default: 'false', description: 'Disables the overflow-auto wrapper around the table' },
|
||||
{ name: 'className', type: 'string', default: '—', description: 'CSS classes applied to the <table> element' },
|
||||
];
|
||||
|
||||
const tableRowProps = [
|
||||
{ name: 'data-state', type: '"selected"', default: '—', description: 'Set to "selected" to apply the selected row styling (elevated background + foreground text)' },
|
||||
{ name: 'className', type: 'string', default: '—', description: 'CSS classes applied to the <tr> element' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Table"
|
||||
description="Styled HTML table primitives"
|
||||
>
|
||||
<PageHeader
|
||||
title="Table"
|
||||
description="Composable table primitives for tabular data."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
A set of styled wrappers around native HTML table elements.
|
||||
Each sub-component (<strong className="text-foreground">Table</strong>,{' '}
|
||||
<strong className="text-foreground">TableHeader</strong>,{' '}
|
||||
<strong className="text-foreground">TableBody</strong>,{' '}
|
||||
<strong className="text-foreground">TableRow</strong>,{' '}
|
||||
<strong className="text-foreground">TableHead</strong>,{' '}
|
||||
<strong className="text-foreground">TableCell</strong>,{' '}
|
||||
<strong className="text-foreground">TableFooter</strong>,{' '}
|
||||
<strong className="text-foreground">TableCaption</strong>) forwards refs and accepts all native attributes plus optional styling overrides.
|
||||
</p>
|
||||
<p>
|
||||
Rows respond to <strong className="text-foreground">hover</strong> with highlighted text
|
||||
and to <strong className="text-foreground">data-state="selected"</strong> with
|
||||
an elevated background — no additional props needed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Basic table">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Study</TableHead>
|
||||
<TableHead>Patient</TableHead>
|
||||
<TableHead>Modality</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead className="text-right">Series</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{studies.map(study => (
|
||||
<TableRow key={study.id}>
|
||||
<TableCell className="font-medium">{study.id}</TableCell>
|
||||
<TableCell>{study.patient}</TableCell>
|
||||
<TableCell>{study.modality}</TableCell>
|
||||
<TableCell>{study.date}</TableCell>
|
||||
<TableCell className="text-right">{study.series}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="With footer and caption">
|
||||
<Table>
|
||||
<TableCaption>Tracked measurements for current study</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Label</TableHead>
|
||||
<TableHead>Tool</TableHead>
|
||||
<TableHead>Value</TableHead>
|
||||
<TableHead>Location</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{measurements.map(m => (
|
||||
<TableRow key={m.label}>
|
||||
<TableCell className="font-medium">{m.label}</TableCell>
|
||||
<TableCell>{m.tool}</TableCell>
|
||||
<TableCell>{m.value}</TableCell>
|
||||
<TableCell>{m.location}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell colSpan={3}>Total measurements</TableCell>
|
||||
<TableCell className="text-right">{measurements.length}</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="No scroll wrapper" last>
|
||||
<Table noScroll>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Label</TableHead>
|
||||
<TableHead>Tool</TableHead>
|
||||
<TableHead>Value</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{measurements.map(m => (
|
||||
<TableRow key={m.label}>
|
||||
<TableCell className="font-medium">{m.label}</TableCell>
|
||||
<TableCell>{m.tool}</TableCell>
|
||||
<TableCell>{m.value}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import {
|
||||
Table, TableHeader, TableBody, TableFooter,
|
||||
TableHead, TableRow, TableCell, TableCaption,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Patient</TableHead>
|
||||
<TableHead>Modality</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{studies.map(study => (
|
||||
<TableRow key={study.id}>
|
||||
<TableCell>{study.patient}</TableCell>
|
||||
<TableCell>{study.modality}</TableCell>
|
||||
<TableCell>{study.date}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
// Row selection via data attribute
|
||||
<TableRow data-state={isSelected ? 'selected' : undefined}>
|
||||
…
|
||||
</TableRow>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Table Props">
|
||||
<PropsTable props={tableProps} />
|
||||
</Section>
|
||||
|
||||
<Section title="TableRow Props">
|
||||
<PropsTable props={tableRowProps} />
|
||||
</Section>
|
||||
|
||||
<div className="mb-10">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
All sub-components (<strong className="text-foreground">TableHeader</strong>,{' '}
|
||||
<strong className="text-foreground">TableBody</strong>,{' '}
|
||||
<strong className="text-foreground">TableFooter</strong>,{' '}
|
||||
<strong className="text-foreground">TableHead</strong>,{' '}
|
||||
<strong className="text-foreground">TableCell</strong>,{' '}
|
||||
<strong className="text-foreground">TableCaption</strong>) accept{' '}
|
||||
<strong className="text-foreground">className</strong> and forward all native HTML attributes and refs.
|
||||
</p>
|
||||
</div>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TablePage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <TablePageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
117
platform/docs/src/pages/components/tabs.tsx
Normal file
117
platform/docs/src/pages/components/tabs.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function TabsPageContent() {
|
||||
const { Tabs, TabsList, TabsTrigger, TabsContent } = require('../../../../ui-next/src/components/Tabs');
|
||||
const { Separator } = require('../../../../ui-next/src/components/Separator');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const props = [
|
||||
{ name: 'defaultValue', type: 'string', default: '—', description: 'Initial active tab value (uncontrolled)' },
|
||||
{ name: 'value', type: 'string', default: '—', description: 'Controlled active tab value' },
|
||||
{ name: 'onValueChange', type: '(value: string) => void', default: '—', description: 'Called when the active tab changes' },
|
||||
{ name: 'orientation', type: '"horizontal" | "vertical"', default: '"horizontal"', description: 'Layout direction of the tab list' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Tabs"
|
||||
description="Segmented control for switching between views"
|
||||
>
|
||||
<PageHeader
|
||||
title="Tabs"
|
||||
description="Organizes content into switchable panels triggered by a tab bar."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
Tabs is a multi-part component built on Radix UI primitives:{' '}
|
||||
<strong className="text-foreground">Tabs</strong> (root),{' '}
|
||||
<strong className="text-foreground">TabsList</strong> (tab bar),{' '}
|
||||
<strong className="text-foreground">TabsTrigger</strong> (individual tabs),{' '}
|
||||
and <strong className="text-foreground">TabsContent</strong> (panels).
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, tabs appear as{' '}
|
||||
<strong className="text-foreground">segmented controls</strong> for tool settings
|
||||
(e.g. brush shape: Circle / Sphere / Square), panel section switchers, and
|
||||
configuration option groups.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Basic tabs with content">
|
||||
<Tabs defaultValue="overview" className="w-[320px]">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="details">Details</TabsTrigger>
|
||||
<TabsTrigger value="history">History</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="overview">
|
||||
<p className="text-secondary-foreground text-sm pt-2">Overview panel content goes here.</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="details">
|
||||
<p className="text-secondary-foreground text-sm pt-2">Details panel content goes here.</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="history">
|
||||
<p className="text-secondary-foreground text-sm pt-2">History panel content goes here.</p>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Segmented control with separators">
|
||||
<Tabs defaultValue="circle" className="w-[300px]">
|
||||
<TabsList>
|
||||
<TabsTrigger value="circle">Circle</TabsTrigger>
|
||||
<Separator orientation="vertical" />
|
||||
<TabsTrigger value="sphere">Sphere</TabsTrigger>
|
||||
<Separator orientation="vertical" />
|
||||
<TabsTrigger value="square">Square</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Two tabs" last>
|
||||
<Tabs defaultValue="dark" className="w-[200px]">
|
||||
<TabsList>
|
||||
<TabsTrigger value="dark">Dark</TabsTrigger>
|
||||
<TabsTrigger value="light">Light</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { Tabs, TabsList, TabsTrigger, TabsContent } from '@ohif/ui-next';
|
||||
|
||||
<Tabs defaultValue="tab1" onValueChange={(v) => console.log(v)}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
||||
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="tab1">First panel</TabsContent>
|
||||
<TabsContent value="tab2">Second panel</TabsContent>
|
||||
</Tabs>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TabsPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <TabsPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
184
platform/docs/src/pages/components/toast.tsx
Normal file
184
platform/docs/src/pages/components/toast.tsx
Normal file
@ -0,0 +1,184 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function ToastPageContent() {
|
||||
const { Toaster, toast } = require('../../../../ui-next/src/components/Sonner');
|
||||
const { Button } = require('../../../../ui-next/src/components/Button');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const toastOptions = [
|
||||
{ name: 'message', type: 'string', default: '—', description: 'The toast message text (first positional argument)' },
|
||||
{ name: 'description', type: 'string', default: '—', description: 'Secondary text below the message' },
|
||||
{ name: 'duration', type: 'number', default: '4000', description: 'Time in ms before auto-dismiss' },
|
||||
{ name: 'action', type: 'ReactNode', default: '—', description: 'Action button rendered inside the toast' },
|
||||
{ name: 'cancel', type: 'ReactNode', default: '—', description: 'Cancel button rendered inside the toast' },
|
||||
];
|
||||
|
||||
const toasterProps = [
|
||||
{ name: 'position', type: 'string', default: '"bottom-right"', description: 'Where toasts appear on screen' },
|
||||
{ name: 'expand', type: 'boolean', default: 'false', description: 'Expand toasts by default instead of stacking' },
|
||||
{ name: 'richColors', type: 'boolean', default: 'false', description: 'Use Sonner built-in colored backgrounds per type' },
|
||||
{ name: 'duration', type: 'number', default: '4000', description: 'Default auto-dismiss duration for all toasts' },
|
||||
];
|
||||
|
||||
return (
|
||||
<ComponentLayout
|
||||
title="Toast"
|
||||
description="Temporary notification messages"
|
||||
>
|
||||
<PageHeader
|
||||
title="Toast"
|
||||
description="Brief, non-blocking notifications that appear temporarily and stack when multiple fire."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
Toasts are powered by <strong className="text-foreground">Sonner</strong> with
|
||||
custom OHIF icons for each type. Call <strong className="text-foreground">toast()</strong>,{' '}
|
||||
<strong className="text-foreground">toast.success()</strong>,{' '}
|
||||
<strong className="text-foreground">toast.error()</strong>,{' '}
|
||||
<strong className="text-foreground">toast.warning()</strong>,{' '}
|
||||
<strong className="text-foreground">toast.info()</strong>, or{' '}
|
||||
<strong className="text-foreground">toast.promise()</strong> to trigger a notification.
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF Viewer, toasts report{' '}
|
||||
<strong className="text-foreground">segmentation loading progress</strong>,{' '}
|
||||
<strong className="text-foreground">export results</strong>,{' '}
|
||||
<strong className="text-foreground">error states</strong>, and other transient
|
||||
feedback. Multiple toasts stack into a single unit.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Simple messages">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="default" onClick={() => toast.success('Segmentation saved')}>
|
||||
Success
|
||||
</Button>
|
||||
<Button variant="default" onClick={() => toast.error('Failed to load series')}>
|
||||
Error
|
||||
</Button>
|
||||
<Button variant="default" onClick={() => toast.info('3 measurements exported')}>
|
||||
Info
|
||||
</Button>
|
||||
<Button variant="default" onClick={() => toast.warning('Unsaved changes')}>
|
||||
Warning
|
||||
</Button>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="With description">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
toast.success('Export complete', {
|
||||
description: 'DICOM SR saved to local storage.',
|
||||
})
|
||||
}
|
||||
>
|
||||
Success + Description
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
toast.error('Load failed', {
|
||||
description: 'The DICOM file could not be parsed.',
|
||||
})
|
||||
}
|
||||
>
|
||||
Error + Description
|
||||
</Button>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Promise toast">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const promise = new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
toast.promise(promise, {
|
||||
loading: 'Loading segmentation...',
|
||||
success: 'Segmentation loaded',
|
||||
error: 'Failed to load segmentation',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Loading → Success (3s)
|
||||
</Button>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="With action buttons" last>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
toast.warning('Unsaved changes', {
|
||||
description: 'You have unsaved measurement labels.',
|
||||
action: (
|
||||
<Button size="sm" variant="ghost" onClick={() => toast.dismiss()}>
|
||||
Save
|
||||
</Button>
|
||||
),
|
||||
cancel: (
|
||||
<Button size="sm" variant="ghost" onClick={() => toast.dismiss()}>
|
||||
Discard
|
||||
</Button>
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
Warning + Buttons
|
||||
</Button>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { Toaster, toast } from '@ohif/ui-next';
|
||||
|
||||
// Add <Toaster /> once at your app root
|
||||
<Toaster />
|
||||
|
||||
// Trigger toasts anywhere
|
||||
toast.success('Saved');
|
||||
toast.error('Failed to load');
|
||||
toast.info('3 items exported');
|
||||
toast.warning('Unsaved changes', {
|
||||
description: 'Details here.',
|
||||
});
|
||||
|
||||
// Async promise toast
|
||||
toast.promise(fetchData(), {
|
||||
loading: 'Loading...',
|
||||
success: 'Done',
|
||||
error: 'Failed',
|
||||
});`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="toast() Options">
|
||||
<PropsTable props={toastOptions} />
|
||||
</Section>
|
||||
|
||||
<Section title="Toaster Props">
|
||||
<PropsTable props={toasterProps} />
|
||||
</Section>
|
||||
|
||||
<Toaster />
|
||||
</ComponentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ToastPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <ToastPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
214
platform/docs/src/pages/components/tool-button-list.tsx
Normal file
214
platform/docs/src/pages/components/tool-button-list.tsx
Normal file
@ -0,0 +1,214 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function ToolButtonListPageContent() {
|
||||
const {
|
||||
ToolButton,
|
||||
ToolButtonList,
|
||||
ToolButtonListDefault,
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
} = require('../../../../ui-next/src/components/ToolButton');
|
||||
const { TooltipProvider } = require('../../../../ui-next/src/components/Tooltip');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const props = [
|
||||
{ name: 'ToolButtonList', type: 'component', default: '—', description: 'Root flex container for the button + dropdown group' },
|
||||
{ name: 'ToolButtonListDefault', type: 'component', default: '—', description: 'Wrapper for the primary ToolButton, with optional tooltip' },
|
||||
{ name: 'ToolButtonListDropDown', type: 'component', default: '—', description: 'Chevron trigger that opens a dropdown of related tools' },
|
||||
{ name: 'ToolButtonListItem', type: 'component', default: '—', description: 'Menu item inside the dropdown, with optional icon' },
|
||||
{ name: 'ToolButtonListDivider', type: 'component', default: '—', description: 'Vertical divider between the primary button and dropdown chevron' },
|
||||
];
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<ComponentLayout
|
||||
title="ToolButtonList"
|
||||
description="Grouped tool button with dropdown"
|
||||
>
|
||||
<PageHeader
|
||||
title="ToolButtonList"
|
||||
description="A compound component that pairs a primary tool button with a dropdown of related tools."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
ToolButtonList groups a primary{' '}
|
||||
<strong className="text-foreground">ToolButton</strong> with a dropdown chevron
|
||||
that reveals related tool options. It's composed of five sub-components:{' '}
|
||||
<strong className="text-foreground">ToolButtonList</strong> (root),{' '}
|
||||
<strong className="text-foreground">ToolButtonListDefault</strong> (primary slot),{' '}
|
||||
<strong className="text-foreground">ToolButtonListDivider</strong>,{' '}
|
||||
<strong className="text-foreground">ToolButtonListDropDown</strong> (chevron + menu),{' '}
|
||||
and <strong className="text-foreground">ToolButtonListItem</strong> (menu items).
|
||||
</p>
|
||||
<p>
|
||||
In the OHIF toolbar, this pattern is used for measurement tool groups (Length,
|
||||
Bidirectional, Angle), annotation tools, and other sets where one tool is the
|
||||
default and others are accessible via the dropdown.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Measurement tools">
|
||||
<div className="bg-popover flex h-11 items-center rounded p-2">
|
||||
<ToolButtonList>
|
||||
<ToolButtonListDefault>
|
||||
<ToolButton
|
||||
id="Length"
|
||||
icon="ToolLength"
|
||||
label="Length"
|
||||
tooltip="Length Tool"
|
||||
onInteraction={({ itemId }) => console.debug(`Clicked ${itemId}`)}
|
||||
/>
|
||||
</ToolButtonListDefault>
|
||||
<ToolButtonListDivider />
|
||||
<ToolButtonListDropDown>
|
||||
<ToolButtonListItem
|
||||
icon="ToolLength"
|
||||
onSelect={() => console.debug('Selected Length')}
|
||||
>
|
||||
<span className="pl-1">Length</span>
|
||||
</ToolButtonListItem>
|
||||
<ToolButtonListItem
|
||||
icon="ToolBidirectional"
|
||||
onSelect={() => console.debug('Selected Bidirectional')}
|
||||
>
|
||||
<span className="pl-1">Bidirectional</span>
|
||||
</ToolButtonListItem>
|
||||
<ToolButtonListItem
|
||||
icon="ToolAnnotate"
|
||||
onSelect={() => console.debug('Selected Annotation')}
|
||||
>
|
||||
<span className="pl-1">Annotation</span>
|
||||
</ToolButtonListItem>
|
||||
</ToolButtonListDropDown>
|
||||
</ToolButtonList>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Active primary tool">
|
||||
<div className="bg-popover flex h-11 items-center rounded p-2">
|
||||
<ToolButtonList>
|
||||
<ToolButtonListDefault>
|
||||
<ToolButton
|
||||
id="Length"
|
||||
icon="ToolLength"
|
||||
label="Length"
|
||||
tooltip="Length Tool"
|
||||
isActive
|
||||
onInteraction={() => {}}
|
||||
/>
|
||||
</ToolButtonListDefault>
|
||||
<ToolButtonListDivider />
|
||||
<ToolButtonListDropDown>
|
||||
<ToolButtonListItem
|
||||
icon="ToolLength"
|
||||
onSelect={() => {}}
|
||||
>
|
||||
<span className="pl-1">Length</span>
|
||||
</ToolButtonListItem>
|
||||
<ToolButtonListItem
|
||||
icon="ToolBidirectional"
|
||||
onSelect={() => {}}
|
||||
>
|
||||
<span className="pl-1">Bidirectional</span>
|
||||
</ToolButtonListItem>
|
||||
</ToolButtonListDropDown>
|
||||
</ToolButtonList>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Multiple groups in a toolbar" last>
|
||||
<div className="bg-popover flex h-11 items-center gap-1 rounded p-2">
|
||||
<ToolButtonList>
|
||||
<ToolButtonListDefault>
|
||||
<ToolButton
|
||||
id="Length"
|
||||
icon="ToolLength"
|
||||
label="Length"
|
||||
tooltip="Length"
|
||||
isActive
|
||||
onInteraction={() => {}}
|
||||
/>
|
||||
</ToolButtonListDefault>
|
||||
<ToolButtonListDivider />
|
||||
<ToolButtonListDropDown>
|
||||
<ToolButtonListItem icon="ToolLength" onSelect={() => {}}>
|
||||
<span className="pl-1">Length</span>
|
||||
</ToolButtonListItem>
|
||||
<ToolButtonListItem icon="ToolBidirectional" onSelect={() => {}}>
|
||||
<span className="pl-1">Bidirectional</span>
|
||||
</ToolButtonListItem>
|
||||
</ToolButtonListDropDown>
|
||||
</ToolButtonList>
|
||||
|
||||
<ToolButton
|
||||
id="Zoom"
|
||||
icon="ToolZoom"
|
||||
label="Zoom"
|
||||
tooltip="Zoom"
|
||||
onInteraction={() => {}}
|
||||
/>
|
||||
<ToolButton
|
||||
id="Pan"
|
||||
icon="ToolMove"
|
||||
label="Pan"
|
||||
tooltip="Pan"
|
||||
onInteraction={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import {
|
||||
ToolButton, ToolButtonList, ToolButtonListDefault,
|
||||
ToolButtonListDropDown, ToolButtonListItem, ToolButtonListDivider,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
<ToolButtonList>
|
||||
<ToolButtonListDefault>
|
||||
<ToolButton
|
||||
id="Length"
|
||||
icon="ToolLength"
|
||||
label="Length"
|
||||
tooltip="Length Tool"
|
||||
onInteraction={({ itemId }) => handleTool(itemId)}
|
||||
/>
|
||||
</ToolButtonListDefault>
|
||||
<ToolButtonListDivider />
|
||||
<ToolButtonListDropDown>
|
||||
<ToolButtonListItem icon="ToolLength" onSelect={() => selectTool('Length')}>
|
||||
<span className="pl-1">Length</span>
|
||||
</ToolButtonListItem>
|
||||
<ToolButtonListItem icon="ToolBidirectional" onSelect={() => selectTool('Bidirectional')}>
|
||||
<span className="pl-1">Bidirectional</span>
|
||||
</ToolButtonListItem>
|
||||
</ToolButtonListDropDown>
|
||||
</ToolButtonList>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Sub-components">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ToolButtonListPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <ToolButtonListPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
158
platform/docs/src/pages/components/tool-button.tsx
Normal file
158
platform/docs/src/pages/components/tool-button.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
import React, { useState } from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function ToolButtonPageContent() {
|
||||
const { ToolButton } = require('../../../../ui-next/src/components/ToolButton');
|
||||
const { TooltipProvider } = require('../../../../ui-next/src/components/Tooltip');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const InteractivePicker = require('./_layout/InteractivePicker').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const [activeId, setActiveId] = useState('Zoom');
|
||||
|
||||
const states = [
|
||||
{ value: 'default', label: 'Default', description: 'Idle state. Transparent background, foreground icon.' },
|
||||
{ value: 'active', label: 'Active', description: 'Currently selected tool. Highlighted background with inverted icon.' },
|
||||
{ value: 'toggled', label: 'Toggled', description: 'Toggled on. Transparent background with highlight-colored icon.' },
|
||||
{ value: 'disabled', label: 'Disabled', description: 'Unavailable. Reduced opacity, cursor not-allowed.' },
|
||||
];
|
||||
|
||||
const props = [
|
||||
{ name: 'id', type: 'string', default: '—', description: 'Unique identifier, passed to onInteraction' },
|
||||
{ name: 'icon', type: 'string', default: '—', description: 'Icon name from the OHIF icon registry' },
|
||||
{ name: 'label', type: 'string', default: '—', description: 'Accessible label text' },
|
||||
{ name: 'tooltip', type: 'string', default: '—', description: 'Tooltip text shown on hover' },
|
||||
{ name: 'size', type: '"default" | "small"', default: '"default"', description: 'Button size (default: 40×40, small: 32×32)' },
|
||||
{ name: 'isActive', type: 'boolean', default: 'false', description: 'Active tool state (highlighted background)' },
|
||||
{ name: 'isToggled', type: 'boolean', default: 'false', description: 'Toggled state (highlight-colored icon)' },
|
||||
{ name: 'disabled', type: 'boolean', default: 'false', description: 'Disables interaction' },
|
||||
{ name: 'onInteraction', type: '({ itemId, commands }) => void', default: '—', description: 'Called when the button is clicked' },
|
||||
];
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<ComponentLayout
|
||||
title="ToolButton"
|
||||
description="Toolbar icon button with active states"
|
||||
>
|
||||
<PageHeader
|
||||
title="ToolButton"
|
||||
description="An icon button for the OHIF toolbar with active, toggled, and disabled states."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
ToolButton is the primary interactive element in the OHIF toolbar. Each button
|
||||
represents a tool (Zoom, Pan, Window/Level, measurement tools, etc.) and
|
||||
visually reflects whether it's the{' '}
|
||||
<strong className="text-foreground">active</strong> tool,{' '}
|
||||
<strong className="text-foreground">toggled</strong> on, or{' '}
|
||||
<strong className="text-foreground">disabled</strong>.
|
||||
</p>
|
||||
<p>
|
||||
It renders an icon from the OHIF icon registry via{' '}
|
||||
<strong className="text-foreground">Icons.ByName</strong> and wraps disabled
|
||||
buttons in a span so tooltips still work. Use{' '}
|
||||
<strong className="text-foreground">ToolButtonList</strong> to group a primary
|
||||
tool with a dropdown of related tools.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="States">
|
||||
<InteractivePicker
|
||||
options={states}
|
||||
defaultValue="default"
|
||||
renderPreview={(active) => (
|
||||
<div className="bg-popover flex h-11 items-center justify-center rounded px-4">
|
||||
<ToolButton
|
||||
id="demo"
|
||||
icon="ToolZoom"
|
||||
label="Zoom"
|
||||
tooltip="Zoom"
|
||||
isActive={active === 'active'}
|
||||
isToggled={active === 'toggled'}
|
||||
disabled={active === 'disabled'}
|
||||
disabledText={active === 'disabled' ? 'Tool unavailable' : undefined}
|
||||
onInteraction={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Toolbar row">
|
||||
<div className="bg-popover flex h-11 items-center justify-center rounded px-2">
|
||||
{[
|
||||
{ id: 'Zoom', icon: 'ToolZoom', label: 'Zoom' },
|
||||
{ id: 'Pan', icon: 'ToolMove', label: 'Pan' },
|
||||
{ id: 'WL', icon: 'ToolWindowLevel', label: 'Window Level' },
|
||||
].map((tool) => (
|
||||
<ToolButton
|
||||
key={tool.id}
|
||||
id={tool.id}
|
||||
icon={tool.icon}
|
||||
label={tool.label}
|
||||
tooltip={tool.label}
|
||||
isActive={activeId === tool.id}
|
||||
onInteraction={({ itemId }) => setActiveId(itemId)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Size comparison" last>
|
||||
<div className="bg-popover flex h-11 items-center gap-3 rounded px-4">
|
||||
<ToolButton
|
||||
id="default-size"
|
||||
icon="ToolZoom"
|
||||
label="Default"
|
||||
tooltip="Default size"
|
||||
onInteraction={() => {}}
|
||||
/>
|
||||
<ToolButton
|
||||
id="small-size"
|
||||
icon="ToolZoom"
|
||||
label="Small"
|
||||
tooltip="Small size"
|
||||
size="small"
|
||||
onInteraction={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import { ToolButton } from '@ohif/ui-next';
|
||||
|
||||
<ToolButton
|
||||
id="Zoom"
|
||||
icon="ToolZoom"
|
||||
label="Zoom"
|
||||
tooltip="Zoom Tool"
|
||||
isActive={activeTool === 'Zoom'}
|
||||
onInteraction={({ itemId }) => setActiveTool(itemId)}
|
||||
/>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ToolButtonPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <ToolButtonPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
148
platform/docs/src/pages/components/tooltip.tsx
Normal file
148
platform/docs/src/pages/components/tooltip.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
function TooltipPageContent() {
|
||||
const {
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
} = require('../../../../ui-next/src/components/Tooltip');
|
||||
const { Button } = require('../../../../ui-next/src/components/Button');
|
||||
const { Icons } = require('../../../../ui-next/src/components/Icons');
|
||||
const ComponentLayout = require('./_layout/ComponentLayout').default;
|
||||
const PageHeader = require('./_layout/PageHeader').default;
|
||||
const Section = require('./_layout/Section').default;
|
||||
const CodeBlock = require('./_layout/CodeBlock').default;
|
||||
const ExampleBlock = require('./_layout/ExampleBlock').default;
|
||||
const PropsTable = require('./_layout/PropsTable').default;
|
||||
|
||||
const props = [
|
||||
{ name: 'side', type: '"top" | "right" | "bottom" | "left"', default: '"top"', description: 'Which side of the trigger the tooltip appears on (on TooltipContent)' },
|
||||
{ name: 'sideOffset', type: 'number', default: '4', description: 'Distance in pixels from the trigger (on TooltipContent)' },
|
||||
{ name: 'delayDuration', type: 'number', default: '700', description: 'Delay in ms before the tooltip opens (on TooltipProvider)' },
|
||||
];
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<ComponentLayout
|
||||
title="Tooltip"
|
||||
description="Helper text on hover or focus"
|
||||
>
|
||||
<PageHeader
|
||||
title="Tooltip"
|
||||
description="A brief label that appears on hover or keyboard focus to describe an element."
|
||||
/>
|
||||
|
||||
<div className="mb-10">
|
||||
<div className="text-secondary-foreground space-y-3 text-lg leading-relaxed">
|
||||
<p>
|
||||
Tooltip is a multi-part component built on Radix UI primitives:{' '}
|
||||
<strong className="text-foreground">TooltipProvider</strong> (context),{' '}
|
||||
<strong className="text-foreground">Tooltip</strong> (root),{' '}
|
||||
<strong className="text-foreground">TooltipTrigger</strong>, and{' '}
|
||||
<strong className="text-foreground">TooltipContent</strong>.
|
||||
</p>
|
||||
<p>
|
||||
Tooltips are non-interactive — they display text only and dismiss on pointer
|
||||
leave. In the OHIF Viewer, tooltips label{' '}
|
||||
<strong className="text-foreground">toolbar icon buttons</strong>,{' '}
|
||||
<strong className="text-foreground">viewport action icons</strong>, and other
|
||||
controls where space is too tight for visible text.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Examples">
|
||||
<ExampleBlock title="Basic tooltip">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">?</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Tooltip content</TooltipContent>
|
||||
</Tooltip>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Side positions">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="sm">Top</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Appears above</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="sm">Right</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Appears right</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="sm">Bottom</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Appears below</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="sm">Left</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">Appears left</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
|
||||
<ExampleBlock title="Icon button with tooltip" last>
|
||||
<div className="flex gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Icons.Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Settings</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Icons.More className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>More options</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</ExampleBlock>
|
||||
</Section>
|
||||
|
||||
<Section title="Usage">
|
||||
<CodeBlock
|
||||
code={`import {
|
||||
Tooltip, TooltipTrigger, TooltipContent, TooltipProvider,
|
||||
Button,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
// Wrap your app (or a subtree) in TooltipProvider once
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">?</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Helpful description</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Props">
|
||||
<PropsTable props={props} />
|
||||
</Section>
|
||||
</ComponentLayout>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TooltipPage() {
|
||||
return (
|
||||
<BrowserOnly fallback={<></>}>{() => <TooltipPageContent />}</BrowserOnly>
|
||||
);
|
||||
}
|
||||
@ -1,210 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import '../css/custom.css';
|
||||
|
||||
import Layout from '@theme/Layout';
|
||||
import { Button } from '../../../ui-next/src/components/Button';
|
||||
import { Icons } from '../../../ui-next/src/components/Icons';
|
||||
import { Card, CardHeader, CardTitle, CardDescription } from '../../../ui-next/src/components/Card';
|
||||
|
||||
interface ShowcaseRowProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export default function ComponentShowcase() {
|
||||
// Update function to handle paths correctly
|
||||
const openLinkInNewWindow = (url: string) => {
|
||||
// Remove leading dot if present to fix production paths
|
||||
const cleanUrl = url.startsWith('.') ? url.substring(1) : url;
|
||||
window.open(cleanUrl, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
title="Patterns"
|
||||
description="Patterns and example layouts"
|
||||
>
|
||||
<div className="text-foreground min-h-screen bg-background">
|
||||
<div className="mx-auto my-4 max-w-5xl pt-4 pb-6">
|
||||
{/* Navigation cards */}
|
||||
<div className="mb-8 grid grid-cols-1 gap-5 md:grid-cols-3">
|
||||
<a
|
||||
href="/colors-and-type"
|
||||
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
|
||||
>
|
||||
<Card className="hover:bg-primary/30 w-full transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground text-xl">
|
||||
<Icons.ColorChange className="h-12 w-12" />
|
||||
Colors & Typography
|
||||
</CardTitle>
|
||||
<CardDescription className="text-lg">
|
||||
Color Palette and Typography Guidelines
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</a>
|
||||
<a
|
||||
href="/components-list"
|
||||
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
|
||||
>
|
||||
<Card className="hover:bg-primary/30 w-full transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground text-xl">
|
||||
<Icons.ColorChange className="h-12 w-12" />
|
||||
Components
|
||||
</CardTitle>
|
||||
<CardDescription className="text-lg">
|
||||
Essential UI Components with Variants
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</a>
|
||||
<a
|
||||
href="/patterns"
|
||||
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
|
||||
>
|
||||
<Card className="hover:bg-primary/30 w-full transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground text-xl">
|
||||
<Icons.ColorChange className="h-12 w-12" />
|
||||
Patterns
|
||||
</CardTitle>
|
||||
<CardDescription className="text-lg">
|
||||
Component-Based Layout Examples
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h1 className="text-foreground ml-6 mb-6 text-5xl">Patterns</h1>
|
||||
|
||||
<ShowcaseRow
|
||||
title="Segmentation List"
|
||||
description={
|
||||
<div className="space-y-4">
|
||||
<div className="block">
|
||||
Uses the Data Row component to displays a list of segments. The current
|
||||
"Segmentation" is chosen with a Select above the current list.
|
||||
</div>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => openLinkInNewWindow('/patterns/patterns-segmentation')}
|
||||
>
|
||||
Launch Segmentation Example
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
code={`
|
||||
aaa
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4"></div>
|
||||
<div className="mt-6 mb-5 flex flex-wrap gap-4">
|
||||
<img
|
||||
src="/img/patterns-segmentation.png"
|
||||
alt="Segmentation Panel"
|
||||
width={274}
|
||||
></img>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
|
||||
<ShowcaseRow
|
||||
title="Measurement List"
|
||||
description={
|
||||
<div className="space-y-4">
|
||||
<div className="block">
|
||||
Uses the Data Row component to displays a list of measurements. A custom "Label"
|
||||
starts each row with measurement data appearing on the secondary row
|
||||
</div>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => openLinkInNewWindow('/patterns/patterns-measurements')}
|
||||
>
|
||||
Launch Measurements Example
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
code={`
|
||||
aaa
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4"></div>
|
||||
<div className="mt-6 flex flex-wrap gap-4">
|
||||
<img
|
||||
src="/img/patterns-measurements.png"
|
||||
alt="Measurements Panel"
|
||||
width={274}
|
||||
></img>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) {
|
||||
const [showCode, setShowCode] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="bg-background mb-8 rounded-lg p-6">
|
||||
<div className="mb-4 flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="text-highlight text-2xl">{title}</h2>
|
||||
</div>
|
||||
<Button
|
||||
className="text-primary"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowCode(!showCode)}
|
||||
></Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-3">
|
||||
<div className="text-lg md:col-span-1">
|
||||
{description && <p className="text-secondary-foreground mt-2">{description}</p>}
|
||||
</div>
|
||||
<div className="flex min-h-[120px] items-center md:col-span-2">
|
||||
<div className="showcase-content">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
{showCode && (
|
||||
<pre className="border-input mt-4 overflow-x-auto rounded-md border bg-background p-4 text-sm">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) {
|
||||
// const [showCode, setShowCode] = useState(false);
|
||||
|
||||
// return (
|
||||
// <div className="bg-background mb-8 rounded-lg p-6">
|
||||
// <div className="mb-4 flex items-start justify-between">
|
||||
// <div>
|
||||
// <h2 className="text-2xl font-bold">{title}</h2>
|
||||
// {description && <p className="text-muted-foreground mt-1">{description}</p>}
|
||||
// </div>
|
||||
// <Button
|
||||
// className="text-primary"
|
||||
// variant="outline"
|
||||
// size="sm"
|
||||
// onClick={() => setShowCode(!showCode)}
|
||||
// >
|
||||
// {showCode ? 'Hide Code' : 'Show Code'} <Icons.Code className="ml-2 h-4 w-4" />
|
||||
// </Button>
|
||||
// </div>
|
||||
// <div className="showcase-content mb-4">{children}</div>
|
||||
// {showCode && (
|
||||
// <pre className="mt-4 overflow-x-auto rounded-md bg-background p-4 text-sm">
|
||||
// <code>{code}</code>
|
||||
// </pre>
|
||||
// )}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
@ -1,89 +0,0 @@
|
||||
import React from 'react';
|
||||
import { DataRow } from '../../../../ui-next/src/components/DataRow';
|
||||
|
||||
// Mock data to demonstrate DataRow usage
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Segment 1',
|
||||
description: 'Description for Segment 1',
|
||||
optionalField: 'Optional Info 1',
|
||||
colorHex: '#FF5733',
|
||||
details: 'Secondary details or text',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Segment 2',
|
||||
description: 'Description for Segment 2',
|
||||
optionalField: 'Optional Info 2',
|
||||
colorHex: '#33C1FF',
|
||||
details: 'Secondary details or text',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Segment 3',
|
||||
description: 'Description for Segment 3',
|
||||
optionalField: 'Optional Info 3',
|
||||
colorHex: '#5533FF',
|
||||
details: 'Secondary details or text',
|
||||
},
|
||||
];
|
||||
|
||||
// Mock action options map
|
||||
const actionOptionsMap = {
|
||||
'ROI Tools': ['Edit', 'Delete', 'View'],
|
||||
};
|
||||
|
||||
interface DataItem {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
optionalField?: string;
|
||||
colorHex?: string;
|
||||
details?: string;
|
||||
series?: string;
|
||||
}
|
||||
|
||||
interface ListGroup {
|
||||
type: string;
|
||||
items: DataItem[];
|
||||
}
|
||||
|
||||
const DataRowExample: React.FC = () => {
|
||||
const [selectedRowId, setSelectedRowId] = React.useState<string | null>(null);
|
||||
|
||||
const handleAction = (id: string, action: string) => {
|
||||
console.log(`Action "${action}" triggered for item with id: ${id}`);
|
||||
// Implement actual action logic here
|
||||
};
|
||||
|
||||
const handleRowSelect = (id: string) => {
|
||||
setSelectedRowId(prevSelectedId => (prevSelectedId === id ? null : id));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-[280px] space-y-px">
|
||||
{mockData.map((item, index) => {
|
||||
const compositeId = `ROI Tools-${item.id}-panel`; // Ensure unique composite ID
|
||||
return (
|
||||
<DataRow
|
||||
key={`panel-${compositeId}`} // Prefix to ensure uniqueness
|
||||
number={index + 1}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
optionalField={item.optionalField}
|
||||
colorHex={item.colorHex}
|
||||
details={item.details}
|
||||
series={item.series}
|
||||
actionOptions={actionOptionsMap['ROI Tools'] || ['Action']}
|
||||
onAction={(action: string) => handleAction(compositeId, action)}
|
||||
isSelected={selectedRowId === compositeId}
|
||||
onSelect={() => handleRowSelect(compositeId)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataRowExample;
|
||||
@ -1,33 +0,0 @@
|
||||
import React from 'react';
|
||||
import Layout from '@theme/Layout';
|
||||
import { useHistory } from '@docusaurus/router';
|
||||
|
||||
export default function Patterns() {
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div>
|
||||
<h1>Patterns</h1>
|
||||
<button
|
||||
className="bg-slate-400"
|
||||
onClick={() => history.push('/patterns/patterns-segmentation')}
|
||||
>
|
||||
{'Segmentation Panel'}
|
||||
</button>
|
||||
<button
|
||||
className="bg-slate-400"
|
||||
onClick={() => history.push('/patterns/patterns-measurements')}
|
||||
>
|
||||
{'Measurements Panel'}
|
||||
</button>
|
||||
<button
|
||||
className="bg-slate-400"
|
||||
onClick={() => history.push('/patterns/patterns-tmtv')}
|
||||
>
|
||||
{'tmtv'}
|
||||
</button>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@ -1,134 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
import { Icons } from '../../../../ui-next/src/components/Icons';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
AccordionContent,
|
||||
} from '../../../../ui-next/src/components/Accordion';
|
||||
import { DataRow } from '../../../../ui-next/src/components/DataRow';
|
||||
import { actionOptionsMap, dataList } from '../../../../ui-next/assets/data';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip';
|
||||
|
||||
interface DataItem {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
optionalField?: string;
|
||||
colorHex?: string;
|
||||
details?: string;
|
||||
series?: string;
|
||||
}
|
||||
|
||||
interface ListGroup {
|
||||
type: string;
|
||||
items: DataItem[];
|
||||
}
|
||||
|
||||
export default function Measurements() {
|
||||
const [selectedRowId, setSelectedRowId] = useState<string | null>(null);
|
||||
const handleAction = (id: string, action: string) => {
|
||||
console.log(`Action "${action}" triggered for item with id: ${id}`);
|
||||
// Implement actual action logic here
|
||||
};
|
||||
const handleRowSelect = (id: string) => {
|
||||
setSelectedRowId(prevSelectedId => (prevSelectedId === id ? null : id));
|
||||
};
|
||||
|
||||
const organSegmentationGroup = dataList.find(
|
||||
listGroup => listGroup.type === 'Organ Segmentation'
|
||||
);
|
||||
const roiToolsGroup = dataList.find(listGroup => listGroup.type === 'ROI Tools');
|
||||
|
||||
if (!organSegmentationGroup || !roiToolsGroup) {
|
||||
return null; // Avoid rendering until these groups are ready.
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserOnly>
|
||||
{() => (
|
||||
<div className="px-auto my-4 flex min-h-screen w-full justify-center bg-background py-12">
|
||||
<TooltipProvider>
|
||||
{/* Simulated Panel List for "Segmentation" */}
|
||||
<div className="w-64 space-y-0">
|
||||
<Accordion
|
||||
type="multiple"
|
||||
defaultValue={['measurements-list', 'measurements-additional']}
|
||||
tabIndex={0}
|
||||
>
|
||||
{/* Segmentation Tools */}
|
||||
<AccordionItem value="measurements-list">
|
||||
<AccordionTrigger className="bg-popover hover:bg-accent text-muted-foreground my-0.5 flex h-7 w-full items-center justify-between rounded py-2 pr-1 pl-2 font-normal">
|
||||
<span>Measurements</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="mx-2 my-0">
|
||||
<div className="text-foreground text-sm">2024-Jan-01</div>
|
||||
<div className="text-muted-foreground border-input border-b-2 pb-1 text-sm">
|
||||
Study title lorem ipsum
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-9 w-full items-center rounded pr-0.5">
|
||||
<div className="flex space-x-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="pl-1.5"
|
||||
>
|
||||
<Icons.Download />
|
||||
<span className="pl-1">CSV</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="pl-0.5"
|
||||
>
|
||||
<Icons.Add />
|
||||
Create DICOM SR
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-px">
|
||||
{roiToolsGroup.items.map((item, index) => {
|
||||
const compositeId = `${roiToolsGroup.type}-${item.id}-panel`; // Ensure unique composite ID
|
||||
return (
|
||||
<DataRow
|
||||
key={`panel-${compositeId}`} // Prefix to ensure uniqueness
|
||||
number={index + 1}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
optionalField={item.optionalField}
|
||||
colorHex={item.colorHex}
|
||||
details={item.details}
|
||||
series={item.series} // Pass the new series field
|
||||
actionOptions={actionOptionsMap[roiToolsGroup.type] || ['Action']}
|
||||
onAction={(action: string) => handleAction(compositeId, action)}
|
||||
isSelected={selectedRowId === compositeId}
|
||||
onSelect={() => handleRowSelect(compositeId)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Additional Findings */}
|
||||
<AccordionItem value="measurements-additional">
|
||||
<AccordionTrigger className="bg-popover hover:bg-accent text-muted-foreground my-0.5 flex h-7 w-full items-center justify-between rounded py-2 pr-1 pl-2 font-normal">
|
||||
<span>Additional Findings</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="bg-muted mb-0.5 h-12 rounded-b pb-3"></div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
@ -1,503 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { DataRow } from '../../../../ui-next/src/components/DataRow';
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
import {
|
||||
Select,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from '../../../../ui-next/src/components/Select';
|
||||
import { Icons } from '../../../../ui-next/src/components/Icons';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuPortal,
|
||||
} from '../../../../ui-next/src/components/DropdownMenu';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
AccordionContent,
|
||||
} from '../../../../ui-next/src/components/Accordion';
|
||||
import { Slider } from '../../../../ui-next/src/components/Slider';
|
||||
import { Switch } from '../../../../ui-next/src/components/Switch';
|
||||
import { Label } from '../../../../ui-next/src/components/Label';
|
||||
import { Input } from '../../../../ui-next/src/components/Input';
|
||||
import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs';
|
||||
import { actionOptionsMap, dataList } from '../../../../ui-next/assets/data';
|
||||
import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip';
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardTrigger,
|
||||
HoverCardContent,
|
||||
} from '../../../../ui-next/src/components/HoverCard';
|
||||
import { DataItem, ListGroup } from '../../../../ui-next/assets/data';
|
||||
export default function SegmentationPanel() {
|
||||
const [selectedRowId, setSelectedRowId] = useState<string | null>(null);
|
||||
const [selectedTab, setSelectedTab] = useState<string>('Fill & Outline');
|
||||
const handleAction = (id: string, action: string) => {
|
||||
console.log(`Action "${action}" triggered for item with id: ${id}`);
|
||||
// Implement actual action logic here
|
||||
};
|
||||
|
||||
// Handle row selection
|
||||
const handleRowSelect = (id: string) => {
|
||||
setSelectedRowId(prevSelectedId => (prevSelectedId === id ? null : id));
|
||||
};
|
||||
|
||||
const organSegmentationGroup = dataList.find(
|
||||
(listGroup: any) => listGroup.type === 'Organ Segmentation'
|
||||
) as unknown as ListGroup;
|
||||
|
||||
if (!organSegmentationGroup) {
|
||||
return <div className="text-red-500">Organ Segmentation data not found.</div>;
|
||||
}
|
||||
|
||||
// Create a state to track which item's statistics to show
|
||||
|
||||
// Function to render statistics panel
|
||||
const renderStatisticsPanel = (item: DataItem) => {
|
||||
if (!item.statistics) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stats = item.statistics;
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<div
|
||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
||||
style={{ backgroundColor: item.colorHex }}
|
||||
></div>
|
||||
<h3 className="text-muted-foreground break-words text-lg font-semibold">{item.title}</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<div className="">Centroid X</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.centroidX.value}</span>{' '}
|
||||
<span className="">{stats.centroidX.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Centroid Y</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.centroidY.value}</span>{' '}
|
||||
<span className="">{stats.centroidY.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Centroid Z</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.centroidZ.value}</span>{' '}
|
||||
<span className="">{stats.centroidZ.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Frame Duration</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.frameDuration.value}</span>{' '}
|
||||
<span className="">{stats.frameDuration.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Kurtosis</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.kurtosis.value}</span>{' '}
|
||||
<span className="">{stats.kurtosis.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Max</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.max.value}</span>{' '}
|
||||
<span className="">{stats.max.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Max Slice</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.maxSlice.value}</span>{' '}
|
||||
<span className="">{stats.maxSlice.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Mean</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.mean.value}</span>{' '}
|
||||
<span className="">{stats.mean.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Median</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.median.value}</span>{' '}
|
||||
<span className="">{stats.median.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Min</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.min.value}</span>{' '}
|
||||
<span className="">{stats.min.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Regions</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.regions.value}</span>{' '}
|
||||
<span className="">{stats.regions.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Skewness</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.skewness.value}</span>{' '}
|
||||
<span className="">{stats.skewness.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Sphere Diameter</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.sphereDiameter.value}</span>{' '}
|
||||
<span className="">{stats.sphereDiameter.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Standard Deviation</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.standardDeviation.value}</span>{' '}
|
||||
<span className="">{stats.standardDeviation.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">SUV Peak</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.suvPeak.value}</span>{' '}
|
||||
<span className="">{stats.suvPeak.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Total</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.total.value}</span>{' '}
|
||||
<span className="">{stats.total.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Glycolysis</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.glycolysis.value}</span>{' '}
|
||||
<span className="">{stats.glycolysis.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Volume</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.volume.value}</span>{' '}
|
||||
<span className="">{stats.volume.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="">Voxel Count</div>
|
||||
<div>
|
||||
<span className="text-white">{stats.voxelCount.value}</span>{' '}
|
||||
<span className="">{stats.voxelCount.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-auto flex min-h-screen w-full justify-center bg-background py-12">
|
||||
<div className="w-64 space-y-0">
|
||||
<TooltipProvider>
|
||||
<Accordion
|
||||
type="multiple"
|
||||
defaultValue={['segmentation-tools', 'segmentation-list']}
|
||||
>
|
||||
{/* Segmentation Tools */}
|
||||
<AccordionItem value="segmentation-tools">
|
||||
<AccordionTrigger className="bg-popover hover:bg-accent text-muted-foreground my-0.5 flex h-7 w-full items-center justify-between rounded py-2 pr-1 pl-2 font-normal">
|
||||
<span>Segmentation Tools</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="bg-muted mb-0.5 h-32 rounded-b pb-3"></div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Segmentation List */}
|
||||
<AccordionItem value="segmentation-list">
|
||||
<AccordionTrigger className="bg-popover hover:bg-accent text-muted-foreground my-0.5 flex h-7 w-full items-center justify-between rounded py-2 pr-1 pl-2 font-normal">
|
||||
<span>Segmentation List</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="mb-0">
|
||||
{/* Header Controls */}
|
||||
<div className="bg-muted flex h-10 w-full items-center space-x-1 rounded-t px-1.5">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<Icons.More className="h-6 w-6" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem>
|
||||
<Icons.Add className="text-foreground" />
|
||||
<span className="pl-2">Create New Segmentation</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Manage Current Segmentation</DropdownMenuLabel>
|
||||
<DropdownMenuItem>
|
||||
<Icons.Series className="text-foreground" />
|
||||
<span className="pl-2">Remove from Viewport</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Icons.Rename className="text-foreground" />
|
||||
<span className="pl-2">Rename</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Icons.Export className="text-foreground" />
|
||||
<span className="pl-2">Export & Download</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem>Export DICOM SEG</DropdownMenuItem>
|
||||
<DropdownMenuItem>Download DICOM SEG</DropdownMenuItem>
|
||||
<DropdownMenuItem>Download DICOM RTSTRUCT</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Icons.Delete className="text-red-600" />
|
||||
<span className="pl-2 text-red-600">Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Select>
|
||||
<SelectTrigger className="w-full overflow-hidden">
|
||||
<SelectValue placeholder="Segmentation 1" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="seg1">Segmentation 1</SelectItem>
|
||||
<SelectItem value="seg2">Segmentation 2</SelectItem>
|
||||
<SelectItem value="seg3">Segmentation Long Name 123</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<Icons.Info className="h-6 w-6" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Appearance Settings */}
|
||||
<AccordionItem value="segmentation-display">
|
||||
<AccordionTrigger className="bg-muted hover:bg-accent mt-0.5 flex h-7 w-full items-center justify-between rounded-b pr-1 pl-2 font-normal text-white">
|
||||
<div className="flex space-x-2">
|
||||
<Icons.Controls className="text-primary" />
|
||||
<span className="text-primary pr-1">Appearance Settings</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="bg-muted mb-0.5 space-y-2 rounded-b px-1.5 pt-0.5 pb-3">
|
||||
<div className="mx-1 mb-2.5 mt-1 flex items-center justify-between space-x-4">
|
||||
{/* Display Label with Selected Tab */}
|
||||
<div className="text-muted-foreground text-xs">Show: {selectedTab}</div>
|
||||
{/* Tabs Controls */}
|
||||
<Tabs
|
||||
value={selectedTab}
|
||||
onValueChange={setSelectedTab}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="Fill & Outline">
|
||||
<Icons.DisplayFillAndOutline className="text-primary" />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="Outline Only">
|
||||
<Icons.DisplayOutlineOnly className="text-primary" />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="Fill Only">
|
||||
<Icons.DisplayFillOnly className="text-primary" />
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
{/* Opacity Slider */}
|
||||
<div className="my-2 flex items-center">
|
||||
<Label className="text-muted-foreground mx-1 w-14 flex-none whitespace-nowrap text-xs">
|
||||
Opacity
|
||||
</Label>
|
||||
<Slider
|
||||
className="mx-1 flex-1"
|
||||
defaultValue={[85]}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
<Input
|
||||
className="mx-1 w-10 flex-none"
|
||||
placeholder="85"
|
||||
/>
|
||||
</div>
|
||||
{/* Border Slider */}
|
||||
<div className="my-2 flex items-center">
|
||||
<Label className="text-muted-foreground mx-1 w-14 flex-none whitespace-nowrap text-xs">
|
||||
Border
|
||||
</Label>
|
||||
<Slider
|
||||
className="mx-1 flex-1"
|
||||
defaultValue={[10]}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
<Input
|
||||
className="mx-1 w-10 flex-none"
|
||||
placeholder="2"
|
||||
/>
|
||||
</div>
|
||||
{/* Sync Changes Switch */}
|
||||
<div className="my-2 flex items-center pl-1 pb-1">
|
||||
<Switch defaultChecked />
|
||||
<Label className="text-muted-foreground mx-2 w-14 flex-none whitespace-nowrap text-xs">
|
||||
Sync changes in all viewports
|
||||
</Label>
|
||||
</div>
|
||||
<div className="border-input w-full border"></div>
|
||||
{/* Display Inactive Segmentations Switch */}
|
||||
<div className="my-2 flex items-center pl-1">
|
||||
<Switch defaultChecked />
|
||||
<Label className="text-muted-foreground mx-2 w-14 flex-none whitespace-nowrap text-xs">
|
||||
Display inactive segmentations
|
||||
</Label>
|
||||
</div>
|
||||
{/* Additional Opacity Slider */}
|
||||
<div className="my-2 flex items-center">
|
||||
<Label className="text-muted-foreground mx-1 w-14 flex-none whitespace-nowrap text-xs">
|
||||
Opacity
|
||||
</Label>
|
||||
<Slider
|
||||
className="mx-1 flex-1"
|
||||
defaultValue={[65]}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
<Input
|
||||
className="mx-1 w-10 flex-none"
|
||||
placeholder="65"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
{/* Action Buttons */}
|
||||
<div className="my-px flex h-9 w-full items-center justify-between rounded pl-0.5 pr-7">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="pr pl-0.5"
|
||||
>
|
||||
<Icons.Add />
|
||||
Add Segment
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<Icons.Hide className="h-6 w-6" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data Rows */}
|
||||
<div className="space-y-px">
|
||||
{organSegmentationGroup.items.map((item, index) => {
|
||||
const compositeId = `${organSegmentationGroup.type}-${item.id}-panel`; // Ensure unique composite ID
|
||||
return (
|
||||
<HoverCard
|
||||
key={`hover-${compositeId}`}
|
||||
openDelay={300}
|
||||
closeDelay={200}
|
||||
// open={true}
|
||||
>
|
||||
<HoverCardTrigger asChild>
|
||||
<div>
|
||||
<DataRow
|
||||
key={`panel-${compositeId}`} // Prefix to ensure uniqueness
|
||||
number={index + 1}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
colorHex={item.colorHex}
|
||||
details={item.details || { primary: [], secondary: [] }}
|
||||
actionOptions={
|
||||
actionOptionsMap[organSegmentationGroup.type] || ['Action']
|
||||
}
|
||||
onAction={(action: string) => handleAction(compositeId, action)}
|
||||
isSelected={selectedRowId === compositeId}
|
||||
onSelect={() => handleRowSelect(compositeId)}
|
||||
isVisible={true}
|
||||
isLocked={false}
|
||||
onToggleVisibility={() => console.debug('Toggle visibility')}
|
||||
onToggleLocked={() => console.debug('Toggle locked')}
|
||||
onRename={() => console.debug('Rename')}
|
||||
onDelete={() => console.debug('Delete')}
|
||||
onColor={() => console.debug('Color')}
|
||||
disableEditing={false}
|
||||
/>
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
side="left"
|
||||
align="start"
|
||||
className="w-72 border"
|
||||
>
|
||||
{renderStatisticsPanel(item)}
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { DataRow } from '../../../../ui-next/src/components/DataRow';
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
import {
|
||||
Select,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from '../../../../ui-next/src/components/Select';
|
||||
import { Icons } from '../../../../ui-next/src/components/Icons';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
AccordionContent,
|
||||
} from '../../../../ui-next/src/components/Accordion';
|
||||
import { Slider } from '../../../../ui-next/src/components/Slider';
|
||||
import { Switch } from '../../../../ui-next/src/components/Switch';
|
||||
import { Label } from '../../../../ui-next/src/components/Label';
|
||||
import { Input } from '../../../../ui-next/src/components/Input';
|
||||
import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs';
|
||||
import { actionOptionsMap, dataList } from '../../../../ui-next/assets/data';
|
||||
|
||||
interface DataItem {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
optionalField?: string;
|
||||
colorHex?: string;
|
||||
details?: string;
|
||||
series?: string;
|
||||
}
|
||||
|
||||
interface ListGroup {
|
||||
type: string;
|
||||
items: DataItem[];
|
||||
}
|
||||
|
||||
export default function SplitPanel() {
|
||||
return <div>hellosssssss</div>;
|
||||
}
|
||||
@ -1,393 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuPortal,
|
||||
} from '../../../../ui-next/src/components/DropdownMenu';
|
||||
import { Icons } from '../../../../ui-next/src/components/Icons/Icons';
|
||||
import { DataRow } from '../../../../ui-next/src/components/DataRow';
|
||||
import { actionOptionsMap, dataList } from '../../../../ui-next/assets/data';
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
AccordionContent,
|
||||
} from '../../../../ui-next/src/components/Accordion/Accordion';
|
||||
import { Slider } from '../../../../ui-next/src/components/Slider';
|
||||
import { Switch } from '../../../../ui-next/src/components/Switch';
|
||||
import { Label } from '../../../../ui-next/src/components/Label';
|
||||
import { Input } from '../../../../ui-next/src/components/Input';
|
||||
import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
interface DataItem {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
optionalField?: string;
|
||||
colorHex?: string;
|
||||
details?: string;
|
||||
series?: string;
|
||||
}
|
||||
|
||||
interface ListGroup {
|
||||
type: string;
|
||||
items: DataItem[];
|
||||
}
|
||||
|
||||
export default function TMTVPatterns() {
|
||||
const [selectedRowId, setSelectedRowId] = useState<string | null>(null);
|
||||
const [selectedTab, setSelectedTab] = useState<string>('Fill & Outline');
|
||||
|
||||
const handleAction = (id: string, action: string) => {
|
||||
console.log(`Action "${action}" triggered for item with id: ${id}`);
|
||||
// Implement actual action logic here
|
||||
};
|
||||
|
||||
// Handle row selection
|
||||
const handleRowSelect = (id: string) => {
|
||||
setSelectedRowId(prevSelectedId => (prevSelectedId === id ? null : id));
|
||||
};
|
||||
|
||||
// Find the "TMTV2" group
|
||||
const tmv2Group = dataList.find((listGroup: ListGroup) => listGroup.type === 'TMTV2');
|
||||
|
||||
// Find the "TMTV1" group
|
||||
const tmvGroup = dataList.find((listGroup: ListGroup) => listGroup.type === 'TMTV1');
|
||||
|
||||
// Check if both groups exist
|
||||
if (!tmv2Group) {
|
||||
return <div className="text-red-500">TMTV2 data not found.</div>;
|
||||
}
|
||||
|
||||
if (!tmvGroup) {
|
||||
return <div className="text-red-500">TMTV1 data not found.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserOnly>
|
||||
{() => (
|
||||
<div className="px-auto my-4 flex min-h-screen w-full justify-center bg-background py-12">
|
||||
<div className="w-64 space-y-0">
|
||||
<Accordion
|
||||
type="multiple"
|
||||
defaultValue={['segmentation-tools', 'segmentation-list', 'tmv1-group', 'tmv2-group']}
|
||||
collapsible
|
||||
>
|
||||
{/* Segmentation Tools */}
|
||||
<AccordionItem value="segmentation-tools">
|
||||
<AccordionTrigger className="bg-popover hover:bg-accent text-muted-foreground my-0.5 flex h-7 w-full items-center justify-between rounded py-2 pr-1 pl-2 font-normal">
|
||||
<span>Segmentation Tools</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="bg-muted mb-0.5 h-32 rounded-b pb-3"></div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
{/* Segmentation List */}
|
||||
<AccordionItem value="segmentation-list">
|
||||
<AccordionTrigger className="bg-popover hover:bg-accent text-muted-foreground my-0.5 flex h-7 w-full items-center justify-between rounded py-2 pr-1 pl-2 font-normal">
|
||||
<span>Segmentation List</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
{/* Appearance Settings */}
|
||||
<AccordionItem value="segmentation-display">
|
||||
<AccordionTrigger className="bg-muted hover:bg-accent mt-0.5 flex h-7 w-full items-center justify-between rounded-b pr-1 pl-2 font-normal text-white">
|
||||
<div className="flex space-x-2">
|
||||
<Icons.Controls className="text-primary" />
|
||||
<span className="text-primary pr-1">Appearance Settings</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="bg-muted mb-0.5 space-y-2 rounded-b px-px pt-0.5 pb-3">
|
||||
<div className="mx-1 mb-2.5 mt-1 flex items-center justify-between space-x-4">
|
||||
{/* Display Label with Selected Tab */}
|
||||
<div className="text-muted-foreground text-xs">Show: {selectedTab}</div>
|
||||
{/* Tabs Controls */}
|
||||
<Tabs
|
||||
value={selectedTab}
|
||||
onValueChange={setSelectedTab}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="Fill & Outline">
|
||||
<Icons.DisplayFillAndOutline className="text-primary" />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="Outline Only">
|
||||
<Icons.DisplayOutlineOnly className="text-primary" />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="Fill Only">
|
||||
<Icons.DisplayFillOnly className="text-primary" />
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
{/* Opacity Slider */}
|
||||
<div className="my-2 flex items-center">
|
||||
<Label className="text-muted-foreground mx-1 w-14 flex-none whitespace-nowrap text-xs">
|
||||
Opacity
|
||||
</Label>
|
||||
<Slider
|
||||
className="mx-1 flex-1"
|
||||
defaultValue={[85]}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
<Input
|
||||
className="mx-1 w-10 flex-none"
|
||||
placeholder="85"
|
||||
/>
|
||||
</div>
|
||||
{/* Border Slider */}
|
||||
<div className="my-2 flex items-center">
|
||||
<Label className="text-muted-foreground mx-1 w-14 flex-none whitespace-nowrap text-xs">
|
||||
Border
|
||||
</Label>
|
||||
<Slider
|
||||
className="mx-1 flex-1"
|
||||
defaultValue={[10]}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
<Input
|
||||
className="mx-1 w-10 flex-none"
|
||||
placeholder="2"
|
||||
/>
|
||||
</div>
|
||||
{/* Sync Changes Switch */}
|
||||
<div className="my-2 flex items-center pl-1 pb-1">
|
||||
<Switch defaultChecked />
|
||||
<Label className="text-muted-foreground mx-2 w-14 flex-none whitespace-nowrap text-xs">
|
||||
Sync changes in all viewports
|
||||
</Label>
|
||||
</div>
|
||||
<div className="border-input w-full border"></div>
|
||||
{/* Display Inactive Segmentations Switch */}
|
||||
<div className="my-2 flex items-center pl-1">
|
||||
<Switch defaultChecked />
|
||||
<Label className="text-muted-foreground mx-2 w-14 flex-none whitespace-nowrap text-xs">
|
||||
Display inactive segmentations
|
||||
</Label>
|
||||
</div>
|
||||
{/* Additional Opacity Slider */}
|
||||
<div className="my-2 flex items-center">
|
||||
<Label className="text-muted-foreground mx-1 w-14 flex-none whitespace-nowrap text-xs">
|
||||
Opacity
|
||||
</Label>
|
||||
<Slider
|
||||
className="mx-1 flex-1"
|
||||
defaultValue={[65]}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
<Input
|
||||
className="mx-1 w-10 flex-none"
|
||||
placeholder="65"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
{/* TMTV1 Group */}
|
||||
<AccordionItem value="tmv1-group">
|
||||
<AccordionTrigger className="hover:bg-popover mr-0 flex h-8 w-full items-center pl-0 pr-1">
|
||||
<div className="text-foreground border-input flex h-8 w-full items-center justify-between border-t-2">
|
||||
{/* Left Group: DropdownMenu and TMTV1 Label */}
|
||||
<div className="flex items-center space-x-1">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-1"
|
||||
>
|
||||
<Icons.More className="h-6 w-6" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem>
|
||||
<Icons.Add className="text-foreground" />
|
||||
<span className="pl-2">Add Segment</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Icons.Series className="text-foreground" />
|
||||
<span className="pl-2">Remove from Viewport</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Icons.Rename className="text-foreground" />
|
||||
<span className="pl-2">Rename</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Icons.Hide className="text-foreground" />
|
||||
<span className="pl-2">Hide or Show all Segments</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Icons.Export className="text-foreground" />
|
||||
<span className="pl-2">Export & Download</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem>Export DICOM SEG</DropdownMenuItem>
|
||||
<DropdownMenuItem>Download DICOM SEG</DropdownMenuItem>
|
||||
<DropdownMenuItem>Download DICOM RTSTRUCT</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Icons.Delete className="text-red-600" />
|
||||
<span className="pl-2 text-red-600">Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="pl-1.5">TMTV1 Segmentation</div>
|
||||
</div>
|
||||
<div className="mr-1 flex items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<Icons.Info className="h-6 w-6" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
{/* Data Rows for TMTV1 */}
|
||||
<div className="space-y-px">
|
||||
{tmvGroup.items.map((item, index) => {
|
||||
const compositeId = `${tmvGroup.type}-${item.id}-panel`; // Ensure unique composite ID
|
||||
return (
|
||||
<DataRow
|
||||
key={`panel-${compositeId}`} // Prefix to ensure uniqueness
|
||||
number={index + 1}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
optionalField={item.optionalField}
|
||||
colorHex={item.colorHex}
|
||||
details={item.details}
|
||||
series={item.series}
|
||||
actionOptions={actionOptionsMap[tmvGroup.type] || ['Action']}
|
||||
onAction={(action: string) => handleAction(compositeId, action)}
|
||||
isSelected={selectedRowId === compositeId}
|
||||
onSelect={() => handleRowSelect(compositeId)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
{/* TMTV2 Group */}
|
||||
<AccordionItem value="tmv2-group">
|
||||
<AccordionTrigger className="hover:bg-popover mr-0 flex h-8 w-full items-center pl-0 pr-1">
|
||||
<div className="text-foreground border-input flex h-8 w-full items-center justify-between border-t-2">
|
||||
<div className="flex items-center space-x-1">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-1"
|
||||
>
|
||||
<Icons.More className="h-6 w-6" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem>
|
||||
<Icons.Add className="text-foreground" />
|
||||
<span className="pl-2">Add Segment</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Icons.Series className="text-foreground" />
|
||||
<span className="pl-2">Remove from Viewport</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Icons.Rename className="text-foreground" />
|
||||
<span className="pl-2">Rename</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Icons.Hide className="text-foreground" />
|
||||
<span className="pl-2">Hide or Show all Segments</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Icons.Export className="text-foreground" />
|
||||
<span className="pl-2">Export & Download</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem>Export DICOM SEG</DropdownMenuItem>
|
||||
<DropdownMenuItem>Download DICOM SEG</DropdownMenuItem>
|
||||
<DropdownMenuItem>Download DICOM RTSTRUCT</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Icons.Delete className="text-red-600" />
|
||||
<span className="pl-2 text-red-600">Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<div className="pl-1.5">TMTV2 Segmentation</div>
|
||||
</div>
|
||||
<div className="mr-1 flex items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<Icons.Info className="h-6 w-6" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
{/* Data Rows for TMTV2 */}
|
||||
<div className="space-y-px">
|
||||
{tmv2Group.items.map((item, index) => {
|
||||
const compositeId = `${tmv2Group.type}-${item.id}-panel`; // Ensure unique composite ID
|
||||
return (
|
||||
<DataRow
|
||||
key={`panel-${compositeId}`} // Prefix to ensure uniqueness
|
||||
number={index + 1}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
optionalField={item.optionalField}
|
||||
colorHex={item.colorHex}
|
||||
details={item.details}
|
||||
series={item.series}
|
||||
actionOptions={actionOptionsMap[tmv2Group.type] || ['Action']}
|
||||
onAction={(action: string) => handleAction(compositeId, action)}
|
||||
isSelected={selectedRowId === compositeId}
|
||||
onSelect={() => handleRowSelect(compositeId)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
{/* Footer or Additional Information */}
|
||||
<div className="bg-popover text-foreground flex h-8 items-center justify-between pl-9 pr-3 text-sm font-semibold">
|
||||
<span>TMTV</span>
|
||||
<span>21.555 mL</span>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
141
platform/docs/src/theme/Footer/index.tsx
Normal file
141
platform/docs/src/theme/Footer/index.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import React from 'react';
|
||||
import Link from '@docusaurus/Link';
|
||||
|
||||
function FooterLink({
|
||||
href,
|
||||
isInternal = true,
|
||||
children,
|
||||
}: {
|
||||
href: string;
|
||||
isInternal?: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const className =
|
||||
'text-[#358cfd] text-[16px] leading-[31px] hover:text-white transition-colors no-underline';
|
||||
|
||||
if (isInternal) {
|
||||
return (
|
||||
<li>
|
||||
<Link className={className} to={href}>
|
||||
{children}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
className={className}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Footer(): JSX.Element {
|
||||
return (
|
||||
<footer className="bg-[#050615] text-white pb-12 md:pb-16 pt-16">
|
||||
<div className="mx-auto max-w-[1200px] px-6 lg:px-2">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-2">
|
||||
{/* Column 1: OHIF Title */}
|
||||
<div>
|
||||
<h4 className="text-[18px] md:text-[20px] leading-[32px] font-normal text-white m-0">
|
||||
Open Health
|
||||
<br />
|
||||
Imaging Foundation
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
{/* Column 2: Internal Links */}
|
||||
<div className="lg:ml-8">
|
||||
<ul className="list-none p-0 m-0 space-y-0.5">
|
||||
<FooterLink href="https://ohif.org/roadmap" isInternal={false}>
|
||||
Roadmap
|
||||
</FooterLink>
|
||||
<FooterLink href="https://ohif.org/team" isInternal={false}>
|
||||
Team
|
||||
</FooterLink>
|
||||
<FooterLink
|
||||
href="https://github.com/OHIF/Viewers/blob/master/LICENSE"
|
||||
isInternal={false}
|
||||
>
|
||||
License
|
||||
</FooterLink>
|
||||
<FooterLink href="https://ohif.org/release-notes" isInternal={false}>
|
||||
Release Notes
|
||||
</FooterLink>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Column 3: External Links */}
|
||||
<div>
|
||||
<ul className="list-none p-0 m-0 space-y-0.5">
|
||||
<FooterLink href="https://ohif.org/get-support" isInternal={false}>
|
||||
<span className="leading-tight">Support & Collaborate</span>
|
||||
</FooterLink>
|
||||
<FooterLink
|
||||
href="https://github.com/OHIF/Viewers"
|
||||
isInternal={false}
|
||||
>
|
||||
GitHub
|
||||
</FooterLink>
|
||||
<FooterLink
|
||||
href="https://join.slack.com/t/cornerstonejs/shared_invite/zt-3108kuc8m-ON1zP3nWsrgPNv9mM0E5fw"
|
||||
isInternal={false}
|
||||
>
|
||||
Slack Group
|
||||
</FooterLink>
|
||||
<FooterLink
|
||||
href="https://community.ohif.org/"
|
||||
isInternal={false}
|
||||
>
|
||||
Discussions
|
||||
</FooterLink>
|
||||
<FooterLink
|
||||
href="https://www.linkedin.com/company/ohif"
|
||||
isInternal={false}
|
||||
>
|
||||
LinkedIn
|
||||
</FooterLink>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Column 4: MGH Logo + License */}
|
||||
<div className="md:col-span-3 lg:col-span-3 md:pt-8 lg:pt-0 lg:pl-24">
|
||||
<div className="mb-6">
|
||||
<a
|
||||
href="https://www.massgeneral.org"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img
|
||||
src="/img/mgh-logo-white.svg"
|
||||
alt="Massachusetts General Hospital - Founding Member, Mass General Brigham"
|
||||
className="h-[33px] w-[317px] hover:opacity-75 transition-opacity"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p className="text-[15px] leading-[22px] text-white/50 m-0">
|
||||
OHIF is open source software released under the{' '}
|
||||
<a
|
||||
href="https://github.com/OHIF/Viewers/blob/master/LICENSE"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mit-license-link"
|
||||
>
|
||||
MIT license
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
78
platform/docs/static/img/mgh-logo-white.svg
vendored
Normal file
78
platform/docs/static/img/mgh-logo-white.svg
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
<svg width="317" height="33" viewBox="0 0 317 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5_1857)">
|
||||
<path d="M36.0015 22.176V23.364H32.5602V26.136H35.6045V27.324H32.5602V30.756H31.1042V22.308H36.0015V22.176Z" fill="white"/>
|
||||
<path d="M39.5753 30.8881C37.7222 30.8881 36.531 29.8321 36.531 27.5881C36.531 25.4761 37.5899 24.2881 39.5753 24.2881C41.4283 24.2881 42.6195 25.3441 42.6195 27.5881C42.6195 29.7001 41.5606 30.8881 39.5753 30.8881ZM41.0312 27.5881C41.0312 26.1361 40.5018 25.3441 39.5753 25.3441C38.5164 25.3441 38.1193 26.2681 38.1193 27.5881C38.1193 29.0401 38.6488 29.8321 39.5753 29.8321C40.6341 29.8321 41.0312 28.9081 41.0312 27.5881Z" fill="white"/>
|
||||
<path d="M48.3109 30.6241L48.0462 29.5681C47.9139 30.2281 47.2521 30.7561 46.1932 30.7561C45.2667 30.7561 44.7372 30.3601 44.4725 29.9641C44.0754 29.4361 44.0754 28.7761 44.0754 27.9841V24.2881H45.5314V27.9841C45.5314 28.3801 45.5314 28.6441 45.7961 29.0401C46.0608 29.3041 46.3255 29.4361 46.855 29.4361C47.9139 29.4361 48.0462 28.5121 48.0462 28.1161V24.2881H49.5022V30.4921H48.3109V30.6241Z" fill="white"/>
|
||||
<path d="M52.4141 24.4201L52.6788 25.4761C52.9435 24.8161 53.473 24.2881 54.5318 24.2881C55.4584 24.2881 55.9878 24.6841 56.3849 25.0801C56.7819 25.6081 56.7819 26.2681 56.7819 26.9281V30.6241H55.326V26.9281C55.326 26.5321 55.326 26.2681 55.0613 25.8721C54.7966 25.6081 54.5318 25.4761 54.1348 25.4761C53.0759 25.4761 52.8112 26.4001 52.8112 26.7961V30.6241H51.3552V24.4201H52.4141Z" fill="white"/>
|
||||
<path d="M64.0616 21.9121V30.7561H62.8704L62.6057 29.7001C62.4733 30.3601 61.8115 30.8881 60.7527 30.8881C59.032 30.8881 58.1055 29.5681 58.1055 27.7201C58.1055 25.7401 59.032 24.4201 60.885 24.4201C61.8115 24.4201 62.341 24.8161 62.6057 25.4761V21.9121H64.0616ZM59.6938 27.5881C59.6938 28.7761 60.0909 29.7001 61.1497 29.7001C62.2086 29.7001 62.6057 28.7761 62.6057 27.5881C62.6057 26.4001 62.2086 25.4761 61.1497 25.4761C60.0909 25.4761 59.6938 26.4001 59.6938 27.5881Z" fill="white"/>
|
||||
<path d="M67.5029 22.704C67.5029 23.232 67.1059 23.628 66.5764 23.628C66.047 23.628 65.6499 23.232 65.6499 22.704C65.6499 22.176 66.047 21.78 66.5764 21.78C67.1059 21.78 67.5029 22.176 67.5029 22.704ZM67.3706 24.42V25.476V30.756H65.9146V24.552H67.3706V24.42Z" fill="white"/>
|
||||
<path d="M70.2825 24.4201L70.5472 25.4761C70.8119 24.8161 71.3414 24.2881 72.4003 24.2881C73.3268 24.2881 73.8562 24.6841 74.2533 25.0801C74.6504 25.6081 74.6504 26.2681 74.6504 26.9281V30.6241H73.1944V26.9281C73.1944 26.5321 73.1944 26.2681 72.9297 25.8721C72.665 25.6081 72.4003 25.4761 72.0032 25.4761C70.9443 25.4761 70.6796 26.4001 70.6796 26.7961V30.6241H69.2236V24.4201H70.2825Z" fill="white"/>
|
||||
<path d="M78.7535 28.3801C78.4888 28.3801 78.2241 28.3801 77.9593 28.2481C77.827 28.3801 77.6946 28.5121 77.6946 28.7761C77.6946 29.3041 78.224 29.3041 78.4888 29.3041H79.4153C80.2094 29.3041 81.9301 29.3041 81.9301 31.0201C81.9301 32.3401 80.8712 33.1321 78.6211 33.1321C76.6357 33.1321 75.7092 32.6041 75.7092 31.6801C75.7092 30.6241 76.9005 30.4921 76.9005 30.4921C76.9005 30.4921 76.1063 30.2281 76.1063 29.4361C76.1063 28.6441 76.9005 28.3801 77.2975 28.2481C76.6357 27.8521 76.2387 27.3241 76.2387 26.5321C76.2387 25.3441 77.2975 24.5521 78.7535 24.5521C79.5476 24.5521 80.2094 24.6841 80.6065 25.0801C80.8712 24.2881 81.533 24.2881 81.9301 24.2881H82.0625V25.4761C82.0625 25.4761 81.7977 25.4761 81.533 25.4761C81.2683 25.4761 81.136 25.4761 81.0036 25.6081C81.136 25.8721 81.2683 26.2681 81.2683 26.6641C81.2683 27.5881 80.3418 28.3801 78.7535 28.3801ZM77.4299 30.4921C77.4299 30.4921 77.0328 30.7561 77.0328 31.2841C77.0328 31.8121 77.5623 32.0761 78.6211 32.0761C79.8124 32.0761 80.4742 31.8121 80.4742 31.1521C80.4742 30.4921 79.68 30.4921 79.2829 30.4921H77.4299ZM79.8124 26.2681C79.8124 25.6081 79.4153 25.3441 78.7535 25.3441C78.0917 25.3441 77.6946 25.7401 77.6946 26.2681C77.6946 26.9281 78.0917 27.1921 78.7535 27.1921C79.4153 27.3241 79.8124 26.9281 79.8124 26.2681Z" fill="white"/>
|
||||
<path d="M88.4158 22.176L91.1953 28.644L93.8425 22.176H95.6955V30.624H94.2396V24.024L91.7248 30.228H90.4012L87.8863 24.024V30.624H86.5627V22.176H88.4158Z" fill="white"/>
|
||||
<path d="M102.313 30.4921C102.313 30.4921 101.519 30.7561 100.328 30.7561C99.1368 30.7561 98.475 30.4921 97.9455 29.9641C97.4161 29.4361 97.1514 28.6441 97.1514 27.5881C97.1514 25.6081 98.2102 24.2881 100.063 24.2881C101.122 24.2881 101.916 24.6841 102.313 25.3441C102.71 26.0041 102.71 26.9281 102.71 27.9841H98.7397C98.7397 29.0401 99.4015 29.7001 100.593 29.7001C101.519 29.7001 102.313 29.3041 102.313 29.3041H102.446V30.4921H102.313ZM101.254 26.9281C101.254 25.8721 100.99 25.3441 100.063 25.3441C99.1367 25.3441 98.872 26.0041 98.7397 26.9281H101.254Z" fill="white"/>
|
||||
<path d="M105.358 24.4201L105.622 25.4761C105.755 24.8161 106.284 24.2881 107.475 24.2881C108.534 24.2881 109.064 24.8161 109.328 25.4761C109.593 24.6841 110.255 24.2881 111.181 24.2881C112.108 24.2881 112.637 24.6841 112.902 25.0801C113.299 25.6081 113.299 26.2681 113.299 26.9281V30.6241H111.843V26.9281C111.843 26.5321 111.843 26.2681 111.711 25.8721C111.579 25.6081 111.314 25.4761 110.784 25.4761C109.726 25.4761 109.593 26.4001 109.593 26.7961V30.6241H108.137V26.9281C108.137 26.5321 108.137 26.2681 108.005 25.8721C107.872 25.6081 107.608 25.4761 107.078 25.4761C106.019 25.4761 105.887 26.4001 105.887 26.7961V30.6241H104.431V24.4201H105.358Z" fill="white"/>
|
||||
<path d="M116.476 21.9121V25.6081C116.741 24.9481 117.27 24.4201 118.329 24.4201C119.917 24.4201 120.976 25.6081 120.976 27.5881C120.976 29.7001 119.917 30.8881 118.197 30.8881C117.27 30.8881 116.608 30.4921 116.344 29.8321L115.947 30.7561H115.02V21.9121H116.476ZM116.476 27.5881C116.476 28.7761 116.873 29.7001 117.932 29.7001C118.991 29.7001 119.388 28.7761 119.388 27.5881C119.388 26.4001 118.991 25.4761 117.932 25.4761C116.873 25.4761 116.476 26.4001 116.476 27.5881Z" fill="white"/>
|
||||
<path d="M127.197 30.4921C127.197 30.4921 126.403 30.7561 125.212 30.7561C124.02 30.7561 123.359 30.4921 122.829 29.9641C122.3 29.4361 122.035 28.6441 122.035 27.5881C122.035 25.6081 123.094 24.2881 124.947 24.2881C126.006 24.2881 126.8 24.6841 127.197 25.3441C127.594 26.0041 127.594 26.9281 127.594 27.9841H123.623C123.623 29.0401 124.285 29.7001 125.476 29.7001C126.403 29.7001 127.197 29.3041 127.197 29.3041H127.329V30.4921H127.197ZM126.006 26.9281C126.006 25.8721 125.741 25.3441 124.814 25.3441C123.888 25.3441 123.623 26.0041 123.491 26.9281H126.006Z" fill="white"/>
|
||||
<path d="M132.624 25.74C132.624 25.74 132.359 25.608 131.962 25.608C130.638 25.608 130.638 26.532 130.638 27.06V30.492H129.182V24.288H130.374L130.638 25.476C130.903 24.42 131.697 24.156 132.491 24.156H132.624V25.74Z" fill="white"/>
|
||||
<path d="M134.212 29.1721L133.285 32.3401H132.227L132.756 29.1721H134.212Z" fill="white"/>
|
||||
<path d="M140.83 22.176L143.609 28.644L146.257 22.176H148.11V30.624H146.654V24.024L144.139 30.228H142.815L140.3 24.024V30.624H138.977V22.176H140.83Z" fill="white"/>
|
||||
<path d="M150.227 24.6839C150.227 24.6839 151.022 24.4199 152.213 24.4199C153.139 24.4199 153.933 24.5519 154.463 25.2119C154.86 25.7399 154.86 26.2679 154.86 26.9279V29.0399C154.86 29.3039 154.86 29.6999 155.389 29.6999C155.522 29.6999 155.654 29.6999 155.654 29.6999V30.7559C155.654 30.7559 155.257 30.8879 154.86 30.8879C154.066 30.8879 153.536 30.6239 153.536 29.8319C153.272 30.4919 152.61 31.0199 151.683 31.0199C150.492 31.0199 149.698 30.2279 149.698 29.1719C149.698 27.8519 150.757 27.1919 152.742 27.1919C153.139 27.1919 153.272 27.1919 153.272 27.1919C153.272 26.6639 153.272 26.3999 153.139 26.1359C153.007 25.8719 152.61 25.7399 152.08 25.7399C151.154 25.7399 150.36 26.0039 150.36 26.0039H150.227V24.6839ZM151.948 29.6999C152.345 29.6999 152.742 29.4359 153.007 29.1719C153.272 28.7759 153.272 28.3799 153.272 28.1159V27.9839H152.875C152.213 27.9839 151.022 27.9839 151.022 28.9079C151.154 29.3039 151.419 29.6999 151.948 29.6999Z" fill="white"/>
|
||||
<path d="M160.551 25.7401C160.419 25.7401 159.625 25.4761 158.963 25.4761C158.301 25.4761 158.037 25.7401 158.037 26.0041C158.037 26.9281 160.816 26.6641 160.816 28.7761C160.816 30.0961 159.757 30.6241 158.301 30.6241C157.242 30.6241 156.581 30.3601 156.581 30.3601V29.1721H156.713C156.713 29.1721 157.639 29.5681 158.434 29.5681C159.095 29.5681 159.36 29.3041 159.36 28.9081C159.36 27.8521 156.581 28.1161 156.581 26.1361C156.581 24.9481 157.507 24.2881 159.095 24.2881C160.022 24.2881 160.684 24.5521 160.684 24.5521V25.7401H160.551Z" fill="white"/>
|
||||
<path d="M165.846 25.7401C165.713 25.7401 164.919 25.4761 164.257 25.4761C163.596 25.4761 163.331 25.7401 163.331 26.0041C163.331 26.9281 166.11 26.6641 166.11 28.7761C166.11 30.0961 165.052 30.6241 163.596 30.6241C162.537 30.6241 161.875 30.3601 161.875 30.3601V29.1721H162.007C162.007 29.1721 162.934 29.5681 163.728 29.5681C164.39 29.5681 164.655 29.3041 164.655 28.9081C164.655 27.8521 161.875 28.1161 161.875 26.1361C161.875 24.9481 162.802 24.2881 164.39 24.2881C165.316 24.2881 165.978 24.5521 165.978 24.5521V25.7401H165.846Z" fill="white"/>
|
||||
<path d="M177.361 26.1359V30.3599C177.361 30.3599 176.302 30.7559 174.714 30.7559C172.067 30.7559 170.346 29.4359 170.346 26.3999C170.346 23.3639 172.199 22.0439 174.979 22.0439C176.17 22.0439 177.096 22.3079 177.096 22.3079V23.4959H176.964C176.964 23.4959 176.037 23.0999 174.979 23.0999C173.125 23.0999 171.934 24.1559 171.934 26.2679C171.934 28.3799 173.125 29.5679 174.714 29.5679C175.376 29.5679 175.773 29.4359 175.773 29.4359V27.1919H174.317V26.0039H177.361V26.1359Z" fill="white"/>
|
||||
<path d="M183.847 30.4921C183.847 30.4921 183.052 30.7561 181.861 30.7561C180.67 30.7561 180.008 30.4921 179.479 29.9641C178.949 29.4361 178.685 28.6441 178.685 27.5881C178.685 25.6081 179.743 24.2881 181.596 24.2881C182.655 24.2881 183.449 24.6841 183.847 25.3441C184.244 26.0041 184.244 26.9281 184.244 27.9841H180.273C180.273 29.0401 180.935 29.7001 182.126 29.7001C183.052 29.7001 183.847 29.3041 183.847 29.3041H183.979V30.4921H183.847ZM182.655 26.9281C182.655 25.8721 182.391 25.3441 181.464 25.3441C180.538 25.3441 180.273 26.0041 180.141 26.9281H182.655Z" fill="white"/>
|
||||
<path d="M186.891 24.4201L187.156 25.4761C187.42 24.8161 187.95 24.2881 189.009 24.2881C189.935 24.2881 190.465 24.6841 190.862 25.0801C191.259 25.6081 191.259 26.2681 191.259 26.9281V30.6241H189.803V26.9281C189.803 26.5321 189.803 26.2681 189.538 25.8721C189.273 25.6081 189.009 25.4761 188.612 25.4761C187.553 25.4761 187.288 26.4001 187.288 26.7961V30.6241H185.832V24.4201H186.891Z" fill="white"/>
|
||||
<path d="M197.744 30.4921C197.744 30.4921 196.95 30.7561 195.759 30.7561C194.568 30.7561 193.906 30.4921 193.376 29.9641C192.847 29.4361 192.582 28.6441 192.582 27.5881C192.582 25.6081 193.641 24.2881 195.494 24.2881C196.553 24.2881 197.347 24.6841 197.744 25.3441C198.141 26.0041 198.141 26.9281 198.141 27.9841H194.171C194.171 29.0401 194.832 29.7001 196.024 29.7001C196.95 29.7001 197.744 29.3041 197.744 29.3041H197.877V30.4921H197.744ZM196.553 26.9281C196.553 25.8721 196.288 25.3441 195.362 25.3441C194.435 25.3441 194.171 26.0041 194.038 26.9281H196.553Z" fill="white"/>
|
||||
<path d="M203.039 25.74C203.039 25.74 202.774 25.608 202.377 25.608C201.053 25.608 201.053 26.532 201.053 27.06V30.492H199.597V24.288H200.789L201.053 25.476C201.318 24.42 202.112 24.156 202.906 24.156H203.039V25.74Z" fill="white"/>
|
||||
<path d="M204.23 24.6839C204.23 24.6839 205.024 24.4199 206.215 24.4199C207.142 24.4199 207.936 24.5519 208.465 25.2119C208.862 25.7399 208.862 26.2679 208.862 26.9279V29.0399C208.862 29.3039 208.862 29.6999 209.392 29.6999C209.524 29.6999 209.657 29.6999 209.657 29.6999V30.7559C209.657 30.7559 209.26 30.8879 208.862 30.8879C208.068 30.8879 207.539 30.6239 207.539 29.8319C207.274 30.4919 206.612 31.0199 205.686 31.0199C204.495 31.0199 203.7 30.2279 203.7 29.1719C203.7 27.8519 204.759 27.1919 206.745 27.1919C207.142 27.1919 207.274 27.1919 207.274 27.1919C207.274 26.6639 207.274 26.3999 207.142 26.1359C207.009 25.8719 206.612 25.7399 206.083 25.7399C205.156 25.7399 204.362 26.0039 204.362 26.0039H204.23V24.6839ZM205.951 29.6999C206.348 29.6999 206.745 29.4359 207.009 29.1719C207.274 28.7759 207.274 28.3799 207.274 28.1159V27.9839H206.877C206.215 27.9839 205.024 27.9839 205.024 28.9079C205.156 29.3039 205.421 29.6999 205.951 29.6999Z" fill="white"/>
|
||||
<path d="M212.172 21.9121V24.4201V30.7561H210.716V21.9121H212.172Z" fill="white"/>
|
||||
<path d="M219.981 22.176C220.642 22.176 221.304 22.176 221.966 22.572C222.496 22.836 222.893 23.364 222.893 24.288C222.893 26.004 221.437 26.136 221.437 26.136C221.437 26.136 223.422 26.268 223.422 28.248C223.422 29.172 223.025 29.832 222.363 30.228C221.569 30.756 220.775 30.756 220.113 30.756H217.333V22.308H219.981V22.176ZM218.657 25.74H219.716C220.113 25.74 220.378 25.74 220.775 25.608C221.04 25.476 221.304 25.08 221.304 24.552C221.304 24.024 221.04 23.628 220.775 23.496C220.51 23.364 220.113 23.364 219.716 23.364H218.657V25.74ZM218.657 29.568H219.848C220.245 29.568 220.642 29.568 221.172 29.304C221.569 29.04 221.834 28.776 221.834 28.116C221.834 27.588 221.569 27.192 221.172 26.928C220.775 26.664 220.245 26.664 219.848 26.664H218.657V29.568Z" fill="white"/>
|
||||
<path d="M228.187 25.74C228.187 25.74 227.922 25.608 227.525 25.608C226.202 25.608 226.202 26.532 226.202 27.06V30.492H224.746V24.288H225.937L226.202 25.476C226.466 24.42 227.26 24.156 228.055 24.156H228.187V25.74Z" fill="white"/>
|
||||
<path d="M230.967 22.704C230.967 23.232 230.569 23.628 230.04 23.628C229.511 23.628 229.114 23.232 229.114 22.704C229.114 22.176 229.511 21.78 230.04 21.78C230.569 21.78 230.967 22.176 230.967 22.704ZM230.834 24.42V25.344V30.624H229.378V24.42H230.834Z" fill="white"/>
|
||||
<path d="M235.07 28.3801C234.805 28.3801 234.54 28.3801 234.275 28.2481C234.143 28.3801 234.011 28.5121 234.011 28.7761C234.011 29.3041 234.54 29.3041 234.805 29.3041H235.864C236.658 29.3041 238.379 29.3041 238.379 31.0201C238.379 32.3401 237.32 33.1321 235.07 33.1321C233.084 33.1321 232.158 32.6041 232.158 31.6801C232.158 30.6241 233.349 30.4921 233.349 30.4921C233.349 30.4921 232.555 30.2281 232.555 29.4361C232.555 28.6441 233.349 28.3801 233.746 28.2481C233.084 27.8521 232.687 27.3241 232.687 26.5321C232.687 25.3441 233.746 24.5521 235.202 24.5521C235.996 24.5521 236.658 24.6841 237.055 25.0801C237.32 24.2881 237.982 24.2881 238.379 24.2881H238.511V25.4761C238.511 25.4761 238.246 25.4761 237.982 25.4761C237.717 25.4761 237.584 25.4761 237.452 25.6081C237.584 25.8721 237.717 26.2681 237.717 26.6641C237.584 27.5881 236.658 28.3801 235.07 28.3801ZM233.746 30.4921C233.746 30.4921 233.349 30.7561 233.349 31.2841C233.349 31.8121 233.878 32.0761 234.937 32.0761C236.128 32.0761 236.79 31.8121 236.79 31.1521C236.79 30.4921 235.996 30.4921 235.599 30.4921H233.746ZM236.128 26.2681C236.128 25.6081 235.731 25.3441 235.07 25.3441C234.408 25.3441 234.011 25.7401 234.011 26.2681C234.011 26.9281 234.408 27.1921 235.07 27.1921C235.731 27.3241 236.128 26.9281 236.128 26.2681Z" fill="white"/>
|
||||
<path d="M241.026 21.9121V25.4761C241.291 24.8161 241.688 24.4201 242.746 24.4201C243.673 24.4201 244.202 24.8161 244.599 25.2121C244.997 25.7401 244.997 26.4001 244.997 27.0601V30.7561H243.541V27.0601C243.541 26.6641 243.541 26.4001 243.276 26.0041C243.011 25.7401 242.746 25.6081 242.349 25.6081C241.291 25.6081 241.026 26.5321 241.026 26.9281V30.7561H239.57V21.9121H241.026Z" fill="white"/>
|
||||
<path d="M246.849 24.6839C246.849 24.6839 247.644 24.4199 248.835 24.4199C249.761 24.4199 250.556 24.5519 251.085 25.2119C251.482 25.7399 251.482 26.2679 251.482 26.9279V29.0399C251.482 29.3039 251.482 29.6999 252.011 29.6999C252.144 29.6999 252.276 29.6999 252.276 29.6999V30.7559C252.276 30.7559 251.879 30.8879 251.482 30.8879C250.688 30.8879 250.158 30.6239 250.158 29.8319C249.894 30.4919 249.232 31.0199 248.305 31.0199C247.114 31.0199 246.32 30.2279 246.32 29.1719C246.32 27.8519 247.379 27.1919 249.364 27.1919C249.761 27.1919 249.894 27.1919 249.894 27.1919C249.894 26.6639 249.894 26.3999 249.761 26.1359C249.629 25.8719 249.232 25.7399 248.703 25.7399C247.776 25.7399 246.982 26.0039 246.982 26.0039H246.849V24.6839ZM248.703 29.6999C249.1 29.6999 249.497 29.4359 249.761 29.1719C250.026 28.7759 250.026 28.3799 250.026 28.1159V27.9839H249.629C248.967 27.9839 247.776 27.9839 247.776 28.9079C247.776 29.3039 248.041 29.6999 248.703 29.6999Z" fill="white"/>
|
||||
<path d="M254.659 24.4201L254.923 25.4761C255.056 24.8161 255.585 24.2881 256.776 24.2881C257.835 24.2881 258.365 24.8161 258.629 25.4761C258.894 24.6841 259.556 24.2881 260.483 24.2881C261.409 24.2881 261.938 24.6841 262.203 25.0801C262.6 25.6081 262.6 26.2681 262.6 26.9281V30.6241H261.144V26.9281C261.144 26.5321 261.144 26.2681 261.012 25.8721C260.88 25.6081 260.615 25.4761 260.085 25.4761C259.027 25.4761 258.894 26.4001 258.894 26.7961V30.6241H257.438V26.9281C257.438 26.5321 257.438 26.2681 257.306 25.8721C257.174 25.6081 256.909 25.4761 256.379 25.4761C255.32 25.4761 255.188 26.4001 255.188 26.7961V30.6241H253.732V24.4201H254.659Z" fill="white"/>
|
||||
<path d="M34.1486 0.924072L38.6488 11.7481L43.0166 0.924072H46.3256V15.0481H43.8108V4.09207L39.5753 14.3881H37.3252L33.0897 4.09207V15.1801H30.8396V0.924072H34.1486Z" fill="white"/>
|
||||
<path d="M49.1052 5.01612C49.1052 5.01612 50.4288 4.62012 52.4142 4.62012C54.0025 4.62012 55.3261 4.88412 56.1203 5.94012C56.6497 6.73212 56.6497 7.78812 56.6497 8.71212V12.2761C56.6497 12.6721 56.6497 13.4641 57.5762 13.4641C57.8409 13.4641 58.1056 13.3321 58.1056 13.3321V15.0481C58.1056 15.0481 57.5762 15.1801 56.7821 15.1801C55.4585 15.1801 54.6643 14.7841 54.5319 13.3321C54.1349 14.5201 52.9436 15.1801 51.4877 15.1801C49.5023 15.1801 48.1787 13.9921 48.1787 12.0121C48.1787 9.76812 50.0317 8.71212 53.3407 8.71212C54.0025 8.71212 54.2672 8.71212 54.2672 8.71212C54.2672 7.78812 54.2672 7.39212 54.0025 6.99612C53.7378 6.60012 53.2084 6.33612 52.1495 6.33612C50.6935 6.33612 49.3699 6.86412 49.3699 6.86412H49.2376V5.01612H49.1052ZM52.1495 13.4641C52.9436 13.4641 53.6054 13.0681 53.8702 12.5401C54.2672 11.7481 54.2672 11.0881 54.2672 10.5601V10.4281H53.7378C52.6789 10.4281 50.6935 10.4281 50.6935 12.0121C50.6935 12.8041 51.223 13.4641 52.1495 13.4641Z" fill="white"/>
|
||||
<path d="M66.1795 6.99612H66.0472C66.0472 6.99612 64.7236 6.46812 63.5323 6.46812C62.4735 6.46812 61.944 6.86412 61.944 7.39212C61.944 8.97612 66.5766 8.58012 66.5766 12.1441C66.5766 14.3881 64.8559 15.3121 62.3411 15.3121C60.4881 15.3121 59.4292 14.7841 59.4292 14.7841V12.8041H59.5616C59.5616 12.8041 61.1499 13.4641 62.4735 13.4641C63.6647 13.4641 64.0618 13.0681 64.0618 12.4081C64.0618 10.5601 59.4292 11.0881 59.4292 7.65612C59.4292 5.80812 60.8851 4.62012 63.5323 4.62012C64.9883 4.62012 66.1795 5.01612 66.1795 5.01612V6.99612Z" fill="white"/>
|
||||
<path d="M74.9151 6.99612H74.7827C74.7827 6.99612 73.4592 6.46812 72.2679 6.46812C71.2091 6.46812 70.6796 6.86412 70.6796 7.39212C70.6796 8.97612 75.3122 8.58012 75.3122 12.1441C75.3122 14.3881 73.5915 15.3121 71.0767 15.3121C69.2237 15.3121 68.1648 14.7841 68.1648 14.7841V12.8041H68.2972C68.2972 12.8041 69.8855 13.4641 71.2091 13.4641C72.4003 13.4641 72.7974 13.0681 72.7974 12.4081C72.7974 10.5601 68.1648 11.0881 68.1648 7.65612C68.1648 5.80812 69.6207 4.62012 72.2679 4.62012C73.7239 4.62012 74.9151 5.01612 74.9151 5.01612V6.99612Z" fill="white"/>
|
||||
<path d="M77.5623 5.01612C77.5623 5.01612 78.8858 4.62012 80.8712 4.62012C82.4595 4.62012 83.7831 4.88412 84.5773 5.94012C85.1067 6.73212 85.1067 7.78812 85.1067 8.71212V12.2761C85.1067 12.6721 85.1067 13.4641 86.0332 13.4641C86.298 13.4641 86.5627 13.3321 86.5627 13.3321V15.0481C86.5627 15.0481 86.0332 15.1801 85.2391 15.1801C83.9155 15.1801 83.1213 14.7841 82.989 13.3321C82.5919 14.5201 81.4007 15.1801 79.9447 15.1801C77.9593 15.1801 76.6357 13.9921 76.6357 12.0121C76.6357 9.76812 78.4888 8.71212 81.7977 8.71212C82.4595 8.71212 82.7243 8.71212 82.7243 8.71212C82.7243 7.78812 82.7243 7.39212 82.4595 6.99612C82.1948 6.60012 81.6654 6.33612 80.6065 6.33612C79.1506 6.33612 77.827 6.86412 77.827 6.86412H77.6946V5.01612H77.5623ZM80.6065 13.4641C81.4007 13.4641 82.0625 13.0681 82.3272 12.5401C82.7243 11.8801 82.7243 11.0881 82.7243 10.6921V10.4281H82.1948C81.1359 10.4281 79.1506 10.4281 79.1506 12.0121C79.1506 12.8041 79.68 13.4641 80.6065 13.4641Z" fill="white"/>
|
||||
<path d="M95.0337 14.916C95.0337 14.916 94.1072 15.18 92.7836 15.18C89.3423 15.18 87.4893 13.2 87.4893 9.90004C87.4893 6.60004 89.2099 4.48804 92.6513 4.48804C93.9749 4.48804 94.9014 4.75204 94.9014 4.75204V6.73204H94.769C94.769 6.73204 93.8425 6.33604 92.7836 6.33604C90.9306 6.33604 90.0041 7.65604 90.0041 9.76804C90.0041 11.88 90.9306 13.2 92.7836 13.2C93.8425 13.2 94.769 12.804 94.769 12.804H94.9014V14.916H95.0337Z" fill="white"/>
|
||||
<path d="M99.4015 0.395996V6.336C99.7986 5.28 100.593 4.488 102.446 4.488C104.034 4.488 104.961 5.148 105.49 5.808C106.152 6.732 106.152 7.656 106.152 8.976V15.048H103.637V8.976C103.637 8.316 103.637 7.788 103.24 7.26C102.843 6.732 102.446 6.468 101.652 6.468C99.7986 6.468 99.5339 7.92 99.5339 8.712V15.048H97.019V0.395996H99.4015Z" fill="white"/>
|
||||
<path d="M115.549 15.048L115.152 13.332C114.888 14.52 113.829 15.312 112.108 15.312C110.652 15.312 109.726 14.784 109.196 13.992C108.534 13.2 108.534 12.144 108.534 10.824V4.75195H111.049V10.824C111.049 11.484 111.049 12.012 111.446 12.54C111.843 13.068 112.373 13.332 113.167 13.332C114.888 13.332 115.02 11.88 115.02 11.088V4.75195H117.535V15.048H115.549Z" fill="white"/>
|
||||
<path d="M126.271 6.99612H126.138C126.138 6.99612 124.815 6.46812 123.623 6.46812C122.565 6.46812 122.035 6.86412 122.035 7.39212C122.035 8.97612 126.668 8.58012 126.668 12.1441C126.668 14.3881 124.947 15.3121 122.432 15.3121C120.579 15.3121 119.52 14.7841 119.52 14.7841V12.8041H119.653C119.653 12.8041 121.241 13.4641 122.565 13.4641C123.756 13.4641 124.153 13.0681 124.153 12.4081C124.153 10.5601 119.52 11.0881 119.52 7.65612C119.52 5.80812 120.976 4.62012 123.623 4.62012C125.079 4.62012 126.271 5.01612 126.271 5.01612V6.99612Z" fill="white"/>
|
||||
<path d="M136.859 14.784C136.859 14.784 135.403 15.18 133.55 15.18C131.565 15.18 130.374 14.652 129.579 13.86C128.653 12.936 128.124 11.616 128.124 9.89996C128.124 6.46796 129.844 4.35596 133.021 4.35596C134.741 4.35596 136.065 5.01596 136.727 6.20396C137.389 7.39196 137.389 8.97596 137.389 10.56H130.638C130.638 12.276 131.697 13.332 133.683 13.332C135.139 13.332 136.595 12.804 136.595 12.804H136.727V14.784H136.859ZM134.874 8.97596C134.874 7.25996 134.344 6.20396 132.888 6.20396C131.433 6.20396 130.903 7.25996 130.638 8.97596H134.874Z" fill="white"/>
|
||||
<path d="M145.463 15.048C145.463 15.048 144.668 15.312 143.61 15.312C142.154 15.312 141.095 14.916 140.565 14.124C140.168 13.464 140.168 12.54 140.168 11.484V6.73198H138.315V4.88398H140.168V2.24398L142.683 1.58398V5.01598H145.595V6.86398H142.683V11.484C142.683 12.144 142.683 12.672 142.948 13.068C143.212 13.464 143.61 13.596 144.007 13.596C144.801 13.596 145.33 13.332 145.33 13.332H145.463V15.048Z" fill="white"/>
|
||||
<path d="M153.669 15.048C153.669 15.048 152.875 15.312 151.816 15.312C150.36 15.312 149.301 14.916 148.772 14.124C148.375 13.464 148.375 12.54 148.375 11.484V6.73198H146.521V4.88398H148.375V2.24398L150.889 1.58398V5.01598H153.801V6.86398H150.889V11.484C150.889 12.144 150.889 12.672 151.154 13.068C151.419 13.464 151.816 13.596 152.213 13.596C153.007 13.596 153.537 13.332 153.537 13.332H153.669V15.048Z" fill="white"/>
|
||||
<path d="M162.007 6.99612H161.875C161.875 6.99612 160.551 6.46812 159.36 6.46812C158.301 6.46812 157.772 6.86412 157.772 7.39212C157.772 8.97612 162.404 8.58012 162.404 12.1441C162.404 14.3881 160.684 15.3121 158.169 15.3121C156.316 15.3121 155.257 14.7841 155.257 14.7841V12.8041H155.389C155.389 12.8041 156.978 13.4641 158.301 13.4641C159.493 13.4641 159.89 13.0681 159.89 12.4081C159.89 10.5601 155.257 11.0881 155.257 7.65612C155.257 5.80812 156.713 4.62012 159.36 4.62012C160.816 4.62012 162.007 5.01612 162.007 5.01612V6.99612Z" fill="white"/>
|
||||
<path d="M180.405 7.65599V14.52C180.405 14.52 178.552 15.312 176.038 15.312C171.67 15.312 168.625 13.068 168.625 8.18399C168.625 3.16799 171.802 0.791992 176.302 0.791992C178.42 0.791992 179.876 1.31999 179.876 1.31999V3.29999H179.744C179.744 3.29999 178.288 2.63999 176.435 2.63999C173.258 2.63999 171.405 4.35599 171.405 7.91999C171.405 11.616 173.39 13.332 176.038 13.332C177.096 13.332 177.891 13.068 177.891 13.068V9.50399H175.376V7.65599H180.405Z" fill="white"/>
|
||||
<path d="M190.994 14.784C190.994 14.784 189.538 15.18 187.685 15.18C185.7 15.18 184.508 14.652 183.714 13.86C182.788 12.936 182.258 11.616 182.258 9.89996C182.258 6.46796 183.979 4.35596 187.156 4.35596C188.876 4.35596 190.2 5.01596 190.862 6.20396C191.523 7.39196 191.523 8.97596 191.523 10.56H184.773C184.773 12.276 185.832 13.332 187.817 13.332C189.273 13.332 190.729 12.804 190.729 12.804H190.862V14.784H190.994ZM189.141 8.97596C189.141 7.25996 188.612 6.20396 187.156 6.20396C185.7 6.20396 185.17 7.25996 184.905 8.97596H189.141Z" fill="white"/>
|
||||
<path d="M195.627 4.75204L196.024 6.46804C196.421 5.28004 197.347 4.48804 199.068 4.48804C200.656 4.48804 201.583 5.14804 202.112 5.80804C202.774 6.73204 202.774 7.65604 202.774 8.97604V15.048H200.259V8.97604C200.259 8.31604 200.259 7.78804 199.862 7.26004C199.465 6.73204 199.068 6.46804 198.274 6.46804C196.421 6.46804 196.156 7.92004 196.156 8.71204V15.048H193.641V4.75204H195.627Z" fill="white"/>
|
||||
<path d="M213.363 14.784C213.363 14.784 211.907 15.18 210.054 15.18C208.068 15.18 206.877 14.652 206.083 13.86C205.156 12.936 204.627 11.616 204.627 9.89996C204.627 6.46796 206.348 4.35596 209.524 4.35596C211.245 4.35596 212.568 5.01596 213.23 6.20396C213.892 7.39196 213.892 8.97596 213.892 10.56H207.142C207.142 12.276 208.201 13.332 210.186 13.332C211.642 13.332 213.098 12.804 213.098 12.804H213.23V14.784H213.363ZM211.377 8.97596C211.377 7.25996 210.848 6.20396 209.392 6.20396C207.936 6.20396 207.406 7.25996 207.142 8.97596H211.377Z" fill="white"/>
|
||||
<path d="M221.834 6.99612C221.834 6.99612 221.304 6.86412 220.775 6.86412C218.525 6.86412 218.525 8.44812 218.525 9.37212V15.1801H216.01V4.75212H217.863L218.392 6.73212C218.79 5.01612 220.113 4.62012 221.569 4.62012H221.834V6.99612Z" fill="white"/>
|
||||
<path d="M223.554 5.01612C223.554 5.01612 224.878 4.62012 226.863 4.62012C228.452 4.62012 229.775 4.88412 230.569 5.94012C231.099 6.73212 231.099 7.78812 231.099 8.71212V12.2761C231.099 12.6721 231.099 13.4641 232.025 13.4641C232.29 13.4641 232.555 13.3321 232.555 13.3321V15.0481C232.555 15.0481 232.025 15.1801 231.231 15.1801C229.908 15.1801 229.114 14.7841 228.981 13.3321C228.584 14.5201 227.393 15.1801 225.937 15.1801C223.952 15.1801 222.628 13.9921 222.628 12.0121C222.628 9.76812 224.481 8.71212 227.79 8.71212C228.452 8.71212 228.716 8.71212 228.716 8.71212C228.716 7.78812 228.716 7.39212 228.452 6.99612C228.187 6.60012 227.658 6.33612 226.599 6.33612C225.143 6.33612 223.819 6.86412 223.819 6.86412H223.687V5.01612H223.554ZM226.466 13.4641C227.261 13.4641 227.922 13.0681 228.187 12.5401C228.584 11.8801 228.584 11.0881 228.584 10.6921V10.4281H228.055C226.996 10.4281 225.01 10.4281 225.01 12.0121C225.01 12.8041 225.54 13.4641 226.466 13.4641Z" fill="white"/>
|
||||
<path d="M236.526 0.395996V4.62V15.048H234.011V0.395996H236.526Z" fill="white"/>
|
||||
<path d="M246.585 0.924072V6.73207H252.806V0.924072H255.321V15.0481H252.806V8.71207H246.585V15.0481H244.07V0.924072H246.585Z" fill="white"/>
|
||||
<path d="M262.733 15.312C259.556 15.312 257.703 13.464 257.703 9.90004C257.703 6.33604 259.556 4.48804 262.865 4.48804C266.042 4.48804 267.895 6.33604 267.895 9.90004C267.762 13.464 265.909 15.312 262.733 15.312ZM265.115 9.90004C265.115 7.52404 264.321 6.20404 262.733 6.20404C261.012 6.20404 260.35 7.65604 260.35 9.90004C260.35 12.276 261.144 13.596 262.733 13.596C264.453 13.596 265.115 12.144 265.115 9.90004Z" fill="white"/>
|
||||
<path d="M275.969 6.99612H275.836C275.836 6.99612 274.513 6.46812 273.321 6.46812C272.263 6.46812 271.733 6.86412 271.733 7.39212C271.733 8.97612 276.366 8.58012 276.366 12.1441C276.366 14.3881 274.645 15.3121 272.13 15.3121C270.277 15.3121 269.218 14.7841 269.218 14.7841V12.8041H269.351C269.351 12.8041 270.939 13.4641 272.263 13.4641C273.454 13.4641 273.851 13.0681 273.851 12.4081C273.851 10.5601 269.218 11.0881 269.218 7.65612C269.218 5.80812 270.674 4.62012 273.321 4.62012C274.777 4.62012 275.969 5.01612 275.969 5.01612V6.99612Z" fill="white"/>
|
||||
<path d="M278.483 18.744V4.75204H280.469L280.998 6.46804C281.263 5.28004 282.454 4.48804 284.175 4.48804C287.087 4.48804 288.543 6.73204 288.543 9.90004C288.543 13.068 286.954 15.312 283.91 15.312C282.454 15.312 281.395 14.784 280.998 13.596V18.876H278.483V18.744ZM285.763 9.90004C285.763 7.78804 285.101 6.33604 283.381 6.33604C281.66 6.33604 280.998 7.78804 280.998 9.90004C280.998 12.012 281.66 13.464 283.381 13.464C285.101 13.464 285.763 12.012 285.763 9.90004Z" fill="white"/>
|
||||
<path d="M293.175 1.848C293.175 2.64 292.514 3.3 291.719 3.3C290.793 3.3 290.263 2.64 290.263 1.848C290.263 1.056 290.925 0.395996 291.719 0.395996C292.646 0.395996 293.175 0.923996 293.175 1.848ZM292.911 4.752V15.048H290.396V6.468V4.752H292.911Z" fill="white"/>
|
||||
<path d="M301.779 15.048C301.779 15.048 300.985 15.312 299.926 15.312C298.47 15.312 297.411 14.916 296.881 14.124C296.484 13.464 296.484 12.54 296.484 11.484V6.73198H294.631V4.88398H296.484V2.24398L298.999 1.58398V5.01598H301.911V6.86398H298.999V11.484C298.999 12.144 298.999 12.672 299.264 13.068C299.529 13.464 299.926 13.596 300.323 13.596C301.117 13.596 301.646 13.332 301.646 13.332H301.779V15.048Z" fill="white"/>
|
||||
<path d="M304.029 5.01612C304.029 5.01612 305.352 4.62012 307.338 4.62012C308.926 4.62012 310.25 4.88412 311.044 5.94012C311.573 6.73212 311.573 7.78812 311.573 8.71212V12.2761C311.573 12.6721 311.573 13.4641 312.5 13.4641C312.765 13.4641 313.029 13.3321 313.029 13.3321V15.0481C313.029 15.0481 312.5 15.1801 311.706 15.1801C310.382 15.1801 309.588 14.7841 309.456 13.3321C309.058 14.5201 307.867 15.1801 306.411 15.1801C304.426 15.1801 303.102 13.9921 303.102 12.0121C303.102 9.76812 304.955 8.71212 308.264 8.71212C308.926 8.71212 309.191 8.71212 309.191 8.71212C309.191 7.78812 309.191 7.39212 308.926 6.99612C308.661 6.60012 308.132 6.33612 307.073 6.33612C305.617 6.33612 304.294 6.86412 304.294 6.86412H304.161V5.01612H304.029ZM307.073 13.4641C307.867 13.4641 308.529 13.0681 308.794 12.5401C309.191 11.8801 309.191 11.0881 309.191 10.6921V10.4281H308.661C307.602 10.4281 305.617 10.4281 305.617 12.0121C305.617 12.8041 306.014 13.4641 307.073 13.4641Z" fill="white"/>
|
||||
<path d="M317 0.395996V4.62V15.18H314.485V0.395996H317Z" fill="white"/>
|
||||
<path d="M3.30887 11.8799H0.396973V16.8959V21.5159H3.30887V11.8799Z" fill="white"/>
|
||||
<path d="M6.75024 11.8799V16.8959V21.5159H9.66214V11.8799H6.75024Z" fill="white"/>
|
||||
<path d="M13.1035 11.8799V16.8959V21.5159H16.1478V11.8799H13.1035Z" fill="white"/>
|
||||
<path d="M0.396973 10.1641H11.3828H22.5009V7.39209H0.396973V10.1641Z" fill="white"/>
|
||||
<path d="M11.3828 0L0.396973 3.696V6.468L11.3828 2.772L22.5009 6.468V3.696L11.3828 0Z" fill="white"/>
|
||||
<path d="M22.5009 21.252C22.5009 21.384 21.3097 23.232 14.2947 23.232H8.60324C1.32349 23.232 0.529332 24.288 0.396973 24.288V27.06C0.529332 26.928 1.32349 26.004 8.60324 26.004H14.2947C21.3097 26.004 22.5009 24.156 22.5009 24.024V21.252Z" fill="white"/>
|
||||
<path d="M22.5009 25.8721C22.5009 26.0041 21.3097 27.8521 14.2947 27.8521H8.60324C1.32349 27.8521 0.529332 28.9081 0.396973 28.9081V31.6801C0.529332 31.6801 1.32349 30.7561 8.60324 30.6241H14.2947C21.3097 30.6241 22.5009 28.7761 22.5009 28.6441V25.8721Z" fill="white"/>
|
||||
<path d="M19.5891 11.8799V20.9879C21.9716 20.4599 22.501 19.5359 22.501 19.4039V11.8799H19.5891Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5_1857">
|
||||
<rect width="317" height="33" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 31 KiB |
BIN
platform/docs/static/img/theming-color-roles.png
vendored
Normal file
BIN
platform/docs/static/img/theming-color-roles.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user