@hoardodile/sdk-react 0.0.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/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wooloo <ayan0312000@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
+ associated documentation files (the "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial
12
+ portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
18
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # @hoardodile/sdk-react
2
+
3
+ React bindings for hoardodile content plugin iframes. Framework-agnostic
4
+ plugins use `@hoardodile/sdk-web` directly instead.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pnpm add @hoardodile/sdk-react
10
+ ```
11
+
12
+ ## What's in it
13
+
14
+ - **`createPluginRoot(config)`** — one-call bootstrap: mounts the iframe
15
+ bridge, wires the typed `PluginAPIProvider`, applies host theme/fonts,
16
+ and remounts on resource rebind
17
+ - **`definePluginAPI<Schema>({ decodeAnchor })`** — declare your plugin
18
+ schema once and get a typed `PluginAPIProvider`, `usePluginAPI` and
19
+ `useAnchorJump`
20
+ - **`useVisibility()`** — pause media/timers when the iframe is parked
21
+ - **`useCacheWriter()`** — debounced per-resource cache writes with a
22
+ flush on `pagehide`/unmount
23
+ - **`createPluginTranslation(bundles)`** — `useTranslation()` backed by
24
+ the shared `@hoardodile/i18n` catalogs and the host's language pushes;
25
+ `@hoardodile/ui` chrome is localized for free in every supported host
26
+ language. Want full control over the i18n stack (namespaces, plurals,
27
+ `Trans`, custom format, lazy backends)? Assemble your own
28
+ react-i18next instance instead — the only hoardodile piece is the
29
+ `languageChanged` notification from `@hoardodile/sdk-web` (see the
30
+ advanced pattern in `skills/hd-plugin/references/client.md`).
31
+ - **`StubPluginAPIProvider`** — render-test fixture
32
+
33
+ ## Quick start
34
+
35
+ ```tsx
36
+ // src/render.tsx
37
+ import { createPluginRoot, definePluginAPI } from "@hoardodile/sdk-react"
38
+ import type { PluginSchema } from "@hoardodile/sdk-types"
39
+
40
+ interface MySchema extends PluginSchema {
41
+ file: { filename: string }
42
+ sourceMeta: { files: readonly { filename: string }[] }
43
+ }
44
+
45
+ const { PluginAPIProvider, usePluginAPI } = definePluginAPI<MySchema>()
46
+
47
+ function Viewer() {
48
+ const api = usePluginAPI()
49
+ const { data: files } = api.useFileList()
50
+ return <div>{files?.length} files</div>
51
+ }
52
+
53
+ createPluginRoot({ render: Viewer, provider: PluginAPIProvider })
54
+ ```
55
+
56
+ ## Where does the code live?
57
+
58
+ The imperative API surface (`WebPluginAPI`) and the wire protocol live in
59
+ `@hoardodile/sdk-web`; this package composes them into the reactive API
60
+ React components see. Shared message/danmaku/anchor types come from
61
+ `@hoardodile/sdk-types`.
@@ -0,0 +1,199 @@
1
+ import * as react from 'react';
2
+ import { Provider, ReactNode, ComponentType } from 'react';
3
+ import { WebPluginAPI, ReactivePluginAPI, DeepPartial, Host, PluginFonts } from '@hoardodile/sdk-web';
4
+ export { DeepPartial, createWebPluginAPI } from '@hoardodile/sdk-web';
5
+ import { PluginSchema } from '@hoardodile/sdk-types';
6
+
7
+ /**
8
+ * The full plugin API as delivered to React plugin components: the
9
+ * imperative {@link WebPluginAPI} plus the reactive hooks implemented by
10
+ * this package's adapter. Typed with default (unknown) schema slots; the
11
+ * typed provider from `definePluginAPI` narrows it per plugin.
12
+ */
13
+ type BasePluginAPI = WebPluginAPI & ReactivePluginAPI;
14
+ /**
15
+ * Provides the plugin API to the component tree. Usually created
16
+ * implicitly via {@link createPluginRoot}; the typed provider from
17
+ * {@link definePluginAPI} narrows `BasePluginAPI` to the plugin's
18
+ * schema.
19
+ */
20
+ declare const PluginAPIProvider: react.Provider<BasePluginAPI | null>;
21
+ declare function usePluginAPI(): BasePluginAPI;
22
+
23
+ /** The full API seen by React plugin components: imperative + hooks. */
24
+ type FullPluginAPI<TSchema extends PluginSchema> = WebPluginAPI<TSchema> & ReactivePluginAPI<TSchema>;
25
+ type DefinePluginAPIOptions<TSchema extends PluginSchema> = {
26
+ /**
27
+ * Validate incoming anchor data (host → plugin) against the schema's
28
+ * `anchor` slot. Anchors that fail decoding are dropped silently and
29
+ * never reach the `useAnchorJump` callback. Declare this whenever the
30
+ * schema declares an `anchor` type.
31
+ */
32
+ readonly decodeAnchor?: (data: unknown) => TSchema["anchor"] | undefined;
33
+ };
34
+ /**
35
+ * Define a typed plugin API context. The schema is declared once at module
36
+ * level — every consumer below gets properly typed access without repeating
37
+ * generics.
38
+ *
39
+ * The returned provider and hook share the same React context as the default
40
+ * {@link PluginAPIProvider}, so plugin roots created by `createPluginRoot`
41
+ * automatically satisfy typed consumers when the same provider is passed in.
42
+ *
43
+ * ```typescript
44
+ * interface VideoSchema { file: VideoFile; sourceMeta: VideoSourceMeta; anchor: VideoTimeAnchor }
45
+ * const { PluginAPIProvider, usePluginAPI, useAnchorJump } = definePluginAPI<VideoSchema>({
46
+ * decodeAnchor: decodeVideoTimeAnchor,
47
+ * })
48
+ *
49
+ * function Viewer() {
50
+ * const api = usePluginAPI()
51
+ * const { data: files } = api.useFileList()
52
+ * // files → readonly VideoFile[] | undefined
53
+ * }
54
+ * ```
55
+ */
56
+ declare function definePluginAPI<TSchema extends PluginSchema = PluginSchema>(options?: DefinePluginAPIOptions<TSchema>): {
57
+ readonly PluginAPIProvider: Provider<FullPluginAPI<TSchema> | null>;
58
+ readonly usePluginAPI: () => FullPluginAPI<TSchema>;
59
+ readonly useAnchorJump: (cb: (anchor: TSchema["anchor"]) => void) => void;
60
+ };
61
+
62
+ /**
63
+ * Wrap children with a stubbed API provider for tests. Builds the stub
64
+ * via `createWebPluginAPI` from `@hoardodile/sdk-web` (re-exported
65
+ * below) — the imperative surface plus no-op reactive hooks, overridable
66
+ * via `api`.
67
+ */
68
+ declare function StubPluginAPIProvider({ api, children, }: {
69
+ readonly api?: DeepPartial<BasePluginAPI>;
70
+ readonly children: ReactNode;
71
+ }): react.JSX.Element;
72
+
73
+ type RawBundle = Record<string, unknown>;
74
+ type InterpolationVars = Record<string, string | number>;
75
+ type PluginTranslation = {
76
+ readonly t: (key: string, vars?: InterpolationVars) => string;
77
+ readonly language: string;
78
+ };
79
+ /**
80
+ * Creates a `useTranslation` hook backed by the given locale bundles plus
81
+ * the shared `ui` catalog namespace (so `@hoardodile/ui` components
82
+ * render localized chrome in every supported host language).
83
+ *
84
+ * Backed by i18next/react-i18next with the same options as the host
85
+ * surfaces: the language follows the plugin context, updates when the
86
+ * host sends a `languageChanged` push, interpolates `{{var}}`
87
+ * placeholders, and falls back to English (via `fallbackLng`) for
88
+ * languages the plugin's own bundle does not ship.
89
+ */
90
+ declare function createPluginTranslation(bundles: Record<string, RawBundle>): {
91
+ readonly useTranslation: () => PluginTranslation;
92
+ };
93
+
94
+ /**
95
+ * Builds the reactive half of the plugin API (`useFileList`,
96
+ * `useMessageList`, `useCreateMessage`, `useDanmakuList`,
97
+ * `useCreateDanmaku`, `usePref`, `useTheme`, `useFont`) on top of the
98
+ * imperative `WebPluginAPI` and the host bridge. Queries refetch
99
+ * automatically on the matching host invalidation push and when the
100
+ * iframe is rebound to another resource.
101
+ *
102
+ * Consumed by {@link createPluginRoot}; call it directly only when
103
+ * composing your own runtime. The returned hooks are bound to the
104
+ * `host` passed in — the one from `ensureHostBridge()`.
105
+ */
106
+ declare function createPluginQueryAPI<TSchema extends PluginSchema = PluginSchema>(host: Host, ctx: {
107
+ readonly resolvedTheme: string;
108
+ readonly palette: string;
109
+ readonly iconStyle: string;
110
+ readonly fonts: PluginFonts;
111
+ readonly resId: string;
112
+ }): ReactivePluginAPI<TSchema>;
113
+
114
+ type PluginRootConfig<TSchema extends PluginSchema = PluginSchema> = {
115
+ /** Root component rendered inside the plugin iframe. */
116
+ readonly render: ComponentType;
117
+ /**
118
+ * Typed provider returned by {@link definePluginAPI}. This is the single
119
+ * source of truth for the plugin schema type.
120
+ */
121
+ readonly provider: Provider<FullPluginAPI<TSchema> | null>;
122
+ /**
123
+ * When `true` (default), the whole plugin tree remounts whenever the
124
+ * iframe is rebound to another resource — the safe choice: all
125
+ * per-resource state resets automatically.
126
+ *
127
+ * Set to `false` for fine-grained updates (e.g. cheap same-plugin
128
+ * navigation): the mounted tree stays alive and only re-renders with
129
+ * the new `api`. Queries refetch automatically, but every piece of
130
+ * per-resource state becomes the plugin's own responsibility — key
131
+ * subtrees and memos by `api.resource.id` and reset any hydration
132
+ * flags yourself.
133
+ */
134
+ readonly remountOnResourceChange?: boolean;
135
+ };
136
+ /**
137
+ * Subscribe to the iframe visibility state from the host: `false` while
138
+ * the iframe is parked offscreen in the preview window. Do NOT gate
139
+ * rendering on it — parked slots are meant to pre-paint so a flip is a
140
+ * style swap, and an empty tree defeats that. Use visibility only to
141
+ * pause active behavior: media playback, autoplay, timers.
142
+ */
143
+ declare function useVisibility(): boolean;
144
+ /**
145
+ * One-call plugin bootstrap. Handles `mountPlugin`, `createRoot` caching,
146
+ * iframe host API, typed `PluginAPIProvider`, reactive theme application, and
147
+ * visibility subscription.
148
+ *
149
+ * The supplied component receives no props; it should call `usePluginAPI()`
150
+ * and `useVisibility()` internally as needed. By default the root remounts
151
+ * when the resource changes (see
152
+ * {@link PluginRootConfig.remountOnResourceChange}); even then, use
153
+ * `api.resource.id` as a key inside your component if you need finer
154
+ * control.
155
+ */
156
+ declare function createPluginRoot<TSchema extends PluginSchema = PluginSchema>(config: PluginRootConfig<TSchema>): void;
157
+
158
+ /**
159
+ * Persist a value to the per-resource plugin cache: debounced writes while
160
+ * the value changes, plus a flush on `pagehide` / `beforeunload` / unmount
161
+ * so no pending update is lost.
162
+ *
163
+ * Use this for reader positions, resume timestamps, and similar
164
+ * continuously-changing state. Pass `undefined` as the value (or
165
+ * `disabled: true`) to skip persistence while the real value is loading.
166
+ */
167
+ declare function useCacheWriter<T>(options: {
168
+ readonly key: string;
169
+ readonly value: T | undefined;
170
+ readonly encode: (value: T) => string;
171
+ readonly disabled?: boolean;
172
+ readonly debounceMs?: number;
173
+ }): void;
174
+
175
+ /**
176
+ * Materialization progress of the plugin's `extractArchive` hook:
177
+ * `"extracting"` while the host reports in-flight work, `"done"` once
178
+ * progress was seen and the record went idle again, `"idle"` when no
179
+ * extraction has ever been observed (or a poll failed — the host may
180
+ * not be serving yet).
181
+ */
182
+ type ExtractProgressState = {
183
+ readonly state: "idle";
184
+ } | {
185
+ readonly state: "extracting";
186
+ readonly done: number;
187
+ readonly total: number;
188
+ } | {
189
+ readonly state: "done";
190
+ };
191
+ /**
192
+ * Reactive materialization progress for the current resource. Polls
193
+ * `api.extractProgressUrl()` and tracks the seen-progress transition:
194
+ * a plugin that called `extractArchive` can show "extracting" while the
195
+ * host materializes, then switch to "done" when the record expires.
196
+ */
197
+ declare function useExtractProgress(): ExtractProgressState;
198
+
199
+ export { type DefinePluginAPIOptions, type ExtractProgressState, type FullPluginAPI, PluginAPIProvider, type PluginRootConfig, StubPluginAPIProvider, createPluginQueryAPI, createPluginRoot, createPluginTranslation, definePluginAPI, useCacheWriter, useExtractProgress, usePluginAPI, useVisibility };