mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-28 11:02:42 +00:00
Compare commits
1 Commits
jamison/on
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3dfe6aa1b |
@@ -1,103 +0,0 @@
|
||||
{{- if .Values.cliServer.enabled }}
|
||||
# Onyx CLI SSH server — provides a terminal interface for chatting with Onyx agents.
|
||||
# Clients connect via SSH and authenticate with their Onyx API key.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "onyx.fullname" . }}-cli-server
|
||||
labels:
|
||||
{{- include "onyx.labels" . | nindent 4 }}
|
||||
{{- with .Values.cliServer.deploymentLabels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
replicas: {{ .Values.cliServer.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "onyx.selectorLabels" . | nindent 6 }}
|
||||
{{- if .Values.cliServer.deploymentLabels }}
|
||||
{{- toYaml .Values.cliServer.deploymentLabels | nindent 6 }}
|
||||
{{- end }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.cliServer.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "onyx.labels" . | nindent 8 }}
|
||||
{{- with .Values.cliServer.deploymentLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.cliServer.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "onyx.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.cliServer.podSecurityContext | nindent 8 }}
|
||||
{{- with .Values.cliServer.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.cliServer.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.cliServer.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: cli-server
|
||||
securityContext:
|
||||
{{- toYaml .Values.cliServer.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.cliServer.image.repository }}:{{ .Values.cliServer.image.tag | default .Values.global.version }}"
|
||||
imagePullPolicy: {{ .Values.cliServer.image.pullPolicy | default .Values.global.pullPolicy }}
|
||||
command:
|
||||
- onyx-cli
|
||||
- serve
|
||||
- --host
|
||||
- "0.0.0.0"
|
||||
- --port
|
||||
- "{{ .Values.cliServer.containerPort }}"
|
||||
{{- if .Values.cliServer.idleTimeout }}
|
||||
- --idle-timeout
|
||||
- "{{ .Values.cliServer.idleTimeout }}"
|
||||
{{- end }}
|
||||
{{- if .Values.cliServer.maxSessionTimeout }}
|
||||
- --max-session-timeout
|
||||
- "{{ .Values.cliServer.maxSessionTimeout }}"
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: ssh
|
||||
containerPort: {{ .Values.cliServer.containerPort }}
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: ONYX_SERVER_URL
|
||||
value: "http://{{ include "onyx.fullname" . }}-api-service:{{ .Values.api.service.servicePort }}"
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: ssh
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: ssh
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
{{- toYaml .Values.cliServer.resources | nindent 12 }}
|
||||
{{- with .Values.cliServer.volumeMounts }}
|
||||
volumeMounts:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.cliServer.volumes }}
|
||||
volumes:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,27 +0,0 @@
|
||||
{{- if .Values.cliServer.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "onyx.fullname" . }}-cli-server-service
|
||||
labels:
|
||||
{{- include "onyx.labels" . | nindent 4 }}
|
||||
{{- if .Values.cliServer.deploymentLabels }}
|
||||
{{- toYaml .Values.cliServer.deploymentLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.cliServer.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.cliServer.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.cliServer.service.servicePort }}
|
||||
targetPort: ssh
|
||||
protocol: TCP
|
||||
name: ssh
|
||||
selector:
|
||||
{{- include "onyx.selectorLabels" . | nindent 4 }}
|
||||
{{- if .Values.cliServer.deploymentLabels }}
|
||||
{{- toYaml .Values.cliServer.deploymentLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -862,44 +862,6 @@ mcpServer:
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
# Onyx CLI SSH Server
|
||||
# Exposes the interactive chat TUI over SSH so users can `ssh <host>` to chat with Onyx.
|
||||
# Users authenticate with their Onyx API key at the SSH prompt.
|
||||
cliServer:
|
||||
enabled: false # Disabled by default
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: onyxdotapp/onyx-cli
|
||||
tag: "latest"
|
||||
pullPolicy: Always
|
||||
containerPort: 22
|
||||
# SSH session timeout settings
|
||||
idleTimeout: "15m"
|
||||
maxSessionTimeout: "8h"
|
||||
podAnnotations: {}
|
||||
podLabels:
|
||||
scope: onyx-cli
|
||||
deploymentLabels:
|
||||
app: cli-server
|
||||
service:
|
||||
type: ClusterIP
|
||||
servicePort: 22
|
||||
annotations: {}
|
||||
podSecurityContext: {}
|
||||
securityContext: {}
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "64Mi"
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "256Mi"
|
||||
volumes: []
|
||||
volumeMounts: []
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
celery_worker_docfetching:
|
||||
replicaCount: 1
|
||||
logLevel: INFO
|
||||
|
||||
@@ -104,7 +104,7 @@ function Button({
|
||||
isLarge ? "default" : size === "2xs" ? "mini" : "compact"
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-1 interactive-foreground">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
{iconWrapper(Icon, size, !!children)}
|
||||
|
||||
{labelEl}
|
||||
|
||||
@@ -67,7 +67,7 @@ function FilterButton({
|
||||
state={active ? "selected" : "empty"}
|
||||
>
|
||||
<Interactive.Container type="button">
|
||||
<div className="interactive-foreground flex flex-row items-center gap-1">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
{iconWrapper(Icon, "lg", true)}
|
||||
<Text font="main-ui-action" color="inherit" nowrap>
|
||||
{children}
|
||||
|
||||
@@ -16,7 +16,7 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
type ContentPassthroughProps = DistributiveOmit<
|
||||
ContentActionProps,
|
||||
"paddingVariant" | "widthVariant" | "ref" | "withInteractive"
|
||||
"paddingVariant" | "widthVariant" | "ref"
|
||||
>;
|
||||
|
||||
type LineItemButtonOwnProps = Pick<
|
||||
@@ -92,7 +92,6 @@ function LineItemButton({
|
||||
>
|
||||
<ContentAction
|
||||
{...(contentActionProps as ContentActionProps)}
|
||||
withInteractive
|
||||
paddingVariant="fit"
|
||||
/>
|
||||
</Interactive.Container>
|
||||
|
||||
@@ -132,7 +132,7 @@ function OpenButton({
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"interactive-foreground flex flex-row items-center",
|
||||
"flex flex-row items-center",
|
||||
justifyContent === "between" ? "w-full justify-between" : "gap-1",
|
||||
foldable &&
|
||||
justifyContent !== "between" &&
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import "@opal/components/buttons/select-button/styles.css";
|
||||
import {
|
||||
Interactive,
|
||||
@@ -84,11 +86,13 @@ function SelectButton({
|
||||
const isLarge = size === "lg";
|
||||
|
||||
const labelEl = children ? (
|
||||
<span className="opal-select-button-label">
|
||||
<Text font={isLarge ? "main-ui-body" : "secondary-body"} color="inherit">
|
||||
{children}
|
||||
</Text>
|
||||
</span>
|
||||
<Text
|
||||
font={isLarge ? "main-ui-body" : "secondary-body"}
|
||||
color="inherit"
|
||||
nowrap
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
) : null;
|
||||
|
||||
const button = (
|
||||
@@ -103,7 +107,7 @@ function SelectButton({
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"opal-select-button interactive-foreground",
|
||||
"opal-select-button",
|
||||
foldable && "interactive-foldable-host"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -3,7 +3,3 @@
|
||||
.opal-select-button {
|
||||
@apply flex flex-row items-center gap-1;
|
||||
}
|
||||
|
||||
.opal-select-button-label {
|
||||
@apply whitespace-nowrap;
|
||||
}
|
||||
|
||||
@@ -390,14 +390,12 @@ function PaginationCount({
|
||||
<span className={textClasses(size, "muted")}>of</span>
|
||||
{totalItems}
|
||||
{units && (
|
||||
<span className="ml-1">
|
||||
<Text
|
||||
color="inherit"
|
||||
font={size === "sm" ? "secondary-body" : "main-ui-muted"}
|
||||
>
|
||||
{units}
|
||||
</Text>
|
||||
</span>
|
||||
<Text
|
||||
color="inherit"
|
||||
font={size === "sm" ? "secondary-body" : "main-ui-muted"}
|
||||
>
|
||||
{units}
|
||||
</Text>
|
||||
)}
|
||||
</span>
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "@opal/components/tag/styles.css";
|
||||
|
||||
import type { IconFunctionComponent, RichStr } from "@opal/types";
|
||||
import { Text } from "@opal/components";
|
||||
import { cn } from "@opal/utils";
|
||||
@@ -46,20 +45,22 @@ function Tag({ icon: Icon, title, color = "gray", size = "sm" }: TagProps) {
|
||||
const config = COLOR_CONFIG[color];
|
||||
|
||||
return (
|
||||
<div className={cn("opal-auxiliary-tag", config.bg)} data-size={size}>
|
||||
<div
|
||||
className={cn("opal-auxiliary-tag", config.bg, config.text)}
|
||||
data-size={size}
|
||||
>
|
||||
{Icon && (
|
||||
<div className="opal-auxiliary-tag-icon-container">
|
||||
<Icon className={cn("opal-auxiliary-tag-icon", config.text)} />
|
||||
</div>
|
||||
)}
|
||||
<span className={cn("opal-auxiliary-tag-title px-[2px]", config.text)}>
|
||||
<Text
|
||||
font={size === "md" ? "secondary-body" : "figure-small-value"}
|
||||
color="inherit"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</span>
|
||||
<Text
|
||||
font={size === "md" ? "secondary-body" : "figure-small-value"}
|
||||
color="inherit"
|
||||
nowrap
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ const SAFE_PROTOCOL = /^https?:|^mailto:|^tel:/i;
|
||||
const ALLOWED_ELEMENTS = ["p", "br", "a", "strong", "em", "code", "del"];
|
||||
|
||||
const INLINE_COMPONENTS = {
|
||||
p: ({ children }: { children?: ReactNode }) => <>{children}</>,
|
||||
p: ({ children }: { children?: ReactNode }) => (
|
||||
<span className="block">{children}</span>
|
||||
),
|
||||
a: ({ children, href }: { children?: ReactNode; href?: string }) => {
|
||||
if (!href || !SAFE_PROTOCOL.test(href)) {
|
||||
return <>{children}</>;
|
||||
|
||||
@@ -125,6 +125,7 @@ function Text({
|
||||
...rest
|
||||
}: TextProps) {
|
||||
const resolvedClassName = cn(
|
||||
"px-[2px]",
|
||||
FONT_CONFIG[font],
|
||||
COLOR_CONFIG[color],
|
||||
nowrap && "whitespace-nowrap",
|
||||
|
||||
@@ -18,9 +18,11 @@ export {
|
||||
import { InteractiveStateless } from "@opal/core/interactive/stateless/components";
|
||||
import { InteractiveStateful } from "@opal/core/interactive/stateful/components";
|
||||
import { InteractiveContainer } from "@opal/core/interactive/container/components";
|
||||
import { InteractiveSimple } from "@opal/core/interactive/simple/components";
|
||||
import { Foldable } from "@opal/core/interactive/foldable/components";
|
||||
|
||||
const Interactive = {
|
||||
Simple: InteractiveSimple,
|
||||
Stateless: InteractiveStateless,
|
||||
Stateful: InteractiveStateful,
|
||||
Container: InteractiveContainer,
|
||||
@@ -50,3 +52,5 @@ export type {
|
||||
} from "@opal/core/interactive/container/components";
|
||||
|
||||
export type { FoldableProps } from "@opal/core/interactive/foldable/components";
|
||||
|
||||
export type { InteractiveSimpleProps } from "@opal/core/interactive/simple/components";
|
||||
|
||||
@@ -9,7 +9,6 @@ const VARIANT_PROMINENCE_MAP: Record<string, string[]> = {
|
||||
default: ["primary", "secondary", "tertiary", "internal"],
|
||||
action: ["primary", "secondary", "tertiary", "internal"],
|
||||
danger: ["primary", "secondary", "tertiary", "internal"],
|
||||
none: [],
|
||||
};
|
||||
|
||||
const SIZE_VARIANTS = ["lg", "md", "sm", "xs", "2xs", "fit"] as const;
|
||||
@@ -43,7 +42,7 @@ export const Default: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border>
|
||||
<span className="interactive-foreground">Secondary</span>
|
||||
<span>Secondary</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
|
||||
@@ -53,7 +52,7 @@ export const Default: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border>
|
||||
<span className="interactive-foreground">Primary</span>
|
||||
<span>Primary</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
|
||||
@@ -63,7 +62,7 @@ export const Default: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border>
|
||||
<span className="interactive-foreground">Tertiary</span>
|
||||
<span>Tertiary</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
</div>
|
||||
@@ -115,9 +114,7 @@ export const VariantMatrix: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border>
|
||||
<span className="interactive-foreground">
|
||||
{prominence}
|
||||
</span>
|
||||
<span>{prominence}</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
<span
|
||||
@@ -150,7 +147,7 @@ export const Sizes: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border heightVariant={size}>
|
||||
<span className="interactive-foreground">{size}</span>
|
||||
<span>{size}</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
))}
|
||||
@@ -168,7 +165,7 @@ export const WidthFull: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border widthVariant="full">
|
||||
<span className="interactive-foreground">Full width container</span>
|
||||
<span>Full width container</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
</div>
|
||||
@@ -187,7 +184,7 @@ export const Rounding: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border roundingVariant={rounding}>
|
||||
<span className="interactive-foreground">{rounding}</span>
|
||||
<span>{rounding}</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
))}
|
||||
@@ -207,7 +204,7 @@ export const DisabledStory: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border>
|
||||
<span className="interactive-foreground">Disabled</span>
|
||||
<span>Disabled</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
</Disabled>
|
||||
@@ -218,7 +215,7 @@ export const DisabledStory: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border>
|
||||
<span className="interactive-foreground">Enabled</span>
|
||||
<span>Enabled</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
</div>
|
||||
@@ -236,7 +233,7 @@ export const Interaction: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border>
|
||||
<span className="interactive-foreground">Forced hover</span>
|
||||
<span>Forced hover</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
|
||||
@@ -247,7 +244,7 @@ export const Interaction: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border>
|
||||
<span className="interactive-foreground">Forced active</span>
|
||||
<span>Forced active</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
|
||||
@@ -257,7 +254,7 @@ export const Interaction: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border>
|
||||
<span className="interactive-foreground">Normal (rest)</span>
|
||||
<span>Normal (rest)</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
</div>
|
||||
@@ -274,7 +271,7 @@ export const WithBorder: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border>
|
||||
<span className="interactive-foreground">With border</span>
|
||||
<span>With border</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
|
||||
@@ -284,7 +281,7 @@ export const WithBorder: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container>
|
||||
<span className="interactive-foreground">Without border</span>
|
||||
<span>Without border</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
</div>
|
||||
@@ -296,7 +293,7 @@ export const AsLink: StoryObj = {
|
||||
render: () => (
|
||||
<Interactive.Stateless variant="action" href="/settings">
|
||||
<Interactive.Container border>
|
||||
<span className="interactive-foreground">Go to Settings</span>
|
||||
<span>Go to Settings</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
),
|
||||
@@ -312,7 +309,7 @@ export const SelectVariant: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border>
|
||||
<span className="interactive-foreground">Selected (light)</span>
|
||||
<span>Selected (light)</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateful>
|
||||
|
||||
@@ -322,7 +319,7 @@ export const SelectVariant: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border>
|
||||
<span className="interactive-foreground">Unselected (light)</span>
|
||||
<span>Unselected (light)</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateful>
|
||||
|
||||
@@ -332,7 +329,7 @@ export const SelectVariant: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border>
|
||||
<span className="interactive-foreground">Selected (heavy)</span>
|
||||
<span>Selected (heavy)</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateful>
|
||||
|
||||
@@ -342,7 +339,7 @@ export const SelectVariant: StoryObj = {
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Interactive.Container border>
|
||||
<span className="interactive-foreground">Unselected (heavy)</span>
|
||||
<span>Unselected (heavy)</span>
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateful>
|
||||
</div>
|
||||
|
||||
@@ -21,9 +21,10 @@
|
||||
--interactive-easing: ease-in-out;
|
||||
}
|
||||
|
||||
/* Base interactive surface */
|
||||
/* Base interactive surface — sets color directly so all descendants inherit. */
|
||||
.interactive {
|
||||
@apply cursor-pointer select-none;
|
||||
color: var(--interactive-foreground);
|
||||
transition:
|
||||
background-color var(--interactive-duration) var(--interactive-easing),
|
||||
--interactive-foreground var(--interactive-duration)
|
||||
@@ -43,14 +44,8 @@
|
||||
@apply border;
|
||||
}
|
||||
|
||||
/* Utility classes — descendants opt in to parent-controlled foreground color.
|
||||
No transition here — the parent interpolates the variables directly. */
|
||||
.interactive-foreground {
|
||||
color: var(--interactive-foreground);
|
||||
}
|
||||
|
||||
/* Icon foreground — reads from --interactive-foreground-icon, which defaults
|
||||
to --interactive-foreground via the variant CSS rules. */
|
||||
/* Icon foreground — reads from --interactive-foreground-icon, which may differ
|
||||
from --interactive-foreground (e.g. muted icons beside normal text). */
|
||||
.interactive-foreground-icon {
|
||||
color: var(--interactive-foreground-icon);
|
||||
}
|
||||
|
||||
100
web/lib/opal/src/core/interactive/simple/components.tsx
Normal file
100
web/lib/opal/src/core/interactive/simple/components.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cn } from "@opal/utils";
|
||||
import { useDisabled } from "@opal/core/disabled/components";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface InteractiveSimpleProps
|
||||
extends Omit<
|
||||
React.HTMLAttributes<HTMLElement>,
|
||||
"className" | "style" | "color"
|
||||
> {
|
||||
ref?: React.Ref<HTMLElement>;
|
||||
|
||||
/**
|
||||
* Tailwind group class (e.g. `"group/Card"`) for `group-hover:*` utilities.
|
||||
*/
|
||||
group?: string;
|
||||
|
||||
/**
|
||||
* URL to navigate to when clicked. Passed through Slot to the child.
|
||||
*/
|
||||
href?: string;
|
||||
|
||||
/**
|
||||
* Link target (e.g. `"_blank"`). Only used when `href` is provided.
|
||||
*/
|
||||
target?: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// InteractiveSimple
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Minimal interactive surface primitive.
|
||||
*
|
||||
* Provides cursor styling, click handling, disabled integration, 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.
|
||||
*
|
||||
* Uses Radix `Slot` — merges props onto a single child element without
|
||||
* adding any DOM node.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Interactive.Simple onClick={handleClick} group="group/Card">
|
||||
* <Card>...</Card>
|
||||
* </Interactive.Simple>
|
||||
* ```
|
||||
*/
|
||||
function InteractiveSimple({
|
||||
ref,
|
||||
group,
|
||||
href,
|
||||
target,
|
||||
...props
|
||||
}: InteractiveSimpleProps) {
|
||||
const { isDisabled, allowClick } = useDisabled();
|
||||
|
||||
const classes = cn(
|
||||
"cursor-pointer select-none",
|
||||
isDisabled && "cursor-not-allowed",
|
||||
!props.onClick && !href && "!cursor-default !select-auto",
|
||||
group
|
||||
);
|
||||
|
||||
const { onClick, ...slotProps } = props;
|
||||
|
||||
const linkAttrs = href
|
||||
? {
|
||||
href: isDisabled ? undefined : href,
|
||||
target,
|
||||
rel: target === "_blank" ? "noopener noreferrer" : undefined,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
className={classes}
|
||||
aria-disabled={isDisabled || undefined}
|
||||
{...linkAttrs}
|
||||
{...slotProps}
|
||||
onClick={
|
||||
isDisabled && !allowClick
|
||||
? href
|
||||
? (e: React.MouseEvent) => e.preventDefault()
|
||||
: undefined
|
||||
: onClick
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { InteractiveSimple, type InteractiveSimpleProps };
|
||||
@@ -15,7 +15,8 @@ type InteractiveStatefulVariant =
|
||||
| "select-heavy"
|
||||
| "select-tinted"
|
||||
| "select-filter"
|
||||
| "sidebar";
|
||||
| "sidebar-heavy"
|
||||
| "sidebar-light";
|
||||
type InteractiveStatefulState = "empty" | "filled" | "selected";
|
||||
type InteractiveStatefulInteraction = "rest" | "hover" | "active";
|
||||
|
||||
@@ -33,7 +34,8 @@ interface InteractiveStatefulProps
|
||||
* - `"select-heavy"` — tinted selected background (for list rows, model pickers)
|
||||
* - `"select-tinted"` — like select-heavy but with a tinted rest background
|
||||
* - `"select-filter"` — like select-tinted for empty/filled; selected state uses inverted tint backgrounds and inverted text (for filter buttons)
|
||||
* - `"sidebar"` — for sidebar navigation items
|
||||
* - `"sidebar-heavy"` — sidebar navigation items: muted when unselected (text-03/text-02), bold when selected (text-04/text-03)
|
||||
* - `"sidebar-light"` — sidebar navigation items: uniformly muted across all states (text-02/text-02)
|
||||
*
|
||||
* @default "select-heavy"
|
||||
*/
|
||||
|
||||
@@ -392,49 +392,115 @@
|
||||
}
|
||||
|
||||
/* ===========================================================================
|
||||
Sidebar
|
||||
Sidebar-Heavy
|
||||
|
||||
Not selected: muted (text-03 / icon text-02)
|
||||
Selected: default (text-04 / icon text-03)
|
||||
=========================================================================== */
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Sidebar — Empty
|
||||
Sidebar-Heavy — Empty
|
||||
--------------------------------------------------------------------------- */
|
||||
.interactive[data-interactive-variant="sidebar"][data-interactive-state="empty"] {
|
||||
.interactive[data-interactive-variant="sidebar-heavy"][data-interactive-state="empty"] {
|
||||
@apply bg-transparent;
|
||||
--interactive-foreground: var(--text-03);
|
||||
--interactive-foreground-icon: var(--text-02);
|
||||
}
|
||||
.interactive[data-interactive-variant="sidebar"][data-interactive-state="empty"]:hover:not(
|
||||
.interactive[data-interactive-variant="sidebar-heavy"][data-interactive-state="empty"]:hover:not(
|
||||
[data-disabled]
|
||||
),
|
||||
.interactive[data-interactive-variant="sidebar"][data-interactive-state="empty"][data-interaction="hover"]:not(
|
||||
.interactive[data-interactive-variant="sidebar-heavy"][data-interactive-state="empty"][data-interaction="hover"]:not(
|
||||
[data-disabled]
|
||||
) {
|
||||
@apply bg-background-tint-03;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Sidebar — Filled
|
||||
Sidebar-Heavy — Filled
|
||||
--------------------------------------------------------------------------- */
|
||||
.interactive[data-interactive-variant="sidebar"][data-interactive-state="filled"] {
|
||||
.interactive[data-interactive-variant="sidebar-heavy"][data-interactive-state="filled"] {
|
||||
@apply bg-transparent;
|
||||
--interactive-foreground: var(--text-03);
|
||||
--interactive-foreground-icon: var(--text-02);
|
||||
}
|
||||
.interactive[data-interactive-variant="sidebar"][data-interactive-state="filled"]:hover:not(
|
||||
.interactive[data-interactive-variant="sidebar-heavy"][data-interactive-state="filled"]:hover:not(
|
||||
[data-disabled]
|
||||
),
|
||||
.interactive[data-interactive-variant="sidebar"][data-interactive-state="filled"][data-interaction="hover"]:not(
|
||||
.interactive[data-interactive-variant="sidebar-heavy"][data-interactive-state="filled"][data-interaction="hover"]:not(
|
||||
[data-disabled]
|
||||
) {
|
||||
@apply bg-background-tint-03;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Sidebar — Selected
|
||||
Sidebar-Heavy — Selected
|
||||
--------------------------------------------------------------------------- */
|
||||
.interactive[data-interactive-variant="sidebar"][data-interactive-state="selected"] {
|
||||
.interactive[data-interactive-variant="sidebar-heavy"][data-interactive-state="selected"] {
|
||||
@apply bg-background-tint-00;
|
||||
--interactive-foreground: var(--text-04);
|
||||
--interactive-foreground-icon: var(--text-03);
|
||||
}
|
||||
.interactive[data-interactive-variant="sidebar"][data-interactive-state="selected"]:hover:not(
|
||||
.interactive[data-interactive-variant="sidebar-heavy"][data-interactive-state="selected"]:hover:not(
|
||||
[data-disabled]
|
||||
),
|
||||
.interactive[data-interactive-variant="sidebar"][data-interactive-state="selected"][data-interaction="hover"]:not(
|
||||
.interactive[data-interactive-variant="sidebar-heavy"][data-interactive-state="selected"][data-interaction="hover"]:not(
|
||||
[data-disabled]
|
||||
) {
|
||||
@apply bg-background-tint-03;
|
||||
}
|
||||
|
||||
/* ===========================================================================
|
||||
Sidebar-Light
|
||||
|
||||
All states: prominence="muted-2x" colors (text-02 / icon text-02)
|
||||
=========================================================================== */
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Sidebar-Light — Empty
|
||||
--------------------------------------------------------------------------- */
|
||||
.interactive[data-interactive-variant="sidebar-light"][data-interactive-state="empty"] {
|
||||
@apply bg-transparent;
|
||||
--interactive-foreground: var(--text-02);
|
||||
--interactive-foreground-icon: var(--text-02);
|
||||
}
|
||||
.interactive[data-interactive-variant="sidebar-light"][data-interactive-state="empty"]:hover:not(
|
||||
[data-disabled]
|
||||
),
|
||||
.interactive[data-interactive-variant="sidebar-light"][data-interactive-state="empty"][data-interaction="hover"]:not(
|
||||
[data-disabled]
|
||||
) {
|
||||
@apply bg-background-tint-03;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Sidebar-Light — Filled
|
||||
--------------------------------------------------------------------------- */
|
||||
.interactive[data-interactive-variant="sidebar-light"][data-interactive-state="filled"] {
|
||||
@apply bg-transparent;
|
||||
--interactive-foreground: var(--text-02);
|
||||
--interactive-foreground-icon: var(--text-02);
|
||||
}
|
||||
.interactive[data-interactive-variant="sidebar-light"][data-interactive-state="filled"]:hover:not(
|
||||
[data-disabled]
|
||||
),
|
||||
.interactive[data-interactive-variant="sidebar-light"][data-interactive-state="filled"][data-interaction="hover"]:not(
|
||||
[data-disabled]
|
||||
) {
|
||||
@apply bg-background-tint-03;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Sidebar-Light — Selected
|
||||
--------------------------------------------------------------------------- */
|
||||
.interactive[data-interactive-variant="sidebar-light"][data-interactive-state="selected"] {
|
||||
@apply bg-background-tint-00;
|
||||
--interactive-foreground: var(--text-02);
|
||||
--interactive-foreground-icon: var(--text-02);
|
||||
}
|
||||
.interactive[data-interactive-variant="sidebar-light"][data-interactive-state="selected"]:hover:not(
|
||||
[data-disabled]
|
||||
),
|
||||
.interactive[data-interactive-variant="sidebar-light"][data-interactive-state="selected"][data-interaction="hover"]:not(
|
||||
[data-disabled]
|
||||
) {
|
||||
@apply bg-background-tint-03;
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { ButtonType, WithoutStyles } from "@opal/types";
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type InteractiveStatelessVariant = "none" | "default" | "action" | "danger";
|
||||
type InteractiveStatelessVariant = "default" | "action" | "danger";
|
||||
type InteractiveStatelessProminence =
|
||||
| "primary"
|
||||
| "secondary"
|
||||
@@ -108,8 +108,8 @@ function InteractiveStateless({
|
||||
);
|
||||
|
||||
const dataAttrs = {
|
||||
"data-interactive-variant": variant !== "none" ? variant : undefined,
|
||||
"data-interactive-prominence": variant !== "none" ? prominence : undefined,
|
||||
"data-interactive-variant": variant,
|
||||
"data-interactive-prominence": prominence,
|
||||
"data-interaction": interaction !== "rest" ? interaction : undefined,
|
||||
"data-disabled": isDisabled ? "true" : undefined,
|
||||
"aria-disabled": isDisabled || undefined,
|
||||
|
||||
@@ -4,10 +4,8 @@ import { Button } from "@opal/components/buttons/button/components";
|
||||
import type { ContainerSizeVariants } from "@opal/types";
|
||||
import SvgEdit from "@opal/icons/edit";
|
||||
import type { IconFunctionComponent, RichStr } from "@opal/types";
|
||||
import {
|
||||
resolveStr,
|
||||
toPlainString,
|
||||
} from "@opal/components/text/InlineMarkdown";
|
||||
import { Text, type TextFont } from "@opal/components/text/components";
|
||||
import { toPlainString } from "@opal/components/text/InlineMarkdown";
|
||||
import { cn } from "@opal/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -24,8 +22,8 @@ interface ContentLgPresetConfig {
|
||||
iconContainerPadding: string;
|
||||
/** Gap between icon container and content (CSS value). */
|
||||
gap: string;
|
||||
/** Tailwind font class for the title. */
|
||||
titleFont: string;
|
||||
/** Opal font name for the title (without `font-` prefix). */
|
||||
titleFont: TextFont;
|
||||
/** Title line-height — also used as icon container min-height (CSS value). */
|
||||
lineHeight: string;
|
||||
/** Button `size` prop for the edit button. Uses the shared `SizeVariant` scale. */
|
||||
@@ -53,9 +51,6 @@ interface ContentLgProps {
|
||||
/** Size preset. Default: `"headline"`. */
|
||||
sizePreset?: ContentLgSizePreset;
|
||||
|
||||
/** When `true`, the title color hooks into `Interactive`'s `--interactive-foreground` variable. */
|
||||
withInteractive?: boolean;
|
||||
|
||||
/** Ref forwarded to the root `<div>`. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
@@ -69,7 +64,7 @@ const CONTENT_LG_PRESETS: Record<ContentLgSizePreset, ContentLgPresetConfig> = {
|
||||
iconSize: "2rem",
|
||||
iconContainerPadding: "p-0.5",
|
||||
gap: "0.25rem",
|
||||
titleFont: "font-heading-h2",
|
||||
titleFont: "heading-h2",
|
||||
lineHeight: "2.25rem",
|
||||
editButtonSize: "md",
|
||||
editButtonPadding: "p-1",
|
||||
@@ -78,7 +73,7 @@ const CONTENT_LG_PRESETS: Record<ContentLgSizePreset, ContentLgPresetConfig> = {
|
||||
iconSize: "1.25rem",
|
||||
iconContainerPadding: "p-1",
|
||||
gap: "0rem",
|
||||
titleFont: "font-heading-h3-muted",
|
||||
titleFont: "heading-h3-muted",
|
||||
lineHeight: "1.75rem",
|
||||
editButtonSize: "sm",
|
||||
editButtonPadding: "p-0.5",
|
||||
@@ -96,7 +91,6 @@ function ContentLg({
|
||||
description,
|
||||
editable,
|
||||
onTitleChange,
|
||||
withInteractive,
|
||||
ref,
|
||||
}: ContentLgProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
@@ -116,12 +110,7 @@ function ContentLg({
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="opal-content-lg"
|
||||
data-interactive={withInteractive || undefined}
|
||||
style={{ gap: config.gap }}
|
||||
>
|
||||
<div ref={ref} className="opal-content-lg" style={{ gap: config.gap }}>
|
||||
{Icon && (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -142,14 +131,17 @@ function ContentLg({
|
||||
{editing ? (
|
||||
<div className="opal-content-lg-input-sizer">
|
||||
<span
|
||||
className={cn("opal-content-lg-input-mirror", config.titleFont)}
|
||||
className={cn(
|
||||
"opal-content-lg-input-mirror",
|
||||
`font-${config.titleFont}`
|
||||
)}
|
||||
>
|
||||
{editValue || "\u00A0"}
|
||||
</span>
|
||||
<input
|
||||
className={cn(
|
||||
"opal-content-lg-input",
|
||||
config.titleFont,
|
||||
`font-${config.titleFont}`,
|
||||
"text-text-04"
|
||||
)}
|
||||
value={editValue}
|
||||
@@ -169,19 +161,15 @@ function ContentLg({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
className={cn(
|
||||
"opal-content-lg-title",
|
||||
config.titleFont,
|
||||
"text-text-04",
|
||||
editable && "cursor-pointer"
|
||||
)}
|
||||
onClick={editable ? startEditing : undefined}
|
||||
style={{ height: config.lineHeight }}
|
||||
<Text
|
||||
font={config.titleFont}
|
||||
color="inherit"
|
||||
maxLines={1}
|
||||
title={toPlainString(title)}
|
||||
onClick={editable ? startEditing : undefined}
|
||||
>
|
||||
{resolveStr(title)}
|
||||
</span>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{editable && !editing && (
|
||||
@@ -204,8 +192,10 @@ function ContentLg({
|
||||
</div>
|
||||
|
||||
{description && toPlainString(description) && (
|
||||
<div className="opal-content-lg-description font-secondary-body text-text-03">
|
||||
{resolveStr(description)}
|
||||
<div className="opal-content-lg-description">
|
||||
<Text font="secondary-body" color="text-03" as="p">
|
||||
{description}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -8,10 +8,8 @@ import SvgAlertTriangle from "@opal/icons/alert-triangle";
|
||||
import SvgEdit from "@opal/icons/edit";
|
||||
import SvgXOctagon from "@opal/icons/x-octagon";
|
||||
import type { IconFunctionComponent, RichStr } from "@opal/types";
|
||||
import {
|
||||
resolveStr,
|
||||
toPlainString,
|
||||
} from "@opal/components/text/InlineMarkdown";
|
||||
import { Text, type TextFont } from "@opal/components/text/components";
|
||||
import { toPlainString } from "@opal/components/text/InlineMarkdown";
|
||||
import { cn } from "@opal/utils";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
@@ -23,16 +21,18 @@ type ContentMdSizePreset = "main-content" | "main-ui" | "secondary";
|
||||
|
||||
type ContentMdAuxIcon = "info-gray" | "info-blue" | "warning" | "error";
|
||||
|
||||
type ContentMdSuffix = "optional" | (string & {});
|
||||
|
||||
interface ContentMdPresetConfig {
|
||||
iconSize: string;
|
||||
iconContainerPadding: string;
|
||||
iconColorClass: string;
|
||||
titleFont: string;
|
||||
titleFont: TextFont;
|
||||
lineHeight: string;
|
||||
/** Button `size` prop for the edit button. Uses the shared `SizeVariant` scale. */
|
||||
editButtonSize: ContainerSizeVariants;
|
||||
editButtonPadding: string;
|
||||
optionalFont: string;
|
||||
optionalFont: TextFont;
|
||||
/** Aux icon size = lineHeight − 2 × p-0.5. */
|
||||
auxIconSize: string;
|
||||
/** Left indent for the description so it aligns with the title (past the icon). */
|
||||
@@ -55,11 +55,11 @@ interface ContentMdProps {
|
||||
/** Called when the user commits an edit. */
|
||||
onTitleChange?: (newTitle: string) => void;
|
||||
|
||||
/** When `true`, renders "(Optional)" beside the title. */
|
||||
optional?: boolean;
|
||||
|
||||
/** Custom muted suffix rendered beside the title. */
|
||||
titleSuffix?: string;
|
||||
/**
|
||||
* Muted suffix rendered beside the title.
|
||||
* Use `"optional"` for the standard "(Optional)" label, or pass any string.
|
||||
*/
|
||||
suffix?: ContentMdSuffix;
|
||||
|
||||
/** Auxiliary status icon rendered beside the title. */
|
||||
auxIcon?: ContentMdAuxIcon;
|
||||
@@ -70,18 +70,6 @@ interface ContentMdProps {
|
||||
/** Size preset. Default: `"main-ui"`. */
|
||||
sizePreset?: ContentMdSizePreset;
|
||||
|
||||
/** When `true`, the title color hooks into `Interactive`'s `--interactive-foreground` variable. */
|
||||
withInteractive?: boolean;
|
||||
|
||||
/** Optional class name applied to the title element. */
|
||||
titleClassName?: string;
|
||||
|
||||
/** Optional class name applied to the icon element. */
|
||||
iconClassName?: string;
|
||||
|
||||
/** Content rendered below the description, indented to align with it. */
|
||||
bottomChildren?: React.ReactNode;
|
||||
|
||||
/** Ref forwarded to the root `<div>`. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
@@ -95,11 +83,11 @@ const CONTENT_MD_PRESETS: Record<ContentMdSizePreset, ContentMdPresetConfig> = {
|
||||
iconSize: "1rem",
|
||||
iconContainerPadding: "p-1",
|
||||
iconColorClass: "text-text-04",
|
||||
titleFont: "font-main-content-emphasis",
|
||||
titleFont: "main-content-emphasis",
|
||||
lineHeight: "1.5rem",
|
||||
editButtonSize: "sm",
|
||||
editButtonPadding: "p-0",
|
||||
optionalFont: "font-main-content-muted",
|
||||
optionalFont: "main-content-muted",
|
||||
auxIconSize: "1.25rem",
|
||||
descriptionIndent: "1.625rem",
|
||||
},
|
||||
@@ -107,11 +95,11 @@ const CONTENT_MD_PRESETS: Record<ContentMdSizePreset, ContentMdPresetConfig> = {
|
||||
iconSize: "1rem",
|
||||
iconContainerPadding: "p-0.5",
|
||||
iconColorClass: "text-text-03",
|
||||
titleFont: "font-main-ui-action",
|
||||
titleFont: "main-ui-action",
|
||||
lineHeight: "1.25rem",
|
||||
editButtonSize: "xs",
|
||||
editButtonPadding: "p-0",
|
||||
optionalFont: "font-main-ui-muted",
|
||||
optionalFont: "main-ui-muted",
|
||||
auxIconSize: "1rem",
|
||||
descriptionIndent: "1.375rem",
|
||||
},
|
||||
@@ -119,11 +107,11 @@ const CONTENT_MD_PRESETS: Record<ContentMdSizePreset, ContentMdPresetConfig> = {
|
||||
iconSize: "0.75rem",
|
||||
iconContainerPadding: "p-0.5",
|
||||
iconColorClass: "text-text-04",
|
||||
titleFont: "font-secondary-action",
|
||||
titleFont: "secondary-action",
|
||||
lineHeight: "1rem",
|
||||
editButtonSize: "2xs",
|
||||
editButtonPadding: "p-0",
|
||||
optionalFont: "font-secondary-action",
|
||||
optionalFont: "secondary-action",
|
||||
auxIconSize: "0.75rem",
|
||||
descriptionIndent: "1.125rem",
|
||||
},
|
||||
@@ -149,15 +137,10 @@ function ContentMd({
|
||||
description,
|
||||
editable,
|
||||
onTitleChange,
|
||||
optional,
|
||||
titleSuffix,
|
||||
suffix,
|
||||
auxIcon,
|
||||
tag,
|
||||
sizePreset = "main-ui",
|
||||
withInteractive,
|
||||
titleClassName,
|
||||
iconClassName,
|
||||
bottomChildren,
|
||||
ref,
|
||||
}: ContentMdProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
@@ -178,11 +161,7 @@ function ContentMd({
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="opal-content-md"
|
||||
data-interactive={withInteractive || undefined}
|
||||
>
|
||||
<div ref={ref} className="opal-content-md">
|
||||
<div
|
||||
className="opal-content-md-header"
|
||||
data-editing={editing || undefined}
|
||||
@@ -196,11 +175,7 @@ function ContentMd({
|
||||
style={{ minHeight: config.lineHeight }}
|
||||
>
|
||||
<Icon
|
||||
className={cn(
|
||||
"opal-content-md-icon",
|
||||
config.iconColorClass,
|
||||
iconClassName
|
||||
)}
|
||||
className={cn("opal-content-md-icon", config.iconColorClass)}
|
||||
style={{ width: config.iconSize, height: config.iconSize }}
|
||||
/>
|
||||
</div>
|
||||
@@ -210,7 +185,10 @@ function ContentMd({
|
||||
{editing ? (
|
||||
<div className="opal-content-md-input-sizer">
|
||||
<span
|
||||
className={cn("opal-content-md-input-mirror", config.titleFont)}
|
||||
className={cn(
|
||||
"opal-content-md-input-mirror",
|
||||
`font-${config.titleFont}`
|
||||
)}
|
||||
>
|
||||
{editValue || "\u00A0"}
|
||||
</span>
|
||||
@@ -218,7 +196,7 @@ function ContentMd({
|
||||
ref={inputRef}
|
||||
className={cn(
|
||||
"opal-content-md-input",
|
||||
config.titleFont,
|
||||
`font-${config.titleFont}`,
|
||||
"text-text-04"
|
||||
)}
|
||||
value={editValue}
|
||||
@@ -238,29 +216,21 @@ function ContentMd({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
className={cn(
|
||||
"opal-content-md-title",
|
||||
config.titleFont,
|
||||
"text-text-04",
|
||||
editable && "cursor-pointer",
|
||||
titleClassName
|
||||
)}
|
||||
<Text
|
||||
font={config.titleFont}
|
||||
color="inherit"
|
||||
maxLines={1}
|
||||
title={toPlainString(title)}
|
||||
onClick={editable ? startEditing : undefined}
|
||||
style={{ height: config.lineHeight }}
|
||||
>
|
||||
{resolveStr(title)}
|
||||
</span>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{(optional || titleSuffix) && (
|
||||
<span
|
||||
className={cn(config.optionalFont, "text-text-03 shrink-0")}
|
||||
style={{ height: config.lineHeight }}
|
||||
>
|
||||
{titleSuffix ?? "(Optional)"}
|
||||
</span>
|
||||
{suffix && (
|
||||
<Text font={config.optionalFont} color="text-03">
|
||||
{suffix === "optional" ? "(Optional)" : suffix}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{auxIcon &&
|
||||
@@ -306,17 +276,12 @@ function ContentMd({
|
||||
|
||||
{description && toPlainString(description) && (
|
||||
<div
|
||||
className="opal-content-md-description font-secondary-body text-text-03"
|
||||
className="opal-content-md-description"
|
||||
style={Icon ? { paddingLeft: config.descriptionIndent } : undefined}
|
||||
>
|
||||
{resolveStr(description)}
|
||||
</div>
|
||||
)}
|
||||
{bottomChildren && (
|
||||
<div
|
||||
style={Icon ? { paddingLeft: config.descriptionIndent } : undefined}
|
||||
>
|
||||
{bottomChildren}
|
||||
<Text font="secondary-body" color="text-03" as="p">
|
||||
{description}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -327,5 +292,6 @@ export {
|
||||
ContentMd,
|
||||
type ContentMdProps,
|
||||
type ContentMdSizePreset,
|
||||
type ContentMdSuffix,
|
||||
type ContentMdAuxIcon,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import type { IconFunctionComponent, RichStr } from "@opal/types";
|
||||
import {
|
||||
resolveStr,
|
||||
toPlainString,
|
||||
} from "@opal/components/text/InlineMarkdown";
|
||||
import { Text, type TextFont } from "@opal/components/text/components";
|
||||
import { toPlainString } from "@opal/components/text/InlineMarkdown";
|
||||
import { cn } from "@opal/utils";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -13,15 +11,15 @@ import { cn } from "@opal/utils";
|
||||
|
||||
type ContentSmSizePreset = "main-content" | "main-ui" | "secondary";
|
||||
type ContentSmOrientation = "vertical" | "inline" | "reverse";
|
||||
type ContentSmProminence = "default" | "muted" | "muted-2x";
|
||||
type ContentSmProminence = "default" | "muted";
|
||||
|
||||
interface ContentSmPresetConfig {
|
||||
/** Icon width/height (CSS value). */
|
||||
iconSize: string;
|
||||
/** Tailwind padding class for the icon container. */
|
||||
iconContainerPadding: string;
|
||||
/** Tailwind font class for the title. */
|
||||
titleFont: string;
|
||||
/** Font preset for the title. */
|
||||
titleFont: TextFont;
|
||||
/** Title line-height — also used as icon container min-height (CSS value). */
|
||||
lineHeight: string;
|
||||
/** Gap between icon container and title (CSS value). */
|
||||
@@ -45,9 +43,6 @@ interface ContentSmProps {
|
||||
/** Title prominence. Default: `"default"`. */
|
||||
prominence?: ContentSmProminence;
|
||||
|
||||
/** When `true`, the title color hooks into `Interactive`'s `--interactive-foreground` variable. */
|
||||
withInteractive?: boolean;
|
||||
|
||||
/** Ref forwarded to the root `<div>`. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
@@ -60,21 +55,21 @@ const CONTENT_SM_PRESETS: Record<ContentSmSizePreset, ContentSmPresetConfig> = {
|
||||
"main-content": {
|
||||
iconSize: "1rem",
|
||||
iconContainerPadding: "p-1",
|
||||
titleFont: "font-main-content-body",
|
||||
titleFont: "main-content-body",
|
||||
lineHeight: "1.5rem",
|
||||
gap: "0.125rem",
|
||||
},
|
||||
"main-ui": {
|
||||
iconSize: "1rem",
|
||||
iconContainerPadding: "p-0.5",
|
||||
titleFont: "font-main-ui-action",
|
||||
titleFont: "main-ui-action",
|
||||
lineHeight: "1.25rem",
|
||||
gap: "0.25rem",
|
||||
},
|
||||
secondary: {
|
||||
iconSize: "0.75rem",
|
||||
iconContainerPadding: "p-0.5",
|
||||
titleFont: "font-secondary-action",
|
||||
titleFont: "secondary-action",
|
||||
lineHeight: "1rem",
|
||||
gap: "0.125rem",
|
||||
},
|
||||
@@ -90,7 +85,6 @@ function ContentSm({
|
||||
sizePreset = "main-ui",
|
||||
orientation = "inline",
|
||||
prominence = "default",
|
||||
withInteractive,
|
||||
ref,
|
||||
}: ContentSmProps) {
|
||||
const config = CONTENT_SM_PRESETS[sizePreset];
|
||||
@@ -101,7 +95,6 @@ function ContentSm({
|
||||
className="opal-content-sm"
|
||||
data-orientation={orientation}
|
||||
data-prominence={prominence}
|
||||
data-interactive={withInteractive || undefined}
|
||||
style={{ gap: config.gap }}
|
||||
>
|
||||
{Icon && (
|
||||
@@ -119,13 +112,14 @@ function ContentSm({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={cn("opal-content-sm-title", config.titleFont)}
|
||||
style={{ height: config.lineHeight }}
|
||||
<Text
|
||||
font={config.titleFont}
|
||||
color="inherit"
|
||||
maxLines={1}
|
||||
title={toPlainString(title)}
|
||||
>
|
||||
{resolveStr(title)}
|
||||
</span>
|
||||
{title}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@ import { Button } from "@opal/components/buttons/button/components";
|
||||
import type { ContainerSizeVariants } from "@opal/types";
|
||||
import SvgEdit from "@opal/icons/edit";
|
||||
import type { IconFunctionComponent, RichStr } from "@opal/types";
|
||||
import {
|
||||
resolveStr,
|
||||
toPlainString,
|
||||
} from "@opal/components/text/InlineMarkdown";
|
||||
import { Text, type TextFont } from "@opal/components/text/components";
|
||||
import { toPlainString } from "@opal/components/text/InlineMarkdown";
|
||||
import { cn } from "@opal/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -30,8 +28,8 @@ interface ContentXlPresetConfig {
|
||||
moreIcon2Size: string;
|
||||
/** Tailwind padding class for the more-icon-2 container. */
|
||||
moreIcon2ContainerPadding: string;
|
||||
/** Tailwind font class for the title. */
|
||||
titleFont: string;
|
||||
/** Opal font name for the title (without `font-` prefix). */
|
||||
titleFont: TextFont;
|
||||
/** Title line-height — also used as icon container min-height (CSS value). */
|
||||
lineHeight: string;
|
||||
/** Button `size` prop for the edit button. Uses the shared `SizeVariant` scale. */
|
||||
@@ -65,9 +63,6 @@ interface ContentXlProps {
|
||||
/** Optional tertiary icon rendered in the icon row. */
|
||||
moreIcon2?: IconFunctionComponent;
|
||||
|
||||
/** When `true`, the title color hooks into `Interactive`'s `--interactive-foreground` variable. */
|
||||
withInteractive?: boolean;
|
||||
|
||||
/** Ref forwarded to the root `<div>`. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
@@ -84,7 +79,7 @@ const CONTENT_XL_PRESETS: Record<ContentXlSizePreset, ContentXlPresetConfig> = {
|
||||
moreIcon1ContainerPadding: "p-0.5",
|
||||
moreIcon2Size: "2rem",
|
||||
moreIcon2ContainerPadding: "p-0.5",
|
||||
titleFont: "font-heading-h2",
|
||||
titleFont: "heading-h2",
|
||||
lineHeight: "2.25rem",
|
||||
editButtonSize: "md",
|
||||
editButtonPadding: "p-1",
|
||||
@@ -96,7 +91,7 @@ const CONTENT_XL_PRESETS: Record<ContentXlSizePreset, ContentXlPresetConfig> = {
|
||||
moreIcon1ContainerPadding: "p-0.5",
|
||||
moreIcon2Size: "1.5rem",
|
||||
moreIcon2ContainerPadding: "p-0.5",
|
||||
titleFont: "font-heading-h3",
|
||||
titleFont: "heading-h3",
|
||||
lineHeight: "1.75rem",
|
||||
editButtonSize: "sm",
|
||||
editButtonPadding: "p-0.5",
|
||||
@@ -116,7 +111,6 @@ function ContentXl({
|
||||
onTitleChange,
|
||||
moreIcon1: MoreIcon1,
|
||||
moreIcon2: MoreIcon2,
|
||||
withInteractive,
|
||||
ref,
|
||||
}: ContentXlProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
@@ -136,11 +130,7 @@ function ContentXl({
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="opal-content-xl"
|
||||
data-interactive={withInteractive || undefined}
|
||||
>
|
||||
<div ref={ref} className="opal-content-xl">
|
||||
{(Icon || MoreIcon1 || MoreIcon2) && (
|
||||
<div className="opal-content-xl-icon-row">
|
||||
{Icon && (
|
||||
@@ -199,14 +189,17 @@ function ContentXl({
|
||||
{editing ? (
|
||||
<div className="opal-content-xl-input-sizer">
|
||||
<span
|
||||
className={cn("opal-content-xl-input-mirror", config.titleFont)}
|
||||
className={cn(
|
||||
"opal-content-xl-input-mirror",
|
||||
`font-${config.titleFont}`
|
||||
)}
|
||||
>
|
||||
{editValue || "\u00A0"}
|
||||
</span>
|
||||
<input
|
||||
className={cn(
|
||||
"opal-content-xl-input",
|
||||
config.titleFont,
|
||||
`font-${config.titleFont}`,
|
||||
"text-text-04"
|
||||
)}
|
||||
value={editValue}
|
||||
@@ -226,19 +219,15 @@ function ContentXl({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
className={cn(
|
||||
"opal-content-xl-title",
|
||||
config.titleFont,
|
||||
"text-text-04",
|
||||
editable && "cursor-pointer"
|
||||
)}
|
||||
onClick={editable ? startEditing : undefined}
|
||||
style={{ height: config.lineHeight }}
|
||||
<Text
|
||||
font={config.titleFont}
|
||||
color="inherit"
|
||||
maxLines={1}
|
||||
title={toPlainString(title)}
|
||||
onClick={editable ? startEditing : undefined}
|
||||
>
|
||||
{resolveStr(title)}
|
||||
</span>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{editable && !editing && (
|
||||
@@ -261,8 +250,10 @@ function ContentXl({
|
||||
</div>
|
||||
|
||||
{description && toPlainString(description) && (
|
||||
<div className="opal-content-xl-description font-secondary-body text-text-03">
|
||||
{resolveStr(description)}
|
||||
<div className="opal-content-xl-description">
|
||||
<Text font="secondary-body" color="text-03" as="p">
|
||||
{description}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,39 +1,3 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// NOTE (@raunakab): Why Content uses resolveStr() instead of <Text>
|
||||
//
|
||||
// Content sub-components (ContentXl, ContentLg, ContentMd, ContentSm) render
|
||||
// titles and descriptions inside styled <span> elements that carry CSS classes
|
||||
// (e.g., `.opal-content-md-title`) for:
|
||||
//
|
||||
// 1. Truncation — `-webkit-box` + `-webkit-line-clamp` for single-line
|
||||
// clamping with ellipsis. This requires the text to be a DIRECT child
|
||||
// of the `-webkit-box` element. Wrapping it in a child <span> (which
|
||||
// is what <Text> renders) breaks the clamping behavior.
|
||||
//
|
||||
// 2. Pixel-exact sizing — the wrapper <span> has an explicit `height`
|
||||
// matching the font's `line-height`. Adding a child <Text> <span>
|
||||
// inside creates a double-span where the inner element's line-height
|
||||
// conflicts with the outer element's height, causing a ~4px vertical
|
||||
// offset.
|
||||
//
|
||||
// 3. Interactive color overrides — CSS selectors like
|
||||
// `.opal-content-md[data-interactive] .opal-content-md-title` set
|
||||
// `color: var(--interactive-foreground)`. <Text> with `color="inherit"`
|
||||
// can inherit this, but <Text> with any explicit color prop overrides
|
||||
// it. And the wrapper <span> needs the CSS class for the selector to
|
||||
// match — removing it breaks the cascade.
|
||||
//
|
||||
// 4. Horizontal padding — the title CSS class applies `padding: 0 0.125rem`
|
||||
// (2px). Since <Text> uses WithoutStyles (no className/style), this
|
||||
// padding cannot be applied to <Text> directly. A wrapper <div> was
|
||||
// attempted but introduced additional layout conflicts.
|
||||
//
|
||||
// For these reasons, Content uses `resolveStr()` from InlineMarkdown.tsx to
|
||||
// handle `string | RichStr` rendering. `resolveStr()` returns a ReactNode
|
||||
// that slots directly into the existing single <span> — no extra wrapper,
|
||||
// no layout conflicts, pixel-exact match with main.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import "@opal/layouts/content/styles.css";
|
||||
import {
|
||||
ContentSm,
|
||||
@@ -98,9 +62,6 @@ interface ContentBaseProps {
|
||||
*/
|
||||
widthVariant?: ExtremaSizeVariants;
|
||||
|
||||
/** When `true`, the title color hooks into `Interactive.Stateful`/`Interactive.Stateless`'s `--interactive-foreground` variable. */
|
||||
withInteractive?: boolean;
|
||||
|
||||
/** Ref forwarded to the root `<div>` of the resolved layout. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
@@ -130,20 +91,12 @@ type LgContentProps = ContentBaseProps & {
|
||||
type MdContentProps = ContentBaseProps & {
|
||||
sizePreset: "main-content" | "main-ui" | "secondary";
|
||||
variant?: "section";
|
||||
/** When `true`, renders "(Optional)" beside the title in the muted font variant. */
|
||||
optional?: boolean;
|
||||
/** Custom muted suffix rendered beside the title. */
|
||||
titleSuffix?: string;
|
||||
/** Muted suffix rendered beside the title. Use `"optional"` for "(Optional)". */
|
||||
suffix?: "optional" | (string & {});
|
||||
/** Auxiliary status icon rendered beside the title. */
|
||||
auxIcon?: "info-gray" | "info-blue" | "warning" | "error";
|
||||
/** Tag rendered beside the title. */
|
||||
tag?: TagProps;
|
||||
/** Optional class name applied to the title element. */
|
||||
titleClassName?: string;
|
||||
/** Optional class name applied to the icon element. */
|
||||
iconClassName?: string;
|
||||
/** Content rendered below the description, indented to align with it. */
|
||||
bottomChildren?: React.ReactNode;
|
||||
};
|
||||
|
||||
/** ContentSm does not support descriptions or inline editing. */
|
||||
@@ -174,7 +127,6 @@ function Content(props: ContentProps) {
|
||||
sizePreset = "headline",
|
||||
variant = "heading",
|
||||
widthVariant = "full",
|
||||
withInteractive,
|
||||
ref,
|
||||
...rest
|
||||
} = props;
|
||||
@@ -187,7 +139,6 @@ function Content(props: ContentProps) {
|
||||
layout = (
|
||||
<ContentXl
|
||||
sizePreset={sizePreset}
|
||||
withInteractive={withInteractive}
|
||||
ref={ref}
|
||||
{...(rest as Omit<ContentXlProps, "sizePreset">)}
|
||||
/>
|
||||
@@ -196,7 +147,6 @@ function Content(props: ContentProps) {
|
||||
layout = (
|
||||
<ContentLg
|
||||
sizePreset={sizePreset}
|
||||
withInteractive={withInteractive}
|
||||
ref={ref}
|
||||
{...(rest as Omit<ContentLgProps, "sizePreset">)}
|
||||
/>
|
||||
@@ -210,7 +160,6 @@ function Content(props: ContentProps) {
|
||||
layout = (
|
||||
<ContentMd
|
||||
sizePreset={sizePreset}
|
||||
withInteractive={withInteractive}
|
||||
ref={ref}
|
||||
{...(rest as Omit<ContentMdProps, "sizePreset">)}
|
||||
/>
|
||||
@@ -222,7 +171,6 @@ function Content(props: ContentProps) {
|
||||
layout = (
|
||||
<ContentSm
|
||||
sizePreset={sizePreset}
|
||||
withInteractive={withInteractive}
|
||||
ref={ref}
|
||||
{...(rest as Omit<
|
||||
React.ComponentProps<typeof ContentSm>,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-xl {
|
||||
@apply flex flex-col items-start;
|
||||
@apply flex flex-col items-start text-text-04;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
@@ -63,15 +63,6 @@
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.opal-content-xl-title {
|
||||
@apply text-left overflow-hidden text-text-04;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
padding: 0 0.125rem;
|
||||
min-width: 0.0625rem;
|
||||
}
|
||||
|
||||
.opal-content-xl-input-sizer {
|
||||
display: inline-grid;
|
||||
align-items: stretch;
|
||||
@@ -110,7 +101,6 @@
|
||||
|
||||
.opal-content-xl-description {
|
||||
@apply text-left w-full;
|
||||
padding: 0 0.125rem;
|
||||
}
|
||||
|
||||
/* ===========================================================================
|
||||
@@ -127,7 +117,7 @@
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-lg {
|
||||
@apply flex flex-row items-start;
|
||||
@apply flex flex-row items-start text-text-04;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
@@ -162,15 +152,6 @@
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.opal-content-lg-title {
|
||||
@apply text-left overflow-hidden text-text-04;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
padding: 0 0.125rem;
|
||||
min-width: 0.0625rem;
|
||||
}
|
||||
|
||||
.opal-content-lg-input-sizer {
|
||||
display: inline-grid;
|
||||
align-items: stretch;
|
||||
@@ -209,7 +190,6 @@
|
||||
|
||||
.opal-content-lg-description {
|
||||
@apply text-left w-full;
|
||||
padding: 0 0.125rem;
|
||||
}
|
||||
|
||||
/* ===========================================================================
|
||||
@@ -224,7 +204,7 @@
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-md {
|
||||
@apply flex flex-col items-start;
|
||||
@apply flex flex-col items-start text-text-04;
|
||||
}
|
||||
|
||||
.opal-content-md-header {
|
||||
@@ -255,15 +235,6 @@
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.opal-content-md-title {
|
||||
@apply text-left overflow-hidden text-text-04;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
padding: 0 0.125rem;
|
||||
min-width: 0.0625rem;
|
||||
}
|
||||
|
||||
.opal-content-md-input-sizer {
|
||||
display: inline-grid;
|
||||
align-items: stretch;
|
||||
@@ -313,7 +284,6 @@
|
||||
|
||||
.opal-content-md-description {
|
||||
@apply text-left w-full;
|
||||
padding: 0 0.125rem;
|
||||
}
|
||||
|
||||
/* ===========================================================================
|
||||
@@ -325,7 +295,7 @@
|
||||
reverse : flex-row-reverse — title left, icon right
|
||||
|
||||
Icon color is always text-03. Title color varies by prominence
|
||||
(text-04 default, text-03 muted, text-02 muted-2x) via data-prominence.
|
||||
(text-04 default, text-03 muted) via data-prominence.
|
||||
=========================================================================== */
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
@@ -334,7 +304,7 @@
|
||||
|
||||
.opal-content-sm {
|
||||
/* since `ContentSm` doesn't have a description, it's possible to center-align the icon and text */
|
||||
@apply flex items-center;
|
||||
@apply flex items-center text-text-04;
|
||||
}
|
||||
|
||||
.opal-content-sm[data-orientation="inline"] {
|
||||
@@ -367,68 +337,51 @@
|
||||
@apply text-text-02;
|
||||
}
|
||||
|
||||
.opal-content-sm[data-prominence="muted-2x"] .opal-content-sm-icon {
|
||||
@apply text-text-02;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Title
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-sm-title {
|
||||
@apply text-left overflow-hidden text-text-04 truncate;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
padding: 0 0.125rem;
|
||||
min-width: 0.0625rem;
|
||||
}
|
||||
|
||||
.opal-content-sm[data-prominence="muted"] .opal-content-sm-title {
|
||||
.opal-content-sm[data-prominence="muted"] {
|
||||
@apply text-text-03;
|
||||
}
|
||||
|
||||
.opal-content-sm[data-prominence="muted-2x"] .opal-content-sm-title {
|
||||
@apply text-text-02;
|
||||
}
|
||||
|
||||
/* ===========================================================================
|
||||
Interactive-foreground opt-in
|
||||
Interactive override
|
||||
|
||||
When a Content variant is nested inside an Interactive and
|
||||
`withInteractive` is set, the title and icon delegate their color to the
|
||||
`--interactive-foreground` / `--interactive-foreground-icon` CSS variables
|
||||
controlled by the ancestor Interactive variant.
|
||||
When a Content variant is nested inside an `.interactive` element,
|
||||
the title inherits color from the Interactive's `--interactive-foreground`
|
||||
and icons switch to `--interactive-foreground-icon`. This is automatic —
|
||||
no opt-in prop is required.
|
||||
=========================================================================== */
|
||||
|
||||
.opal-content-xl[data-interactive] .opal-content-xl-title {
|
||||
color: var(--interactive-foreground);
|
||||
.interactive .opal-content-xl {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.opal-content-xl[data-interactive] .opal-content-xl-icon {
|
||||
.interactive .opal-content-xl .opal-content-xl-icon {
|
||||
color: var(--interactive-foreground-icon);
|
||||
}
|
||||
|
||||
.opal-content-lg[data-interactive] .opal-content-lg-title {
|
||||
color: var(--interactive-foreground);
|
||||
.interactive .opal-content-lg {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.opal-content-lg[data-interactive] .opal-content-lg-icon {
|
||||
.interactive .opal-content-lg .opal-content-lg-icon {
|
||||
color: var(--interactive-foreground-icon);
|
||||
}
|
||||
|
||||
.opal-content-md[data-interactive] .opal-content-md-title {
|
||||
color: var(--interactive-foreground);
|
||||
.interactive .opal-content-md {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.opal-content-md[data-interactive] .opal-content-md-icon {
|
||||
.interactive .opal-content-md .opal-content-md-icon {
|
||||
color: var(--interactive-foreground-icon);
|
||||
}
|
||||
|
||||
.opal-content-sm[data-interactive] .opal-content-sm-title {
|
||||
color: var(--interactive-foreground);
|
||||
.interactive .opal-content-sm {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.opal-content-sm[data-interactive] .opal-content-sm-icon {
|
||||
.interactive .opal-content-sm .opal-content-sm-icon {
|
||||
color: var(--interactive-foreground-icon);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,14 @@ export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
/** Wraps a string for inline markdown parsing by `Text` and other Opal components. */
|
||||
export function markdown(content: string): RichStr {
|
||||
return { __brand: "RichStr", raw: content };
|
||||
/**
|
||||
* Wraps strings for inline markdown parsing by `Text` and other Opal components.
|
||||
*
|
||||
* Multiple arguments are joined with newlines, so each string renders on its own line:
|
||||
* ```tsx
|
||||
* markdown("Line one", "Line two", "Line three")
|
||||
* ```
|
||||
*/
|
||||
export function markdown(...lines: string[]): RichStr {
|
||||
return { __brand: "RichStr", raw: lines.join("\n") };
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ export const RenderField: FC<RenderFieldProps> = ({
|
||||
name={field.name}
|
||||
title={label}
|
||||
description={description}
|
||||
optional={field.optional}
|
||||
suffix={field.optional ? "optional" : undefined}
|
||||
>
|
||||
<InputTextAreaField
|
||||
name={field.name}
|
||||
|
||||
@@ -15,9 +15,8 @@ interface OrientationLayoutProps {
|
||||
nonInteractive?: boolean;
|
||||
children?: React.ReactNode;
|
||||
title: string | RichStr;
|
||||
titleSuffix?: string;
|
||||
description?: string | RichStr;
|
||||
optional?: boolean;
|
||||
suffix?: "optional" | (string & {});
|
||||
sizePreset?: "main-content" | "main-ui";
|
||||
}
|
||||
|
||||
@@ -53,18 +52,16 @@ function VerticalInputLayout({
|
||||
children,
|
||||
subDescription,
|
||||
title,
|
||||
titleSuffix,
|
||||
description,
|
||||
optional,
|
||||
suffix,
|
||||
sizePreset = "main-content",
|
||||
}: VerticalLayoutProps) {
|
||||
const content = (
|
||||
<Section gap={0.25} alignItems="start">
|
||||
<Content
|
||||
title={title}
|
||||
titleSuffix={titleSuffix}
|
||||
description={description}
|
||||
optional={optional}
|
||||
suffix={suffix}
|
||||
sizePreset={sizePreset}
|
||||
variant="section"
|
||||
/>
|
||||
@@ -130,9 +127,8 @@ function HorizontalInputLayout({
|
||||
children,
|
||||
center,
|
||||
title,
|
||||
titleSuffix,
|
||||
description,
|
||||
optional,
|
||||
suffix,
|
||||
sizePreset = "main-content",
|
||||
}: HorizontalLayoutProps) {
|
||||
const content = (
|
||||
@@ -145,9 +141,8 @@ function HorizontalInputLayout({
|
||||
<div className="flex flex-col flex-1 min-w-0 self-stretch">
|
||||
<Content
|
||||
title={title}
|
||||
titleSuffix={titleSuffix}
|
||||
description={description}
|
||||
optional={optional}
|
||||
suffix={suffix}
|
||||
sizePreset={sizePreset}
|
||||
variant="section"
|
||||
widthVariant="full"
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function SidebarTab({
|
||||
const content = (
|
||||
<div className="relative">
|
||||
<Interactive.Stateful
|
||||
variant="sidebar"
|
||||
variant={lowlight ? "sidebar-light" : "sidebar-heavy"}
|
||||
state={selected ? "selected" : "empty"}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
@@ -90,9 +90,6 @@ export default function SidebarTab({
|
||||
title={folded ? "" : children}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
prominence={
|
||||
lowlight ? "muted-2x" : selected ? "default" : "muted"
|
||||
}
|
||||
widthVariant="full"
|
||||
paddingVariant="fit"
|
||||
rightChildren={truncationSpacer}
|
||||
|
||||
@@ -1268,7 +1268,7 @@ export default function AgentEditorPage({
|
||||
<InputLayouts.Vertical
|
||||
name="description"
|
||||
title="Description"
|
||||
optional
|
||||
suffix="optional"
|
||||
>
|
||||
<InputTextAreaField
|
||||
name="description"
|
||||
@@ -1293,7 +1293,7 @@ export default function AgentEditorPage({
|
||||
<InputLayouts.Vertical
|
||||
name="instructions"
|
||||
title="Instructions"
|
||||
optional
|
||||
suffix="optional"
|
||||
description="Add instructions to tailor the response for this agent."
|
||||
>
|
||||
<InputTextAreaField
|
||||
@@ -1306,7 +1306,7 @@ export default function AgentEditorPage({
|
||||
name="starter_messages"
|
||||
title="Conversation Starters"
|
||||
description="Example messages that help users understand what this agent can do and how to interact with it effectively."
|
||||
optional
|
||||
suffix="optional"
|
||||
>
|
||||
<StarterMessages />
|
||||
</InputLayouts.Vertical>
|
||||
@@ -1546,7 +1546,7 @@ export default function AgentEditorPage({
|
||||
<InputLayouts.Horizontal
|
||||
name="knowledge_cutoff_date"
|
||||
title="Knowledge Cutoff Date"
|
||||
optional
|
||||
suffix="optional"
|
||||
description="Documents with a last-updated date prior to this will be ignored."
|
||||
>
|
||||
<InputDatePickerField
|
||||
@@ -1557,7 +1557,7 @@ export default function AgentEditorPage({
|
||||
<InputLayouts.Horizontal
|
||||
name="replace_base_system_prompt"
|
||||
title="Overwrite System Prompt"
|
||||
titleSuffix="(Not Recommended)"
|
||||
suffix="(Not Recommended)"
|
||||
description='Remove the base system prompt which includes useful instructions (e.g. "You can use Markdown tables"). This may affect response quality.'
|
||||
>
|
||||
<SwitchField name="replace_base_system_prompt" />
|
||||
@@ -1568,7 +1568,7 @@ export default function AgentEditorPage({
|
||||
<InputLayouts.Vertical
|
||||
name="reminders"
|
||||
title="Reminders"
|
||||
optional
|
||||
suffix="optional"
|
||||
>
|
||||
<InputTextAreaField
|
||||
name="reminders"
|
||||
|
||||
@@ -5,7 +5,8 @@ import { toast } from "@/hooks/useToast";
|
||||
import { Button } from "@opal/components";
|
||||
import { Disabled } from "@opal/core";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import { markdown } from "@opal/utils";
|
||||
import { Content } from "@opal/layouts";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
@@ -310,110 +311,109 @@ export default function ConnectedHookCard({
|
||||
!hook.is_active && "!bg-background-neutral-02"
|
||||
)}
|
||||
>
|
||||
<ContentAction
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="sm"
|
||||
icon={HookIcon}
|
||||
title={hook.name}
|
||||
titleClassName={!hook.is_active ? "line-through" : undefined}
|
||||
iconClassName="text-text-04"
|
||||
description={`Hook Point: ${spec?.display_name ?? hook.hook_point}`}
|
||||
bottomChildren={
|
||||
spec?.docs_url ? (
|
||||
<div className="w-full flex flex-row">
|
||||
<div className="flex-1 p-2">
|
||||
<Content
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
icon={HookIcon}
|
||||
title={!hook.is_active ? markdown(`~~${hook.name}~~`) : hook.name}
|
||||
description={`Hook Point: ${
|
||||
spec?.display_name ?? hook.hook_point
|
||||
}`}
|
||||
/>
|
||||
|
||||
{spec?.docs_url && (
|
||||
<a
|
||||
href={spec.docs_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 w-fit font-secondary-body text-text-03"
|
||||
className="pl-6 flex items-center gap-1"
|
||||
>
|
||||
<span className="underline">Documentation</span>
|
||||
<span className="underline font-secondary-body text-text-03">
|
||||
Documentation
|
||||
</span>
|
||||
<SvgExternalLink size={12} className="shrink-0" />
|
||||
</a>
|
||||
) : undefined
|
||||
}
|
||||
rightChildren={
|
||||
<Section
|
||||
flexDirection="column"
|
||||
alignItems="end"
|
||||
width="fit"
|
||||
height="fit"
|
||||
gap={0}
|
||||
>
|
||||
<div className="flex items-center gap-1 p-2">
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Section
|
||||
flexDirection="column"
|
||||
alignItems="end"
|
||||
width="fit"
|
||||
height="fit"
|
||||
gap={0}
|
||||
>
|
||||
<div className="flex items-center gap-1 p-2">
|
||||
{hook.is_active ? (
|
||||
<>
|
||||
<Text mainUiAction text03>
|
||||
Connected
|
||||
</Text>
|
||||
<SvgCheckCircle
|
||||
size={16}
|
||||
className="text-status-success-05"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1",
|
||||
isBusy ? "opacity-50 pointer-events-none" : "cursor-pointer"
|
||||
)}
|
||||
onClick={handleActivate}
|
||||
>
|
||||
<Text mainUiAction text03>
|
||||
Reconnect
|
||||
</Text>
|
||||
<SvgPlug size={16} className="text-text-03 shrink-0" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Disabled disabled={isBusy}>
|
||||
<div className="flex items-center gap-0.5 pl-1 pr-1 pb-1">
|
||||
{hook.is_active ? (
|
||||
<>
|
||||
<Text mainUiAction text03>
|
||||
Connected
|
||||
</Text>
|
||||
<SvgCheckCircle
|
||||
size={16}
|
||||
className="text-status-success-05"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1",
|
||||
isBusy
|
||||
? "opacity-50 pointer-events-none"
|
||||
: "cursor-pointer"
|
||||
)}
|
||||
onClick={handleActivate}
|
||||
>
|
||||
<Text mainUiAction text03>
|
||||
Reconnect
|
||||
</Text>
|
||||
<SvgPlug size={16} className="text-text-03 shrink-0" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Disabled disabled={isBusy}>
|
||||
{/* Plain div instead of Section: Section applies style={{ padding }} inline which
|
||||
overrides Tailwind padding classes, making per-side padding (pl/pr/pb) ineffective. */}
|
||||
<div className="flex items-center gap-0.5 pl-1 pr-1 pb-1">
|
||||
{hook.is_active ? (
|
||||
<>
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
size="sm"
|
||||
icon={SvgUnplug}
|
||||
onClick={() => setDisconnectConfirmOpen(true)}
|
||||
tooltip="Disconnect Hook"
|
||||
aria-label="Deactivate hook"
|
||||
/>
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
size="sm"
|
||||
icon={SvgRefreshCw}
|
||||
onClick={handleValidate}
|
||||
tooltip="Test Connection"
|
||||
aria-label="Re-validate hook"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
size="sm"
|
||||
icon={SvgTrash}
|
||||
onClick={() => setDeleteConfirmOpen(true)}
|
||||
tooltip="Delete"
|
||||
aria-label="Delete hook"
|
||||
icon={SvgUnplug}
|
||||
onClick={() => setDisconnectConfirmOpen(true)}
|
||||
tooltip="Disconnect Hook"
|
||||
aria-label="Deactivate hook"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
size="sm"
|
||||
icon={SvgRefreshCw}
|
||||
onClick={handleValidate}
|
||||
tooltip="Test Connection"
|
||||
aria-label="Re-validate hook"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
size="sm"
|
||||
icon={SvgSettings}
|
||||
onClick={onEdit}
|
||||
tooltip="Manage"
|
||||
aria-label="Configure hook"
|
||||
icon={SvgTrash}
|
||||
onClick={() => setDeleteConfirmOpen(true)}
|
||||
tooltip="Delete"
|
||||
aria-label="Delete hook"
|
||||
/>
|
||||
</div>
|
||||
</Disabled>
|
||||
</Section>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
size="sm"
|
||||
icon={SvgSettings}
|
||||
onClick={onEdit}
|
||||
tooltip="Manage"
|
||||
aria-label="Configure hook"
|
||||
/>
|
||||
</div>
|
||||
</Disabled>
|
||||
</Section>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useHookSpecs } from "@/hooks/useHookSpecs";
|
||||
import { useHooks } from "@/hooks/useHooks";
|
||||
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
|
||||
import { Button } from "@opal/components";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import { Content } from "@opal/layouts";
|
||||
import InputSearch from "@/refresh-components/inputs/InputSearch";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
HookPointMeta,
|
||||
HookResponse,
|
||||
} from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
import { markdown } from "@opal/utils";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main component
|
||||
@@ -145,37 +146,39 @@ export default function HooksContent() {
|
||||
gap={0}
|
||||
className="hover:border-border-02"
|
||||
>
|
||||
<ContentAction
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
paddingVariant="sm"
|
||||
icon={UnconnectedIcon}
|
||||
title={spec.display_name}
|
||||
iconClassName="text-text-04"
|
||||
description={spec.description}
|
||||
bottomChildren={
|
||||
spec.docs_url ? (
|
||||
<div className="w-full flex flex-row">
|
||||
<div className="flex-1 p-2">
|
||||
<Content
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
icon={UnconnectedIcon}
|
||||
title={spec.display_name}
|
||||
description={spec.description}
|
||||
/>
|
||||
|
||||
{spec.docs_url && (
|
||||
<a
|
||||
href={spec.docs_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 w-fit font-secondary-body text-text-03"
|
||||
className="pl-6 flex items-center gap-1"
|
||||
>
|
||||
<span className="underline">Documentation</span>
|
||||
<span className="underline font-secondary-body text-text-03">
|
||||
Documentation
|
||||
</span>
|
||||
<SvgExternalLink size={12} className="shrink-0" />
|
||||
</a>
|
||||
) : undefined
|
||||
}
|
||||
rightChildren={
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
rightIcon={SvgArrowExchange}
|
||||
onClick={() => setConnectSpec(spec)}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
rightIcon={SvgArrowExchange}
|
||||
onClick={() => setConnectSpec(spec)}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -216,7 +216,7 @@ function ExistingProviderCard({
|
||||
icon={getProviderIcon(provider.provider)}
|
||||
title={provider.name}
|
||||
description={getProviderDisplayName(provider.provider)}
|
||||
sizePreset="main-content"
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
tag={isDefault ? { title: "Default", color: "blue" } : undefined}
|
||||
rightChildren={
|
||||
@@ -280,7 +280,7 @@ function NewProviderCard({
|
||||
icon={getProviderIcon(provider.name)}
|
||||
title={getProviderProductName(provider.name)}
|
||||
description={getProviderDisplayName(provider.name)}
|
||||
sizePreset="main-content"
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
rightChildren={
|
||||
<Button
|
||||
@@ -316,7 +316,7 @@ function NewCustomProviderCard({
|
||||
icon={getProviderIcon("custom")}
|
||||
title={getProviderProductName("custom")}
|
||||
description={getProviderDisplayName("custom")}
|
||||
sizePreset="main-content"
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
rightChildren={
|
||||
<Button
|
||||
|
||||
@@ -158,7 +158,7 @@ export default function AddMCPServerModal({
|
||||
<InputLayouts.Vertical
|
||||
name="description"
|
||||
title="Description"
|
||||
optional
|
||||
suffix="optional"
|
||||
>
|
||||
<InputTextAreaField
|
||||
name="description"
|
||||
|
||||
@@ -127,10 +127,9 @@ export default function AgentCard({ agent }: AgentCardProps) {
|
||||
{fullAgent && <AgentViewerModal agent={fullAgent} />}
|
||||
</agentViewerModal.Provider>
|
||||
|
||||
<Interactive.Stateless
|
||||
<Interactive.Simple
|
||||
onClick={() => agentViewerModal.toggle(true)}
|
||||
group="group/AgentCard"
|
||||
variant="none"
|
||||
>
|
||||
<Card
|
||||
padding={0}
|
||||
@@ -232,7 +231,7 @@ export default function AgentCard({ agent }: AgentCardProps) {
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Interactive.Stateless>
|
||||
</Interactive.Simple>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,13 +29,12 @@ export default function DocumentSetCard({
|
||||
disabled={!disabled || !disabledTooltip}
|
||||
>
|
||||
<div className="max-w-[12rem]">
|
||||
<Interactive.Stateless
|
||||
<Interactive.Simple
|
||||
onClick={
|
||||
disabled || isSelected === undefined
|
||||
? undefined
|
||||
: () => onSelectToggle?.(!isSelected)
|
||||
}
|
||||
variant="none"
|
||||
>
|
||||
<Interactive.Container
|
||||
data-testid={`document-set-card-${documentSet.id}`}
|
||||
@@ -64,7 +63,7 @@ export default function DocumentSetCard({
|
||||
/>
|
||||
<Spacer horizontal rem={0.5} />
|
||||
</Interactive.Container>
|
||||
</Interactive.Stateless>
|
||||
</Interactive.Simple>
|
||||
</div>
|
||||
</SimpleTooltip>
|
||||
);
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function FeedbackModal({
|
||||
<InputLayouts.Vertical
|
||||
name="additional_feedback"
|
||||
title="Provide Additional Details"
|
||||
optional={feedbackType === "like"}
|
||||
suffix={feedbackType === "like" ? "optional" : undefined}
|
||||
>
|
||||
<InputTextAreaField
|
||||
name="additional_feedback"
|
||||
|
||||
@@ -126,7 +126,7 @@ function BifrostModalInternals({
|
||||
<InputLayouts.Vertical
|
||||
name="api_key"
|
||||
title="API Key"
|
||||
optional={true}
|
||||
suffix="optional"
|
||||
subDescription={markdown(
|
||||
"Paste your API key from [Bifrost](https://docs.getbifrost.ai/overview) to access your models."
|
||||
)}
|
||||
|
||||
@@ -147,7 +147,7 @@ function LMStudioFormInternals({
|
||||
name="custom_config.LM_STUDIO_API_KEY"
|
||||
title="API Key"
|
||||
subDescription="Optional API key if your LM Studio server requires authentication."
|
||||
optional
|
||||
suffix="optional"
|
||||
>
|
||||
<PasswordInputTypeInField
|
||||
name="custom_config.LM_STUDIO_API_KEY"
|
||||
|
||||
@@ -103,7 +103,7 @@ export function APIKeyField({
|
||||
? `Paste your API key from ${providerName} to access your models.`
|
||||
: "Paste your API key to access your models."
|
||||
}
|
||||
optional={optional}
|
||||
suffix={optional ? "optional" : undefined}
|
||||
>
|
||||
<PasswordInputTypeInField name="api_key" placeholder="API Key" />
|
||||
</InputLayouts.Vertical>
|
||||
|
||||
Reference in New Issue
Block a user