| 1 | /** |
| 2 | * The client half of a provider package. |
| 3 | * |
| 4 | * Every provider package exports a `ProviderClientDefinition` from its |
| 5 | * `./client` entry point. Clients render settings, labels, and badges from |
| 6 | * these definitions plus the server's `ServerProvider` snapshots, so a new |
| 7 | * provider needs no per-driver branches in web or mobile. |
| 8 | * |
| 9 | * Definitions are browser- and React Native-safe: plain data and Effect |
| 10 | * schemas, no platform UI imports. |
| 11 | * |
| 12 | * @module provider-core/client |
| 13 | */ |
| 14 | import type { ProviderDriverKind } from "@t3tools/contracts"; |
| 15 | import type * as Schema from "effect/Schema"; |
| 16 | |
| 17 | /** |
| 18 | * A provider's instance settings schema. Fields carry `providerSettingsForm` |
| 19 | * annotations, which the settings form renders generically. |
| 20 | */ |
| 21 | export type ProviderSettingsSchema = { |
| 22 | readonly fields: Readonly<Record<string, Schema.Top>>; |
| 23 | } & Schema.Top; |
| 24 | |
| 25 | /** An environment variable the settings UI offers for every instance of a driver. */ |
| 26 | export interface ProviderEnvironmentField { |
| 27 | readonly name: string; |
| 28 | readonly label: string; |
| 29 | readonly description?: string; |
| 30 | readonly placeholder?: string; |
| 31 | readonly sensitive?: boolean; |
| 32 | } |
| 33 | |
| 34 | /** |
| 35 | * A provider glyph as plain data, so web and mobile can each draw it with |
| 36 | * their own SVG renderer. Colors are per theme; a path without a fill or |
| 37 | * gradient uses the icon's `fill`. |
| 38 | */ |
| 39 | export interface ProviderIcon { |
| 40 | readonly viewBox: string; |
| 41 | readonly fill: { readonly light: string; readonly dark: string }; |
| 42 | readonly paths: ReadonlyArray<{ |
| 43 | readonly d: string; |
| 44 | readonly fillRule?: "evenodd" | "nonzero"; |
| 45 | readonly fill?: { readonly light: string; readonly dark: string }; |
| 46 | /** A brand gradient, the same in both themes. Coordinates are in viewBox units. */ |
| 47 | readonly gradient?: ProviderIconGradient; |
| 48 | }>; |
| 49 | } |
| 50 | |
| 51 | export interface ProviderIconGradient { |
| 52 | readonly x1: number; |
| 53 | readonly y1: number; |
| 54 | readonly x2: number; |
| 55 | readonly y2: number; |
| 56 | readonly stops: ReadonlyArray<{ readonly offset: number; readonly color: string }>; |
| 57 | } |
| 58 | |
| 59 | export interface ProviderClientDefinition { |
| 60 | readonly driverKind: ProviderDriverKind; |
| 61 | readonly label: string; |
| 62 | /** Omitted for drivers whose glyph a client still draws itself. */ |
| 63 | readonly icon?: ProviderIcon; |
| 64 | readonly settingsSchema: ProviderSettingsSchema; |
| 65 | readonly environmentFields?: ReadonlyArray<ProviderEnvironmentField>; |
| 66 | /** Whether the driver has a default instance backed by legacy `providers.<kind>` settings. */ |
| 67 | readonly hasDefaultInstance?: boolean; |
| 68 | /** |
| 69 | * Short warning badge beside every instance of the driver, such as "Beta". |
| 70 | * It marks the driver kind, so built-in and custom instances show it alike. |
| 71 | */ |
| 72 | readonly badgeLabel?: string; |
| 73 | } |
| 74 | |
| 75 | export function defineProviderClient<const Definition extends ProviderClientDefinition>( |
| 76 | definition: Definition, |
| 77 | ): Definition { |
| 78 | return definition; |
| 79 | } |
| 80 | |
| 81 | /** The provider definitions a client loaded, in presentation order. */ |
| 82 | export interface ProviderClientRegistry { |
| 83 | readonly definitions: ReadonlyArray<ProviderClientDefinition>; |
| 84 | /** `undefined` for drivers this client does not ship, such as a fork's driver. */ |
| 85 | readonly get: ( |
| 86 | driverKind: ProviderDriverKind | undefined, |
| 87 | ) => ProviderClientDefinition | undefined; |
| 88 | } |
| 89 | |
| 90 | export function makeProviderClientRegistry( |
| 91 | definitions: ReadonlyArray<ProviderClientDefinition>, |
| 92 | ): ProviderClientRegistry { |
| 93 | const byDriverKind = new Map<ProviderDriverKind, ProviderClientDefinition>(); |
| 94 | for (const definition of definitions) { |
| 95 | if (byDriverKind.has(definition.driverKind)) { |
| 96 | throw new Error(`Provider driver '${definition.driverKind}' is defined more than once.`); |
| 97 | } |
| 98 | byDriverKind.set(definition.driverKind, definition); |
| 99 | } |
| 100 | return { |
| 101 | definitions, |
| 102 | get: (driverKind) => (driverKind === undefined ? undefined : byDriverKind.get(driverKind)), |
| 103 | }; |
| 104 | } |