@aicut/react 0.3.0 → 0.4.1

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 CHANGED
@@ -160,32 +160,41 @@ import "@aicut/core/styles.css";
160
160
 
161
161
  function Relight() {
162
162
  const apiRef = useRef<LightingEditorApi | null>(null);
163
+
164
+ const onGenerate = (): void => {
165
+ const cfg = apiRef.current?.getConfig();
166
+ if (cfg) fetch("/relight", { method: "POST", body: JSON.stringify(cfg) });
167
+ };
168
+
169
+ // Library renders ONLY the picker (scene + controls). Host lays out
170
+ // the Smart mode panel beside it in their own flex/grid.
163
171
  return (
164
- <LightingEditor
165
- apiRef={apiRef}
166
- subjectImageUrl="/frames/subject.jpg"
167
- smartEnabled
168
- smartPanel={
169
- <>
170
- <textarea placeholder="Describe the mood…" />
171
- <button onClick={() => apiRef.current?.requestGenerate()}>
172
- Generate
173
- </button>
174
- </>
175
- }
176
- onChange={(cfg: LightingConfig) => console.log(cfg)}
177
- onGenerate={(cfg) =>
178
- fetch("/relight", {
179
- method: "POST",
180
- body: JSON.stringify(cfg),
181
- })
182
- }
183
- />
172
+ <div style={{ display: "flex", gap: 16 }}>
173
+ <LightingEditor
174
+ apiRef={apiRef}
175
+ subjectImageUrl="/frames/subject.jpg"
176
+ onChange={(cfg: LightingConfig) => console.log(cfg)}
177
+ // Reset / Generate / save-preset / etc. buttons go into the
178
+ // controls column's footer slot — the only host-supplied
179
+ // surface the library reserves space for.
180
+ controlsFooter={
181
+ <button onClick={() => apiRef.current?.reset()}>Reset</button>
182
+ }
183
+ />
184
+ <aside>
185
+ <textarea placeholder="Describe the mood…" />
186
+ <button onClick={onGenerate}>Generate</button>
187
+ </aside>
188
+ </div>
184
189
  );
185
190
  }
