@huanlin/dsh-plugin-sidebar-brand-text 0.3.0
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.
- package/README.md +118 -0
- package/cordis.patch.yml +18 -0
- package/lib/client.js +661 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +235 -0
- package/lib/invariant.js +21 -0
- package/lib/types/client/BrandText.d.ts +37 -0
- package/lib/types/client/BrandTextCard.d.ts +22 -0
- package/lib/types/client/bindSnapshotSelector.d.ts +7 -0
- package/lib/types/client/controller.d.ts +66 -0
- package/lib/types/client/index.d.ts +38 -0
- package/lib/types/client/locales.d.ts +13 -0
- package/lib/types/client/styles.d.ts +21 -0
- package/lib/types/config.d.ts +24 -0
- package/lib/types/gateway.d.ts +31 -0
- package/lib/types/index.d.ts +32 -0
- package/lib/types/invariant.d.ts +10 -0
- package/lib/types/settings.d.ts +31 -0
- package/lib/types/types.d.ts +21 -0
- package/package.json +123 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":["DEFAULT_BRAND_TEXT_CONFIG: BrandTextConfig","cfg: BrandTextConfig","cardStyle: CSSProperties","headerStyle: CSSProperties","headTextStyle: CSSProperties","nameStyle: CSSProperties","descStyle: CSSProperties","pendingStyle: CSSProperties","bodyStyle: CSSProperties","formStyle: CSSProperties","fieldStyle: CSSProperties","labelStyle: CSSProperties","inputStyle: CSSProperties","hintStyle: CSSProperties","footerStyle: CSSProperties","btnBase: CSSProperties","noticeStyle: CSSProperties","savedStyle: CSSProperties","errorStyle: CSSProperties","body: React.ReactNode","en: Record<BrandTextKey, string>","zh: Record<BrandTextKey, string>"],"sources":["../src/types.ts","../src/client/BrandText.tsx","../src/client/BrandTextCard.tsx","../src/client/controller.ts","../src/client/bindSnapshotSelector.ts","../src/client/locales.ts","../src/client/styles.ts","../src/client/index.ts"],"sourcesContent":["/** Shared config surface of the sidebar-brand-text plugin (host + client halves). */\n\n/**\n * The plugin's user-facing settings, persisted through the settings seam\n * under the `sidebar-brand-text` namespace in `$DSH_HOME/settings.yaml`.\n * The browser half reads these via the `/sbbt/api/get` HTTP route.\n */\nexport interface BrandTextConfig {\n /**\n * Brand name text shown in the sidebar's top-left brand row, beside the\n * mark slot. Replaces the shell's \"DSH Local Build\" fallback.\n */\n name: string\n /**\n * Revision badge text rendered beside the brand name. Empty string hides\n * the badge entirely. Replaces the shell's 7-character\n * `DSH_CLIENT_COMMIT_HASH` fallback.\n */\n revision: string\n}\n\n/** Runtime defaults applied when no config arrives (defensive only). */\nexport const DEFAULT_BRAND_TEXT_CONFIG: BrandTextConfig = {\n name: 'DSH Local Build',\n revision: '',\n}\n","/**\n * The `sidebar.brand.name` slot occupant — renders the configured brand\n * name text and optional revision badge.\n *\n * Reads from the shared `BrandTextSettingsController` store via\n * `useSnapshot`, so a save in the settings card is instantly reflected\n * here without a DOM event or RPC re-fetch.\n *\n * Replaces the shell's fallback (`DSH Local Build` + 7-character\n * `DSH_CLIENT_COMMIT_HASH` badge). The mark slot (`sidebar.brand.mark`)\n * is untouched: the fish logo stays unless another plugin (e.g.\n * `ui-brand-official`) replaces it.\n *\n * @module @huanlin/dsh-plugin-sidebar-brand-text/client/BrandText\n */\nimport type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'\nimport type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { BrandTextConfig } from '../types.ts'\nimport { DEFAULT_BRAND_TEXT_CONFIG } from '../types.ts'\n\n/** Inject face: the selector hook bound to the shared controller store. */\nexport interface BrandTextInjected {\n readonly useSnapshot: SnapshotSelectorHook<BrandTextStateShell>\n}\n\n/** The slice of state the brand component reads. */\nexport interface BrandTextStateShell {\n draft: BrandTextConfig\n available: boolean\n}\n\n/** Full props: the slot's runtime share plus the plugin's inject face. */\nexport type BrandTextProps = PropsRuntime<'sidebar.brand.name'> & BrandTextInjected\n\n/**\n * Render the configured brand name and optional revision badge.\n *\n * While loading or on error, falls back to the shell defaults.\n * @param props - the `useSnapshot` inject face (plus the slot's runtime share, unused).\n * @returns the brand-name span and optional revision-badge span.\n */\nexport function BrandText({ useSnapshot }: BrandTextProps) {\n const state = useSnapshot((s) => s)\n const cfg: BrandTextConfig = state.available ? state.draft : DEFAULT_BRAND_TEXT_CONFIG\n\n return (\n <>\n <span className=\"sbbt-brand-name\">{cfg.name}</span>\n {cfg.revision !== '' ? (\n <span className=\"sbbt-build-revision\">{cfg.revision}</span>\n ) : null}\n </>\n )\n}\n","/**\n * BrandTextCard — the `settings.plugin.item` slot occupant.\n *\n * An expandable card (mirrors the ego-browser `EgoBrowserCard` pattern)\n * with two text inputs: brand name and revision badge. Reads/writes\n * through the shared `BrandTextSettingsController` which uses\n * `fetch('/sbbt/api/get')` and `fetch('/sbbt/api/set')`.\n *\n * Registered under the `settings.plugin.item` keyed slot with\n * `key: 'sidebar-brand-text'` — appears in the Plugin Config page\n * alongside the built-in cards.\n *\n * @module @huanlin/dsh-plugin-sidebar-brand-text/client/BrandTextCard\n */\nimport type { CSSProperties } from 'react'\nimport type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { BrandTextState } from './controller.ts'\n\n/** Inject face: controller + selector hook. */\nexport interface BrandTextCardInjected {\n readonly controller: {\n readonly load: () => Promise<void>\n readonly edit: (field: 'name' | 'revision', value: string) => void\n readonly discard: () => void\n readonly save: () => Promise<void>\n readonly toggle: () => void\n }\n readonly useSnapshot: SnapshotSelectorHook<BrandTextState>\n}\n\n/** Full props: locale seat + inject. */\nexport type BrandTextCardProps = PropsLocale<'dsh-plugin-sidebar-brand-text'> & BrandTextCardInjected\n\nconst cardStyle: CSSProperties = {\n border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.22))',\n background: 'var(--dsw-alias-bg-layer-3, transparent)',\n borderRadius: 12,\n listStyle: 'none',\n transition: 'border-color .16s, background .16s',\n}\n\nconst headerStyle: CSSProperties = {\n appearance: 'none',\n width: '100%',\n font: 'inherit',\n color: 'inherit',\n textAlign: 'left',\n cursor: 'pointer',\n background: 'transparent',\n border: 0,\n borderRadius: 12,\n alignItems: 'center',\n gap: 12,\n padding: '14px 16px',\n display: 'flex',\n}\n\nconst headTextStyle: CSSProperties = {\n flexDirection: 'column',\n flex: 1,\n gap: 4,\n minWidth: 0,\n display: 'flex',\n}\n\nconst nameStyle: CSSProperties = {\n color: 'var(--dsw-alias-label-primary, inherit)',\n fontSize: 15,\n fontWeight: 600,\n lineHeight: 1.4,\n}\n\nconst descStyle: CSSProperties = {\n color: 'var(--dsw-alias-label-tertiary, rgba(128,128,128,0.7))',\n fontSize: 13,\n lineHeight: 1.5,\n}\n\nconst pendingStyle: CSSProperties = {\n whiteSpace: 'nowrap',\n background: 'var(--dsw-alias-bg-module-platform, rgba(128,128,128,0.12))',\n color: 'var(--dsw-alias-label-secondary, inherit)',\n borderRadius: 999,\n flex: 'none',\n padding: '1px 8px',\n fontSize: 11,\n fontWeight: 500,\n lineHeight: '17px',\n}\n\nconst chevronStyle = (open: boolean): CSSProperties => ({\n color: 'var(--dsw-alias-label-tertiary, inherit)',\n flex: 'none',\n transition: 'transform .16s',\n display: 'inline-flex',\n alignItems: 'center',\n transform: open ? 'rotate(180deg)' : 'none',\n})\n\nconst bodyStyle: CSSProperties = {\n borderTop: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.22))',\n margin: '0 16px',\n padding: '12px 0 4px',\n}\n\nconst formStyle: CSSProperties = {\n display: 'flex',\n flexDirection: 'column',\n gap: 12,\n}\n\nconst fieldStyle: CSSProperties = {\n display: 'flex',\n flexDirection: 'column',\n gap: 4,\n}\n\nconst labelStyle: CSSProperties = {\n display: 'block',\n fontSize: 13,\n fontWeight: 500,\n color: 'var(--dsw-alias-label-primary, inherit)',\n}\n\nconst inputStyle: CSSProperties = {\n width: '100%',\n padding: '6px 10px',\n fontSize: 13,\n borderRadius: 8,\n border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.3))',\n background: 'var(--dsw-alias-bg-layer-3, transparent)',\n color: 'var(--dsw-alias-label-primary, inherit)',\n boxSizing: 'border-box',\n fontFamily: 'inherit',\n}\n\nconst hintStyle: CSSProperties = {\n fontSize: 12,\n color: 'var(--dsw-alias-label-tertiary, rgba(128,128,128,0.6))',\n margin: 0,\n lineHeight: 1.5,\n}\n\nconst footerStyle: CSSProperties = {\n borderTop: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.22))',\n justifyContent: 'flex-end',\n alignItems: 'center',\n gap: 8,\n padding: '12px 0 4px',\n display: 'flex',\n}\n\nconst btnBase: CSSProperties = {\n appearance: 'none',\n font: 'inherit',\n cursor: 'pointer',\n border: '1px solid transparent',\n borderRadius: 8,\n padding: '5px 14px',\n fontSize: 13,\n fontWeight: 500,\n lineHeight: '20px',\n color: 'var(--dsw-alias-label-primary, inherit)',\n background: 'var(--dsw-alias-bg-module-platform, rgba(128,128,128,0.12))',\n transition: 'background .16s, opacity .16s',\n}\n\nconst noticeStyle: CSSProperties = {\n color: 'var(--dsw-alias-label-tertiary, rgba(128,128,128,0.7))',\n margin: '0 0 8px',\n fontSize: 12,\n lineHeight: 1.5,\n}\n\nconst savedStyle: CSSProperties = {\n color: 'var(--dsw-alias-state-success-primary, #30d158)',\n margin: '0 0 12px',\n fontSize: 12,\n lineHeight: 1.5,\n}\n\nconst errorStyle: CSSProperties = {\n color: 'var(--dsw-alias-label-error, #ff453a)',\n margin: '0 0 12px',\n fontSize: 12,\n lineHeight: 1.5,\n minWidth: 0,\n}\n\nconst CHEVRON_SVG = '<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 9l6 6 6-6\"/></svg>'\n\n/**\n * Render the sidebar-brand-text settings card.\n * @param props - locale + controller/useSnapshot inject.\n * @returns a `<li>` card element.\n */\nexport function BrandTextCard({ t, controller, useSnapshot }: BrandTextCardProps) {\n const state = useSnapshot((s) => s)\n if (state.status === 'idle') void controller.load()\n\n const degraded = state.status === 'ready' && !state.available\n const open = state._open || degraded\n const applyState = state.applyState ?? { kind: 'idle' }\n const saving = applyState.kind === 'saving'\n const saved = applyState.kind === 'saved'\n const errorText = applyState.kind === 'error' ? applyState.message : undefined\n const busy = !state.writable || saving\n\n const header = (\n <button\n type=\"button\"\n style={headerStyle}\n aria-expanded={open}\n aria-label={t('card.title')}\n onClick={() => { if (!degraded) controller.toggle() }}\n >\n <span style={headTextStyle}>\n <span style={nameStyle}>{t('card.title')}</span>\n <span style={descStyle}>{t('card.intro')}</span>\n </span>\n {state.dirty ? <span style={pendingStyle}>{t('card.unsaved')}</span> : null}\n <span style={chevronStyle(open)} dangerouslySetInnerHTML={{ __html: CHEVRON_SVG }} />\n </button>\n )\n\n let body: React.ReactNode = null\n if (open) {\n if (!state.available) {\n body = (\n <div style={bodyStyle}>\n <p style={noticeStyle} role=\"status\">{t('card.unavailable')}</p>\n <div style={footerStyle}>\n <button\n type=\"button\"\n style={btnBase}\n onClick={() => { void controller.load() }}\n >\n {t('card.retry')}\n </button>\n </div>\n </div>\n )\n } else {\n body = (\n <div style={bodyStyle}>\n {saved ? <p style={savedStyle} role=\"status\">{t('card.saved')}</p> : null}\n {errorText !== undefined ? <p style={errorStyle} role=\"status\">{errorText}</p> : null}\n <div style={formStyle}>\n <div style={fieldStyle}>\n <label style={labelStyle} htmlFor=\"sbbt-name\">{t('field.name.label')}</label>\n <input\n id=\"sbbt-name\"\n type=\"text\"\n style={inputStyle}\n value={state.draft.name}\n placeholder={t('field.name.placeholder')}\n disabled={busy}\n onChange={(e) => controller.edit('name', e.target.value)}\n />\n <p style={hintStyle}>{t('field.name.hint')}</p>\n </div>\n <div style={fieldStyle}>\n <label style={labelStyle} htmlFor=\"sbbt-revision\">{t('field.revision.label')}</label>\n <input\n id=\"sbbt-revision\"\n type=\"text\"\n style={inputStyle}\n value={state.draft.revision}\n placeholder={t('field.revision.placeholder')}\n disabled={busy}\n onChange={(e) => controller.edit('revision', e.target.value)}\n />\n <p style={hintStyle}>{t('field.revision.hint')}</p>\n </div>\n </div>\n <div style={footerStyle}>\n <button\n type=\"button\"\n style={{ ...btnBase, opacity: (!state.dirty || saving) ? 0.5 : 1 }}\n disabled={!state.dirty || saving}\n onClick={() => controller.discard()}\n >\n {t('card.discard')}\n </button>\n <button\n type=\"button\"\n style={{\n ...btnBase,\n background: 'var(--dsw-alias-brand-primary, #0a84ff)',\n color: 'var(--dsw-alias-bg-layer-1, #fff)',\n opacity: (!state.dirty || saving) ? 0.5 : 1,\n }}\n disabled={!state.dirty || saving}\n onClick={() => { void controller.save() }}\n >\n {saving ? t('card.saving') : t('card.save')}\n </button>\n </div>\n </div>\n )\n }\n }\n\n return (\n <li style={cardStyle}>\n {header}\n {open ? body : null}\n </li>\n )\n}\n","/**\n * `BrandTextSettingsController` — client-side state store for the\n * sidebar-brand-text config.\n *\n * Loads the config from the host's `/sbbt/api/get` route, stages edits,\n * and saves via `/sbbt/api/set`. The `sidebar.brand.name` slot occupant\n * and the `settings.plugin.item` card both read from the same store via\n * `bindSnapshotSelector`, so a save is instantly reflected in the sidebar\n * without a DOM event or page reload.\n *\n * Mirrors the ego-browser `EgoBrowserSettingsController` pattern.\n *\n * @module @huanlin/dsh-plugin-sidebar-brand-text/client/controller\n */\nimport { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'\nimport { DEFAULT_BRAND_TEXT_CONFIG, type BrandTextConfig } from '../types.ts'\n\n/** The controller's snapshot state. */\nexport interface BrandTextState {\n /** 'idle' | 'loading' | 'ready' */\n status: 'idle' | 'loading' | 'ready'\n /** True after a successful `/sbbt/api/get`; false when the route is unreachable. */\n available: boolean\n /** False when the settings service is absent (read-only). */\n writable: boolean\n /** Current draft values (edited by the card, read by the brand slot). */\n draft: BrandTextConfig\n /** True when the draft differs from the last-saved config. */\n dirty: boolean\n /** Apply lifecycle: 'idle' | 'saving' | 'saved' | 'error'. */\n applyState: { kind: 'idle' } | { kind: 'saving' } | { kind: 'saved' } | { kind: 'error', message: string }\n /** Card expand state (toggled by the card header button). */\n _open: boolean\n}\n\n/** Initial state before the first load. */\nfunction initialState(): BrandTextState {\n return {\n status: 'idle',\n available: false,\n writable: false,\n draft: { ...DEFAULT_BRAND_TEXT_CONFIG },\n dirty: false,\n applyState: { kind: 'idle' },\n _open: false,\n }\n}\n\n/**\n * Controller managing the brand-text config lifecycle.\n *\n * Constructed once in the client `apply()` and shared between the\n * `sidebar.brand.name` slot and the `settings.plugin.item` card.\n */\nexport class BrandTextSettingsController {\n readonly store: SnapshotStore<BrandTextState>\n loaded = false\n private generation = 0\n\n constructor() {\n this.store = createSnapshotStore<BrandTextState>(initialState())\n }\n\n /** Fetch the config from `/sbbt/api/get` and update the store. */\n async load(): Promise<void> {\n const gen = ++this.generation\n this.store.update((s) => { s.status = 'loading' })\n try {\n const res = await fetch('/sbbt/api/get', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: '{}',\n })\n if (!res.ok) {\n this.markUnavailable(gen)\n return\n }\n const parsed = await res.json().catch(() => null)\n if (gen !== this.generation) return\n if (!parsed || parsed.ok !== true || !parsed.value) {\n this.markUnavailable(gen)\n return\n }\n const config = parsed.value.config as BrandTextConfig | undefined\n this.loaded = true\n this.store.update((s) => {\n s.status = 'ready'\n s.available = true\n s.writable = true\n if (config) {\n s.draft = { name: config.name, revision: config.revision }\n }\n s.dirty = false\n s.applyState = { kind: 'idle' }\n })\n } catch {\n this.markUnavailable(gen)\n }\n }\n\n /** Stage an edit to a field (does not save). */\n edit(field: 'name' | 'revision', value: string): void {\n this.store.update((s) => {\n if (field === 'name') s.draft.name = value\n else s.draft.revision = value\n s.dirty = true\n s.applyState = { kind: 'idle' }\n })\n }\n\n /** Discard staged edits and reload from the host. */\n discard(): void {\n void this.load()\n }\n\n /** Save the staged draft via `/sbbt/api/set`. */\n async save(): Promise<void> {\n const gen = ++this.generation\n const snapshot = this.store.getSnapshot()\n if (!snapshot.dirty) return\n this.store.update((s) => { s.applyState = { kind: 'saving' } })\n try {\n const res = await fetch('/sbbt/api/set', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ patch: snapshot.draft }),\n })\n const parsed = await res.json().catch(() => null)\n if (gen !== this.generation) return\n if (!parsed || parsed.ok !== true || !parsed.value) {\n const message = parsed?.error?.message ?? 'Save failed'\n this.store.update((s) => { s.applyState = { kind: 'error', message } })\n return\n }\n const config = parsed.value.config as BrandTextConfig | undefined\n this.store.update((s) => {\n s.applyState = { kind: 'saved' }\n if (config) {\n s.draft = { name: config.name, revision: config.revision }\n }\n s.dirty = false\n })\n } catch (error) {\n if (gen !== this.generation) return\n const message = error instanceof Error ? error.message : String(error)\n this.store.update((s) => { s.applyState = { kind: 'error', message } })\n }\n }\n\n /** Toggle the card's expand state (mirrors ego-browser `controller.toggle()`). */\n toggle(): void {\n this.store.update((s) => { s._open = !s._open })\n }\n\n /** Mark the store as unavailable (route unreachable or settings service absent). */\n private markUnavailable(gen: number): void {\n if (gen !== this.generation) return\n this.store.update((s) => {\n s.status = 'ready'\n s.available = false\n s.writable = false\n })\n }\n}\n","/**\n * Inlined `bindSnapshotSelector` — rc.8 dropped this from\n * `@deepseek-ai/dsh-client-ui-renderer`'s package root, so business plugins\n * carry their own copy. Uses React 18's built-in `useSyncExternalStore`\n * with per-snapshot selector memoization via `useRef` (same approach as\n * the ego-browser plugin).\n *\n * @module @huanlin/dsh-plugin-sidebar-brand-text/client/bindSnapshotSelector\n */\nimport { useRef, useSyncExternalStore } from 'react'\nimport type { HostObservable, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'\n\n/**\n * Bind a React selector hook to a {@link HostObservable} snapshot source.\n * @param source - the observable snapshot store.\n * @returns a `useSelector(sel, eq?)` hook.\n */\nexport function bindSnapshotSelector<T>(source: HostObservable<T>): SnapshotSelectorHook<T> {\n const subscribe = (fn: () => void): (() => void) => source.subscribe(fn)\n const getSnapshot = (): T => source.getSnapshot()\n return function useSelector<S>(sel: (s: T) => S): S {\n const snapshot = useSyncExternalStore(subscribe, getSnapshot)\n const prevSnapshotRef = useRef<T | undefined>(undefined)\n const prevSelectedRef = useRef<S | undefined>(undefined)\n if (prevSnapshotRef.current !== snapshot) {\n prevSnapshotRef.current = snapshot\n prevSelectedRef.current = sel(snapshot)\n }\n return prevSelectedRef.current as S\n }\n}\n","/**\n * Locale dictionaries for the `dsh-plugin-sidebar-brand-text` namespace.\n *\n * @module @huanlin/dsh-plugin-sidebar-brand-text/client/locales\n */\n\n/** The locale keys the settings card reads. */\nexport type BrandTextKey =\n | 'card.title'\n | 'card.intro'\n | 'card.unsaved'\n | 'card.saved'\n | 'card.saving'\n | 'card.discard'\n | 'card.save'\n | 'card.unavailable'\n | 'card.retry'\n | 'field.name.label'\n | 'field.name.placeholder'\n | 'field.name.hint'\n | 'field.revision.label'\n | 'field.revision.placeholder'\n | 'field.revision.hint'\n\n/** The locale namespace name; matches the `locale: NS` passed at slot register. */\nexport const NS = 'dsh-plugin-sidebar-brand-text'\n\n/** English dictionary. */\nexport const en: Record<BrandTextKey, string> = {\n 'card.title': 'Sidebar Brand Text',\n 'card.intro': 'Replace the sidebar brand name and revision badge with custom text.',\n 'card.unsaved': 'Unsaved',\n 'card.saved': 'Saved',\n 'card.saving': 'Saving…',\n 'card.discard': 'Discard',\n 'card.save': 'Save',\n 'card.unavailable': 'The sidebar-brand-text configuration channel is unavailable. Please retry later.',\n 'card.retry': 'Retry',\n 'field.name.label': 'Brand name',\n 'field.name.placeholder': 'DSH Local Build',\n 'field.name.hint': 'Text shown in the sidebar next to the logo. Replaces the default \"DSH Local Build\".',\n 'field.revision.label': 'Revision badge',\n 'field.revision.placeholder': 'e.g. v1.0.0 or abc1234',\n 'field.revision.hint': 'Small badge text beside the brand name. Leave empty to hide the badge.',\n}\n\n/** Chinese dictionary. */\nexport const zh: Record<BrandTextKey, string> = {\n 'card.title': '侧边栏品牌文案',\n 'card.intro': '替换侧边栏左上角的品牌名与构建徽标文案。',\n 'card.unsaved': '未保存',\n 'card.saved': '已保存',\n 'card.saving': '保存中…',\n 'card.discard': '放弃',\n 'card.save': '保存',\n 'card.unavailable': '侧边栏品牌文案配置通道不可用,请稍后重试。',\n 'card.retry': '重试',\n 'field.name.label': '品牌名称',\n 'field.name.placeholder': 'DSH Local Build',\n 'field.name.hint': '侧边栏 logo 右侧显示的文案。替换默认的「DSH Local Build」。',\n 'field.revision.label': '版本徽标',\n 'field.revision.placeholder': '如 v1.0.0 或 abc1234',\n 'field.revision.hint': '品牌名右侧的小徽标文案。留空则不显示徽标。',\n}\n","/**\n * One scoped stylesheet injected for the lifetime of the client activation.\n *\n * The shell's `sidebar.brand.name` fallback renders two CSS-Module-hashed\n * spans (`.fallbackBrandName` + `.buildRevision`); those class names are\n * not stable across builds and not addressable from outside the sidebar\n * package. This plugin ships its own class names with the same visual\n * intent, all colors and typography drawn from the shared `--dsw-*`\n * tokens (never literals) so the badge tracks the active theme.\n *\n * The parent `.brandName` span (inline-flex, gap: 6px, font-size: 18px,\n * font-weight: 600) is owned by the sidebar shell and wraps whatever the\n * slot occupant returns, so this stylesheet only needs to style the two\n * child spans.\n */\nexport const CSS = `\n.sbbt-brand-name {\n font-size: 17px;\n letter-spacing: 0px;\n white-space: nowrap;\n}\n\n.sbbt-build-revision {\n display: inline-flex;\n align-items: center;\n height: 16px;\n padding: 0 4px;\n border-radius: 3px;\n color: var(--dsw-alias-label-primary-inverted);\n background: var(--dsw-alias-label-primary);\n font-family: var(--ds-font-family-code);\n font-size: 8px;\n font-weight: 500;\n line-height: 16px;\n}\n`\n\n/**\n * Install the stylesheet and return its disposer.\n * @returns a cleanup function that removes the injected `<style>` tag.\n */\nexport function installStyles(): () => void {\n if (typeof document === 'undefined') return () => {}\n const style = document.createElement('style')\n style.setAttribute('data-sidebar-brand-text-style', '')\n style.textContent = CSS\n document.head.appendChild(style)\n return () => { style.remove() }\n}\n","/**\n * sidebar-brand-text — browser half.\n *\n * Two registrations:\n * - `settings.plugin.item` keyed slot (key `sidebar-brand-text`) — a card\n * in the Plugin Config page with two text inputs (name + revision).\n * Reads/writes through the `/sbbt/api` HTTP route via the shared\n * `BrandTextSettingsController`.\n * - `sidebar.brand.name` single slot — renders the configured brand name\n * text and optional revision badge. Reads from the same controller\n * store via `useSnapshot`, so a save in the card is instantly\n * reflected in the sidebar.\n *\n * The mark slot (`sidebar.brand.mark`) is deliberately NOT registered:\n * the fish logo stays in place unless another plugin (e.g.\n * `ui-brand-official`) replaces it.\n *\n * @module @huanlin/dsh-plugin-sidebar-brand-text/client\n */\n\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'\nimport { BrandText, type BrandTextInjected } from './BrandText.tsx'\nimport { BrandTextCard, type BrandTextCardInjected } from './BrandTextCard.tsx'\nimport { BrandTextSettingsController } from './controller.ts'\nimport { bindSnapshotSelector } from './bindSnapshotSelector.ts'\nimport { en, NS, zh, type BrandTextKey } from './locales.ts'\nimport { installStyles } from './styles.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The settings card labels. */\n 'dsh-plugin-sidebar-brand-text': BrandTextKey\n }\n}\n\n/** Required services: slots + locale. */\nexport const inject = ['slots', 'locale']\n\n/**\n * Client plugin body: register the brand-name slot occupant, the settings\n * card, the locale dictionary, and the stylesheet.\n *\n * A single `BrandTextSettingsController` is shared between the card and\n * the brand text so a save is instantly reflected in the sidebar.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'sidebar-brand-text: dictionaries')\n ctx.effect(installStyles, 'sidebar-brand-text: styles')\n\n const controller = new BrandTextSettingsController()\n const useSnapshot = bindSnapshotSelector(controller.store)\n\n void controller.load()\n\n const brandInjected = (): BrandTextInjected => ({ useSnapshot })\n ctx.slots.inject('sidebar.brand.name', () =>\n ctx.slots.register(\n {\n name: 'sidebar.brand.name',\n inject: brandInjected,\n },\n BrandText,\n ),\n )\n\n const cardInjected = (): BrandTextCardInjected => ({ controller, useSnapshot })\n ctx.slots.inject('settings.plugin.item', function* () {\n yield ctx.slots.register(\n {\n name: 'settings.plugin.item',\n key: 'sidebar-brand-text',\n locale: NS,\n inject: cardInjected,\n },\n BrandTextCard,\n )\n })\n}\n"],"mappings":"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"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
3
|
+
|
|
4
|
+
//#region src/config.ts
|
|
5
|
+
/** Schemastery schema for the composition entry and the settings namespace. */
|
|
6
|
+
const Config = z.object({
|
|
7
|
+
name: z.string().default("DSH Local Build").description("Brand name text shown in the sidebar next to the logo."),
|
|
8
|
+
revision: z.string().default("").description("Revision badge text shown beside the brand name. Empty string hides the badge.")
|
|
9
|
+
});
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a raw config object into a complete {@link BrandTextConfig}.
|
|
12
|
+
*
|
|
13
|
+
* Unknown keys are dropped; missing or wrong-typed keys fall back to
|
|
14
|
+
* the defaults. This runs on every gateway read so the client always
|
|
15
|
+
* sees a well-formed value.
|
|
16
|
+
* @param config - raw config (entry source or settings layer).
|
|
17
|
+
* @returns the resolved config with defaults applied.
|
|
18
|
+
*/
|
|
19
|
+
function resolveConfig(config = {}) {
|
|
20
|
+
return {
|
|
21
|
+
name: typeof config.name === "string" ? config.name : DEFAULT.name,
|
|
22
|
+
revision: typeof config.revision === "string" ? config.revision : DEFAULT.revision
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Defaults used when no config arrives (defensive only). */
|
|
26
|
+
const DEFAULT = {
|
|
27
|
+
name: "DSH Local Build",
|
|
28
|
+
revision: ""
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/settings.ts
|
|
33
|
+
/** Settings namespace under which brand-text config persists. */
|
|
34
|
+
const SETTINGS_NAMESPACE = settingsNamespace("sidebar-brand-text");
|
|
35
|
+
/**
|
|
36
|
+
* Mirror of the dsh-settings internal `isUnloading` guard. The cordis const
|
|
37
|
+
* enum for fiber state is erased at compile time, so the literal states are
|
|
38
|
+
* matched numerically: 4 = DISPOSED, 5 = UNLOADING.
|
|
39
|
+
*/
|
|
40
|
+
function isUnloading(ctx) {
|
|
41
|
+
const state = ctx.fiber?.state;
|
|
42
|
+
return state === 4 || state === 5;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Install the `sidebar-brand-text` settings namespace and return the bridge.
|
|
46
|
+
*
|
|
47
|
+
* @param ctx - host context.
|
|
48
|
+
* @param entry - raw composition-layer config seed.
|
|
49
|
+
* @returns the settings bridge.
|
|
50
|
+
*/
|
|
51
|
+
function installBrandTextSettings(ctx, entry) {
|
|
52
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
53
|
+
let source = () => entry;
|
|
54
|
+
const notify = () => {
|
|
55
|
+
for (const listener of [...listeners]) listener();
|
|
56
|
+
};
|
|
57
|
+
ctx.inject(["settings"], (sctx) => {
|
|
58
|
+
let scope;
|
|
59
|
+
try {
|
|
60
|
+
scope = sctx.settings.register(SETTINGS_NAMESPACE, Config, { base: entry });
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (!(error instanceof Error) || !error.message.includes("already registered")) throw error;
|
|
63
|
+
ctx.logger("sidebar-brand-text")?.debug("settings namespace already registered — entry-source fallback");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
source = () => scope.get();
|
|
67
|
+
sctx.effect(() => () => {
|
|
68
|
+
if (isUnloading(ctx)) return;
|
|
69
|
+
source = () => entry;
|
|
70
|
+
notify();
|
|
71
|
+
});
|
|
72
|
+
notify();
|
|
73
|
+
scope.watch(() => {
|
|
74
|
+
if (isUnloading(ctx)) return;
|
|
75
|
+
notify();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
source: () => source(),
|
|
80
|
+
onChange: (cb) => {
|
|
81
|
+
listeners.add(cb);
|
|
82
|
+
return () => {
|
|
83
|
+
listeners.delete(cb);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/gateway.ts
|
|
91
|
+
/** HTTP route prefix owning every sidebar-brand-text API request. */
|
|
92
|
+
const API_PREFIX = "/sbbt/api";
|
|
93
|
+
/** Config keys the `set` endpoint accepts (allow-list; unknown keys are dropped). */
|
|
94
|
+
const ALLOWED_KEYS = new Set(["name", "revision"]);
|
|
95
|
+
/**
|
|
96
|
+
* Register the `/sbbt/api` HTTP route on the host's web server.
|
|
97
|
+
*
|
|
98
|
+
* @param ctx - host context carrying `webServer`.
|
|
99
|
+
* @param bridge - the settings bridge the route reads through.
|
|
100
|
+
*/
|
|
101
|
+
function registerBrandTextGateway(ctx, bridge) {
|
|
102
|
+
let settings;
|
|
103
|
+
ctx.inject(["settings"], (sctx) => {
|
|
104
|
+
settings = sctx.settings;
|
|
105
|
+
return () => {
|
|
106
|
+
settings = void 0;
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
ctx.effect(() => {
|
|
110
|
+
const webServer = ctx.webServer;
|
|
111
|
+
if (!webServer || typeof webServer.register !== "function") return () => {};
|
|
112
|
+
return webServer.register({
|
|
113
|
+
kind: "prefix",
|
|
114
|
+
path: API_PREFIX,
|
|
115
|
+
handler: async (req, res) => {
|
|
116
|
+
if ((req.method ?? "") !== "POST") {
|
|
117
|
+
writeJson(res, 405, envelopeError("method-not-allowed", "POST only"));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const origin = req.headers.origin;
|
|
121
|
+
if (typeof origin === "string" && origin) {
|
|
122
|
+
let originHost;
|
|
123
|
+
try {
|
|
124
|
+
originHost = new URL(origin).host;
|
|
125
|
+
} catch {
|
|
126
|
+
writeJson(res, 400, envelopeError("invalid-origin", "invalid Origin header"));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const reqHost = req.headers.host;
|
|
130
|
+
if (typeof reqHost === "string" && originHost !== reqHost) {
|
|
131
|
+
writeJson(res, 403, envelopeError("origin-not-allowed", "same-origin requests only"));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
136
|
+
writeJson(res, 415, envelopeError("content-type-not-supported", "application/json required"));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
|
|
140
|
+
const method = pathname.startsWith(`${API_PREFIX}/`) ? pathname.slice(`${API_PREFIX}/`.length) : void 0;
|
|
141
|
+
if (method === void 0 || method.includes("/")) {
|
|
142
|
+
writeJson(res, 404, envelopeError("not-found", "unknown sidebar-brand-text API method"));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const body = await readJsonBody(req);
|
|
147
|
+
if (method === "get") writeJson(res, 200, envelopeOk({ config: resolveConfig(bridge.source()) }));
|
|
148
|
+
else if (method === "set") writeJson(res, 200, envelopeOk(await handleSet(body, settings, bridge)));
|
|
149
|
+
else writeJson(res, 404, envelopeError("not-found", `unknown sidebar-brand-text API method "${method}"`));
|
|
150
|
+
} catch (error) {
|
|
151
|
+
writeJson(res, 500, envelopeError("internal", error instanceof Error ? error.message : String(error)));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}, "sidebar-brand-text: /sbbt/api routes");
|
|
156
|
+
}
|
|
157
|
+
/** Handle the `set` method: validate patch, write user layer, return resolved config. */
|
|
158
|
+
async function handleSet(body, settings, bridge) {
|
|
159
|
+
const patch = extractPatch(body);
|
|
160
|
+
if (Object.keys(patch).length === 0) return { config: resolveConfig(bridge.source()) };
|
|
161
|
+
if (settings === void 0) throw new Error("sidebar-brand-text: settings service is unavailable — configuration cannot be written");
|
|
162
|
+
await settings.update(SETTINGS_NAMESPACE, patch);
|
|
163
|
+
return { config: resolveConfig(bridge.source()) };
|
|
164
|
+
}
|
|
165
|
+
/** Extract and validate the patch from the request body. */
|
|
166
|
+
function extractPatch(body) {
|
|
167
|
+
if (typeof body !== "object" || body === null) return {};
|
|
168
|
+
const raw = Reflect.get(body, "patch");
|
|
169
|
+
if (typeof raw !== "object" || raw === null) return {};
|
|
170
|
+
const normalized = {};
|
|
171
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
172
|
+
if (!ALLOWED_KEYS.has(key)) continue;
|
|
173
|
+
if (value === null || value === void 0) continue;
|
|
174
|
+
if (typeof value === "string") {
|
|
175
|
+
if (key === "name") normalized.name = value;
|
|
176
|
+
else if (key === "revision") normalized.revision = value;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return normalized;
|
|
180
|
+
}
|
|
181
|
+
/** Read and parse a JSON body from a node:http request. */
|
|
182
|
+
async function readJsonBody(req, maxBytes = 8192) {
|
|
183
|
+
const chunks = [];
|
|
184
|
+
let bytes = 0;
|
|
185
|
+
for await (const chunk of req) {
|
|
186
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
187
|
+
bytes += buffer.length;
|
|
188
|
+
if (bytes > maxBytes) throw new Error("request body too large");
|
|
189
|
+
chunks.push(buffer);
|
|
190
|
+
}
|
|
191
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
192
|
+
if (text === "") return {};
|
|
193
|
+
return JSON.parse(text);
|
|
194
|
+
}
|
|
195
|
+
/** Write a JSON response envelope. */
|
|
196
|
+
function writeJson(res, status, body) {
|
|
197
|
+
const json = JSON.stringify(body);
|
|
198
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
199
|
+
res.end(json);
|
|
200
|
+
}
|
|
201
|
+
/** Build a success envelope. */
|
|
202
|
+
function envelopeOk(value) {
|
|
203
|
+
return {
|
|
204
|
+
ok: true,
|
|
205
|
+
value
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** Build an error envelope. */
|
|
209
|
+
function envelopeError(code, message) {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
error: {
|
|
213
|
+
code,
|
|
214
|
+
message
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/index.ts
|
|
221
|
+
const name = "sidebar-brand-text";
|
|
222
|
+
/** `webServer` is required for the HTTP gateway that backs the settings card. */
|
|
223
|
+
const inject = ["webServer"];
|
|
224
|
+
/**
|
|
225
|
+
* Plugin body: install the settings bridge and register the HTTP gateway.
|
|
226
|
+
*
|
|
227
|
+
* @param ctx - host context carrying `webServer`.
|
|
228
|
+
* @param config - resolved config (seed values).
|
|
229
|
+
*/
|
|
230
|
+
function apply(ctx, config) {
|
|
231
|
+
registerBrandTextGateway(ctx, installBrandTextSettings(ctx, resolveConfig(config)));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
//#endregion
|
|
235
|
+
export { Config, apply, inject, name };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/invariant.ts
|
|
2
|
+
const PACKAGE_NAME = "@huanlin/dsh-plugin-sidebar-brand-text";
|
|
3
|
+
const name = "sidebar-brand-text-invariant";
|
|
4
|
+
const inject = ["invariants"];
|
|
5
|
+
/**
|
|
6
|
+
* No runtime invariant: the single `sidebar.brand.name` slot registration
|
|
7
|
+
* is a registry-owned contribution whose disposal is proven by the
|
|
8
|
+
* declaration-aware `slots.inject()` rollback. The plugin retains no
|
|
9
|
+
* mutable state beyond the config snapshot closed over by the inject
|
|
10
|
+
* factory.
|
|
11
|
+
*/
|
|
12
|
+
const install = () => {};
|
|
13
|
+
/**
|
|
14
|
+
* Register this package's invariant companion.
|
|
15
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
16
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
17
|
+
*/
|
|
18
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sidebar.brand.name` slot occupant — renders the configured brand
|
|
3
|
+
* name text and optional revision badge.
|
|
4
|
+
*
|
|
5
|
+
* Reads from the shared `BrandTextSettingsController` store via
|
|
6
|
+
* `useSnapshot`, so a save in the settings card is instantly reflected
|
|
7
|
+
* here without a DOM event or RPC re-fetch.
|
|
8
|
+
*
|
|
9
|
+
* Replaces the shell's fallback (`DSH Local Build` + 7-character
|
|
10
|
+
* `DSH_CLIENT_COMMIT_HASH` badge). The mark slot (`sidebar.brand.mark`)
|
|
11
|
+
* is untouched: the fish logo stays unless another plugin (e.g.
|
|
12
|
+
* `ui-brand-official`) replaces it.
|
|
13
|
+
*
|
|
14
|
+
* @module @huanlin/dsh-plugin-sidebar-brand-text/client/BrandText
|
|
15
|
+
*/
|
|
16
|
+
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
17
|
+
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
|
|
18
|
+
import type { BrandTextConfig } from '../types.ts';
|
|
19
|
+
/** Inject face: the selector hook bound to the shared controller store. */
|
|
20
|
+
export interface BrandTextInjected {
|
|
21
|
+
readonly useSnapshot: SnapshotSelectorHook<BrandTextStateShell>;
|
|
22
|
+
}
|
|
23
|
+
/** The slice of state the brand component reads. */
|
|
24
|
+
export interface BrandTextStateShell {
|
|
25
|
+
draft: BrandTextConfig;
|
|
26
|
+
available: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Full props: the slot's runtime share plus the plugin's inject face. */
|
|
29
|
+
export type BrandTextProps = PropsRuntime<'sidebar.brand.name'> & BrandTextInjected;
|
|
30
|
+
/**
|
|
31
|
+
* Render the configured brand name and optional revision badge.
|
|
32
|
+
*
|
|
33
|
+
* While loading or on error, falls back to the shell defaults.
|
|
34
|
+
* @param props - the `useSnapshot` inject face (plus the slot's runtime share, unused).
|
|
35
|
+
* @returns the brand-name span and optional revision-badge span.
|
|
36
|
+
*/
|
|
37
|
+
export declare function BrandText({ useSnapshot }: BrandTextProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
+
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
|
+
import type { BrandTextState } from './controller.ts';
|
|
4
|
+
/** Inject face: controller + selector hook. */
|
|
5
|
+
export interface BrandTextCardInjected {
|
|
6
|
+
readonly controller: {
|
|
7
|
+
readonly load: () => Promise<void>;
|
|
8
|
+
readonly edit: (field: 'name' | 'revision', value: string) => void;
|
|
9
|
+
readonly discard: () => void;
|
|
10
|
+
readonly save: () => Promise<void>;
|
|
11
|
+
readonly toggle: () => void;
|
|
12
|
+
};
|
|
13
|
+
readonly useSnapshot: SnapshotSelectorHook<BrandTextState>;
|
|
14
|
+
}
|
|
15
|
+
/** Full props: locale seat + inject. */
|
|
16
|
+
export type BrandTextCardProps = PropsLocale<'dsh-plugin-sidebar-brand-text'> & BrandTextCardInjected;
|
|
17
|
+
/**
|
|
18
|
+
* Render the sidebar-brand-text settings card.
|
|
19
|
+
* @param props - locale + controller/useSnapshot inject.
|
|
20
|
+
* @returns a `<li>` card element.
|
|
21
|
+
*/
|
|
22
|
+
export declare function BrandTextCard({ t, controller, useSnapshot }: BrandTextCardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HostObservable, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
+
/**
|
|
3
|
+
* Bind a React selector hook to a {@link HostObservable} snapshot source.
|
|
4
|
+
* @param source - the observable snapshot store.
|
|
5
|
+
* @returns a `useSelector(sel, eq?)` hook.
|
|
6
|
+
*/
|
|
7
|
+
export declare function bindSnapshotSelector<T>(source: HostObservable<T>): SnapshotSelectorHook<T>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `BrandTextSettingsController` — client-side state store for the
|
|
3
|
+
* sidebar-brand-text config.
|
|
4
|
+
*
|
|
5
|
+
* Loads the config from the host's `/sbbt/api/get` route, stages edits,
|
|
6
|
+
* and saves via `/sbbt/api/set`. The `sidebar.brand.name` slot occupant
|
|
7
|
+
* and the `settings.plugin.item` card both read from the same store via
|
|
8
|
+
* `bindSnapshotSelector`, so a save is instantly reflected in the sidebar
|
|
9
|
+
* without a DOM event or page reload.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors the ego-browser `EgoBrowserSettingsController` pattern.
|
|
12
|
+
*
|
|
13
|
+
* @module @huanlin/dsh-plugin-sidebar-brand-text/client/controller
|
|
14
|
+
*/
|
|
15
|
+
import { type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
|
|
16
|
+
import { type BrandTextConfig } from '../types.ts';
|
|
17
|
+
/** The controller's snapshot state. */
|
|
18
|
+
export interface BrandTextState {
|
|
19
|
+
/** 'idle' | 'loading' | 'ready' */
|
|
20
|
+
status: 'idle' | 'loading' | 'ready';
|
|
21
|
+
/** True after a successful `/sbbt/api/get`; false when the route is unreachable. */
|
|
22
|
+
available: boolean;
|
|
23
|
+
/** False when the settings service is absent (read-only). */
|
|
24
|
+
writable: boolean;
|
|
25
|
+
/** Current draft values (edited by the card, read by the brand slot). */
|
|
26
|
+
draft: BrandTextConfig;
|
|
27
|
+
/** True when the draft differs from the last-saved config. */
|
|
28
|
+
dirty: boolean;
|
|
29
|
+
/** Apply lifecycle: 'idle' | 'saving' | 'saved' | 'error'. */
|
|
30
|
+
applyState: {
|
|
31
|
+
kind: 'idle';
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'saving';
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'saved';
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'error';
|
|
38
|
+
message: string;
|
|
39
|
+
};
|
|
40
|
+
/** Card expand state (toggled by the card header button). */
|
|
41
|
+
_open: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Controller managing the brand-text config lifecycle.
|
|
45
|
+
*
|
|
46
|
+
* Constructed once in the client `apply()` and shared between the
|
|
47
|
+
* `sidebar.brand.name` slot and the `settings.plugin.item` card.
|
|
48
|
+
*/
|
|
49
|
+
export declare class BrandTextSettingsController {
|
|
50
|
+
readonly store: SnapshotStore<BrandTextState>;
|
|
51
|
+
loaded: boolean;
|
|
52
|
+
private generation;
|
|
53
|
+
constructor();
|
|
54
|
+
/** Fetch the config from `/sbbt/api/get` and update the store. */
|
|
55
|
+
load(): Promise<void>;
|
|
56
|
+
/** Stage an edit to a field (does not save). */
|
|
57
|
+
edit(field: 'name' | 'revision', value: string): void;
|
|
58
|
+
/** Discard staged edits and reload from the host. */
|
|
59
|
+
discard(): void;
|
|
60
|
+
/** Save the staged draft via `/sbbt/api/set`. */
|
|
61
|
+
save(): Promise<void>;
|
|
62
|
+
/** Toggle the card's expand state (mirrors ego-browser `controller.toggle()`). */
|
|
63
|
+
toggle(): void;
|
|
64
|
+
/** Mark the store as unavailable (route unreachable or settings service absent). */
|
|
65
|
+
private markUnavailable;
|
|
66
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sidebar-brand-text — browser half.
|
|
3
|
+
*
|
|
4
|
+
* Two registrations:
|
|
5
|
+
* - `settings.plugin.item` keyed slot (key `sidebar-brand-text`) — a card
|
|
6
|
+
* in the Plugin Config page with two text inputs (name + revision).
|
|
7
|
+
* Reads/writes through the `/sbbt/api` HTTP route via the shared
|
|
8
|
+
* `BrandTextSettingsController`.
|
|
9
|
+
* - `sidebar.brand.name` single slot — renders the configured brand name
|
|
10
|
+
* text and optional revision badge. Reads from the same controller
|
|
11
|
+
* store via `useSnapshot`, so a save in the card is instantly
|
|
12
|
+
* reflected in the sidebar.
|
|
13
|
+
*
|
|
14
|
+
* The mark slot (`sidebar.brand.mark`) is deliberately NOT registered:
|
|
15
|
+
* the fish logo stays in place unless another plugin (e.g.
|
|
16
|
+
* `ui-brand-official`) replaces it.
|
|
17
|
+
*
|
|
18
|
+
* @module @huanlin/dsh-plugin-sidebar-brand-text/client
|
|
19
|
+
*/
|
|
20
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
21
|
+
import { type BrandTextKey } from './locales.ts';
|
|
22
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
23
|
+
interface LocaleNamespaceMap {
|
|
24
|
+
/** The settings card labels. */
|
|
25
|
+
'dsh-plugin-sidebar-brand-text': BrandTextKey;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Required services: slots + locale. */
|
|
29
|
+
export declare const inject: string[];
|
|
30
|
+
/**
|
|
31
|
+
* Client plugin body: register the brand-name slot occupant, the settings
|
|
32
|
+
* card, the locale dictionary, and the stylesheet.
|
|
33
|
+
*
|
|
34
|
+
* A single `BrandTextSettingsController` is shared between the card and
|
|
35
|
+
* the brand text so a save is instantly reflected in the sidebar.
|
|
36
|
+
* @param ctx - client root context.
|
|
37
|
+
*/
|
|
38
|
+
export declare function apply(ctx: ClientContext): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale dictionaries for the `dsh-plugin-sidebar-brand-text` namespace.
|
|
3
|
+
*
|
|
4
|
+
* @module @huanlin/dsh-plugin-sidebar-brand-text/client/locales
|
|
5
|
+
*/
|
|
6
|
+
/** The locale keys the settings card reads. */
|
|
7
|
+
export type BrandTextKey = 'card.title' | 'card.intro' | 'card.unsaved' | 'card.saved' | 'card.saving' | 'card.discard' | 'card.save' | 'card.unavailable' | 'card.retry' | 'field.name.label' | 'field.name.placeholder' | 'field.name.hint' | 'field.revision.label' | 'field.revision.placeholder' | 'field.revision.hint';
|
|
8
|
+
/** The locale namespace name; matches the `locale: NS` passed at slot register. */
|
|
9
|
+
export declare const NS = "dsh-plugin-sidebar-brand-text";
|
|
10
|
+
/** English dictionary. */
|
|
11
|
+
export declare const en: Record<BrandTextKey, string>;
|
|
12
|
+
/** Chinese dictionary. */
|
|
13
|
+
export declare const zh: Record<BrandTextKey, string>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One scoped stylesheet injected for the lifetime of the client activation.
|
|
3
|
+
*
|
|
4
|
+
* The shell's `sidebar.brand.name` fallback renders two CSS-Module-hashed
|
|
5
|
+
* spans (`.fallbackBrandName` + `.buildRevision`); those class names are
|
|
6
|
+
* not stable across builds and not addressable from outside the sidebar
|
|
7
|
+
* package. This plugin ships its own class names with the same visual
|
|
8
|
+
* intent, all colors and typography drawn from the shared `--dsw-*`
|
|
9
|
+
* tokens (never literals) so the badge tracks the active theme.
|
|
10
|
+
*
|
|
11
|
+
* The parent `.brandName` span (inline-flex, gap: 6px, font-size: 18px,
|
|
12
|
+
* font-weight: 600) is owned by the sidebar shell and wraps whatever the
|
|
13
|
+
* slot occupant returns, so this stylesheet only needs to style the two
|
|
14
|
+
* child spans.
|
|
15
|
+
*/
|
|
16
|
+
export declare const CSS = "\n.sbbt-brand-name {\n font-size: 17px;\n letter-spacing: 0px;\n white-space: nowrap;\n}\n\n.sbbt-build-revision {\n display: inline-flex;\n align-items: center;\n height: 16px;\n padding: 0 4px;\n border-radius: 3px;\n color: var(--dsw-alias-label-primary-inverted);\n background: var(--dsw-alias-label-primary);\n font-family: var(--ds-font-family-code);\n font-size: 8px;\n font-weight: 500;\n line-height: 16px;\n}\n";
|
|
17
|
+
/**
|
|
18
|
+
* Install the stylesheet and return its disposer.
|
|
19
|
+
* @returns a cleanup function that removes the injected `<style>` tag.
|
|
20
|
+
*/
|
|
21
|
+
export declare function installStyles(): () => void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schemastery schema + resolver for the `sidebar-brand-text` config.
|
|
3
|
+
*
|
|
4
|
+
* The composition `Config` (cordis.patch.yml seed) and the settings namespace
|
|
5
|
+
* schema share the same shape: `name` (brand text) + `revision` (badge text,
|
|
6
|
+
* empty = hidden). `resolveConfig` applies defaults so a partially-populated
|
|
7
|
+
* entry or settings layer still yields a complete value.
|
|
8
|
+
*
|
|
9
|
+
* @module @huanlin/dsh-plugin-sidebar-brand-text/config
|
|
10
|
+
*/
|
|
11
|
+
import z from '@deepseek-ai/schemastery';
|
|
12
|
+
import type { BrandTextConfig } from './types.ts';
|
|
13
|
+
/** Schemastery schema for the composition entry and the settings namespace. */
|
|
14
|
+
export declare const Config: z<BrandTextConfig>;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a raw config object into a complete {@link BrandTextConfig}.
|
|
17
|
+
*
|
|
18
|
+
* Unknown keys are dropped; missing or wrong-typed keys fall back to
|
|
19
|
+
* the defaults. This runs on every gateway read so the client always
|
|
20
|
+
* sees a well-formed value.
|
|
21
|
+
* @param config - raw config (entry source or settings layer).
|
|
22
|
+
* @returns the resolved config with defaults applied.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveConfig(config?: Record<string, unknown>): BrandTextConfig;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side HTTP gateway exposing the `sidebar-brand-text` config to the
|
|
3
|
+
* browser through a self-hosted `/sbbt/api` route.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the ego-browser / better-sidebar / dsh-plugin-interpreters pattern:
|
|
6
|
+
* `ctx.webServer.register` claims a prefix route, the handler reads/writes
|
|
7
|
+
* the settings seam in-process (no wire-layer allowlist gate), and the
|
|
8
|
+
* browser reaches it through `fetch('/sbbt/api/<method>')`.
|
|
9
|
+
*
|
|
10
|
+
* Route shape:
|
|
11
|
+
* POST /sbbt/api/get → { ok: true, value: { config } }
|
|
12
|
+
* POST /sbbt/api/set body: { patch: { name?, revision? } }
|
|
13
|
+
* → { ok: true, value: { config } }
|
|
14
|
+
* Errors carry { ok: false, error: { code, message } }.
|
|
15
|
+
*
|
|
16
|
+
* @module @huanlin/dsh-plugin-sidebar-brand-text/gateway
|
|
17
|
+
*/
|
|
18
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
19
|
+
import { type BrandTextSettingsBridge } from './settings.ts';
|
|
20
|
+
import type { BrandTextConfig } from './types.ts';
|
|
21
|
+
/** Wire shape for `get` / `set` responses. */
|
|
22
|
+
export interface BrandTextGatewayResponse {
|
|
23
|
+
config: BrandTextConfig;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Register the `/sbbt/api` HTTP route on the host's web server.
|
|
27
|
+
*
|
|
28
|
+
* @param ctx - host context carrying `webServer`.
|
|
29
|
+
* @param bridge - the settings bridge the route reads through.
|
|
30
|
+
*/
|
|
31
|
+
export declare function registerBrandTextGateway(ctx: Context, bridge: BrandTextSettingsBridge): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sidebar-brand-text — host plugin entry.
|
|
3
|
+
*
|
|
4
|
+
* Registers a `sidebar-brand-text` settings namespace (persisted to
|
|
5
|
+
* `$DSH_HOME/settings.yaml`) and a self-hosted `/sbbt/api` HTTP route
|
|
6
|
+
* (`get` / `set`). The browser half's settings card reads/writes the
|
|
7
|
+
* brand name and revision badge through this route; the browser half's
|
|
8
|
+
* `sidebar.brand.name` slot occupant reads the same values from the
|
|
9
|
+
* shared `BrandTextSettingsController` store.
|
|
10
|
+
*
|
|
11
|
+
* The cordis.yml `config` block is the composition `base` (first-boot
|
|
12
|
+
* seed); user edits live in settings.yaml under the `sidebar-brand-text`
|
|
13
|
+
* namespace and take effect on the next gateway read — no restart needed.
|
|
14
|
+
*
|
|
15
|
+
* @module @huanlin/dsh-plugin-sidebar-brand-text
|
|
16
|
+
*/
|
|
17
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
18
|
+
import { Config } from './config.ts';
|
|
19
|
+
import type { BrandTextConfig } from './types.ts';
|
|
20
|
+
export declare const name = "sidebar-brand-text";
|
|
21
|
+
/** `webServer` is required for the HTTP gateway that backs the settings card. */
|
|
22
|
+
export declare const inject: string[];
|
|
23
|
+
export type { BrandTextConfig } from './types.ts';
|
|
24
|
+
/** Re-export the schemastery schema for cordis's composition loader. */
|
|
25
|
+
export { Config };
|
|
26
|
+
/**
|
|
27
|
+
* Plugin body: install the settings bridge and register the HTTP gateway.
|
|
28
|
+
*
|
|
29
|
+
* @param ctx - host context carrying `webServer`.
|
|
30
|
+
* @param config - resolved config (seed values).
|
|
31
|
+
*/
|
|
32
|
+
export declare function apply(ctx: Context, config: BrandTextConfig): void;
|