docs(site): add ui-next component documentation and upgrade Docusaurus (#6102)
This commit is contained in:
1 parent
03b180f185
commit
ac45a9497f
112 files changed
+9665
-6674
No files matched your search
+122
-202
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user