[优化] 增加 OpenResty 配置选项和性能页面工具提示

This commit is contained in:
ryan
2026-03-13 00:14:39 +08:00
parent 2ad6e9a2d0
commit a693d98457
4 changed files with 373 additions and 202 deletions
+6
View File
@@ -65,10 +65,13 @@ var OpenRestyKeepaliveTimeout = 65
var OpenRestyKeepaliveRequests = 1000 var OpenRestyKeepaliveRequests = 1000
var OpenRestyClientHeaderTimeout = 15 var OpenRestyClientHeaderTimeout = 15
var OpenRestyClientBodyTimeout = 15 var OpenRestyClientBodyTimeout = 15
var OpenRestyClientMaxBodySize = "64m"
var OpenRestyLargeClientHeaderBuffers = "4 16k"
var OpenRestySendTimeout = 30 var OpenRestySendTimeout = 30
var OpenRestyProxyConnectTimeout = 5 var OpenRestyProxyConnectTimeout = 5
var OpenRestyProxySendTimeout = 60 var OpenRestyProxySendTimeout = 60
var OpenRestyProxyReadTimeout = 60 var OpenRestyProxyReadTimeout = 60
var OpenRestyProxyRequestBufferingEnabled = true
var OpenRestyProxyBufferingEnabled = true var OpenRestyProxyBufferingEnabled = true
var OpenRestyProxyBuffers = "16 16k" var OpenRestyProxyBuffers = "16 16k"
var OpenRestyProxyBufferSize = "8k" var OpenRestyProxyBufferSize = "8k"
@@ -104,10 +107,13 @@ http {
keepalive_requests {{OpenRestyKeepaliveRequests}}; keepalive_requests {{OpenRestyKeepaliveRequests}};
client_header_timeout {{OpenRestyClientHeaderTimeout}}; client_header_timeout {{OpenRestyClientHeaderTimeout}};
client_body_timeout {{OpenRestyClientBodyTimeout}}; client_body_timeout {{OpenRestyClientBodyTimeout}};
client_max_body_size {{OpenRestyClientMaxBodySize}};
large_client_header_buffers {{OpenRestyLargeClientHeaderBuffers}};
send_timeout {{OpenRestySendTimeout}}; send_timeout {{OpenRestySendTimeout}};
proxy_connect_timeout {{OpenRestyProxyConnectTimeout}}; proxy_connect_timeout {{OpenRestyProxyConnectTimeout}};
proxy_send_timeout {{OpenRestyProxySendTimeout}}; proxy_send_timeout {{OpenRestyProxySendTimeout}};
proxy_read_timeout {{OpenRestyProxyReadTimeout}}; proxy_read_timeout {{OpenRestyProxyReadTimeout}};
proxy_request_buffering {{OpenRestyProxyRequestBuffering}};
proxy_buffering {{OpenRestyProxyBuffering}}; proxy_buffering {{OpenRestyProxyBuffering}};
proxy_buffers {{OpenRestyProxyBuffers}}; proxy_buffers {{OpenRestyProxyBuffers}};
proxy_buffer_size {{OpenRestyProxyBufferSize}}; proxy_buffer_size {{OpenRestyProxyBufferSize}};
+13
View File
@@ -64,10 +64,13 @@ func InitOptionMap() {
common.OptionMap["OpenRestyKeepaliveRequests"] = strconv.Itoa(common.OpenRestyKeepaliveRequests) common.OptionMap["OpenRestyKeepaliveRequests"] = strconv.Itoa(common.OpenRestyKeepaliveRequests)
common.OptionMap["OpenRestyClientHeaderTimeout"] = strconv.Itoa(common.OpenRestyClientHeaderTimeout) common.OptionMap["OpenRestyClientHeaderTimeout"] = strconv.Itoa(common.OpenRestyClientHeaderTimeout)
common.OptionMap["OpenRestyClientBodyTimeout"] = strconv.Itoa(common.OpenRestyClientBodyTimeout) common.OptionMap["OpenRestyClientBodyTimeout"] = strconv.Itoa(common.OpenRestyClientBodyTimeout)
common.OptionMap["OpenRestyClientMaxBodySize"] = common.OpenRestyClientMaxBodySize
common.OptionMap["OpenRestyLargeClientHeaderBuffers"] = common.OpenRestyLargeClientHeaderBuffers
common.OptionMap["OpenRestySendTimeout"] = strconv.Itoa(common.OpenRestySendTimeout) common.OptionMap["OpenRestySendTimeout"] = strconv.Itoa(common.OpenRestySendTimeout)
common.OptionMap["OpenRestyProxyConnectTimeout"] = strconv.Itoa(common.OpenRestyProxyConnectTimeout) common.OptionMap["OpenRestyProxyConnectTimeout"] = strconv.Itoa(common.OpenRestyProxyConnectTimeout)
common.OptionMap["OpenRestyProxySendTimeout"] = strconv.Itoa(common.OpenRestyProxySendTimeout) common.OptionMap["OpenRestyProxySendTimeout"] = strconv.Itoa(common.OpenRestyProxySendTimeout)
common.OptionMap["OpenRestyProxyReadTimeout"] = strconv.Itoa(common.OpenRestyProxyReadTimeout) common.OptionMap["OpenRestyProxyReadTimeout"] = strconv.Itoa(common.OpenRestyProxyReadTimeout)
common.OptionMap["OpenRestyProxyRequestBufferingEnabled"] = strconv.FormatBool(common.OpenRestyProxyRequestBufferingEnabled)
common.OptionMap["OpenRestyProxyBufferingEnabled"] = strconv.FormatBool(common.OpenRestyProxyBufferingEnabled) common.OptionMap["OpenRestyProxyBufferingEnabled"] = strconv.FormatBool(common.OpenRestyProxyBufferingEnabled)
common.OptionMap["OpenRestyProxyBuffers"] = common.OpenRestyProxyBuffers common.OptionMap["OpenRestyProxyBuffers"] = common.OpenRestyProxyBuffers
common.OptionMap["OpenRestyProxyBufferSize"] = common.OpenRestyProxyBufferSize common.OptionMap["OpenRestyProxyBufferSize"] = common.OpenRestyProxyBufferSize
@@ -240,6 +243,14 @@ func updateOptionMap(key string, value string) {
if v, err := strconv.Atoi(value); err == nil && v > 0 { if v, err := strconv.Atoi(value); err == nil && v > 0 {
common.OpenRestyClientBodyTimeout = v common.OpenRestyClientBodyTimeout = v
} }
case "OpenRestyClientMaxBodySize":
if strings.TrimSpace(value) != "" {
common.OpenRestyClientMaxBodySize = value
}
case "OpenRestyLargeClientHeaderBuffers":
if strings.TrimSpace(value) != "" {
common.OpenRestyLargeClientHeaderBuffers = value
}
case "OpenRestySendTimeout": case "OpenRestySendTimeout":
if v, err := strconv.Atoi(value); err == nil && v > 0 { if v, err := strconv.Atoi(value); err == nil && v > 0 {
common.OpenRestySendTimeout = v common.OpenRestySendTimeout = v
@@ -256,6 +267,8 @@ func updateOptionMap(key string, value string) {
if v, err := strconv.Atoi(value); err == nil && v > 0 { if v, err := strconv.Atoi(value); err == nil && v > 0 {
common.OpenRestyProxyReadTimeout = v common.OpenRestyProxyReadTimeout = v
} }
case "OpenRestyProxyRequestBufferingEnabled":
common.OpenRestyProxyRequestBufferingEnabled = value == "true"
case "OpenRestyProxyBufferingEnabled": case "OpenRestyProxyBufferingEnabled":
common.OpenRestyProxyBufferingEnabled = value == "true" common.OpenRestyProxyBufferingEnabled = value == "true"
case "OpenRestyProxyBuffers": case "OpenRestyProxyBuffers":
@@ -61,6 +61,47 @@ const defaultPerformanceFields = {
'error timeout updating http_500 http_502 http_503 http_504', 'error timeout updating http_500 http_502 http_503 http_504',
}; };
const performanceFieldTooltips: Record<string, string> = {
worker_processes:
'Nginx worker 进程数。通常建议保持 auto,让 OpenResty 按 CPU 核数自动分配。',
worker_connections:
'每个 worker 可同时处理的最大连接数。值越大,可承载的并发连接越多。',
worker_rlimit_nofile:
'提升 worker 可打开的文件描述符上限,避免高并发下连接或文件句柄不足。',
events_use:
'指定事件驱动模型。Linux 常见是 epoll,留空时由 OpenResty 自动选择。',
multi_accept:
'开启后,worker 会尽可能一次接受多个新连接,适合高吞吐接入场景。',
keepalive_timeout: '客户端 Keep-Alive 空闲保持时间,单位秒。',
keepalive_requests: '单个长连接允许复用的最大请求数。',
client_header_timeout: '读取客户端请求头的超时时间,单位秒。',
client_body_timeout: '读取客户端请求体的超时时间,单位秒。',
send_timeout: '向客户端发送响应时的超时时间,单位秒。',
proxy_connect_timeout: '连接上游源站的超时时间,单位秒。',
proxy_send_timeout: '向上游发送请求的超时时间,单位秒。',
proxy_read_timeout: '等待上游返回响应的超时时间,单位秒。',
proxy_buffering:
'控制是否启用代理响应缓冲。开启后通常有更平滑的吞吐,但会增加内存占用。',
proxy_buffers: '设置代理响应缓冲区的数量和大小,例如 16 16k。',
proxy_buffer_size: '保存响应头等小块数据的基础缓冲区大小。',
proxy_busy_buffers_size: '限制 busy 状态下可同时占用的缓冲区总大小。',
gzip: '控制是否启用 gzip 压缩响应。',
gzip_min_length:
'只有响应体超过该字节数时才会启用 gzip,避免对极小响应做无意义压缩。',
gzip_comp_level: 'gzip 压缩等级,1 更省 CPU,9 压缩更高但更耗 CPU。',
proxy_cache_path: '缓存目录路径,对应 proxy_cache_path 指令中的磁盘位置。',
levels: '缓存目录层级,例如 1:2,可控制缓存文件的目录分布。',
inactive: '缓存对象在未命中访问时的失活时间,例如 30m。',
max_size:
'缓存目录允许占用的最大磁盘空间,会渲染到 proxy_cache_path 的 max_size。',
cache_key_template: '生成缓存 Key 的模板,决定不同请求如何命中同一缓存对象。',
proxy_cache_lock:
'启用后,同一缓存 Key 未命中时只允许一个请求回源,减少击穿。',
proxy_cache_lock_timeout: '等待缓存锁的最长时间,例如 5s。',
proxy_cache_use_stale:
'上游异常时允许返回旧缓存的条件列表,例如 error、timeout、http_500。',
};
type PerformanceTab = 'settings' | 'editor'; type PerformanceTab = 'settings' | 'editor';
type FeedbackState = { type FeedbackState = {
@@ -609,7 +650,10 @@ export function PerformancePage() {
} }
> >
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3"> <div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
<ResourceField label="worker_processes"> <ResourceField
label="worker_processes"
tooltip={performanceFieldTooltips.worker_processes}
>
<ResourceInput <ResourceInput
value={performanceFields.OpenRestyWorkerProcesses} value={performanceFields.OpenRestyWorkerProcesses}
onChange={(event) => onChange={(event) =>
@@ -621,7 +665,10 @@ export function PerformancePage() {
placeholder="auto" placeholder="auto"
/> />
</ResourceField> </ResourceField>
<ResourceField label="worker_connections"> <ResourceField
label="worker_connections"
tooltip={performanceFieldTooltips.worker_connections}
>
<ResourceInput <ResourceInput
type="number" type="number"
value={performanceFields.OpenRestyWorkerConnections} value={performanceFields.OpenRestyWorkerConnections}
@@ -633,7 +680,10 @@ export function PerformancePage() {
} }
/> />
</ResourceField> </ResourceField>
<ResourceField label="worker_rlimit_nofile"> <ResourceField
label="worker_rlimit_nofile"
tooltip={performanceFieldTooltips.worker_rlimit_nofile}
>
<ResourceInput <ResourceInput
type="number" type="number"
value={performanceFields.OpenRestyWorkerRlimitNofile} value={performanceFields.OpenRestyWorkerRlimitNofile}
@@ -645,7 +695,11 @@ export function PerformancePage() {
} }
/> />
</ResourceField> </ResourceField>
<ResourceField label="events use" hint="留空表示不显式渲染。"> <ResourceField
label="events use"
hint="留空表示不显式渲染。"
tooltip={performanceFieldTooltips.events_use}
>
<ResourceSelect <ResourceSelect
value={performanceFields.OpenRestyEventsUse} value={performanceFields.OpenRestyEventsUse}
onChange={(event) => onChange={(event) =>
@@ -665,6 +719,7 @@ export function PerformancePage() {
<ToggleField <ToggleField
label="multi_accept" label="multi_accept"
description="" description=""
tooltip={performanceFieldTooltips.multi_accept}
checked={performanceFields.OpenRestyEventsMultiAcceptEnabled} checked={performanceFields.OpenRestyEventsMultiAcceptEnabled}
onChange={(checked) => onChange={(checked) =>
setPerformanceFields((previous) => ({ setPerformanceFields((previous) => ({
@@ -673,7 +728,10 @@ export function PerformancePage() {
})) }))
} }
/> />
<ResourceField label="keepalive_timeout (秒)"> <ResourceField
label="keepalive_timeout (秒)"
tooltip={performanceFieldTooltips.keepalive_timeout}
>
<ResourceInput <ResourceInput
type="number" type="number"
value={performanceFields.OpenRestyKeepaliveTimeout} value={performanceFields.OpenRestyKeepaliveTimeout}
@@ -685,7 +743,10 @@ export function PerformancePage() {
} }
/> />
</ResourceField> </ResourceField>
<ResourceField label="keepalive_requests"> <ResourceField
label="keepalive_requests"
tooltip={performanceFieldTooltips.keepalive_requests}
>
<ResourceInput <ResourceInput
type="number" type="number"
value={performanceFields.OpenRestyKeepaliveRequests} value={performanceFields.OpenRestyKeepaliveRequests}
@@ -697,7 +758,10 @@ export function PerformancePage() {
} }
/> />
</ResourceField> </ResourceField>
<ResourceField label="client_header_timeout (秒)"> <ResourceField
label="client_header_timeout (秒)"
tooltip={performanceFieldTooltips.client_header_timeout}
>
<ResourceInput <ResourceInput
type="number" type="number"
value={performanceFields.OpenRestyClientHeaderTimeout} value={performanceFields.OpenRestyClientHeaderTimeout}
@@ -709,7 +773,10 @@ export function PerformancePage() {
} }
/> />
</ResourceField> </ResourceField>
<ResourceField label="client_body_timeout (秒)"> <ResourceField
label="client_body_timeout (秒)"
tooltip={performanceFieldTooltips.client_body_timeout}
>
<ResourceInput <ResourceInput
type="number" type="number"
value={performanceFields.OpenRestyClientBodyTimeout} value={performanceFields.OpenRestyClientBodyTimeout}
@@ -721,7 +788,10 @@ export function PerformancePage() {
} }
/> />
</ResourceField> </ResourceField>
<ResourceField label="send_timeout (秒)"> <ResourceField
label="send_timeout (秒)"
tooltip={performanceFieldTooltips.send_timeout}
>
<ResourceInput <ResourceInput
type="number" type="number"
value={performanceFields.OpenRestySendTimeout} value={performanceFields.OpenRestySendTimeout}
@@ -753,7 +823,10 @@ export function PerformancePage() {
} }
> >
<div className="grid gap-5 md:grid-cols-2"> <div className="grid gap-5 md:grid-cols-2">
<ResourceField label="proxy_connect_timeout (秒)"> <ResourceField
label="proxy_connect_timeout (秒)"
tooltip={performanceFieldTooltips.proxy_connect_timeout}
>
<ResourceInput <ResourceInput
type="number" type="number"
value={performanceFields.OpenRestyProxyConnectTimeout} value={performanceFields.OpenRestyProxyConnectTimeout}
@@ -765,7 +838,10 @@ export function PerformancePage() {
} }
/> />
</ResourceField> </ResourceField>
<ResourceField label="proxy_send_timeout (秒)"> <ResourceField
label="proxy_send_timeout (秒)"
tooltip={performanceFieldTooltips.proxy_send_timeout}
>
<ResourceInput <ResourceInput
type="number" type="number"
value={performanceFields.OpenRestyProxySendTimeout} value={performanceFields.OpenRestyProxySendTimeout}
@@ -777,7 +853,10 @@ export function PerformancePage() {
} }
/> />
</ResourceField> </ResourceField>
<ResourceField label="proxy_read_timeout (秒)"> <ResourceField
label="proxy_read_timeout (秒)"
tooltip={performanceFieldTooltips.proxy_read_timeout}
>
<ResourceInput <ResourceInput
type="number" type="number"
value={performanceFields.OpenRestyProxyReadTimeout} value={performanceFields.OpenRestyProxyReadTimeout}
@@ -792,6 +871,7 @@ export function PerformancePage() {
<ToggleField <ToggleField
label="proxy_buffering" label="proxy_buffering"
description="是否启用 proxy_buffering。" description="是否启用 proxy_buffering。"
tooltip={performanceFieldTooltips.proxy_buffering}
checked={performanceFields.OpenRestyProxyBufferingEnabled} checked={performanceFields.OpenRestyProxyBufferingEnabled}
onChange={(checked) => onChange={(checked) =>
setPerformanceFields((previous) => ({ setPerformanceFields((previous) => ({
@@ -800,7 +880,10 @@ export function PerformancePage() {
})) }))
} }
/> />
<ResourceField label="proxy_buffers"> <ResourceField
label="proxy_buffers"
tooltip={performanceFieldTooltips.proxy_buffers}
>
<ResourceInput <ResourceInput
value={performanceFields.OpenRestyProxyBuffers} value={performanceFields.OpenRestyProxyBuffers}
onChange={(event) => onChange={(event) =>
@@ -812,7 +895,10 @@ export function PerformancePage() {
placeholder="16 16k" placeholder="16 16k"
/> />
</ResourceField> </ResourceField>
<ResourceField label="proxy_buffer_size"> <ResourceField
label="proxy_buffer_size"
tooltip={performanceFieldTooltips.proxy_buffer_size}
>
<ResourceInput <ResourceInput
value={performanceFields.OpenRestyProxyBufferSize} value={performanceFields.OpenRestyProxyBufferSize}
onChange={(event) => onChange={(event) =>
@@ -824,7 +910,10 @@ export function PerformancePage() {
placeholder="8k" placeholder="8k"
/> />
</ResourceField> </ResourceField>
<ResourceField label="proxy_busy_buffers_size"> <ResourceField
label="proxy_busy_buffers_size"
tooltip={performanceFieldTooltips.proxy_busy_buffers_size}
>
<ResourceInput <ResourceInput
value={performanceFields.OpenRestyProxyBusyBuffersSize} value={performanceFields.OpenRestyProxyBusyBuffersSize}
onChange={(event) => onChange={(event) =>
@@ -860,6 +949,7 @@ export function PerformancePage() {
<ToggleField <ToggleField
label="gzip" label="gzip"
description="是否启用 gzip on。" description="是否启用 gzip on。"
tooltip={performanceFieldTooltips.gzip}
checked={performanceFields.OpenRestyGzipEnabled} checked={performanceFields.OpenRestyGzipEnabled}
onChange={(checked) => onChange={(checked) =>
setPerformanceFields((previous) => ({ setPerformanceFields((previous) => ({
@@ -882,7 +972,10 @@ export function PerformancePage() {
</p> </p>
)} )}
</div> </div>
<ResourceField label="gzip_min_length"> <ResourceField
label="gzip_min_length"
tooltip={performanceFieldTooltips.gzip_min_length}
>
<ResourceInput <ResourceInput
type="number" type="number"
value={performanceFields.OpenRestyGzipMinLength} value={performanceFields.OpenRestyGzipMinLength}
@@ -894,7 +987,10 @@ export function PerformancePage() {
} }
/> />
</ResourceField> </ResourceField>
<ResourceField label="gzip_comp_level"> <ResourceField
label="gzip_comp_level"
tooltip={performanceFieldTooltips.gzip_comp_level}
>
<ResourceInput <ResourceInput
type="number" type="number"
min="1" min="1"
@@ -1018,6 +1114,7 @@ export function PerformancePage() {
> >
<ResourceField <ResourceField
label="proxy_cache_path" label="proxy_cache_path"
tooltip={performanceFieldTooltips.proxy_cache_path}
hint={ hint={
performanceFields.OpenRestyCacheEnabled performanceFields.OpenRestyCacheEnabled
? '缓存目录,启用缓存时必填。' ? '缓存目录,启用缓存时必填。'
@@ -1036,7 +1133,10 @@ export function PerformancePage() {
placeholder="/var/cache/openresty/atsflare" placeholder="/var/cache/openresty/atsflare"
/> />
</ResourceField> </ResourceField>
<ResourceField label="levels"> <ResourceField
label="levels"
tooltip={performanceFieldTooltips.levels}
>
<ResourceInput <ResourceInput
value={performanceFields.OpenRestyCacheLevels} value={performanceFields.OpenRestyCacheLevels}
disabled={!performanceFields.OpenRestyCacheEnabled} disabled={!performanceFields.OpenRestyCacheEnabled}
@@ -1049,7 +1149,10 @@ export function PerformancePage() {
placeholder="1:2" placeholder="1:2"
/> />
</ResourceField> </ResourceField>
<ResourceField label="inactive"> <ResourceField
label="inactive"
tooltip={performanceFieldTooltips.inactive}
>
<ResourceInput <ResourceInput
value={performanceFields.OpenRestyCacheInactive} value={performanceFields.OpenRestyCacheInactive}
disabled={!performanceFields.OpenRestyCacheEnabled} disabled={!performanceFields.OpenRestyCacheEnabled}
@@ -1062,7 +1165,10 @@ export function PerformancePage() {
placeholder="30m" placeholder="30m"
/> />
</ResourceField> </ResourceField>
<ResourceField label="max_size"> <ResourceField
label="max_size"
tooltip={performanceFieldTooltips.max_size}
>
<ResourceInput <ResourceInput
value={performanceFields.OpenRestyCacheMaxSize} value={performanceFields.OpenRestyCacheMaxSize}
disabled={!performanceFields.OpenRestyCacheEnabled} disabled={!performanceFields.OpenRestyCacheEnabled}
@@ -1075,7 +1181,10 @@ export function PerformancePage() {
placeholder="1g" placeholder="1g"
/> />
</ResourceField> </ResourceField>
<ResourceField label="cache key template"> <ResourceField
label="cache key template"
tooltip={performanceFieldTooltips.cache_key_template}
>
<ResourceInput <ResourceInput
value={performanceFields.OpenRestyCacheKeyTemplate} value={performanceFields.OpenRestyCacheKeyTemplate}
disabled={!performanceFields.OpenRestyCacheEnabled} disabled={!performanceFields.OpenRestyCacheEnabled}
@@ -1090,6 +1199,7 @@ export function PerformancePage() {
<ToggleField <ToggleField
label="proxy_cache_lock" label="proxy_cache_lock"
description="是否启用 proxy_cache_lock。" description="是否启用 proxy_cache_lock。"
tooltip={performanceFieldTooltips.proxy_cache_lock}
checked={performanceFields.OpenRestyCacheLockEnabled} checked={performanceFields.OpenRestyCacheLockEnabled}
disabled={!performanceFields.OpenRestyCacheEnabled} disabled={!performanceFields.OpenRestyCacheEnabled}
onChange={(checked) => onChange={(checked) =>
@@ -1099,7 +1209,12 @@ export function PerformancePage() {
})) }))
} }
/> />
<ResourceField label="proxy_cache_lock_timeout"> <ResourceField
label="proxy_cache_lock_timeout"
tooltip={
performanceFieldTooltips.proxy_cache_lock_timeout
}
>
<ResourceInput <ResourceInput
value={performanceFields.OpenRestyCacheLockTimeout} value={performanceFields.OpenRestyCacheLockTimeout}
disabled={!performanceFields.OpenRestyCacheEnabled} disabled={!performanceFields.OpenRestyCacheEnabled}
@@ -1112,7 +1227,10 @@ export function PerformancePage() {
placeholder="5s" placeholder="5s"
/> />
</ResourceField> </ResourceField>
<ResourceField label="proxy_cache_use_stale"> <ResourceField
label="proxy_cache_use_stale"
tooltip={performanceFieldTooltips.proxy_cache_use_stale}
>
<ResourceInput <ResourceInput
value={performanceFields.OpenRestyCacheUseStale} value={performanceFields.OpenRestyCacheUseStale}
disabled={!performanceFields.OpenRestyCacheEnabled} disabled={!performanceFields.OpenRestyCacheEnabled}
@@ -1,179 +1,213 @@
import type { import type {
ButtonHTMLAttributes, ButtonHTMLAttributes,
InputHTMLAttributes, InputHTMLAttributes,
ReactNode, ReactNode,
SelectHTMLAttributes, SelectHTMLAttributes,
TextareaHTMLAttributes, TextareaHTMLAttributes,
} from 'react'; } from 'react';
import { cn } from '@/lib/utils/cn'; import { cn } from '@/lib/utils/cn';
interface ResourceFieldProps { interface ResourceFieldProps {
label: string; label: string;
hint?: string; hint?: string;
error?: string; error?: string;
className?: string; className?: string;
children: ReactNode; tooltip?: string;
} children: ReactNode;
}
export function ResourceField({
label, function FieldTooltip({ content }: { content: string }) {
hint, return (
error, <span className="group/tooltip relative inline-flex">
className, <span className="inline-flex h-5 w-5 items-center justify-center rounded-full border border-[var(--border-default)] bg-[var(--surface-elevated)] text-[11px] font-semibold text-[var(--foreground-secondary)] transition group-hover/tooltip:border-[var(--border-strong)] group-hover/tooltip:text-[var(--foreground-primary)]">
children, ?
}: ResourceFieldProps) { </span>
return ( <span className="pointer-events-none absolute top-full left-1/2 z-20 mt-2 w-64 -translate-x-1/2 rounded-2xl border border-[var(--border-default)] bg-[var(--surface-panel)] px-3 py-2 text-xs leading-5 text-[var(--foreground-secondary)] opacity-0 shadow-[var(--shadow-soft)] transition-opacity delay-700 duration-200 group-hover/tooltip:opacity-100">
<label className={cn('block space-y-2', className)}> {content}
<span className='text-sm font-medium text-[var(--foreground-primary)]'>{label}</span> </span>
{children} </span>
{error ? ( );
<span className='block text-xs text-[var(--status-danger-foreground)]'>{error}</span> }
) : hint ? (
<span className='block text-xs text-[var(--foreground-secondary)]'>{hint}</span> export function ResourceField({
) : null} label,
</label> hint,
); error,
} className,
tooltip,
export function ResourceInput(props: InputHTMLAttributes<HTMLInputElement>) { children,
return ( }: ResourceFieldProps) {
<input return (
{...props} <label className={cn('block space-y-2', className)}>
className={cn( <span className="flex items-center gap-2 text-sm font-medium text-[var(--foreground-primary)]">
'w-full rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 text-sm text-[var(--foreground-primary)] outline-none transition placeholder:text-[var(--foreground-muted)] focus:border-[var(--border-strong)] focus:ring-2 focus:ring-[var(--accent-soft)] disabled:cursor-not-allowed disabled:opacity-60', <span>{label}</span>
props.className, {tooltip ? <FieldTooltip content={tooltip} /> : null}
)} </span>
/> {children}
); {error ? (
} <span className="block text-xs text-[var(--status-danger-foreground)]">
{error}
export function ResourceTextarea(props: TextareaHTMLAttributes<HTMLTextAreaElement>) { </span>
return ( ) : hint ? (
<textarea <span className="block text-xs text-[var(--foreground-secondary)]">
{...props} {hint}
className={cn( </span>
'min-h-28 w-full rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 text-sm text-[var(--foreground-primary)] outline-none transition placeholder:text-[var(--foreground-muted)] focus:border-[var(--border-strong)] focus:ring-2 focus:ring-[var(--accent-soft)] disabled:cursor-not-allowed disabled:opacity-60', ) : null}
props.className, </label>
)} );
/> }
);
} export function ResourceInput(props: InputHTMLAttributes<HTMLInputElement>) {
return (
export function ResourceSelect(props: SelectHTMLAttributes<HTMLSelectElement>) { <input
return ( {...props}
<select className={cn(
{...props} 'w-full rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 text-sm text-[var(--foreground-primary)] transition outline-none placeholder:text-[var(--foreground-muted)] focus:border-[var(--border-strong)] focus:ring-2 focus:ring-[var(--accent-soft)] disabled:cursor-not-allowed disabled:opacity-60',
className={cn( props.className,
'w-full rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 text-sm text-[var(--foreground-primary)] outline-none transition focus:border-[var(--border-strong)] focus:ring-2 focus:ring-[var(--accent-soft)] disabled:cursor-not-allowed disabled:opacity-60', )}
props.className, />
)} );
/> }
);
} export function ResourceTextarea(
props: TextareaHTMLAttributes<HTMLTextAreaElement>,
function baseButtonClassName(className?: string) { ) {
return cn( return (
'inline-flex items-center justify-center rounded-2xl px-4 py-3 text-sm font-medium transition disabled:cursor-not-allowed disabled:opacity-60', <textarea
className, {...props}
); className={cn(
} 'min-h-28 w-full rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 text-sm text-[var(--foreground-primary)] transition outline-none placeholder:text-[var(--foreground-muted)] focus:border-[var(--border-strong)] focus:ring-2 focus:ring-[var(--accent-soft)] disabled:cursor-not-allowed disabled:opacity-60',
props.className,
export function PrimaryButton({ )}
className, />
...props );
}: ButtonHTMLAttributes<HTMLButtonElement>) { }
return (
<button export function ResourceSelect(props: SelectHTMLAttributes<HTMLSelectElement>) {
{...props} return (
className={baseButtonClassName( <select
cn('bg-[var(--brand-primary)] text-[var(--foreground-inverse)] hover:opacity-90', className), {...props}
)} className={cn(
/> 'w-full rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 text-sm text-[var(--foreground-primary)] transition outline-none focus:border-[var(--border-strong)] focus:ring-2 focus:ring-[var(--accent-soft)] disabled:cursor-not-allowed disabled:opacity-60',
); props.className,
} )}
/>
export function SecondaryButton({ );
className, }
...props
}: ButtonHTMLAttributes<HTMLButtonElement>) { function baseButtonClassName(className?: string) {
return ( return cn(
<button 'inline-flex items-center justify-center rounded-2xl px-4 py-3 text-sm font-medium transition disabled:cursor-not-allowed disabled:opacity-60',
{...props} className,
className={baseButtonClassName( );
cn( }
'border border-[var(--border-default)] bg-[var(--control-background)] text-[var(--foreground-primary)] hover:bg-[var(--control-background-hover)]',
className, export function PrimaryButton({
), className,
)} ...props
/> }: ButtonHTMLAttributes<HTMLButtonElement>) {
); return (
} <button
{...props}
export function DangerButton({ className={baseButtonClassName(
className, cn(
...props 'bg-[var(--brand-primary)] text-[var(--foreground-inverse)] hover:opacity-90',
}: ButtonHTMLAttributes<HTMLButtonElement>) { className,
return ( ),
<button )}
{...props} />
className={baseButtonClassName( );
cn( }
'border border-[var(--status-danger-border)] bg-[var(--status-danger-soft)] text-[var(--status-danger-foreground)] hover:opacity-90',
className, export function SecondaryButton({
), className,
)} ...props
/> }: ButtonHTMLAttributes<HTMLButtonElement>) {
); return (
} <button
{...props}
interface ToggleFieldProps { className={baseButtonClassName(
label: string; cn(
description: string; 'border border-[var(--border-default)] bg-[var(--control-background)] text-[var(--foreground-primary)] hover:bg-[var(--control-background-hover)]',
checked: boolean; className,
disabled?: boolean; ),
onChange: (checked: boolean) => void; )}
} />
);
export function ToggleField({ }
label,
description, export function DangerButton({
checked, className,
disabled, ...props
onChange, }: ButtonHTMLAttributes<HTMLButtonElement>) {
}: ToggleFieldProps) { return (
return ( <button
<label className='flex cursor-pointer items-start gap-3 rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3'> {...props}
<input className={baseButtonClassName(
type='checkbox' cn(
checked={checked} 'border border-[var(--status-danger-border)] bg-[var(--status-danger-soft)] text-[var(--status-danger-foreground)] hover:opacity-90',
disabled={disabled} className,
onChange={(event) => onChange(event.target.checked)} ),
className='mt-1 h-4 w-4 rounded border-[var(--border-default)] accent-[var(--brand-primary)]' )}
/> />
<span className='space-y-1'> );
<span className='block text-sm font-medium text-[var(--foreground-primary)]'>{label}</span> }
<span className='block text-xs leading-5 text-[var(--foreground-secondary)]'>{description}</span>
</span> interface ToggleFieldProps {
</label> label: string;
); description: string;
} checked: boolean;
disabled?: boolean;
export function CodeBlock({ tooltip?: string;
children, onChange: (checked: boolean) => void;
className, }
}: {
children: ReactNode; export function ToggleField({
className?: string; label,
}) { description,
return ( checked,
<pre disabled,
className={cn( tooltip,
'overflow-x-auto rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4 text-xs leading-6 text-[var(--foreground-primary)]', onChange,
className, }: ToggleFieldProps) {
)} return (
> <label className="flex cursor-pointer items-start gap-3 rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3">
{children} <input
</pre> type="checkbox"
); checked={checked}
} disabled={disabled}
onChange={(event) => onChange(event.target.checked)}
className="mt-1 h-4 w-4 rounded border-[var(--border-default)] accent-[var(--brand-primary)]"
/>
<span className="space-y-1">
<span className="flex items-center gap-2 text-sm font-medium text-[var(--foreground-primary)]">
<span>{label}</span>
{tooltip ? <FieldTooltip content={tooltip} /> : null}
</span>
<span className="block text-xs leading-5 text-[var(--foreground-secondary)]">
{description}
</span>
</span>
</label>
);
}
export function CodeBlock({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<pre
className={cn(
'overflow-x-auto rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4 text-xs leading-6 text-[var(--foreground-primary)]',
className,
)}
>
{children}
</pre>
);
}