186
191
  ```
187
192
 
188
- Props: `subjectImageUrl`, `defaultConfig`, `defaultView`, `theme`, `locale`, `smartEnabled`, `smartOpen`, `smartPanel`, `onChange`, `onGenerate`, `onSmartOpenChange`. The host's `smartPanel` is portaled into the editor's slot; the library renders the × close button + a "Smart mode" header pill to re-open it.
193
+ Props: `subjectImageUrl`, `defaultConfig`, `defaultView`, `theme`, `locale`, `controlsFooter`, `onChange`.
194
+
195
+ Imperative API (`apiRef.current`): `setConfig`, `getConfig`, `setSubjectImage`, `setView`, `getView`, `reset`.
196
+
197
+ The library is intentionally focused on the picker — Smart mode UI, Generate buttons, close handling, layout all live in host code.
189
198
 
190
199
  ## Standalone `<Timeline>`
191
200
 
package/dist/lighting.cjs CHANGED
@@ -32,12 +32,13 @@ module.exports = __toCommonJS(lighting_exports);
32
32
 
33
33
  // src/LightingEditor.tsx
34
34
  var import_react = require("react");
35
+ var import_react_dom = require("react-dom");
35
36
  var import_lighting = require("@aicut/core/lighting");
36
37
  var import_jsx_runtime = require("react/jsx-runtime");
37
38
  function LightingEditor(props) {
38
39
  const hostRef = (0, import_react.useRef)(null);
39
40
  const editorRef = (0, import_react.useRef)(null);
40
- const [ready, setReady] = (0, import_react.useState)(false);
41
+ const [footerSlot, setFooterSlot] = (0, import_react.useState)(null);
41
42
  const cbRef = (0, import_react.useRef)(props);
42
43
  cbRef.current = props;
43
44
  (0, import_react.useEffect)(() => {
@@ -53,11 +54,11 @@ function LightingEditor(props) {
53
54
  onChange: (cfg) => cbRef.current.onChange?.(cfg)
54
55
  });
55
56
  editorRef.current = editor;
56
- setReady(true);
57
+ setFooterSlot(editor.controlsFooter);
57
58
  return () => {
58
59
  editor.destroy();
59
60
  editorRef.current = null;
60
- setReady(false);
61
+ setFooterSlot(null);
61
62
  };
62
63
  }, []);
63
64
  (0, import_react.useEffect)(() => {
@@ -80,10 +81,12 @@ function LightingEditor(props) {
80
81
  getConfig: () => ed.getConfig(),
81
82
  setSubjectImage: (url) => ed.setSubjectImage(url),
82
83
  setView: (v) => ed.setView(v),
83
- getView: () => ed.getView()
84
+ getView: () => ed.getView(),
85
+ reset: () => ed.reset()
84
86
  };
85
87
  },
86
- [ready]
88
+ // Keyed on footerSlot — same null-lock fix the VideoEditor uses.
89
+ [footerSlot]
87
90
  );
88
91
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
89
92
  "div",
@@ -91,7 +94,8 @@ function LightingEditor(props) {
91
94
  ref: hostRef,
92
95
  className: props.className,
93
96
  style: props.style,
94
- "data-aicut-lighting-host": ""
97
+ "data-aicut-lighting-host": "",
98
+ children: footerSlot && props.controlsFooter != null ? (0, import_react_dom.createPortal)(props.controlsFooter, footerSlot) : null
95
99
  }
96
100
  );
97
101
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lighting.ts","../src/LightingEditor.tsx"],"sourcesContent":["/**\n * @aicut/react/lighting — separate entry that pulls three.js. Users\n * who never import this path don't pay the three.js bundle cost.\n */\nexport { LightingEditor } from \"./LightingEditor.js\";\nexport type {\n LightingEditorProps,\n LightingEditorApi,\n} from \"./LightingEditor.js\";\n\n// Re-export the data + locale exports from the core sub-entry so\n// hosts only need a single import line for everything lighting-related.\nexport {\n DEFAULT_LIGHTING_CONFIG,\n PRESET_DIRECTIONS,\n lightingLocaleEn,\n lightingLocaleZh,\n mergeLightingLocale,\n snapToPreset,\n} from \"@aicut/core/lighting\";\nexport type {\n KeyPreset,\n LightingConfig,\n LightingEditorOptions,\n LightingLocale,\n LightingView,\n} from \"@aicut/core/lighting\";\n","import {\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type Ref,\n} from \"react\";\nimport {\n LightingEditor as CoreLightingEditor,\n type LightingConfig,\n type LightingEditorOptions,\n type LightingView,\n} from \"@aicut/core/lighting\";\nimport type { Theme } from \"@aicut/core\";\n\nexport interface LightingEditorApi {\n setConfig(partial: Partial<LightingConfig>): void;\n getConfig(): LightingConfig;\n setSubjectImage(url: string): void;\n setView(v: LightingView): void;\n getView(): LightingView;\n}\n\nexport interface LightingEditorProps {\n /** Initial subject image (URL or data URI). Reactive. */\n subjectImageUrl?: string;\n /** Initial config. */\n defaultConfig?: Partial<LightingConfig>;\n /** Initial view. Default `\"perspective\"`. */\n defaultView?: LightingView;\n /** Theme — reactive (calls editor.setTheme). */\n theme?: Theme;\n /** Locale partial — reactive (calls editor.setLocale). */\n locale?: LightingEditorOptions[\"locale\"];\n\n className?: string;\n style?: CSSProperties;\n apiRef?: Ref<LightingEditorApi | null>;\n\n onChange?: (cfg: LightingConfig) => void;\n}\n\n/**\n * React shell for the 3D lighting picker. Renders scene + controls;\n * nothing else. Host code lays out their own surrounding UI (smart\n * mode panel, generate button, etc.) alongside this component in\n * whatever flex/grid the host prefers.\n */\nexport function LightingEditor(props: LightingEditorProps) {\n const hostRef = useRef<HTMLDivElement | null>(null);\n const editorRef = useRef<CoreLightingEditor | null>(null);\n // Triggers a re-render the moment the editor is created, so the\n // useImperativeHandle factory below can return the real instance\n // instead of locking at null forever (same trick as VideoEditor).\n const [ready, setReady] = useState(false);\n\n const cbRef = useRef(props);\n cbRef.current = props;\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const editor = CoreLightingEditor.create({\n container: host,\n subjectImageUrl: cbRef.current.subjectImageUrl,\n config: cbRef.current.defaultConfig,\n view: cbRef.current.defaultView,\n theme: cbRef.current.theme,\n locale: cbRef.current.locale,\n onChange: (cfg) => cbRef.current.onChange?.(cfg),\n });\n editorRef.current = editor;\n setReady(true);\n return () => {\n editor.destroy();\n editorRef.current = null;\n setReady(false);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (props.theme) editorRef.current?.setTheme(props.theme);\n }, [props.theme]);\n useEffect(() => {\n // Always push — `undefined` is the \"reset to English defaults\"\n // signal. Without this, toggling ZH→EN in the host kept ZH labels.\n editorRef.current?.setLocale(props.locale ?? {});\n }, [props.locale]);\n useEffect(() => {\n if (props.subjectImageUrl)\n editorRef.current?.setSubjectImage(props.subjectImageUrl);\n }, [props.subjectImageUrl]);\n\n useImperativeHandle<LightingEditorApi | null, LightingEditorApi | null>(\n props.apiRef,\n () => {\n const ed = editorRef.current;\n if (!ed) return null;\n return {\n setConfig: (p) => ed.setConfig(p),\n getConfig: () => ed.getConfig(),\n setSubjectImage: (url) => ed.setSubjectImage(url),\n setView: (v) => ed.setView(v),\n getView: () => ed.getView(),\n };\n },\n [ready],\n );\n\n return (\n <div\n ref={hostRef}\n className={props.className}\n style={props.style}\n data-aicut-lighting-host=\"\"\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAOO;AACP,sBAKO;AAmGH;AA/DG,SAAS,eAAe,OAA4B;AACzD,QAAM,cAAU,qBAA8B,IAAI;AAClD,QAAM,gBAAY,qBAAkC,IAAI;AAIxD,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAS,KAAK;AAExC,QAAM,YAAQ,qBAAO,KAAK;AAC1B,QAAM,UAAU;AAEhB,8BAAU,MAAM;AACd,UAAM,OAAO,QAAQ;AACrB,QAAI,CAAC,KAAM;AACX,UAAM,SAAS,gBAAAA,eAAmB,OAAO;AAAA,MACvC,WAAW;AAAA,MACX,iBAAiB,MAAM,QAAQ;AAAA,MAC/B,QAAQ,MAAM,QAAQ;AAAA,MACtB,MAAM,MAAM,QAAQ;AAAA,MACpB,OAAO,MAAM,QAAQ;AAAA,MACrB,QAAQ,MAAM,QAAQ;AAAA,MACtB,UAAU,CAAC,QAAQ,MAAM,QAAQ,WAAW,GAAG;AAAA,IACjD,CAAC;AACD,cAAU,UAAU;AACpB,aAAS,IAAI;AACb,WAAO,MAAM;AACX,aAAO,QAAQ;AACf,gBAAU,UAAU;AACpB,eAAS,KAAK;AAAA,IAChB;AAAA,EAEF,GAAG,CAAC,CAAC;AAEL,8BAAU,MAAM;AACd,QAAI,MAAM,MAAO,WAAU,SAAS,SAAS,MAAM,KAAK;AAAA,EAC1D,GAAG,CAAC,MAAM,KAAK,CAAC;AAChB,8BAAU,MAAM;AAGd,cAAU,SAAS,UAAU,MAAM,UAAU,CAAC,CAAC;AAAA,EACjD,GAAG,CAAC,MAAM,MAAM,CAAC;AACjB,8BAAU,MAAM;AACd,QAAI,MAAM;AACR,gBAAU,SAAS,gBAAgB,MAAM,eAAe;AAAA,EAC5D,GAAG,CAAC,MAAM,eAAe,CAAC;AAE1B;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AACJ,YAAM,KAAK,UAAU;AACrB,UAAI,CAAC,GAAI,QAAO;AAChB,aAAO;AAAA,QACL,WAAW,CAAC,MAAM,GAAG,UAAU,CAAC;AAAA,QAChC,WAAW,MAAM,GAAG,UAAU;AAAA,QAC9B,iBAAiB,CAAC,QAAQ,GAAG,gBAAgB,GAAG;AAAA,QAChD,SAAS,CAAC,MAAM,GAAG,QAAQ,CAAC;AAAA,QAC5B,SAAS,MAAM,GAAG,QAAQ;AAAA,MAC5B;AAAA,IACF;AAAA,IACA,CAAC,KAAK;AAAA,EACR;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,WAAW,MAAM;AAAA,MACjB,OAAO,MAAM;AAAA,MACb,4BAAyB;AAAA;AAAA,EAC3B;AAEJ;;;AD3GA,IAAAC,mBAOO;","names":["CoreLightingEditor","import_lighting"]}
1
+ {"version":3,"sources":["../src/lighting.ts","../src/LightingEditor.tsx"],"sourcesContent":["/**\n * @aicut/react/lighting — separate entry that pulls three.js. Users\n * who never import this path don't pay the three.js bundle cost.\n */\nexport { LightingEditor } from \"./LightingEditor.js\";\nexport type {\n LightingEditorProps,\n LightingEditorApi,\n} from \"./LightingEditor.js\";\n\n// Re-export the data + locale exports from the core sub-entry so\n// hosts only need a single import line for everything lighting-related.\nexport {\n DEFAULT_LIGHTING_CONFIG,\n PRESET_DIRECTIONS,\n lightingLocaleEn,\n lightingLocaleZh,\n mergeLightingLocale,\n snapToPreset,\n} from \"@aicut/core/lighting\";\nexport type {\n KeyPreset,\n LightingConfig,\n LightingEditorOptions,\n LightingLocale,\n LightingView,\n} from \"@aicut/core/lighting\";\n","import {\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n type Ref,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n LightingEditor as CoreLightingEditor,\n type LightingConfig,\n type LightingEditorOptions,\n type LightingView,\n} from \"@aicut/core/lighting\";\nimport type { Theme } from \"@aicut/core\";\n\nexport interface LightingEditorApi {\n setConfig(partial: Partial<LightingConfig>): void;\n getConfig(): LightingConfig;\n setSubjectImage(url: string): void;\n setView(v: LightingView): void;\n getView(): LightingView;\n /** Convenience — restores config to safe defaults. */\n reset(): void;\n}\n\nexport interface LightingEditorProps {\n /** Initial subject image (URL or data URI). Reactive. */\n subjectImageUrl?: string;\n /** Initial config. */\n defaultConfig?: Partial<LightingConfig>;\n /** Initial view. Default `\"perspective\"`. */\n defaultView?: LightingView;\n /** Theme — reactive (calls editor.setTheme). */\n theme?: Theme;\n /** Locale partial — reactive (calls editor.setLocale). */\n locale?: LightingEditorOptions[\"locale\"];\n\n /**\n * Any React node — portaled into the editor's controls footer slot\n * (where the built-in Reset button used to live). Hosts put their\n * Reset / Generate / save-preset / etc. buttons here. Empty until\n * populated; the library renders nothing into the slot.\n */\n controlsFooter?: ReactNode;\n\n className?: string;\n style?: CSSProperties;\n apiRef?: Ref<LightingEditorApi | null>;\n\n onChange?: (cfg: LightingConfig) => void;\n}\n\n/**\n * React shell for the 3D lighting picker. Renders scene + controls;\n * the host owns everything else (smart panel beside, action buttons\n * in the controlsFooter slot, layout, theming the surrounding page).\n */\nexport function LightingEditor(props: LightingEditorProps) {\n const hostRef = useRef<HTMLDivElement | null>(null);\n const editorRef = useRef<CoreLightingEditor | null>(null);\n // Hold the footer slot DOM node in state so the portal mounts after\n // editor creation. Same lifecycle dance VideoEditor does for its\n // toolbar slots.\n const [footerSlot, setFooterSlot] = useState<HTMLElement | null>(null);\n\n const cbRef = useRef(props);\n cbRef.current = props;\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const editor = CoreLightingEditor.create({\n container: host,\n subjectImageUrl: cbRef.current.subjectImageUrl,\n config: cbRef.current.defaultConfig,\n view: cbRef.current.defaultView,\n theme: cbRef.current.theme,\n locale: cbRef.current.locale,\n onChange: (cfg) => cbRef.current.onChange?.(cfg),\n });\n editorRef.current = editor;\n setFooterSlot(editor.controlsFooter);\n return () => {\n editor.destroy();\n editorRef.current = null;\n setFooterSlot(null);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (props.theme) editorRef.current?.setTheme(props.theme);\n }, [props.theme]);\n useEffect(() => {\n editorRef.current?.setLocale(props.locale ?? {});\n }, [props.locale]);\n useEffect(() => {\n if (props.subjectImageUrl)\n editorRef.current?.setSubjectImage(props.subjectImageUrl);\n }, [props.subjectImageUrl]);\n\n useImperativeHandle<LightingEditorApi | null, LightingEditorApi | null>(\n props.apiRef,\n () => {\n const ed = editorRef.current;\n if (!ed) return null;\n return {\n setConfig: (p) => ed.setConfig(p),\n getConfig: () => ed.getConfig(),\n setSubjectImage: (url) => ed.setSubjectImage(url),\n setView: (v) => ed.setView(v),\n getView: () => ed.getView(),\n reset: () => ed.reset(),\n };\n },\n // Keyed on footerSlot — same null-lock fix the VideoEditor uses.\n [footerSlot],\n );\n\n return (\n <div\n ref={hostRef}\n className={props.className}\n style={props.style}\n data-aicut-lighting-host=\"\"\n >\n {footerSlot && props.controlsFooter != null\n ? createPortal(props.controlsFooter, footerSlot)\n : null}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAQO;AACP,uBAA6B;AAC7B,sBAKO;AA4GH;AA/DG,SAAS,eAAe,OAA4B;AACzD,QAAM,cAAU,qBAA8B,IAAI;AAClD,QAAM,gBAAY,qBAAkC,IAAI;AAIxD,QAAM,CAAC,YAAY,aAAa,QAAI,uBAA6B,IAAI;AAErE,QAAM,YAAQ,qBAAO,KAAK;AAC1B,QAAM,UAAU;AAEhB,8BAAU,MAAM;AACd,UAAM,OAAO,QAAQ;AACrB,QAAI,CAAC,KAAM;AACX,UAAM,SAAS,gBAAAA,eAAmB,OAAO;AAAA,MACvC,WAAW;AAAA,MACX,iBAAiB,MAAM,QAAQ;AAAA,MAC/B,QAAQ,MAAM,QAAQ;AAAA,MACtB,MAAM,MAAM,QAAQ;AAAA,MACpB,OAAO,MAAM,QAAQ;AAAA,MACrB,QAAQ,MAAM,QAAQ;AAAA,MACtB,UAAU,CAAC,QAAQ,MAAM,QAAQ,WAAW,GAAG;AAAA,IACjD,CAAC;AACD,cAAU,UAAU;AACpB,kBAAc,OAAO,cAAc;AACnC,WAAO,MAAM;AACX,aAAO,QAAQ;AACf,gBAAU,UAAU;AACpB,oBAAc,IAAI;AAAA,IACpB;AAAA,EAEF,GAAG,CAAC,CAAC;AAEL,8BAAU,MAAM;AACd,QAAI,MAAM,MAAO,WAAU,SAAS,SAAS,MAAM,KAAK;AAAA,EAC1D,GAAG,CAAC,MAAM,KAAK,CAAC;AAChB,8BAAU,MAAM;AACd,cAAU,SAAS,UAAU,MAAM,UAAU,CAAC,CAAC;AAAA,EACjD,GAAG,CAAC,MAAM,MAAM,CAAC;AACjB,8BAAU,MAAM;AACd,QAAI,MAAM;AACR,gBAAU,SAAS,gBAAgB,MAAM,eAAe;AAAA,EAC5D,GAAG,CAAC,MAAM,eAAe,CAAC;AAE1B;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AACJ,YAAM,KAAK,UAAU;AACrB,UAAI,CAAC,GAAI,QAAO;AAChB,aAAO;AAAA,QACL,WAAW,CAAC,MAAM,GAAG,UAAU,CAAC;AAAA,QAChC,WAAW,MAAM,GAAG,UAAU;AAAA,QAC9B,iBAAiB,CAAC,QAAQ,GAAG,gBAAgB,GAAG;AAAA,QAChD,SAAS,CAAC,MAAM,GAAG,QAAQ,CAAC;AAAA,QAC5B,SAAS,MAAM,GAAG,QAAQ;AAAA,QAC1B,OAAO,MAAM,GAAG,MAAM;AAAA,MACxB;AAAA,IACF;AAAA;AAAA,IAEA,CAAC,UAAU;AAAA,EACb;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,WAAW,MAAM;AAAA,MACjB,OAAO,MAAM;AAAA,MACb,4BAAyB;AAAA,MAExB,wBAAc,MAAM,kBAAkB,WACnC,+BAAa,MAAM,gBAAgB,UAAU,IAC7C;AAAA;AAAA,EACN;AAEJ;;;AD1HA,IAAAC,mBAOO;","names":["CoreLightingEditor","import_lighting"]}
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { CSSProperties, Ref } from 'react';
2
+ import { ReactNode, CSSProperties, Ref } from 'react';
3
3
  import { LightingConfig, LightingView, LightingEditorOptions } from '@aicut/core/lighting';
4
4
  export { DEFAULT_LIGHTING_CONFIG, KeyPreset, LightingConfig, LightingEditorOptions, LightingLocale, LightingView, PRESET_DIRECTIONS, lightingLocaleEn, lightingLocaleZh, mergeLightingLocale, snapToPreset } from '@aicut/core/lighting';
5
5
  import { Theme } from '@aicut/core';
@@ -10,6 +10,8 @@ interface LightingEditorApi {
10
10
  setSubjectImage(url: string): void;
11
11
  setView(v: LightingView): void;
12
12
  getView(): LightingView;
13
+ /** Convenience — restores config to safe defaults. */
14
+ reset(): void;
13
15
  }
14
16
  interface LightingEditorProps {
15
17
  /** Initial subject image (URL or data URI). Reactive. */
@@ -22,6 +24,13 @@ interface LightingEditorProps {
22
24
  theme?: Theme;
23
25
  /** Locale partial — reactive (calls editor.setLocale). */
24
26
  locale?: LightingEditorOptions["locale"];
27
+ /**
28
+ * Any React node — portaled into the editor's controls footer slot
29
+ * (where the built-in Reset button used to live). Hosts put their
30
+ * Reset / Generate / save-preset / etc. buttons here. Empty until
31
+ * populated; the library renders nothing into the slot.
32
+ */
33
+ controlsFooter?: ReactNode;
25
34
  className?: string;
26
35
  style?: CSSProperties;
27
36
  apiRef?: Ref<LightingEditorApi | null>;
@@ -29,9 +38,8 @@ interface LightingEditorProps {
29
38
  }
30
39
  /**
31
40
  * React shell for the 3D lighting picker. Renders scene + controls;
32
- * nothing else. Host code lays out their own surrounding UI (smart
33
- * mode panel, generate button, etc.) alongside this component in
34
- * whatever flex/grid the host prefers.
41
+ * the host owns everything else (smart panel beside, action buttons
42
+ * in the controlsFooter slot, layout, theming the surrounding page).
35
43
  */
36
44
  declare function LightingEditor(props: LightingEditorProps): react.JSX.Element;
37
45
 
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { CSSProperties, Ref } from 'react';
2
+ import { ReactNode, CSSProperties, Ref } from 'react';
3
3
  import { LightingConfig, LightingView, LightingEditorOptions } from '@aicut/core/lighting';
4
4
  export { DEFAULT_LIGHTING_CONFIG, KeyPreset, LightingConfig, LightingEditorOptions, LightingLocale, LightingView, PRESET_DIRECTIONS, lightingLocaleEn, lightingLocaleZh, mergeLightingLocale, snapToPreset } from '@aicut/core/lighting';
5
5
  import { Theme } from '@aicut/core';
@@ -10,6 +10,8 @@ interface LightingEditorApi {
10
10
  setSubjectImage(url: string): void;
11
11
  setView(v: LightingView): void;
12
12
  getView(): LightingView;
13
+ /** Convenience — restores config to safe defaults. */
14
+ reset(): void;
13
15
  }
14
16
  interface LightingEditorProps {
15
17
  /** Initial subject image (URL or data URI). Reactive. */
@@ -22,6 +24,13 @@ interface LightingEditorProps {
22
24
  theme?: Theme;
23
25
  /** Locale partial — reactive (calls editor.setLocale). */
24
26
  locale?: LightingEditorOptions["locale"];
27
+ /**
28
+ * Any React node — portaled into the editor's controls footer slot
29
+ * (where the built-in Reset button used to live). Hosts put their
30
+ * Reset / Generate / save-preset / etc. buttons here. Empty until
31
+ * populated; the library renders nothing into the slot.
32
+ */
33
+ controlsFooter?: ReactNode;
25
34
  className?: string;
26
35
  style?: CSSProperties;
27
36
  apiRef?: Ref<LightingEditorApi | null>;
@@ -29,9 +38,8 @@ interface LightingEditorProps {
29
38
  }
30
39
  /**
31
40
  * React shell for the 3D lighting picker. Renders scene + controls;
32
- * nothing else. Host code lays out their own surrounding UI (smart
33
- * mode panel, generate button, etc.) alongside this component in
34
- * whatever flex/grid the host prefers.
41
+ * the host owns everything else (smart panel beside, action buttons
42
+ * in the controlsFooter slot, layout, theming the surrounding page).
35
43
  */
36
44
  declare function LightingEditor(props: LightingEditorProps): react.JSX.Element;
37
45
 
package/dist/lighting.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  useRef,
6
6
  useState
7
7
  } from "react";
8
+ import { createPortal } from "react-dom";
8
9
  import {
9
10
  LightingEditor as CoreLightingEditor
10
11
  } from "@aicut/core/lighting";
@@ -12,7 +13,7 @@ import { jsx } from "react/jsx-runtime";
12
13
  function LightingEditor(props) {
13
14
  const hostRef = useRef(null);
14
15
  const editorRef = useRef(null);
15
- const [ready, setReady] = useState(false);
16
+ const [footerSlot, setFooterSlot] = useState(null);
16
17
  const cbRef = useRef(props);
17
18
  cbRef.current = props;
18
19
  useEffect(() => {
@@ -28,11 +29,11 @@ function LightingEditor(props) {
28
29
  onChange: (cfg) => cbRef.current.onChange?.(cfg)
29
30
  });
30
31
  editorRef.current = editor;
31
- setReady(true);
32
+ setFooterSlot(editor.controlsFooter);
32
33
  return () => {
33
34
  editor.destroy();
34
35
  editorRef.current = null;
35
- setReady(false);
36
+ setFooterSlot(null);
36
37
  };
37
38
  }, []);
38
39
  useEffect(() => {
@@ -55,10 +56,12 @@ function LightingEditor(props) {
55
56
  getConfig: () => ed.getConfig(),
56
57
  setSubjectImage: (url) => ed.setSubjectImage(url),
57
58
  setView: (v) => ed.setView(v),
58
- getView: () => ed.getView()
59
+ getView: () => ed.getView(),
60
+ reset: () => ed.reset()
59
61
  };
60
62
  },
61
- [ready]
63
+ // Keyed on footerSlot — same null-lock fix the VideoEditor uses.
64
+ [footerSlot]
62
65
  );
63
66
  return /* @__PURE__ */ jsx(
64
67
  "div",
@@ -66,7 +69,8 @@ function LightingEditor(props) {
66
69
  ref: hostRef,
67
70
  className: props.className,
68
71
  style: props.style,
69
- "data-aicut-lighting-host": ""
72
+ "data-aicut-lighting-host": "",
73
+ children: footerSlot && props.controlsFooter != null ? createPortal(props.controlsFooter, footerSlot) : null
70
74
  }
71
75
  );
72
76
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/LightingEditor.tsx","../src/lighting.ts"],"sourcesContent":["import {\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type Ref,\n} from \"react\";\nimport {\n LightingEditor as CoreLightingEditor,\n type LightingConfig,\n type LightingEditorOptions,\n type LightingView,\n} from \"@aicut/core/lighting\";\nimport type { Theme } from \"@aicut/core\";\n\nexport interface LightingEditorApi {\n setConfig(partial: Partial<LightingConfig>): void;\n getConfig(): LightingConfig;\n setSubjectImage(url: string): void;\n setView(v: LightingView): void;\n getView(): LightingView;\n}\n\nexport interface LightingEditorProps {\n /** Initial subject image (URL or data URI). Reactive. */\n subjectImageUrl?: string;\n /** Initial config. */\n defaultConfig?: Partial<LightingConfig>;\n /** Initial view. Default `\"perspective\"`. */\n defaultView?: LightingView;\n /** Theme — reactive (calls editor.setTheme). */\n theme?: Theme;\n /** Locale partial — reactive (calls editor.setLocale). */\n locale?: LightingEditorOptions[\"locale\"];\n\n className?: string;\n style?: CSSProperties;\n apiRef?: Ref<LightingEditorApi | null>;\n\n onChange?: (cfg: LightingConfig) => void;\n}\n\n/**\n * React shell for the 3D lighting picker. Renders scene + controls;\n * nothing else. Host code lays out their own surrounding UI (smart\n * mode panel, generate button, etc.) alongside this component in\n * whatever flex/grid the host prefers.\n */\nexport function LightingEditor(props: LightingEditorProps) {\n const hostRef = useRef<HTMLDivElement | null>(null);\n const editorRef = useRef<CoreLightingEditor | null>(null);\n // Triggers a re-render the moment the editor is created, so the\n // useImperativeHandle factory below can return the real instance\n // instead of locking at null forever (same trick as VideoEditor).\n const [ready, setReady] = useState(false);\n\n const cbRef = useRef(props);\n cbRef.current = props;\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const editor = CoreLightingEditor.create({\n container: host,\n subjectImageUrl: cbRef.current.subjectImageUrl,\n config: cbRef.current.defaultConfig,\n view: cbRef.current.defaultView,\n theme: cbRef.current.theme,\n locale: cbRef.current.locale,\n onChange: (cfg) => cbRef.current.onChange?.(cfg),\n });\n editorRef.current = editor;\n setReady(true);\n return () => {\n editor.destroy();\n editorRef.current = null;\n setReady(false);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (props.theme) editorRef.current?.setTheme(props.theme);\n }, [props.theme]);\n useEffect(() => {\n // Always push — `undefined` is the \"reset to English defaults\"\n // signal. Without this, toggling ZH→EN in the host kept ZH labels.\n editorRef.current?.setLocale(props.locale ?? {});\n }, [props.locale]);\n useEffect(() => {\n if (props.subjectImageUrl)\n editorRef.current?.setSubjectImage(props.subjectImageUrl);\n }, [props.subjectImageUrl]);\n\n useImperativeHandle<LightingEditorApi | null, LightingEditorApi | null>(\n props.apiRef,\n () => {\n const ed = editorRef.current;\n if (!ed) return null;\n return {\n setConfig: (p) => ed.setConfig(p),\n getConfig: () => ed.getConfig(),\n setSubjectImage: (url) => ed.setSubjectImage(url),\n setView: (v) => ed.setView(v),\n getView: () => ed.getView(),\n };\n },\n [ready],\n );\n\n return (\n <div\n ref={hostRef}\n className={props.className}\n style={props.style}\n data-aicut-lighting-host=\"\"\n />\n );\n}\n","/**\n * @aicut/react/lighting — separate entry that pulls three.js. Users\n * who never import this path don't pay the three.js bundle cost.\n */\nexport { LightingEditor } from \"./LightingEditor.js\";\nexport type {\n LightingEditorProps,\n LightingEditorApi,\n} from \"./LightingEditor.js\";\n\n// Re-export the data + locale exports from the core sub-entry so\n// hosts only need a single import line for everything lighting-related.\nexport {\n DEFAULT_LIGHTING_CONFIG,\n PRESET_DIRECTIONS,\n lightingLocaleEn,\n lightingLocaleZh,\n mergeLightingLocale,\n snapToPreset,\n} from \"@aicut/core/lighting\";\nexport type {\n KeyPreset,\n LightingConfig,\n LightingEditorOptions,\n LightingLocale,\n LightingView,\n} from \"@aicut/core/lighting\";\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE,kBAAkB;AAAA,OAIb;AAmGH;AA/DG,SAAS,eAAe,OAA4B;AACzD,QAAM,UAAU,OAA8B,IAAI;AAClD,QAAM,YAAY,OAAkC,IAAI;AAIxD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK;AAExC,QAAM,QAAQ,OAAO,KAAK;AAC1B,QAAM,UAAU;AAEhB,YAAU,MAAM;AACd,UAAM,OAAO,QAAQ;AACrB,QAAI,CAAC,KAAM;AACX,UAAM,SAAS,mBAAmB,OAAO;AAAA,MACvC,WAAW;AAAA,MACX,iBAAiB,MAAM,QAAQ;AAAA,MAC/B,QAAQ,MAAM,QAAQ;AAAA,MACtB,MAAM,MAAM,QAAQ;AAAA,MACpB,OAAO,MAAM,QAAQ;AAAA,MACrB,QAAQ,MAAM,QAAQ;AAAA,MACtB,UAAU,CAAC,QAAQ,MAAM,QAAQ,WAAW,GAAG;AAAA,IACjD,CAAC;AACD,cAAU,UAAU;AACpB,aAAS,IAAI;AACb,WAAO,MAAM;AACX,aAAO,QAAQ;AACf,gBAAU,UAAU;AACpB,eAAS,KAAK;AAAA,IAChB;AAAA,EAEF,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,QAAI,MAAM,MAAO,WAAU,SAAS,SAAS,MAAM,KAAK;AAAA,EAC1D,GAAG,CAAC,MAAM,KAAK,CAAC;AAChB,YAAU,MAAM;AAGd,cAAU,SAAS,UAAU,MAAM,UAAU,CAAC,CAAC;AAAA,EACjD,GAAG,CAAC,MAAM,MAAM,CAAC;AACjB,YAAU,MAAM;AACd,QAAI,MAAM;AACR,gBAAU,SAAS,gBAAgB,MAAM,eAAe;AAAA,EAC5D,GAAG,CAAC,MAAM,eAAe,CAAC;AAE1B;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AACJ,YAAM,KAAK,UAAU;AACrB,UAAI,CAAC,GAAI,QAAO;AAChB,aAAO;AAAA,QACL,WAAW,CAAC,MAAM,GAAG,UAAU,CAAC;AAAA,QAChC,WAAW,MAAM,GAAG,UAAU;AAAA,QAC9B,iBAAiB,CAAC,QAAQ,GAAG,gBAAgB,GAAG;AAAA,QAChD,SAAS,CAAC,MAAM,GAAG,QAAQ,CAAC;AAAA,QAC5B,SAAS,MAAM,GAAG,QAAQ;AAAA,MAC5B;AAAA,IACF;AAAA,IACA,CAAC,KAAK;AAAA,EACR;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,WAAW,MAAM;AAAA,MACjB,OAAO,MAAM;AAAA,MACb,4BAAyB;AAAA;AAAA,EAC3B;AAEJ;;;AC3GA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
1
+ {"version":3,"sources":["../src/LightingEditor.tsx","../src/lighting.ts"],"sourcesContent":["import {\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n type Ref,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n LightingEditor as CoreLightingEditor,\n type LightingConfig,\n type LightingEditorOptions,\n type LightingView,\n} from \"@aicut/core/lighting\";\nimport type { Theme } from \"@aicut/core\";\n\nexport interface LightingEditorApi {\n setConfig(partial: Partial<LightingConfig>): void;\n getConfig(): LightingConfig;\n setSubjectImage(url: string): void;\n setView(v: LightingView): void;\n getView(): LightingView;\n /** Convenience — restores config to safe defaults. */\n reset(): void;\n}\n\nexport interface LightingEditorProps {\n /** Initial subject image (URL or data URI). Reactive. */\n subjectImageUrl?: string;\n /** Initial config. */\n defaultConfig?: Partial<LightingConfig>;\n /** Initial view. Default `\"perspective\"`. */\n defaultView?: LightingView;\n /** Theme — reactive (calls editor.setTheme). */\n theme?: Theme;\n /** Locale partial — reactive (calls editor.setLocale). */\n locale?: LightingEditorOptions[\"locale\"];\n\n /**\n * Any React node — portaled into the editor's controls footer slot\n * (where the built-in Reset button used to live). Hosts put their\n * Reset / Generate / save-preset / etc. buttons here. Empty until\n * populated; the library renders nothing into the slot.\n */\n controlsFooter?: ReactNode;\n\n className?: string;\n style?: CSSProperties;\n apiRef?: Ref<LightingEditorApi | null>;\n\n onChange?: (cfg: LightingConfig) => void;\n}\n\n/**\n * React shell for the 3D lighting picker. Renders scene + controls;\n * the host owns everything else (smart panel beside, action buttons\n * in the controlsFooter slot, layout, theming the surrounding page).\n */\nexport function LightingEditor(props: LightingEditorProps) {\n const hostRef = useRef<HTMLDivElement | null>(null);\n const editorRef = useRef<CoreLightingEditor | null>(null);\n // Hold the footer slot DOM node in state so the portal mounts after\n // editor creation. Same lifecycle dance VideoEditor does for its\n // toolbar slots.\n const [footerSlot, setFooterSlot] = useState<HTMLElement | null>(null);\n\n const cbRef = useRef(props);\n cbRef.current = props;\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const editor = CoreLightingEditor.create({\n container: host,\n subjectImageUrl: cbRef.current.subjectImageUrl,\n config: cbRef.current.defaultConfig,\n view: cbRef.current.defaultView,\n theme: cbRef.current.theme,\n locale: cbRef.current.locale,\n onChange: (cfg) => cbRef.current.onChange?.(cfg),\n });\n editorRef.current = editor;\n setFooterSlot(editor.controlsFooter);\n return () => {\n editor.destroy();\n editorRef.current = null;\n setFooterSlot(null);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (props.theme) editorRef.current?.setTheme(props.theme);\n }, [props.theme]);\n useEffect(() => {\n editorRef.current?.setLocale(props.locale ?? {});\n }, [props.locale]);\n useEffect(() => {\n if (props.subjectImageUrl)\n editorRef.current?.setSubjectImage(props.subjectImageUrl);\n }, [props.subjectImageUrl]);\n\n useImperativeHandle<LightingEditorApi | null, LightingEditorApi | null>(\n props.apiRef,\n () => {\n const ed = editorRef.current;\n if (!ed) return null;\n return {\n setConfig: (p) => ed.setConfig(p),\n getConfig: () => ed.getConfig(),\n setSubjectImage: (url) => ed.setSubjectImage(url),\n setView: (v) => ed.setView(v),\n getView: () => ed.getView(),\n reset: () => ed.reset(),\n };\n },\n // Keyed on footerSlot — same null-lock fix the VideoEditor uses.\n [footerSlot],\n );\n\n return (\n <div\n ref={hostRef}\n className={props.className}\n style={props.style}\n data-aicut-lighting-host=\"\"\n >\n {footerSlot && props.controlsFooter != null\n ? createPortal(props.controlsFooter, footerSlot)\n : null}\n </div>\n );\n}\n","/**\n * @aicut/react/lighting — separate entry that pulls three.js. Users\n * who never import this path don't pay the three.js bundle cost.\n */\nexport { LightingEditor } from \"./LightingEditor.js\";\nexport type {\n LightingEditorProps,\n LightingEditorApi,\n} from \"./LightingEditor.js\";\n\n// Re-export the data + locale exports from the core sub-entry so\n// hosts only need a single import line for everything lighting-related.\nexport {\n DEFAULT_LIGHTING_CONFIG,\n PRESET_DIRECTIONS,\n lightingLocaleEn,\n lightingLocaleZh,\n mergeLightingLocale,\n snapToPreset,\n} from \"@aicut/core/lighting\";\nexport type {\n KeyPreset,\n LightingConfig,\n LightingEditorOptions,\n LightingLocale,\n LightingView,\n} from \"@aicut/core/lighting\";\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,SAAS,oBAAoB;AAC7B;AAAA,EACE,kBAAkB;AAAA,OAIb;AA4GH;AA/DG,SAAS,eAAe,OAA4B;AACzD,QAAM,UAAU,OAA8B,IAAI;AAClD,QAAM,YAAY,OAAkC,IAAI;AAIxD,QAAM,CAAC,YAAY,aAAa,IAAI,SAA6B,IAAI;AAErE,QAAM,QAAQ,OAAO,KAAK;AAC1B,QAAM,UAAU;AAEhB,YAAU,MAAM;AACd,UAAM,OAAO,QAAQ;AACrB,QAAI,CAAC,KAAM;AACX,UAAM,SAAS,mBAAmB,OAAO;AAAA,MACvC,WAAW;AAAA,MACX,iBAAiB,MAAM,QAAQ;AAAA,MAC/B,QAAQ,MAAM,QAAQ;AAAA,MACtB,MAAM,MAAM,QAAQ;AAAA,MACpB,OAAO,MAAM,QAAQ;AAAA,MACrB,QAAQ,MAAM,QAAQ;AAAA,MACtB,UAAU,CAAC,QAAQ,MAAM,QAAQ,WAAW,GAAG;AAAA,IACjD,CAAC;AACD,cAAU,UAAU;AACpB,kBAAc,OAAO,cAAc;AACnC,WAAO,MAAM;AACX,aAAO,QAAQ;AACf,gBAAU,UAAU;AACpB,oBAAc,IAAI;AAAA,IACpB;AAAA,EAEF,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,QAAI,MAAM,MAAO,WAAU,SAAS,SAAS,MAAM,KAAK;AAAA,EAC1D,GAAG,CAAC,MAAM,KAAK,CAAC;AAChB,YAAU,MAAM;AACd,cAAU,SAAS,UAAU,MAAM,UAAU,CAAC,CAAC;AAAA,EACjD,GAAG,CAAC,MAAM,MAAM,CAAC;AACjB,YAAU,MAAM;AACd,QAAI,MAAM;AACR,gBAAU,SAAS,gBAAgB,MAAM,eAAe;AAAA,EAC5D,GAAG,CAAC,MAAM,eAAe,CAAC;AAE1B;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AACJ,YAAM,KAAK,UAAU;AACrB,UAAI,CAAC,GAAI,QAAO;AAChB,aAAO;AAAA,QACL,WAAW,CAAC,MAAM,GAAG,UAAU,CAAC;AAAA,QAChC,WAAW,MAAM,GAAG,UAAU;AAAA,QAC9B,iBAAiB,CAAC,QAAQ,GAAG,gBAAgB,GAAG;AAAA,QAChD,SAAS,CAAC,MAAM,GAAG,QAAQ,CAAC;AAAA,QAC5B,SAAS,MAAM,GAAG,QAAQ;AAAA,QAC1B,OAAO,MAAM,GAAG,MAAM;AAAA,MACxB;AAAA,IACF;AAAA;AAAA,IAEA,CAAC,UAAU;AAAA,EACb;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,WAAW,MAAM;AAAA,MACjB,OAAO,MAAM;AAAA,MACb,4BAAyB;AAAA,MAExB,wBAAc,MAAM,kBAAkB,OACnC,aAAa,MAAM,gBAAgB,UAAU,IAC7C;AAAA;AAAA,EACN;AAEJ;;;AC1HA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicut/react",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "React wrapper for the AiCut video editor + lighting picker — thin declarative shells over @aicut/core.",
5
5
  "license": "MIT",
6
6
  "author": "ziqiang <ziqiangytu@gmail.com>",
@@ -59,7 +59,7 @@
59
59
  "README.md"
60
60
  ],
61
61
  "dependencies": {
62
- "@aicut/core": "0.3.0"
62
+ "@aicut/core": "0.4.1"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "react": "^18.0.0 || ^19.0.0",