Refactor application structure and simplify implementation

This commit is contained in:
2026-07-22 00:08:09 +03:00
parent dbba3806cc
commit 90b2eb507c
74 changed files with 11362 additions and 6814 deletions
+56 -49
View File
@@ -1,4 +1,4 @@
import { MoreHorizontal } from 'lucide-react';
import { MoreHorizontal } from "lucide-react";
import {
useEffect,
useId,
@@ -6,9 +6,9 @@ import {
useRef,
useState,
type CSSProperties,
} from 'react';
import { createPortal } from 'react-dom';
import { IconButton } from './IconButton';
} from "react";
import { createPortal } from "react-dom";
import { IconButton } from "./IconButton";
export interface ActionMenuItem {
label: string;
@@ -29,7 +29,7 @@ interface ActionMenuPosition {
top: number;
left: number;
width: number;
placement: 'top' | 'bottom';
placement: "top" | "bottom";
}
const MENU_WIDTH = 190;
@@ -50,7 +50,7 @@ export function ActionMenu({
top: 0,
left: 0,
width: MENU_WIDTH,
placement: 'bottom',
placement: "bottom",
});
useEffect(() => {
@@ -65,22 +65,28 @@ export function ActionMenu({
if (!trigger) return;
const rect = trigger.getBoundingClientRect();
const width = Math.min(MENU_WIDTH, Math.max(180, window.innerWidth - VIEWPORT_MARGIN * 2));
const width = Math.min(
MENU_WIDTH,
Math.max(180, window.innerWidth - VIEWPORT_MARGIN * 2),
);
const popoverHeight = popoverRef.current?.offsetHeight ?? 0;
const left = Math.max(
VIEWPORT_MARGIN,
Math.min(rect.right - width, window.innerWidth - width - VIEWPORT_MARGIN),
Math.min(
rect.right - width,
window.innerWidth - width - VIEWPORT_MARGIN,
),
);
let top = rect.bottom + MENU_OFFSET;
let placement: ActionMenuPosition['placement'] = 'bottom';
let placement: ActionMenuPosition["placement"] = "bottom";
if (
popoverHeight
&& top + popoverHeight > window.innerHeight - VIEWPORT_MARGIN
&& rect.top > popoverHeight + VIEWPORT_MARGIN + MENU_OFFSET
popoverHeight &&
top + popoverHeight > window.innerHeight - VIEWPORT_MARGIN &&
rect.top > popoverHeight + VIEWPORT_MARGIN + MENU_OFFSET
) {
top = rect.top - popoverHeight - MENU_OFFSET;
placement = 'top';
placement = "top";
}
const maxTop = popoverHeight
@@ -97,13 +103,13 @@ export function ActionMenu({
updatePosition();
const frame = window.requestAnimationFrame(updatePosition);
window.addEventListener('resize', updatePosition);
window.addEventListener('scroll', updatePosition, true);
window.addEventListener("resize", updatePosition);
window.addEventListener("scroll", updatePosition, true);
return () => {
window.cancelAnimationFrame(frame);
window.removeEventListener('resize', updatePosition);
window.removeEventListener('scroll', updatePosition, true);
window.removeEventListener("resize", updatePosition);
window.removeEventListener("scroll", updatePosition, true);
};
}, [open]);
@@ -118,16 +124,16 @@ export function ActionMenu({
};
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key !== 'Escape') return;
if (event.key !== "Escape") return;
onOpenChange(false);
};
document.addEventListener('pointerdown', closeOnOutsidePointer);
document.addEventListener('keydown', closeOnEscape);
document.addEventListener("pointerdown", closeOnOutsidePointer);
document.addEventListener("keydown", closeOnEscape);
return () => {
document.removeEventListener('pointerdown', closeOnOutsidePointer);
document.removeEventListener('keydown', closeOnEscape);
document.removeEventListener("pointerdown", closeOnOutsidePointer);
document.removeEventListener("keydown", closeOnEscape);
};
}, [onOpenChange, open]);
@@ -148,34 +154,35 @@ export function ActionMenu({
aria-expanded={open}
aria-haspopup="menu"
/>
{open && typeof document !== 'undefined' ? createPortal(
<div
className="ui-action-menu-popover"
data-placement={position.placement}
id={menuId}
ref={popoverRef}
role="menu"
style={popoverStyle}
>
{items.map((item) => (
<button
type="button"
role="menuitem"
className={item.danger ? 'is-danger' : ''}
onClick={() => {
onOpenChange(false);
item.onClick();
}}
disabled={item.disabled}
key={item.label}
{open && typeof document !== "undefined"
? createPortal(
<div
className="ui-action-menu-popover"
data-placement={position.placement}
id={menuId}
ref={popoverRef}
role="menu"
style={popoverStyle}
>
{item.label}
</button>
))}
</div>,
document.body,
) : null}
{items.map((item) => (
<button
type="button"
role="menuitem"
className={item.danger ? "is-danger" : ""}
onClick={() => {
onOpenChange(false);
item.onClick();
}}
disabled={item.disabled}
key={item.label}
>
{item.label}
</button>
))}
</div>,
document.body,
)
: null}
</div>
);
}
+1 -1
View File
@@ -3,7 +3,7 @@ export interface BusyRingProps {
}
export function BusyRing({ className }: BusyRingProps) {
const classes = ['ui-busy-ring', className ?? ''].filter(Boolean).join(' ');
const classes = ["ui-busy-ring", className ?? ""].filter(Boolean).join(" ");
return (
<span className={classes} aria-hidden="true">
+23 -14
View File
@@ -1,8 +1,8 @@
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { BusyRing } from './BusyRing';
import type { ButtonHTMLAttributes, ReactNode } from "react";
import { BusyRing } from "./BusyRing";
export type ButtonVariant = 'primary' | 'neutral' | 'add' | 'danger';
export type ButtonSize = 'sm' | 'md' | 'lg';
export type ButtonVariant = "primary" | "neutral" | "add" | "danger";
export type ButtonSize = "sm" | "md" | "lg";
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
@@ -14,8 +14,8 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
}
export function Button({
variant = 'neutral',
size = 'md',
variant = "neutral",
size = "md",
loading = false,
loadingLabel,
leftIcon,
@@ -26,12 +26,14 @@ export function Button({
...props
}: ButtonProps) {
const classes = [
'ui-button',
"ui-button",
`ui-button--${variant}`,
`ui-button--${size}`,
loading ? 'is-loading' : '',
className ?? '',
].filter(Boolean).join(' ');
loading ? "is-loading" : "",
className ?? "",
]
.filter(Boolean)
.join(" ");
return (
<button
@@ -42,11 +44,18 @@ export function Button({
>
{loading ? <BusyRing /> : null}
{!loading && leftIcon ? (
<span className="ui-button-icon" aria-hidden="true">{leftIcon}</span>
<span className="ui-button-icon" aria-hidden="true">
{leftIcon}
</span>
) : null}
<span className="ui-button-label">
{loading && loadingLabel ? loadingLabel : children}
</span>
{!loading && rightIcon ? (
<span className="ui-button-icon" aria-hidden="true">
{rightIcon}
</span>
) : null}
<span className="ui-button-label">{loading && loadingLabel ? loadingLabel : children}</span>
{!loading && rightIcon ? <span className="ui-button-icon" aria-hidden="true">{rightIcon}</span> : null}
</button>
);
}
+45 -31
View File
@@ -8,12 +8,15 @@ import {
type CSSProperties,
type MouseEvent,
type ReactNode,
} from 'react';
import { createPortal } from 'react-dom';
} from "react";
import { createPortal } from "react-dom";
export type DetailsPopoverAlign = 'start' | 'center' | 'end';
export type DetailsPopoverAlign = "start" | "center" | "end";
export interface DetailsPopoverProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'title'> {
export interface DetailsPopoverProps extends Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
"title"
> {
details: string | string[];
children: ReactNode;
popoverLabel?: string;
@@ -26,7 +29,7 @@ interface DetailsPopoverPosition {
left: number;
width: number;
arrowLeft: number;
placement: 'top' | 'bottom';
placement: "top" | "bottom";
}
const VIEWPORT_MARGIN = 12;
@@ -35,8 +38,8 @@ export function DetailsPopover({
details,
children,
className,
popoverLabel = 'Детали',
align = 'start',
popoverLabel = "Детали",
align = "start",
maxWidth = 360,
disabled,
onClick,
@@ -51,12 +54,14 @@ export function DetailsPopover({
left: 0,
width: Math.min(maxWidth, 360),
arrowLeft: 24,
placement: 'bottom',
placement: "bottom",
});
const detailLines = Array.isArray(details)
? details.filter(Boolean)
: [details].filter(Boolean);
const classes = ['ui-details-popover-trigger', className ?? ''].filter(Boolean).join(' ');
const classes = ["ui-details-popover-trigger", className ?? ""]
.filter(Boolean)
.join(" ");
useEffect(() => {
if (disabled && open) setOpen(false);
@@ -70,26 +75,35 @@ export function DetailsPopover({
if (!trigger) return;
const rect = trigger.getBoundingClientRect();
const width = Math.min(maxWidth, Math.max(220, window.innerWidth - VIEWPORT_MARGIN * 2));
const width = Math.min(
maxWidth,
Math.max(220, window.innerWidth - VIEWPORT_MARGIN * 2),
);
let left = rect.left;
if (align === 'center') left = rect.left + rect.width / 2 - width / 2;
if (align === 'end') left = rect.right - width;
if (align === "center") left = rect.left + rect.width / 2 - width / 2;
if (align === "end") left = rect.right - width;
left = Math.max(VIEWPORT_MARGIN, Math.min(left, window.innerWidth - width - VIEWPORT_MARGIN));
left = Math.max(
VIEWPORT_MARGIN,
Math.min(left, window.innerWidth - width - VIEWPORT_MARGIN),
);
const popoverHeight = popoverRef.current?.offsetHeight ?? 0;
let top = rect.bottom + 8;
let placement: DetailsPopoverPosition['placement'] = 'bottom';
let placement: DetailsPopoverPosition["placement"] = "bottom";
if (
popoverHeight
&& top + popoverHeight > window.innerHeight - VIEWPORT_MARGIN
&& rect.top > popoverHeight + VIEWPORT_MARGIN + 8
popoverHeight &&
top + popoverHeight > window.innerHeight - VIEWPORT_MARGIN &&
rect.top > popoverHeight + VIEWPORT_MARGIN + 8
) {
top = rect.top - popoverHeight - 8;
placement = 'top';
placement = "top";
} else if (popoverHeight) {
top = Math.min(top, window.innerHeight - popoverHeight - VIEWPORT_MARGIN);
top = Math.min(
top,
window.innerHeight - popoverHeight - VIEWPORT_MARGIN,
);
}
const arrowLeft = Math.max(
@@ -108,13 +122,13 @@ export function DetailsPopover({
updatePosition();
const frame = window.requestAnimationFrame(updatePosition);
window.addEventListener('resize', updatePosition);
window.addEventListener('scroll', updatePosition, true);
window.addEventListener("resize", updatePosition);
window.addEventListener("scroll", updatePosition, true);
return () => {
window.cancelAnimationFrame(frame);
window.removeEventListener('resize', updatePosition);
window.removeEventListener('scroll', updatePosition, true);
window.removeEventListener("resize", updatePosition);
window.removeEventListener("scroll", updatePosition, true);
};
}, [align, maxWidth, open]);
@@ -129,17 +143,17 @@ export function DetailsPopover({
};
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key !== 'Escape') return;
if (event.key !== "Escape") return;
setOpen(false);
triggerRef.current?.focus();
};
document.addEventListener('pointerdown', closeOnOutsidePointer);
document.addEventListener('keydown', closeOnEscape);
document.addEventListener("pointerdown", closeOnOutsidePointer);
document.addEventListener("keydown", closeOnEscape);
return () => {
document.removeEventListener('pointerdown', closeOnOutsidePointer);
document.removeEventListener('keydown', closeOnEscape);
document.removeEventListener("pointerdown", closeOnOutsidePointer);
document.removeEventListener("keydown", closeOnEscape);
};
}, [open]);
@@ -152,7 +166,7 @@ export function DetailsPopover({
top: position.top,
left: position.left,
width: position.width,
'--details-popover-arrow-left': `${position.arrowLeft}px`,
"--details-popover-arrow-left": `${position.arrowLeft}px`,
} as CSSProperties;
return (
@@ -160,7 +174,7 @@ export function DetailsPopover({
<button
{...props}
ref={triggerRef}
type={props.type ?? 'button'}
type={props.type ?? "button"}
className={classes}
aria-controls={open ? detailsId : undefined}
aria-expanded={open}
@@ -170,7 +184,7 @@ export function DetailsPopover({
>
{children}
</button>
{open && detailLines.length && typeof document !== 'undefined'
{open && detailLines.length && typeof document !== "undefined"
? createPortal(
<div
ref={popoverRef}
+11 -5
View File
@@ -1,4 +1,4 @@
import type { InputHTMLAttributes, ReactNode } from 'react';
import type { InputHTMLAttributes, ReactNode } from "react";
export interface FieldProps extends InputHTMLAttributes<HTMLInputElement> {
label: string;
@@ -16,11 +16,11 @@ export function Field({
id,
...props
}: FieldProps) {
const inputId = id ?? `field-${label.toLowerCase().replace(/\s+/g, '-')}`;
const inputId = id ?? `field-${label.toLowerCase().replace(/\s+/g, "-")}`;
const helpId = `${inputId}-help`;
return (
<label className={`ui-field ${className ?? ''}`.trim()} htmlFor={inputId}>
<label className={`ui-field ${className ?? ""}`.trim()} htmlFor={inputId}>
<span className="ui-field-label">{label}</span>
<div className="ui-field-row">
<input
@@ -31,8 +31,14 @@ export function Field({
/>
{action}
</div>
{error || hint ? <span id={helpId} className={`ui-field-help ${error ? 'is-error' : ''}`.trim()}>{error ?? hint}</span> : null}
{error || hint ? (
<span
id={helpId}
className={`ui-field-help ${error ? "is-error" : ""}`.trim()}
>
{error ?? hint}
</span>
) : null}
</label>
);
}
+5 -3
View File
@@ -1,4 +1,4 @@
import type { HTMLAttributes, ReactNode } from 'react';
import type { HTMLAttributes, ReactNode } from "react";
export interface HoverDetailsProps extends HTMLAttributes<HTMLSpanElement> {
details: string | string[];
@@ -12,9 +12,11 @@ export function HoverDetails({
...props
}: HoverDetailsProps) {
const detailText = Array.isArray(details)
? details.filter(Boolean).join('\n')
? details.filter(Boolean).join("\n")
: details;
const classes = ['ui-hover-details', className ?? ''].filter(Boolean).join(' ');
const classes = ["ui-hover-details", className ?? ""]
.filter(Boolean)
.join(" ");
return (
<span
+16 -12
View File
@@ -1,9 +1,12 @@
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { BusyRing } from './BusyRing';
import type { ButtonHTMLAttributes, ReactNode } from "react";
import { BusyRing } from "./BusyRing";
export type IconButtonVariant = 'neutral' | 'add' | 'danger';
export type IconButtonVariant = "neutral" | "add" | "danger";
export interface IconButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'title'> {
export interface IconButtonProps extends Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
"title"
> {
label: string;
icon: ReactNode;
variant?: IconButtonVariant;
@@ -14,25 +17,27 @@ export interface IconButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonEle
export function IconButton({
label,
icon,
variant = 'neutral',
variant = "neutral",
loading = false,
tooltip,
className,
disabled,
...props
}: IconButtonProps) {
const tooltipText = tooltip === '' ? undefined : tooltip ?? label;
const tooltipText = tooltip === "" ? undefined : (tooltip ?? label);
const classes = [
'ui-icon-button',
"ui-icon-button",
`ui-icon-button--${variant}`,
loading ? 'is-loading' : '',
className ?? '',
].filter(Boolean).join(' ');
loading ? "is-loading" : "",
className ?? "",
]
.filter(Boolean)
.join(" ");
return (
<button
{...props}
type={props.type ?? 'button'}
type={props.type ?? "button"}
className={classes}
aria-label={label}
data-tooltip={tooltipText}
@@ -44,4 +49,3 @@ export function IconButton({
</button>
);
}
+40 -21
View File
@@ -1,8 +1,8 @@
import { Button } from './Button';
import { Button } from "./Button";
export interface LogDockEntry {
id: string;
kind: 'success' | 'error' | 'info';
kind: "success" | "error" | "info";
title: string;
text: string;
at: number;
@@ -18,7 +18,10 @@ export interface LogDockProps {
function isNativePreviewError(entry: LogDockEntry | null) {
if (!entry) return false;
return entry.text.includes("reading 'invoke'") || entry.text.includes('undefined (reading');
return (
entry.text.includes("reading 'invoke'") ||
entry.text.includes("undefined (reading")
);
}
function displayEntry(entry: LogDockEntry | null) {
@@ -26,8 +29,8 @@ function displayEntry(entry: LogDockEntry | null) {
if (!isNativePreviewError(entry)) return entry;
return {
...entry,
title: 'Desktop-команды недоступны',
text: 'Запусти клиент через Tauri, чтобы управлять службами и применять конфиг.',
title: "Desktop-команды недоступны",
text: "Запусти клиент через Tauri, чтобы управлять службами и применять конфиг.",
};
}
@@ -41,8 +44,11 @@ export function LogDock({
const current = displayEntry(activeEntry);
return (
<footer className={`log-dock ${current?.kind ?? 'idle'}`} aria-live="polite">
<div className={`log-current ${current ? 'visible' : 'hidden'}`}>
<footer
className={`log-dock ${current?.kind ?? "idle"}`}
aria-live="polite"
>
<div className={`log-current ${current ? "visible" : "hidden"}`}>
{current ? (
<>
<strong>{current.title}</strong>
@@ -52,24 +58,37 @@ export function LogDock({
<span className="log-muted">Журнал событий</span>
)}
</div>
<Button type="button" variant="neutral" size="sm" className="log-toggle" onClick={onToggle}>
{open ? 'Скрыть' : 'Посмотреть'} <span className="log-count">{entries.length}</span>
<Button
type="button"
variant="neutral"
size="sm"
className="log-toggle"
onClick={onToggle}
>
{open ? "Скрыть" : "Посмотреть"}{" "}
<span className="log-count">{entries.length}</span>
</Button>
{open ? (
<div className="log-history">
{entries.length ? entries.map((entry) => {
const friendly = displayEntry(entry);
return (
<div className={`log-history-row ${entry.kind}`} key={entry.id}>
<time>{formatTime(entry.at)}</time>
<div>
<strong>{friendly?.title ?? entry.title}</strong>
<span>{friendly?.text ?? entry.text}</span>
{isNativePreviewError(entry) ? <span className="log-raw-detail">Детали: {entry.text}</span> : null}
{entries.length ? (
entries.map((entry) => {
const friendly = displayEntry(entry);
return (
<div className={`log-history-row ${entry.kind}`} key={entry.id}>
<time>{formatTime(entry.at)}</time>
<div>
<strong>{friendly?.title ?? entry.title}</strong>
<span>{friendly?.text ?? entry.text}</span>
{isNativePreviewError(entry) ? (
<span className="log-raw-detail">
Детали: {entry.text}
</span>
) : null}
</div>
</div>
</div>
);
}) : (
);
})
) : (
<div className="log-history-row">
<time>--:--:--</time>
<span>Событий пока нет.</span>
+16 -12
View File
@@ -1,8 +1,9 @@
import type { ReactNode } from 'react';
import { Button, type ButtonVariant } from './Button';
import { ActionMenu, type ActionMenuItem } from './ActionMenu';
import type { ReactNode } from "react";
import { Button, type ButtonVariant } from "./Button";
import { ActionMenu, type ActionMenuItem } from "./ActionMenu";
export type ServiceControlState = 'checking' | 'missing' | 'installed' | 'running' | 'stopped' | 'error';
export type ServiceControlState =
"checking" | "missing" | "installed" | "running" | "stopped" | "error";
export interface ServicePrimaryAction {
label: string;
@@ -25,7 +26,7 @@ export interface ServiceControlRowProps {
items: ActionMenuItem[];
disabled?: boolean;
};
visualState?: 'working' | 'settling' | null;
visualState?: "working" | "settling" | null;
className?: string;
inlineActions?: ReactNode;
children?: ReactNode;
@@ -43,11 +44,13 @@ export function ServiceControlRow({
children,
}: ServiceControlRowProps) {
const classes = [
'ui-service-row',
"ui-service-row",
`ui-service-row--${state}`,
visualState ? `ui-service-row--${visualState}` : '',
className ?? '',
].filter(Boolean).join(' ');
visualState ? `ui-service-row--${visualState}` : "",
className ?? "",
]
.filter(Boolean)
.join(" ");
return (
<div className={classes}>
@@ -61,7 +64,9 @@ export function ServiceControlRow({
<div className="ui-service-text">
<div className="ui-service-title-line">
<strong>{title}</strong>
{inlineActions ? <div className="ui-service-inline-actions">{inlineActions}</div> : null}
{inlineActions ? (
<div className="ui-service-inline-actions">{inlineActions}</div>
) : null}
</div>
<span>{detail}</span>
</div>
@@ -69,7 +74,7 @@ export function ServiceControlRow({
{primaryAction ? (
<Button
type="button"
variant={primaryAction.variant ?? 'neutral'}
variant={primaryAction.variant ?? "neutral"}
onClick={primaryAction.onClick}
disabled={primaryAction.disabled}
loading={primaryAction.loading}
@@ -92,4 +97,3 @@ export function ServiceControlRow({
</div>
);
}
+5 -6
View File
@@ -1,21 +1,20 @@
import { BusyRing } from './BusyRing';
import { BusyRing } from "./BusyRing";
export type StatusPillTone = 'ok' | 'warning' | 'error' | 'checking' | 'muted';
export type StatusPillTone = "ok" | "warning" | "error" | "checking" | "muted";
export interface StatusPillProps {
tone?: StatusPillTone;
children: string;
}
export function StatusPill({ tone = 'muted', children }: StatusPillProps) {
export function StatusPill({ tone = "muted", children }: StatusPillProps) {
return (
<span
className={`ui-status-pill ui-status-pill--${tone}`}
aria-busy={tone === 'checking' || undefined}
aria-busy={tone === "checking" || undefined}
>
{tone === 'checking' ? <BusyRing /> : null}
{tone === "checking" ? <BusyRing /> : null}
{children}
</span>
);
}
+9 -8
View File
@@ -1,4 +1,4 @@
import { useRef, type KeyboardEvent } from 'react';
import { useRef, type KeyboardEvent } from "react";
export interface TabItem<T extends string> {
id: T;
@@ -30,24 +30,26 @@ export function Tabs<T extends string>({
}
function handleKeyDown(event: KeyboardEvent<HTMLButtonElement>, id: T) {
if (event.key === 'ArrowRight') {
if (event.key === "ArrowRight") {
event.preventDefault();
moveFocus(id, 1);
} else if (event.key === 'ArrowLeft') {
} else if (event.key === "ArrowLeft") {
event.preventDefault();
moveFocus(id, -1);
} else if (event.key === 'Home') {
} else if (event.key === "Home") {
event.preventDefault();
const first = items[0];
if (!first) return;
onChange(first.id);
window.requestAnimationFrame(() => refs.current[0]?.focus());
} else if (event.key === 'End') {
} else if (event.key === "End") {
event.preventDefault();
const last = items[items.length - 1];
if (!last) return;
onChange(last.id);
window.requestAnimationFrame(() => refs.current[items.length - 1]?.focus());
window.requestAnimationFrame(() =>
refs.current[items.length - 1]?.focus(),
);
}
}
@@ -63,7 +65,7 @@ export function Tabs<T extends string>({
aria-controls={`panel-${item.id}`}
aria-selected={active}
tabIndex={active ? 0 : -1}
className={`ui-tab ${active ? 'is-active' : ''}`.trim()}
className={`ui-tab ${active ? "is-active" : ""}`.trim()}
key={item.id}
ref={(node) => {
refs.current[index] = node;
@@ -78,4 +80,3 @@ export function Tabs<T extends string>({
</div>
);
}
+28 -23
View File
@@ -1,23 +1,28 @@
export { ActionMenu } from './ActionMenu';
export type { ActionMenuItem } from './ActionMenu';
export { BusyRing } from './BusyRing';
export type { BusyRingProps } from './BusyRing';
export { Button } from './Button';
export type { ButtonProps, ButtonSize, ButtonVariant } from './Button';
export { DetailsPopover } from './DetailsPopover';
export type { DetailsPopoverAlign, DetailsPopoverProps } from './DetailsPopover';
export { Field } from './Field';
export type { FieldProps } from './Field';
export { HoverDetails } from './HoverDetails';
export type { HoverDetailsProps } from './HoverDetails';
export { IconButton } from './IconButton';
export type { IconButtonProps, IconButtonVariant } from './IconButton';
export { LogDock } from './LogDock';
export type { LogDockEntry, LogDockProps } from './LogDock';
export { ServiceControlRow } from './ServiceControlRow';
export type { ServiceControlRowProps, ServiceControlState } from './ServiceControlRow';
export { StatusPill } from './StatusPill';
export type { StatusPillProps, StatusPillTone } from './StatusPill';
export { Tabs } from './Tabs';
export type { TabItem, TabsProps } from './Tabs';
export { ActionMenu } from "./ActionMenu";
export type { ActionMenuItem } from "./ActionMenu";
export { BusyRing } from "./BusyRing";
export type { BusyRingProps } from "./BusyRing";
export { Button } from "./Button";
export type { ButtonProps, ButtonSize, ButtonVariant } from "./Button";
export { DetailsPopover } from "./DetailsPopover";
export type {
DetailsPopoverAlign,
DetailsPopoverProps,
} from "./DetailsPopover";
export { Field } from "./Field";
export type { FieldProps } from "./Field";
export { HoverDetails } from "./HoverDetails";
export type { HoverDetailsProps } from "./HoverDetails";
export { IconButton } from "./IconButton";
export type { IconButtonProps, IconButtonVariant } from "./IconButton";
export { LogDock } from "./LogDock";
export type { LogDockEntry, LogDockProps } from "./LogDock";
export { ServiceControlRow } from "./ServiceControlRow";
export type {
ServiceControlRowProps,
ServiceControlState,
} from "./ServiceControlRow";
export { StatusPill } from "./StatusPill";
export type { StatusPillProps, StatusPillTone } from "./StatusPill";
export { Tabs } from "./Tabs";
export type { TabItem, TabsProps } from "./Tabs";