@hachej/boring-ui-kit 0.4.8 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,10 +8,10 @@
8
8
  "react-dom@^19.0.0"
9
9
  ],
10
10
  "registryDependencies": [
11
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.8/r/theme.json",
12
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.8/r/pi-chat.json",
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.8/r/utils.json",
14
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.8/r/dock.json",
11
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.10/r/theme.json",
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.10/r/pi-chat.json",
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.10/r/utils.json",
14
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.10/r/dock.json",
15
15
  "badge",
16
16
  "card"
17
17
  ],
@@ -9,10 +9,10 @@
9
9
  "lucide-react@^1.51.0"
10
10
  ],
11
11
  "registryDependencies": [
12
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.8/r/pi-app.json",
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.8/r/pi-ambient.json",
14
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.8/r/app-shell.json",
15
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.8/r/utils.json",
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.10/r/pi-app.json",
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.10/r/pi-ambient.json",
14
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.10/r/app-shell.json",
15
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.10/r/utils.json",
16
16
  "button"
17
17
  ],
18
18
  "files": [
@@ -9,8 +9,8 @@
9
9
  "lucide-react@^1.51.0"
10
10
  ],
11
11
  "registryDependencies": [
12
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.8/r/theme.json",
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.8/r/utils.json",
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.10/r/theme.json",
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.10/r/utils.json",
14
14
  "button",
15
15
  "input",
16
16
  "label",
@@ -20,7 +20,7 @@
20
20
  "files": [
21
21
  {
22
22
  "path": "registry/provider-setup/provider-setup.tsx",
23
- "content": "'use client';\n\nimport { useEffect, useState } from 'react';\nimport type { FormEvent, ReactNode } from 'react';\nimport { CheckIcon, ChevronDownIcon, SettingsIcon } from 'lucide-react';\nimport { cn } from '../utils/utils';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { NativeSelect, NativeSelectOption } from '@/components/ui/native-select';\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';\n\n/** One model a provider offers. */\nexport interface ProviderModel { readonly id: string; readonly name?: string }\n\n/** One model provider the host has registered. The host decides the list; this component only displays it. */\nexport interface ProviderOption {\n readonly id: string;\n readonly name: string;\n readonly models: readonly ProviderModel[];\n /** How the person can authorize this provider: an API key, a subscription sign-in (device code), both or neither. */\n readonly auth: readonly ('api_key' | 'oauth')[];\n /** Credentials are already stored for this provider. */\n readonly configured: boolean;\n /** Browsers cannot call this provider directly; a gateway must be set. */\n readonly needsGateway?: boolean;\n /** Button text for the sign-in, such as \"Sign in with ChatGPT\". */\n readonly loginLabel?: string;\n}\n\n/** The current choice. */\nexport interface ProviderSetupValue { readonly provider: string; readonly modelId: string; readonly gateway?: string }\n\n/** What the person asked to save. `apiKey` is present only when they typed one. */\nexport interface ProviderSetupChange { readonly provider: string; readonly modelId: string; readonly gateway: string; readonly apiKey?: string }\n\n/** Progress of a device-code sign-in the host runs. */\nexport type ProviderLoginState =\n | { readonly state: 'idle' }\n | { readonly state: 'starting' }\n | { readonly state: 'pending'; readonly userCode: string; readonly verificationUri: string }\n | { readonly state: 'done' }\n | { readonly state: 'failed'; readonly message: string };\n\nexport interface ProviderSetupProps {\n readonly providers: readonly ProviderOption[];\n readonly value: ProviderSetupValue;\n /** Save the choice (and the key, when typed). Throw or reject with an Error to show its message. The component never fetches. */\n readonly onSave: (change: ProviderSetupChange) => void | Promise<void>;\n /** Start a subscription sign-in for one provider. Without it the sign-in button is hidden. */\n readonly onLogin?: (providerId: string) => void | Promise<void>;\n readonly loginState?: ProviderLoginState;\n /** Shown as the gateway field's placeholder. */\n readonly gatewayPlaceholder?: string;\n /** Hide the gateway field when the host has no gateway. */\n readonly showGateway?: boolean;\n /** Replaces the default trigger button. The popover anchors to it. */\n readonly trigger?: ReactNode;\n readonly label?: string;\n /** Where the panel opens relative to the trigger. */\n readonly side?: 'bottom' | 'top';\n readonly align?: 'start' | 'end';\n readonly defaultOpen?: boolean;\n readonly className?: string;\n}\n\n/** Popover for choosing a provider and model and giving access to it. Transport-agnostic: props in, callbacks out. */\nexport function ProviderSetup({ providers, value, onSave, onLogin, loginState = { state: 'idle' }, gatewayPlaceholder = 'https://…/gateway', showGateway = true,\n trigger, label = 'Model', side = 'bottom', align = 'end', defaultOpen = false, className }: ProviderSetupProps) {\n const current = providers.find(item => item.id === value.provider);\n const [provider, setProvider] = useState(value.provider);\n const [modelId, setModelId] = useState(value.modelId);\n const [gateway, setGateway] = useState(value.gateway ?? '');\n const [apiKey, setApiKey] = useState('');\n const [saving, setSaving] = useState(false);\n const [saved, setSaved] = useState(false);\n const [error, setError] = useState<string | undefined>();\n const chosen = providers.find(item => item.id === provider) ?? providers[0];\n\n // A new value from the host (after a save or a sign-in) replaces the form's draft.\n useEffect(() => { setProvider(value.provider); setModelId(value.modelId); setGateway(value.gateway ?? ''); }, [value.provider, value.modelId, value.gateway]);\n\n if (!chosen) return null;\n const models = chosen.models.some(model => model.id === modelId) || !modelId ? chosen.models : [{ id: modelId }, ...chosen.models];\n const canKey = chosen.auth.includes('api_key'), canLogin = chosen.auth.includes('oauth') && onLogin !== undefined;\n const gatewayMissing = chosen.needsGateway === true && !gateway.trim();\n\n async function save(event: FormEvent) {\n event.preventDefault();\n setSaving(true); setError(undefined); setSaved(false);\n try {\n await onSave({ provider: chosen!.id, modelId, gateway: gateway.trim(), ...(apiKey ? { apiKey } : {}) });\n setApiKey(''); setSaved(true);\n } catch (cause) { setError(cause instanceof Error ? cause.message : 'Could not save.'); }\n finally { setSaving(false); }\n }\n\n return <Popover defaultOpen={defaultOpen}>\n <PopoverTrigger render={<Button variant=\"ghost\" data-testid=\"provider-setup-trigger\" data-boring=\"provider-setup\" className={cn(trigger ? 'contents' : 'max-w-56', className)} />}>\n {trigger ?? <><SettingsIcon data-icon=\"inline-start\" aria-hidden=\"true\" /><span className=\"truncate\">{current ? `${current.name} · ${value.modelId}` : label}</span><ChevronDownIcon data-icon=\"inline-end\" aria-hidden=\"true\" /></>}\n </PopoverTrigger>\n <PopoverContent side={side} align={align} aria-label=\"Model access\" data-testid=\"provider-setup-panel\" className=\"pi-chat w-[min(24rem,calc(100vw-2rem))]\">\n <form onSubmit={save} className=\"flex flex-col gap-3\">\n <Label className=\"flex-col items-stretch\">Provider\n <NativeSelect data-testid=\"provider-select\" value={chosen.id} className=\"w-full\"\n onChange={event => { const next = providers.find(item => item.id === event.target.value); if (!next) return; setProvider(next.id); setModelId(next.models[0]?.id ?? ''); setApiKey(''); setSaved(false); }}>\n {providers.map(item => <NativeSelectOption key={item.id} value={item.id}>{item.name}{item.configured ? ' ✓' : ''}</NativeSelectOption>)}\n </NativeSelect></Label>\n <Label className=\"flex-col items-stretch\">Model\n <NativeSelect data-testid=\"model-select\" value={modelId} className=\"w-full\" onChange={event => { setModelId(event.target.value); setSaved(false); }}>\n {models.map(model => <NativeSelectOption key={model.id} value={model.id}>{model.name ?? model.id}</NativeSelectOption>)}\n </NativeSelect></Label>\n\n {canKey && <Label className=\"flex-col items-stretch\">\n <span className=\"flex items-center justify-between\">API key{chosen.configured && <span data-testid=\"key-saved\" className=\"inline-flex items-center gap-1 text-muted-foreground\"><CheckIcon className=\"size-3\" aria-hidden=\"true\" />saved</span>}</span>\n <Input data-testid=\"api-key-input\" type=\"password\" autoComplete=\"off\" spellCheck={false} value={apiKey}\n placeholder={chosen.configured ? '•••••••• (type to replace)' : 'Paste your key'} onChange={event => { setApiKey(event.target.value); setSaved(false); }} />\n </Label>}\n\n {canLogin && <div className=\"flex flex-col gap-2\">\n <Button variant=\"outline\" data-testid=\"provider-login\" disabled={loginState.state === 'starting' || loginState.state === 'pending'} onClick={() => { void onLogin?.(chosen.id); }}>{chosen.loginLabel ?? `Sign in with ${chosen.name}`}{chosen.auth.includes('oauth') && chosen.configured && loginState.state === 'idle' ? ' again' : ''}</Button>\n {loginState.state === 'starting' && <p role=\"status\" className=\"m-0 text-xs text-muted-foreground\">Requesting a sign-in code…</p>}\n {loginState.state === 'pending' && <p role=\"status\" data-testid=\"login-pending\" className=\"m-0 text-sm\">Open <a href={loginState.verificationUri} target=\"_blank\" rel=\"noopener noreferrer\" className=\"underline underline-offset-2 [overflow-wrap:anywhere]\">{loginState.verificationUri}</a> and enter\n <code data-testid=\"device-code\" className=\"ms-1 bg-muted px-1.5 py-0.5 font-mono tracking-wider\">{loginState.userCode}</code>\n <span className=\"mt-1 block text-xs text-muted-foreground\">Waiting for you to approve…</span></p>}\n {loginState.state === 'done' && <p role=\"status\" data-testid=\"login-done\" className=\"m-0 inline-flex items-center gap-1 text-xs text-muted-foreground\"><CheckIcon className=\"size-3\" aria-hidden=\"true\" />Signed in</p>}\n {loginState.state === 'failed' && <p role=\"alert\" data-testid=\"login-failed\" className=\"m-0 text-xs text-destructive\">{loginState.message}</p>}\n </div>}\n\n {showGateway && <Label className=\"flex-col items-stretch\">Gateway (optional)\n <Input data-testid=\"gateway-input\" value={gateway} placeholder={gatewayPlaceholder} onChange={event => { setGateway(event.target.value); setSaved(false); }} /></Label>}\n {chosen.needsGateway && <p data-testid=\"gateway-note\" className={cn('m-0 text-xs', gatewayMissing ? 'text-foreground' : 'text-muted-foreground')}>\n {chosen.name} refuses model calls made directly from a web page. Set a gateway: a pass-through that adds no secrets.</p>}\n\n <div className=\"flex items-center gap-3\">\n <Button type=\"submit\" data-testid=\"provider-save\" disabled={saving}>{saving ? 'Saving…' : 'Save'}</Button>\n {saved && <span role=\"status\" data-testid=\"provider-saved\" className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\"><CheckIcon className=\"size-3\" aria-hidden=\"true\" />Saved</span>}\n </div>\n {error && <p role=\"alert\" data-testid=\"provider-error\" className=\"m-0 text-xs text-destructive\">{error}</p>}\n </form>\n </PopoverContent>\n </Popover>;\n}\n",
23
+ "content": "'use client';\n\nimport { useEffect, useState } from 'react';\nimport type { FormEvent, ReactNode } from 'react';\nimport { CheckIcon, ChevronDownIcon, SettingsIcon } from 'lucide-react';\nimport { cn } from '../utils/utils';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { NativeSelect, NativeSelectOption } from '@/components/ui/native-select';\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';\n\n/** One model a provider offers. */\nexport interface ProviderModel { readonly id: string; readonly name?: string }\n\n/** One model provider the host has registered. The host decides the list; this component only displays it. */\nexport interface ProviderOption {\n readonly id: string;\n readonly name: string;\n readonly models: readonly ProviderModel[];\n /** How the person can authorize this provider: an API key, a subscription sign-in (device code), both or neither. */\n readonly auth: readonly ('api_key' | 'oauth')[];\n /** Credentials are already stored for this provider. */\n readonly configured: boolean;\n /** Browsers cannot call this provider directly; a gateway must be set. */\n readonly needsGateway?: boolean;\n /** Button text for the sign-in, such as \"Sign in with ChatGPT\". */\n readonly loginLabel?: string;\n}\n\n/** The current choice. */\nexport interface ProviderSetupValue { readonly provider: string; readonly modelId: string; readonly gateway?: string }\n\n/** What the person asked to save. `apiKey` is present only when they typed one. */\nexport interface ProviderSetupChange { readonly provider: string; readonly modelId: string; readonly gateway: string; readonly apiKey?: string }\n\n/** Progress of a device-code sign-in the host runs. */\nexport type ProviderLoginState =\n | { readonly state: 'idle' }\n | { readonly state: 'starting' }\n | { readonly state: 'pending'; readonly userCode: string; readonly verificationUri: string }\n | { readonly state: 'done' }\n | { readonly state: 'failed'; readonly message: string };\n\nexport interface ProviderSetupProps {\n readonly providers: readonly ProviderOption[];\n readonly value: ProviderSetupValue;\n /** Save the choice (and the key, when typed). Throw or reject with an Error to show its message. The component never fetches. */\n readonly onSave: (change: ProviderSetupChange) => void | Promise<void>;\n /** Start a subscription sign-in for one provider. Without it the sign-in button is hidden. */\n readonly onLogin?: (providerId: string) => void | Promise<void>;\n readonly loginState?: ProviderLoginState;\n /** Shown as the gateway field's placeholder. */\n readonly gatewayPlaceholder?: string;\n /** Hide the gateway field when the host has no gateway. */\n readonly showGateway?: boolean;\n /** Replaces the default trigger button. The popover anchors to it. */\n readonly trigger?: ReactNode;\n readonly label?: string;\n /** Where the panel opens relative to the trigger. */\n readonly side?: 'bottom' | 'top';\n readonly align?: 'start' | 'end';\n readonly defaultOpen?: boolean;\n readonly className?: string;\n}\n\n/** Only http(s) addresses become links; any other scheme is shown as text. */\nfunction isWebLink(uri: string): boolean {\n try { return /^https?:$/.test(new URL(uri).protocol); } catch { return false; }\n}\n\n/** Popover for choosing a provider and model and giving access to it. Transport-agnostic: props in, callbacks out. */\nexport function ProviderSetup({ providers, value, onSave, onLogin, loginState = { state: 'idle' }, gatewayPlaceholder = 'https://…/gateway', showGateway = true,\n trigger, label = 'Model', side = 'bottom', align = 'end', defaultOpen = false, className }: ProviderSetupProps) {\n const current = providers.find(item => item.id === value.provider);\n const [provider, setProvider] = useState(value.provider);\n const [modelId, setModelId] = useState(value.modelId);\n const [gateway, setGateway] = useState(value.gateway ?? '');\n const [apiKey, setApiKey] = useState('');\n const [saving, setSaving] = useState(false);\n const [saved, setSaved] = useState(false);\n const [error, setError] = useState<string | undefined>();\n const chosen = providers.find(item => item.id === provider) ?? providers[0];\n\n // A new value from the host (after a save or a sign-in) replaces the form's draft.\n useEffect(() => { setProvider(value.provider); setModelId(value.modelId); setGateway(value.gateway ?? ''); }, [value.provider, value.modelId, value.gateway]);\n\n if (!chosen) return null;\n const models = chosen.models.some(model => model.id === modelId) || !modelId ? chosen.models : [{ id: modelId }, ...chosen.models];\n const canKey = chosen.auth.includes('api_key'), canLogin = chosen.auth.includes('oauth') && onLogin !== undefined;\n const gatewayMissing = chosen.needsGateway === true && !gateway.trim();\n\n async function save(event: FormEvent) {\n event.preventDefault();\n setSaving(true); setError(undefined); setSaved(false);\n try {\n await onSave({ provider: chosen!.id, modelId, gateway: gateway.trim(), ...(apiKey ? { apiKey } : {}) });\n setApiKey(''); setSaved(true);\n } catch (cause) { setError(cause instanceof Error ? cause.message : 'Could not save.'); }\n finally { setSaving(false); }\n }\n\n return <Popover defaultOpen={defaultOpen}>\n <PopoverTrigger render={<Button variant=\"ghost\" data-testid=\"provider-setup-trigger\" data-boring=\"provider-setup\" className={cn(trigger ? 'contents' : 'max-w-56', className)} />}>\n {trigger ?? <><SettingsIcon data-icon=\"inline-start\" aria-hidden=\"true\" /><span className=\"truncate\">{current ? `${current.name} · ${value.modelId}` : label}</span><ChevronDownIcon data-icon=\"inline-end\" aria-hidden=\"true\" /></>}\n </PopoverTrigger>\n <PopoverContent side={side} align={align} aria-label=\"Model access\" data-testid=\"provider-setup-panel\" className=\"pi-chat w-[min(24rem,calc(100vw-2rem))]\">\n <form onSubmit={save} className=\"flex flex-col gap-3\">\n <Label className=\"flex-col items-stretch\">Provider\n <NativeSelect data-testid=\"provider-select\" value={chosen.id} className=\"w-full\"\n onChange={event => { const next = providers.find(item => item.id === event.target.value); if (!next) return; setProvider(next.id); setModelId(next.models[0]?.id ?? ''); setApiKey(''); setSaved(false); }}>\n {providers.map(item => <NativeSelectOption key={item.id} value={item.id}>{item.name}{item.configured ? ' ✓' : ''}</NativeSelectOption>)}\n </NativeSelect></Label>\n <Label className=\"flex-col items-stretch\">Model\n <NativeSelect data-testid=\"model-select\" value={modelId} className=\"w-full\" onChange={event => { setModelId(event.target.value); setSaved(false); }}>\n {models.map(model => <NativeSelectOption key={model.id} value={model.id}>{model.name ?? model.id}</NativeSelectOption>)}\n </NativeSelect></Label>\n\n {canKey && <Label className=\"flex-col items-stretch\">\n <span className=\"flex items-center justify-between\">API key{chosen.configured && <span data-testid=\"key-saved\" className=\"inline-flex items-center gap-1 text-muted-foreground\"><CheckIcon className=\"size-3\" aria-hidden=\"true\" />saved</span>}</span>\n <Input data-testid=\"api-key-input\" type=\"password\" autoComplete=\"off\" spellCheck={false} value={apiKey}\n placeholder={chosen.configured ? '•••••••• (type to replace)' : 'Paste your key'} onChange={event => { setApiKey(event.target.value); setSaved(false); }} />\n </Label>}\n\n {canLogin && <div className=\"flex flex-col gap-2\">\n <Button variant=\"outline\" data-testid=\"provider-login\" disabled={loginState.state === 'starting' || loginState.state === 'pending'} onClick={() => { void onLogin?.(chosen.id); }}>{chosen.loginLabel ?? `Sign in with ${chosen.name}`}{chosen.auth.includes('oauth') && chosen.configured && loginState.state === 'idle' ? ' again' : ''}</Button>\n {loginState.state === 'starting' && <p role=\"status\" className=\"m-0 text-xs text-muted-foreground\">Requesting a sign-in code…</p>}\n {loginState.state === 'pending' && <p role=\"status\" data-testid=\"login-pending\" className=\"m-0 text-sm\">Open {isWebLink(loginState.verificationUri) ? <a href={loginState.verificationUri} target=\"_blank\" rel=\"noopener noreferrer\" className=\"underline underline-offset-2 [overflow-wrap:anywhere]\">{loginState.verificationUri}</a> : <span data-testid=\"login-uri\" className=\"[overflow-wrap:anywhere]\">{loginState.verificationUri}</span>} and enter\n <code data-testid=\"device-code\" className=\"ms-1 bg-muted px-1.5 py-0.5 font-mono tracking-wider\">{loginState.userCode}</code>\n <span className=\"mt-1 block text-xs text-muted-foreground\">Waiting for you to approve…</span></p>}\n {loginState.state === 'done' && <p role=\"status\" data-testid=\"login-done\" className=\"m-0 inline-flex items-center gap-1 text-xs text-muted-foreground\"><CheckIcon className=\"size-3\" aria-hidden=\"true\" />Signed in</p>}\n {loginState.state === 'failed' && <p role=\"alert\" data-testid=\"login-failed\" className=\"m-0 text-xs text-destructive\">{loginState.message}</p>}\n </div>}\n\n {showGateway && <Label className=\"flex-col items-stretch\">Gateway (optional)\n <Input data-testid=\"gateway-input\" value={gateway} placeholder={gatewayPlaceholder} onChange={event => { setGateway(event.target.value); setSaved(false); }} /></Label>}\n {chosen.needsGateway && <p data-testid=\"gateway-note\" className={cn('m-0 text-xs', gatewayMissing ? 'text-foreground' : 'text-muted-foreground')}>\n {chosen.name} refuses model calls made directly from a web page. Set a gateway: a pass-through that adds no secrets.</p>}\n\n <div className=\"flex items-center gap-3\">\n <Button type=\"submit\" data-testid=\"provider-save\" disabled={saving}>{saving ? 'Saving…' : 'Save'}</Button>\n {saved && <span role=\"status\" data-testid=\"provider-saved\" className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\"><CheckIcon className=\"size-3\" aria-hidden=\"true\" />Saved</span>}\n </div>\n {error && <p role=\"alert\" data-testid=\"provider-error\" className=\"m-0 text-xs text-destructive\">{error}</p>}\n </form>\n </PopoverContent>\n </Popover>;\n}\n",
24
24
  "type": "registry:component",
25
25
  "target": "components/provider-setup/provider-setup.tsx"
26
26
  }
@@ -8,7 +8,7 @@
8
8
  "lucide-react@^1.51.0"
9
9
  ],
10
10
  "registryDependencies": [
11
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.8/r/theme.json",
11
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.10/r/theme.json",
12
12
  "table",
13
13
  "badge",
14
14
  "item",