mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-04-09 00:42:47 +00:00
Compare commits
4 Commits
cli/v0.2.1
...
refactor/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22903c9343 | ||
|
|
17b0d19faf | ||
|
|
3b6955468e | ||
|
|
9c091ecd45 |
@@ -193,9 +193,9 @@ hover, active, and disabled states.
|
||||
|
||||
### Disabled (`core/disabled/`)
|
||||
|
||||
Propagates disabled state via React context. `Interactive.Stateless` and `Interactive.Stateful`
|
||||
consume this automatically, so wrapping a subtree in `<Disabled disabled={true}>` disables all
|
||||
interactive descendants.
|
||||
A pure CSS wrapper that applies disabled visuals (`opacity-50`, `cursor-not-allowed`,
|
||||
`pointer-events: none`) to a single child element via Radix `Slot`. Has no React context —
|
||||
Interactive primitives and buttons manage their own disabled state via a `disabled` prop.
|
||||
|
||||
### Hoverable (`core/animations/`)
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Interactive,
|
||||
useDisabled,
|
||||
type InteractiveStatefulProps,
|
||||
type InteractiveStatefulInteraction,
|
||||
} from "@opal/core";
|
||||
@@ -74,6 +73,9 @@ type OpenButtonProps = Omit<InteractiveStatefulProps, "variant"> & {
|
||||
|
||||
/** Override the default rounding derived from `size`. */
|
||||
roundingVariant?: InteractiveContainerRoundingVariant;
|
||||
|
||||
/** Applies disabled styling and suppresses clicks. */
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -92,10 +94,9 @@ function OpenButton({
|
||||
roundingVariant: roundingVariantOverride,
|
||||
interaction,
|
||||
variant = "select-heavy",
|
||||
disabled,
|
||||
...statefulProps
|
||||
}: OpenButtonProps) {
|
||||
const { isDisabled } = useDisabled();
|
||||
|
||||
// Derive open state: explicit prop → Radix data-state (injected via Slot chain)
|
||||
const dataState = (statefulProps as Record<string, unknown>)["data-state"] as
|
||||
| string
|
||||
@@ -119,6 +120,7 @@ function OpenButton({
|
||||
<Interactive.Stateful
|
||||
variant={variant}
|
||||
interaction={resolvedInteraction}
|
||||
disabled={disabled}
|
||||
{...statefulProps}
|
||||
>
|
||||
<Interactive.Container
|
||||
@@ -168,7 +170,7 @@ function OpenButton({
|
||||
);
|
||||
|
||||
const resolvedTooltip =
|
||||
tooltip ?? (foldable && isDisabled && children ? children : undefined);
|
||||
tooltip ?? (foldable && disabled && children ? children : undefined);
|
||||
|
||||
if (!resolvedTooltip) return button;
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import "@opal/components/buttons/select-button/styles.css";
|
||||
import {
|
||||
Interactive,
|
||||
useDisabled,
|
||||
type InteractiveStatefulProps,
|
||||
} from "@opal/core";
|
||||
import { Interactive, type InteractiveStatefulProps } from "@opal/core";
|
||||
import type {
|
||||
ContainerSizeVariants,
|
||||
ExtremaSizeVariants,
|
||||
@@ -64,6 +60,9 @@ type SelectButtonProps = InteractiveStatefulProps &
|
||||
|
||||
/** Which side the tooltip appears on. */
|
||||
tooltipSide?: TooltipSide;
|
||||
|
||||
/** Applies disabled styling and suppresses clicks. */
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -80,9 +79,9 @@ function SelectButton({
|
||||
width,
|
||||
tooltip,
|
||||
tooltipSide = "top",
|
||||
disabled,
|
||||
...statefulProps
|
||||
}: SelectButtonProps) {
|
||||
const { isDisabled } = useDisabled();
|
||||
const isLarge = size === "lg";
|
||||
|
||||
const labelEl = children ? (
|
||||
@@ -96,7 +95,7 @@ function SelectButton({
|
||||
) : null;
|
||||
|
||||
const button = (
|
||||
<Interactive.Stateful {...statefulProps}>
|
||||
<Interactive.Stateful disabled={disabled} {...statefulProps}>
|
||||
<Interactive.Container
|
||||
type={type}
|
||||
heightVariant={size}
|
||||
@@ -128,7 +127,7 @@ function SelectButton({
|
||||
);
|
||||
|
||||
const resolvedTooltip =
|
||||
tooltip ?? (foldable && isDisabled && children ? children : undefined);
|
||||
tooltip ?? (foldable && disabled && children ? children : undefined);
|
||||
|
||||
if (!resolvedTooltip) return button;
|
||||
|
||||
|
||||
59
web/lib/opal/src/components/buttons/sidebar-tab/README.md
Normal file
59
web/lib/opal/src/components/buttons/sidebar-tab/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# SidebarTab
|
||||
|
||||
**Import:** `import { SidebarTab, type SidebarTabProps } from "@opal/components";`
|
||||
|
||||
A sidebar navigation tab built on `Interactive.Stateful` > `Interactive.Container`. Designed for admin and app sidebars.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
div.relative
|
||||
└─ Interactive.Stateful <- variant (sidebar-heavy | sidebar-light), state, disabled
|
||||
└─ Interactive.Container <- rounding, height, width
|
||||
├─ Link? (absolute overlay for client-side navigation)
|
||||
├─ rightChildren? (absolute, above Link for inline actions)
|
||||
└─ ContentAction (icon + title + truncation spacer)
|
||||
```
|
||||
|
||||
- **`sidebar-heavy`** (default) — muted when unselected (text-03/text-02), bold when selected (text-04/text-03)
|
||||
- **`sidebar-light`** (via `lowlight`) — uniformly muted across all states (text-02/text-02)
|
||||
- **Disabled** — both variants use text-02 foreground, transparent background, no hover/active states
|
||||
- **Navigation** uses an absolutely positioned `<Link>` overlay rather than `href` on the Interactive element, so `rightChildren` can sit above it with `pointer-events-auto`.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `icon` | `IconFunctionComponent` | — | Left icon |
|
||||
| `children` | `ReactNode` | — | Label text or custom content |
|
||||
| `selected` | `boolean` | `false` | Active/selected state |
|
||||
| `lowlight` | `boolean` | `false` | Uses muted `sidebar-light` variant |
|
||||
| `disabled` | `boolean` | `false` | Disables the tab |
|
||||
| `folded` | `boolean` | `false` | Collapses label, shows tooltip on hover |
|
||||
| `nested` | `boolean` | `false` | Renders spacer instead of icon for indented items |
|
||||
| `href` | `string` | — | Client-side navigation URL |
|
||||
| `onClick` | `MouseEventHandler` | — | Click handler |
|
||||
| `type` | `ButtonType` | — | HTML button type |
|
||||
| `rightChildren` | `ReactNode` | — | Actions rendered on the right side |
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { SidebarTab } from "@opal/components";
|
||||
import { SvgSettings, SvgLock } from "@opal/icons";
|
||||
|
||||
// Active tab
|
||||
<SidebarTab icon={SvgSettings} href="/admin/settings" selected>
|
||||
Settings
|
||||
</SidebarTab>
|
||||
|
||||
// Disabled enterprise-only tab
|
||||
<SidebarTab icon={SvgLock} disabled>
|
||||
Groups
|
||||
</SidebarTab>
|
||||
|
||||
// Folded sidebar (icon only, tooltip on hover)
|
||||
<SidebarTab icon={SvgSettings} href="/admin/settings" folded>
|
||||
Settings
|
||||
</SidebarTab>
|
||||
```
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { SidebarTab } from "@opal/components/buttons/sidebar-tab/components";
|
||||
import { SvgSettings, SvgUsers, SvgLock, SvgArrowUpCircle } from "@opal/icons";
|
||||
import { Button } from "@opal/components";
|
||||
import { SvgTrash } from "@opal/icons";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
const meta: Meta<typeof SidebarTab> = {
|
||||
title: "opal/components/SidebarTab",
|
||||
component: SidebarTab,
|
||||
tags: ["autodocs"],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<TooltipPrimitive.Provider>
|
||||
<div style={{ width: 260, background: "var(--background-neutral-01)" }}>
|
||||
<Story />
|
||||
</div>
|
||||
</TooltipPrimitive.Provider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SidebarTab>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
icon: SvgSettings,
|
||||
children: "Settings",
|
||||
},
|
||||
};
|
||||
|
||||
export const Selected: Story = {
|
||||
args: {
|
||||
icon: SvgSettings,
|
||||
children: "Settings",
|
||||
selected: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Lowlight: Story = {
|
||||
args: {
|
||||
icon: SvgSettings,
|
||||
children: "Settings",
|
||||
lowlight: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
icon: SvgLock,
|
||||
children: "Enterprise Only",
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithRightChildren: Story = {
|
||||
args: {
|
||||
icon: SvgUsers,
|
||||
children: "Users",
|
||||
rightChildren: (
|
||||
<Button
|
||||
icon={SvgTrash}
|
||||
size="xs"
|
||||
prominence="tertiary"
|
||||
variant="danger"
|
||||
/>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const SidebarExample: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col">
|
||||
<SidebarTab icon={SvgSettings} selected>
|
||||
LLM Models
|
||||
</SidebarTab>
|
||||
<SidebarTab icon={SvgSettings}>Web Search</SidebarTab>
|
||||
<SidebarTab icon={SvgUsers}>Users</SidebarTab>
|
||||
<SidebarTab icon={SvgLock} disabled>
|
||||
Groups
|
||||
</SidebarTab>
|
||||
<SidebarTab icon={SvgLock} disabled>
|
||||
SCIM
|
||||
</SidebarTab>
|
||||
<SidebarTab icon={SvgArrowUpCircle}>Upgrade Plan</SidebarTab>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
159
web/lib/opal/src/components/buttons/sidebar-tab/components.tsx
Normal file
159
web/lib/opal/src/components/buttons/sidebar-tab/components.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import type { ButtonType, IconFunctionComponent } from "@opal/types";
|
||||
import type { Route } from "next";
|
||||
import { Interactive } from "@opal/core";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import { Text } from "@opal/components";
|
||||
import Link from "next/link";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import "@opal/components/tooltip.css";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface SidebarTabProps {
|
||||
/** Collapses the label, showing only the icon. */
|
||||
folded?: boolean;
|
||||
|
||||
/** Marks this tab as the currently active/selected item. */
|
||||
selected?: boolean;
|
||||
|
||||
/** Uses the muted `sidebar-light` variant instead of `sidebar-heavy`. */
|
||||
lowlight?: boolean;
|
||||
|
||||
/** Renders an empty spacer in place of the icon for nested items. */
|
||||
nested?: boolean;
|
||||
|
||||
/** Disables the tab — applies muted colors and suppresses clicks. */
|
||||
disabled?: boolean;
|
||||
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
href?: string;
|
||||
type?: ButtonType;
|
||||
icon?: IconFunctionComponent;
|
||||
children?: React.ReactNode;
|
||||
|
||||
/** Content rendered on the right side (e.g. action buttons). */
|
||||
rightChildren?: React.ReactNode;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SidebarTab
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sidebar navigation tab built on `Interactive.Stateful` > `Interactive.Container`.
|
||||
*
|
||||
* Uses `sidebar-heavy` (default) or `sidebar-light` (when `lowlight`) variants
|
||||
* for color styling. Supports an overlay `Link` for client-side navigation,
|
||||
* `rightChildren` for inline actions, and folded mode with an auto-tooltip.
|
||||
*/
|
||||
function SidebarTab({
|
||||
folded,
|
||||
selected,
|
||||
lowlight,
|
||||
nested,
|
||||
disabled,
|
||||
|
||||
onClick,
|
||||
href,
|
||||
type,
|
||||
icon,
|
||||
rightChildren,
|
||||
children,
|
||||
}: SidebarTabProps) {
|
||||
const Icon =
|
||||
icon ??
|
||||
(nested
|
||||
? ((() => (
|
||||
<div className="w-6" aria-hidden="true" />
|
||||
)) as IconFunctionComponent)
|
||||
: null);
|
||||
|
||||
// The `rightChildren` node is absolutely positioned to sit on top of the
|
||||
// overlay Link. A zero-width spacer reserves truncation space for the title.
|
||||
const truncationSpacer = rightChildren && (
|
||||
<div className="w-0 group-hover/SidebarTab:w-6" />
|
||||
);
|
||||
|
||||
const content = (
|
||||
<div className="relative">
|
||||
<Interactive.Stateful
|
||||
variant={lowlight ? "sidebar-light" : "sidebar-heavy"}
|
||||
state={selected ? "selected" : "empty"}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
group="group/SidebarTab"
|
||||
>
|
||||
<Interactive.Container
|
||||
roundingVariant="sm"
|
||||
heightVariant="lg"
|
||||
widthVariant="full"
|
||||
type={type}
|
||||
>
|
||||
{href && !disabled && (
|
||||
<Link
|
||||
href={href as Route}
|
||||
scroll={false}
|
||||
className="absolute z-[99] inset-0 rounded-08"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!folded && rightChildren && (
|
||||
<div className="absolute z-[100] right-1.5 top-0 bottom-0 flex flex-col justify-center items-center pointer-events-auto">
|
||||
{rightChildren}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{typeof children === "string" ? (
|
||||
<ContentAction
|
||||
icon={Icon ?? undefined}
|
||||
title={folded ? "" : children}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
widthVariant="full"
|
||||
paddingVariant="fit"
|
||||
rightChildren={truncationSpacer}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-row items-center gap-2 flex-1">
|
||||
{Icon && (
|
||||
<div className="flex items-center justify-center p-0.5">
|
||||
<Icon className="h-[1rem] w-[1rem] text-text-03" />
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
{truncationSpacer}
|
||||
</div>
|
||||
)}
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateful>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (typeof children !== "string") return content;
|
||||
if (folded) {
|
||||
return (
|
||||
<TooltipPrimitive.Root>
|
||||
<TooltipPrimitive.Trigger asChild>{content}</TooltipPrimitive.Trigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
className="opal-tooltip"
|
||||
side="right"
|
||||
sideOffset={4}
|
||||
>
|
||||
<Text>{children}</Text>
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
</TooltipPrimitive.Root>
|
||||
);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
export { SidebarTab, type SidebarTabProps };
|
||||
@@ -33,6 +33,12 @@ export {
|
||||
type LineItemButtonProps,
|
||||
} from "@opal/components/buttons/line-item-button/components";
|
||||
|
||||
/* SidebarTab */
|
||||
export {
|
||||
SidebarTab,
|
||||
type SidebarTabProps,
|
||||
} from "@opal/components/buttons/sidebar-tab/components";
|
||||
|
||||
/* Text */
|
||||
export {
|
||||
Text,
|
||||
|
||||
@@ -1,33 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import "@opal/core/disabled/styles.css";
|
||||
import React, { createContext, useContext } from "react";
|
||||
import React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface DisabledContextValue {
|
||||
isDisabled: boolean;
|
||||
allowClick: boolean;
|
||||
}
|
||||
|
||||
const DisabledContext = createContext<DisabledContextValue>({
|
||||
isDisabled: false,
|
||||
allowClick: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the current disabled state from the nearest `<Disabled>` ancestor.
|
||||
*
|
||||
* Used internally by `Interactive.Stateless` and `Interactive.Stateful` to
|
||||
* derive `data-disabled` and `aria-disabled` attributes automatically.
|
||||
*/
|
||||
function useDisabled(): DisabledContextValue {
|
||||
return useContext(DisabledContext);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -56,8 +30,8 @@ interface DisabledProps extends React.HTMLAttributes<HTMLElement> {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Wrapper component that propagates disabled state via context and applies
|
||||
* baseline disabled CSS (opacity, cursor, pointer-events) to its child.
|
||||
* Wrapper component that applies baseline disabled CSS (opacity, cursor,
|
||||
* pointer-events) to its child element.
|
||||
*
|
||||
* Uses Radix `Slot` — merges props onto the single child element without
|
||||
* adding any DOM node. Works correctly inside Radix `asChild` chains.
|
||||
@@ -65,7 +39,7 @@ interface DisabledProps extends React.HTMLAttributes<HTMLElement> {
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Disabled disabled={!canSubmit}>
|
||||
* <Button onClick={handleSubmit}>Save</Button>
|
||||
* <div>...</div>
|
||||
* </Disabled>
|
||||
* ```
|
||||
*/
|
||||
@@ -77,20 +51,16 @@ function Disabled({
|
||||
...rest
|
||||
}: DisabledProps) {
|
||||
return (
|
||||
<DisabledContext.Provider
|
||||
value={{ isDisabled: !!disabled, allowClick: !!allowClick }}
|
||||
<Slot
|
||||
ref={ref}
|
||||
{...rest}
|
||||
aria-disabled={disabled || undefined}
|
||||
data-opal-disabled={disabled || undefined}
|
||||
data-allow-click={disabled && allowClick ? "" : undefined}
|
||||
>
|
||||
<Slot
|
||||
ref={ref}
|
||||
{...rest}
|
||||
aria-disabled={disabled || undefined}
|
||||
data-opal-disabled={disabled || undefined}
|
||||
data-allow-click={disabled && allowClick ? "" : undefined}
|
||||
>
|
||||
{children}
|
||||
</Slot>
|
||||
</DisabledContext.Provider>
|
||||
{children}
|
||||
</Slot>
|
||||
);
|
||||
}
|
||||
|
||||
export { Disabled, useDisabled, type DisabledProps, type DisabledContextValue };
|
||||
export { Disabled, type DisabledProps };
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
/* Disabled */
|
||||
export {
|
||||
Disabled,
|
||||
useDisabled,
|
||||
type DisabledProps,
|
||||
type DisabledContextValue,
|
||||
} from "@opal/core/disabled/components";
|
||||
export { Disabled, type DisabledProps } from "@opal/core/disabled/components";
|
||||
|
||||
/* Animations (formerly Hoverable) */
|
||||
export {
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
widthVariants,
|
||||
type ExtremaSizeVariants,
|
||||
} from "@opal/shared";
|
||||
import { useDisabled } from "@opal/core/disabled/components";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -102,7 +101,6 @@ function InteractiveContainer({
|
||||
widthVariant = "fit",
|
||||
...props
|
||||
}: InteractiveContainerProps) {
|
||||
const { allowClick } = useDisabled();
|
||||
const {
|
||||
className: slotClassName,
|
||||
style: slotStyle,
|
||||
@@ -148,8 +146,7 @@ function InteractiveContainer({
|
||||
if (type) {
|
||||
const ariaDisabled = (rest as Record<string, unknown>)["aria-disabled"];
|
||||
const nativeDisabled =
|
||||
(type === "submit" || !allowClick) &&
|
||||
(ariaDisabled === true || ariaDisabled === "true" || undefined);
|
||||
ariaDisabled === true || ariaDisabled === "true" || undefined;
|
||||
return (
|
||||
<button
|
||||
ref={ref as React.Ref<HTMLButtonElement>}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cn } from "@opal/utils";
|
||||
import { useDisabled } from "@opal/core/disabled/components";
|
||||
import { guardPortalClick } from "@opal/core/interactive/utils";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -29,6 +28,11 @@ interface InteractiveSimpleProps
|
||||
* Link target (e.g. `"_blank"`). Only used when `href` is provided.
|
||||
*/
|
||||
target?: string;
|
||||
|
||||
/**
|
||||
* Applies disabled cursor and suppresses clicks.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -38,8 +42,8 @@ interface InteractiveSimpleProps
|
||||
/**
|
||||
* Minimal interactive surface primitive.
|
||||
*
|
||||
* Provides cursor styling, click handling, disabled integration, and
|
||||
* optional link/group support — but **no color or background styling**.
|
||||
* Provides cursor styling, click handling, and optional link/group
|
||||
* support — but **no color or background styling**.
|
||||
*
|
||||
* Use this for elements that need interactivity (click, cursor, disabled)
|
||||
* without participating in the Interactive color system.
|
||||
@@ -59,9 +63,10 @@ function InteractiveSimple({
|
||||
group,
|
||||
href,
|
||||
target,
|
||||
disabled,
|
||||
...props
|
||||
}: InteractiveSimpleProps) {
|
||||
const { isDisabled, allowClick } = useDisabled();
|
||||
const isDisabled = !!disabled;
|
||||
|
||||
const classes = cn(
|
||||
"cursor-pointer select-none",
|
||||
@@ -88,7 +93,7 @@ function InteractiveSimple({
|
||||
{...linkAttrs}
|
||||
{...slotProps}
|
||||
onClick={
|
||||
isDisabled && !allowClick
|
||||
isDisabled
|
||||
? href
|
||||
? (e: React.MouseEvent) => e.preventDefault()
|
||||
: undefined
|
||||
|
||||
@@ -3,7 +3,6 @@ import "@opal/core/interactive/stateful/styles.css";
|
||||
import React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cn } from "@opal/utils";
|
||||
import { useDisabled } from "@opal/core/disabled/components";
|
||||
import { guardPortalClick } from "@opal/core/interactive/utils";
|
||||
import type { ButtonType, WithoutStyles } from "@opal/types";
|
||||
|
||||
@@ -87,6 +86,11 @@ interface InteractiveStatefulProps
|
||||
* Link target (e.g. `"_blank"`). Only used when `href` is provided.
|
||||
*/
|
||||
target?: string;
|
||||
|
||||
/**
|
||||
* Applies variant-specific disabled colors and suppresses clicks.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -100,8 +104,7 @@ interface InteractiveStatefulProps
|
||||
* (empty/filled/selected). Applies variant/state color styling via CSS
|
||||
* data-attributes and merges onto a single child element via Radix `Slot`.
|
||||
*
|
||||
* Disabled state is consumed from the nearest `<Disabled>` ancestor via
|
||||
* context — there is no `disabled` prop on this component.
|
||||
* Disabled state is controlled via the `disabled` prop.
|
||||
*/
|
||||
function InteractiveStateful({
|
||||
ref,
|
||||
@@ -112,9 +115,10 @@ function InteractiveStateful({
|
||||
type,
|
||||
href,
|
||||
target,
|
||||
disabled,
|
||||
...props
|
||||
}: InteractiveStatefulProps) {
|
||||
const { isDisabled, allowClick } = useDisabled();
|
||||
const isDisabled = !!disabled;
|
||||
|
||||
// onClick/href are always passed directly — Stateful is the outermost Slot,
|
||||
// so Radix Slot-injected handlers don't bypass this guard.
|
||||
@@ -150,7 +154,7 @@ function InteractiveStateful({
|
||||
{...linkAttrs}
|
||||
{...slotProps}
|
||||
onClick={
|
||||
isDisabled && !allowClick
|
||||
isDisabled
|
||||
? href
|
||||
? (e: React.MouseEvent) => e.preventDefault()
|
||||
: undefined
|
||||
|
||||
@@ -550,6 +550,14 @@
|
||||
) {
|
||||
@apply bg-background-tint-03;
|
||||
}
|
||||
/* ---------------------------------------------------------------------------
|
||||
Sidebar-Heavy — Disabled (all states)
|
||||
--------------------------------------------------------------------------- */
|
||||
.interactive[data-interactive-variant="sidebar-heavy"][data-disabled] {
|
||||
@apply bg-transparent;
|
||||
--interactive-foreground: var(--text-02);
|
||||
--interactive-foreground-icon: var(--text-02);
|
||||
}
|
||||
|
||||
/* ===========================================================================
|
||||
Sidebar-Light
|
||||
@@ -607,3 +615,11 @@
|
||||
) {
|
||||
@apply bg-background-tint-03;
|
||||
}
|
||||
/* ---------------------------------------------------------------------------
|
||||
Sidebar-Light — Disabled (all states)
|
||||
--------------------------------------------------------------------------- */
|
||||
.interactive[data-interactive-variant="sidebar-light"][data-disabled] {
|
||||
@apply bg-transparent;
|
||||
--interactive-foreground: var(--text-02);
|
||||
--interactive-foreground-icon: var(--text-02);
|
||||
}
|
||||
|
||||
@@ -378,18 +378,17 @@ const InputBar = memo(
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
<Disabled disabled={disabled}>
|
||||
<SelectButton
|
||||
leftIcon={SvgOrganization}
|
||||
engaged={demoDataEnabled}
|
||||
action
|
||||
folded
|
||||
onClick={() => router.push(CRAFT_CONFIGURE_PATH)}
|
||||
className="bg-action-link-01"
|
||||
>
|
||||
Demo Data Active
|
||||
</SelectButton>
|
||||
</Disabled>
|
||||
<SelectButton
|
||||
disabled={disabled}
|
||||
leftIcon={SvgOrganization}
|
||||
engaged={demoDataEnabled}
|
||||
action
|
||||
folded
|
||||
onClick={() => router.push(CRAFT_CONFIGURE_PATH)}
|
||||
className="bg-action-link-01"
|
||||
>
|
||||
Demo Data Active
|
||||
</SelectButton>
|
||||
</span>
|
||||
</SimpleTooltip>
|
||||
)}
|
||||
|
||||
@@ -1,122 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import type { ButtonType, IconFunctionComponent } from "@opal/types";
|
||||
import type { Route } from "next";
|
||||
import { Interactive } from "@opal/core";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import Link from "next/link";
|
||||
import SimpleTooltip from "@/refresh-components/SimpleTooltip";
|
||||
|
||||
export interface SidebarTabProps {
|
||||
// Button states:
|
||||
folded?: boolean;
|
||||
selected?: boolean;
|
||||
lowlight?: boolean;
|
||||
nested?: boolean;
|
||||
|
||||
// Button properties:
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
href?: string;
|
||||
type?: ButtonType;
|
||||
icon?: IconFunctionComponent;
|
||||
children?: React.ReactNode;
|
||||
rightChildren?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function SidebarTab({
|
||||
folded,
|
||||
selected,
|
||||
lowlight,
|
||||
nested,
|
||||
|
||||
onClick,
|
||||
href,
|
||||
type,
|
||||
icon,
|
||||
rightChildren,
|
||||
children,
|
||||
}: SidebarTabProps) {
|
||||
const Icon =
|
||||
icon ??
|
||||
(nested
|
||||
? ((() => (
|
||||
<div className="w-6" aria-hidden="true" />
|
||||
)) as IconFunctionComponent)
|
||||
: null);
|
||||
|
||||
// NOTE (@raunakab)
|
||||
//
|
||||
// The `rightChildren` node NEEDS to be absolutely positioned since it needs to live on top of the absolutely positioned `Link`.
|
||||
// However, having the `rightChildren` be absolutely positioned means that it cannot appropriately truncate the title.
|
||||
// Therefore, we add a dummy node solely for the truncation effects that we obtain.
|
||||
const truncationSpacer = rightChildren && (
|
||||
<div className="w-0 group-hover/SidebarTab:w-6" />
|
||||
);
|
||||
|
||||
const content = (
|
||||
<div className="relative">
|
||||
<Interactive.Stateful
|
||||
variant={lowlight ? "sidebar-light" : "sidebar-heavy"}
|
||||
state={selected ? "selected" : "empty"}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
group="group/SidebarTab"
|
||||
>
|
||||
<Interactive.Container
|
||||
roundingVariant="sm"
|
||||
heightVariant="lg"
|
||||
widthVariant="full"
|
||||
type={type}
|
||||
>
|
||||
{href && (
|
||||
<Link
|
||||
href={href as Route}
|
||||
scroll={false}
|
||||
className="absolute z-[99] inset-0 rounded-08"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!folded && rightChildren && (
|
||||
<div className="absolute z-[100] right-1.5 top-0 bottom-0 flex flex-col justify-center items-center pointer-events-auto">
|
||||
{rightChildren}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{typeof children === "string" ? (
|
||||
<ContentAction
|
||||
icon={Icon ?? undefined}
|
||||
title={folded ? "" : children}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
widthVariant="full"
|
||||
paddingVariant="fit"
|
||||
rightChildren={truncationSpacer}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-row items-center gap-2 flex-1">
|
||||
{Icon && (
|
||||
<div className="flex items-center justify-center p-0.5">
|
||||
<Icon className="h-[1rem] w-[1rem] text-text-03" />
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
{
|
||||
// NOTE (@raunakab)
|
||||
//
|
||||
// Adding the `truncationSpacer` here for the same reason as above.
|
||||
truncationSpacer
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateful>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (typeof children !== "string") return content;
|
||||
if (folded)
|
||||
return <SimpleTooltip tooltip={children}>{content}</SimpleTooltip>;
|
||||
return content;
|
||||
}
|
||||
export {
|
||||
SidebarTab as default,
|
||||
type SidebarTabProps,
|
||||
} from "@opal/components/buttons/sidebar-tab/components";
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
} from "@opal/icons";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { OpenButton } from "@opal/components";
|
||||
import { Disabled } from "@opal/core";
|
||||
import { LLMOption, LLMOptionGroup } from "./interfaces";
|
||||
|
||||
export interface LLMPopoverProps {
|
||||
@@ -356,21 +355,20 @@ export default function LLMPopover({
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<div data-testid="llm-popover-trigger">
|
||||
<Popover.Trigger asChild disabled={disabled}>
|
||||
<Disabled disabled={disabled}>
|
||||
<OpenButton
|
||||
icon={
|
||||
foldable
|
||||
? SvgRefreshCw
|
||||
: getProviderIcon(
|
||||
llmManager.currentLlm.provider,
|
||||
llmManager.currentLlm.modelName
|
||||
)
|
||||
}
|
||||
foldable={foldable}
|
||||
>
|
||||
{currentLlmDisplayName}
|
||||
</OpenButton>
|
||||
</Disabled>
|
||||
<OpenButton
|
||||
disabled={disabled}
|
||||
icon={
|
||||
foldable
|
||||
? SvgRefreshCw
|
||||
: getProviderIcon(
|
||||
llmManager.currentLlm.provider,
|
||||
llmManager.currentLlm.modelName
|
||||
)
|
||||
}
|
||||
foldable={foldable}
|
||||
>
|
||||
{currentLlmDisplayName}
|
||||
</OpenButton>
|
||||
</Popover.Trigger>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -539,38 +539,36 @@ const AppInputBar = React.memo(
|
||||
/>
|
||||
)}
|
||||
{onToggleTabReading ? (
|
||||
<Disabled disabled={disabled}>
|
||||
<SelectButton
|
||||
icon={SvgGlobe}
|
||||
onClick={onToggleTabReading}
|
||||
state={tabReadingEnabled ? "selected" : "empty"}
|
||||
>
|
||||
{tabReadingEnabled
|
||||
? currentTabUrl
|
||||
? (() => {
|
||||
try {
|
||||
return new URL(currentTabUrl).hostname;
|
||||
} catch {
|
||||
return currentTabUrl;
|
||||
}
|
||||
})()
|
||||
: "Reading tab..."
|
||||
: "Read this tab"}
|
||||
</SelectButton>
|
||||
</Disabled>
|
||||
<SelectButton
|
||||
disabled={disabled}
|
||||
icon={SvgGlobe}
|
||||
onClick={onToggleTabReading}
|
||||
state={tabReadingEnabled ? "selected" : "empty"}
|
||||
>
|
||||
{tabReadingEnabled
|
||||
? currentTabUrl
|
||||
? (() => {
|
||||
try {
|
||||
return new URL(currentTabUrl).hostname;
|
||||
} catch {
|
||||
return currentTabUrl;
|
||||
}
|
||||
})()
|
||||
: "Reading tab..."
|
||||
: "Read this tab"}
|
||||
</SelectButton>
|
||||
) : (
|
||||
showDeepResearch && (
|
||||
<Disabled disabled={disabled}>
|
||||
<SelectButton
|
||||
variant="select-light"
|
||||
icon={SvgHourglass}
|
||||
onClick={toggleDeepResearch}
|
||||
state={deepResearchEnabled ? "selected" : "empty"}
|
||||
foldable={!deepResearchEnabled}
|
||||
>
|
||||
Deep Research
|
||||
</SelectButton>
|
||||
</Disabled>
|
||||
<SelectButton
|
||||
disabled={disabled}
|
||||
variant="select-light"
|
||||
icon={SvgHourglass}
|
||||
onClick={toggleDeepResearch}
|
||||
state={deepResearchEnabled ? "selected" : "empty"}
|
||||
foldable={!deepResearchEnabled}
|
||||
>
|
||||
Deep Research
|
||||
</SelectButton>
|
||||
)
|
||||
)}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { Button, OpenButton, SelectButton } from "@opal/components";
|
||||
import { Disabled } from "@opal/core";
|
||||
import { OpenAISVG } from "@/components/icons/icons";
|
||||
import {
|
||||
SvgPlusCircle,
|
||||
@@ -29,16 +28,14 @@ export default function SharedAppInputBar() {
|
||||
<div className="flex flex-row items-center">
|
||||
<Button disabled icon={SvgPlusCircle} prominence="tertiary" />
|
||||
<Button disabled icon={SvgSliders} prominence="tertiary" />
|
||||
<Disabled disabled>
|
||||
<SelectButton icon={SvgHourglass} />
|
||||
</Disabled>
|
||||
<SelectButton disabled icon={SvgHourglass} />
|
||||
</div>
|
||||
|
||||
{/* Right side controls */}
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<Disabled disabled>
|
||||
<OpenButton icon={OpenAISVG}>GPT-4o</OpenButton>
|
||||
</Disabled>
|
||||
<OpenButton disabled icon={OpenAISVG}>
|
||||
GPT-4o
|
||||
</OpenButton>
|
||||
<Button disabled icon={SvgArrowUp} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user