@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 +18 -0
- package/README.md +61 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.js +481 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
- package/src/context.tsx +28 -0
- package/src/define-api.ts +100 -0
- package/src/fixtures.tsx +25 -0
- package/src/i18n.test.tsx +147 -0
- package/src/i18n.ts +96 -0
- package/src/index.ts +23 -0
- package/src/query.ts +407 -0
- package/src/root.tsx +144 -0
- package/src/use-cache-writer.ts +61 -0
- package/src/use-extract-progress.test.tsx +118 -0
- package/src/use-extract-progress.ts +76 -0
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`.
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|