@aicut/react 0.2.0 → 0.4.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 CHANGED
@@ -160,32 +160,31 @@ 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
+ />
178
+ <aside>
179
+ <textarea placeholder="Describe the mood…" />
180
+ <button onClick={onGenerate}>Generate</button>
181
+ </aside>
182
+ </div>
184
183
  );
185
184
  }
186
185
  ```
187
186
 
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.
187
+ Props: `subjectImageUrl`, `defaultConfig`, `defaultView`, `theme`, `locale`, `onChange`. The library is intentionally focused on the picker Smart mode UI, Generate buttons, close handling, layout all live in host code.
189
188
 
190
189
  ## Standalone `<Timeline>`
191
190
 
package/dist/lighting.cjs CHANGED
@@ -38,7 +38,7 @@ var import_jsx_runtime = require("react/jsx-runtime");
38
38
  function LightingEditor(props) {
39
39
  const hostRef = (0, import_react.useRef)(null);
40
40
  const editorRef = (0, import_react.useRef)(null);
41
- const [slot, setSlot] = (0, import_react.useState)(null);
41
+ const [footerSlot, setFooterSlot] = (0, import_react.useState)(null);
42
42
  const cbRef = (0, import_react.useRef)(props);
43
43
  cbRef.current = props;
44
44
  (0, import_react.useEffect)(() => {
@@ -49,20 +49,16 @@ function LightingEditor(props) {
49
49
  subjectImageUrl: cbRef.current.subjectImageUrl,
50
50
  config: cbRef.current.defaultConfig,
51
51
  view: cbRef.current.defaultView,
52
- smartEnabled: cbRef.current.smartEnabled,
53
- smartOpen: cbRef.current.smartOpen,
54
52
  theme: cbRef.current.theme,
55
53
  locale: cbRef.current.locale,
56
- onChange: (cfg) => cbRef.current.onChange?.(cfg),
57
- onGenerate: (cfg) => cbRef.current.onGenerate?.(cfg),
58
- onSmartOpenChange: (open) => cbRef.current.onSmartOpenChange?.(open)
54
+ onChange: (cfg) => cbRef.current.onChange?.(cfg)
59
55
  });
60
56
  editorRef.current = editor;
61
- setSlot(editor.smartSlot);
57
+ setFooterSlot(editor.controlsFooter);
62
58
  return () => {
63
59
  editor.destroy();
64
60
  editorRef.current = null;
65
- setSlot(null);
61
+ setFooterSlot(null);
66
62
  };
67
63
  }, []);
68
64
  (0, import_react.useEffect)(() => {
@@ -75,14 +71,6 @@ function LightingEditor(props) {
75
71
  if (props.subjectImageUrl)
76
72
  editorRef.current?.setSubjectImage(props.subjectImageUrl);
77
73
  }, [props.subjectImageUrl]);
78
- (0, import_react.useEffect)(() => {
79
- if (props.smartEnabled !== void 0)
80
- editorRef.current?.setSmartEnabled(props.smartEnabled);
81
- }, [props.smartEnabled]);
82
- (0, import_react.useEffect)(() => {
83
- if (props.smartOpen !== void 0)
84
- editorRef.current?.setSmartOpen(props.smartOpen);
85
- }, [props.smartOpen]);
86
74
  (0, import_react.useImperativeHandle)(
87
75
  props.apiRef,
88
76
  () => {
@@ -94,14 +82,11 @@ function LightingEditor(props) {
94
82
  setSubjectImage: (url) => ed.setSubjectImage(url),
95
83
  setView: (v) => ed.setView(v),
96
84
  getView: () => ed.getView(),
97
- setSmartEnabled: (en) => ed.setSmartEnabled(en),
98
- isSmartEnabled: () => ed.isSmartEnabled(),
99
- setSmartOpen: (open) => ed.setSmartOpen(open),
100
- isSmartOpen: () => ed.isSmartOpen(),
101
- requestGenerate: () => ed.requestGenerate()
85
+ reset: () => ed.reset()
102
86
  };
103
87
  },
104
- [slot]
88
+ // Keyed on footerSlot — same null-lock fix the VideoEditor uses.
89
+ [footerSlot]
105
90
  );
106
91
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
107
92
  "div",
@@ -110,7 +95,7 @@ function LightingEditor(props) {
110
95
  className: props.className,
111
96
  style: props.style,
112
97
  "data-aicut-lighting-host": "",
113
- children: slot && props.smartPanel != null ? (0, import_react_dom.createPortal)(props.smartPanel, slot) : null
98
+ children: footerSlot && props.controlsFooter != null ? (0, import_react_dom.createPortal)(props.controlsFooter, footerSlot) : null
114
99
  }
115
100
  );
116
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 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\n/**\n * Imperative handle. Mirrors the core class's mutating surface, plus\n * a `requestGenerate()` shortcut so host buttons inside the smart slot\n * don't need to thread the api ref to fire onGenerate.\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 setSmartEnabled(enabled: boolean): void;\n isSmartEnabled(): boolean;\n setSmartOpen(open: boolean): void;\n isSmartOpen(): boolean;\n requestGenerate(): void;\n}\n\nexport interface LightingEditorProps {\n /** Initial subject image (URL or data URI). */\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 smart slot. Host uses\n * this for prompt textarea, preset grid, generate button, anything.\n * The library renders nothing into the slot until you populate it.\n */\n smartPanel?: ReactNode;\n /**\n * Whether the Smart mode feature is wired in at all. When false,\n * the column AND the controls-header toggle disappear leaving a\n * clean 2-col scene + controls layout. Default `true`. Reactive.\n */\n smartEnabled?: boolean;\n /**\n * When `smartEnabled`, whether the slot drawer starts open. Reactive\n * — flips re-fire the editor's `setSmartOpen` so the panel slides\n * in/out. Default `true`.\n */\n smartOpen?: boolean;\n\n className?: string;\n style?: CSSProperties;\n apiRef?: Ref<LightingEditorApi | null>;\n\n onChange?: (cfg: LightingConfig) => void;\n onGenerate?: (cfg: LightingConfig) => void;\n /** Fires when the user clicks × or the Smart mode header toggle. */\n onSmartOpenChange?: (open: boolean) => void;\n}\n\nexport function LightingEditor(props: LightingEditorProps) {\n const hostRef = useRef<HTMLDivElement | null>(null);\n const editorRef = useRef<CoreLightingEditor | null>(null);\n // smart slot DOM node set once the editor mounts; gates the portal.\n const [slot, setSlot] = useState<HTMLElement | null>(null);\n\n // Stable closure for callbacks the core only ever subscribes to once.\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 smartEnabled: cbRef.current.smartEnabled,\n smartOpen: cbRef.current.smartOpen,\n theme: cbRef.current.theme,\n locale: cbRef.current.locale,\n onChange: (cfg) => cbRef.current.onChange?.(cfg),\n onGenerate: (cfg) => cbRef.current.onGenerate?.(cfg),\n onSmartOpenChange: (open) =>\n cbRef.current.onSmartOpenChange?.(open),\n });\n editorRef.current = editor;\n setSlot(editor.smartSlot);\n return () => {\n editor.destroy();\n editorRef.current = null;\n setSlot(null);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Reactive prop mirror — theme + locale + subject only. Config /\n // view are owned by the editor instance after mount; host should\n // use `apiRef.current?.setConfig(…)` to push them.\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. Previously the `if (props.locale)` guard meant that\n // toggling ZH→EN in the host kept the ZH labels in place.\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 useEffect(() => {\n if (props.smartEnabled !== undefined)\n editorRef.current?.setSmartEnabled(props.smartEnabled);\n }, [props.smartEnabled]);\n useEffect(() => {\n if (props.smartOpen !== undefined)\n editorRef.current?.setSmartOpen(props.smartOpen);\n }, [props.smartOpen]);\n\n // Keyed on `slot` for the same reason VideoEditor's apiRef is —\n // useImperativeHandle's factory runs in the commit phase BEFORE\n // useEffect, so with `[]` deps the ref locks to null forever.\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 setSmartEnabled: (en) => ed.setSmartEnabled(en),\n isSmartEnabled: () => ed.isSmartEnabled(),\n setSmartOpen: (open) => ed.setSmartOpen(open),\n isSmartOpen: () => ed.isSmartOpen(),\n requestGenerate: () => ed.requestGenerate(),\n };\n },\n [slot],\n );\n\n return (\n <div\n ref={hostRef}\n className={props.className}\n style={props.style}\n data-aicut-lighting-host=\"\"\n >\n {slot && props.smartPanel != null\n ? createPortal(props.smartPanel, slot)\n : null}\n </div>\n );\n}\n"],"mappings":"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","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"]}
@@ -4,25 +4,17 @@ import { LightingConfig, LightingView, LightingEditorOptions } from '@aicut/core
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';
6
6
 
7
- /**
8
- * Imperative handle. Mirrors the core class's mutating surface, plus
9
- * a `requestGenerate()` shortcut so host buttons inside the smart slot
10
- * don't need to thread the api ref to fire onGenerate.
11
- */
12
7
  interface LightingEditorApi {
13
8
  setConfig(partial: Partial<LightingConfig>): void;
14
9
  getConfig(): LightingConfig;
15
10
  setSubjectImage(url: string): void;
16
11
  setView(v: LightingView): void;
17
12
  getView(): LightingView;
18
- setSmartEnabled(enabled: boolean): void;
19
- isSmartEnabled(): boolean;
20
- setSmartOpen(open: boolean): void;
21
- isSmartOpen(): boolean;
22
- requestGenerate(): void;
13
+ /** Convenience — restores config to safe defaults. */
14
+ reset(): void;
23
15
  }
24
16
  interface LightingEditorProps {
25
- /** Initial subject image (URL or data URI). */
17
+ /** Initial subject image (URL or data URI). Reactive. */
26
18
  subjectImageUrl?: string;
27
19
  /** Initial config. */
28
20
  defaultConfig?: Partial<LightingConfig>;
@@ -33,31 +25,22 @@ interface LightingEditorProps {
33
25
  /** Locale partial — reactive (calls editor.setLocale). */
34
26
  locale?: LightingEditorOptions["locale"];
35
27
  /**
36
- * Any React node — portaled into the editor's smart slot. Host uses
37
- * this for prompt textarea, preset grid, generate button, anything.
38
- * The library renders nothing into the slot until you populate it.
39
- */
40
- smartPanel?: ReactNode;
41
- /**
42
- * Whether the Smart mode feature is wired in at all. When false,
43
- * the column AND the controls-header toggle disappear — leaving a
44
- * clean 2-col scene + controls layout. Default `true`. Reactive.
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.
45
32
  */
46
- smartEnabled?: boolean;
47
- /**
48
- * When `smartEnabled`, whether the slot drawer starts open. Reactive
49
- * — flips re-fire the editor's `setSmartOpen` so the panel slides
50
- * in/out. Default `true`.
51
- */
52
- smartOpen?: boolean;
33
+ controlsFooter?: ReactNode;
53
34
  className?: string;
54
35
  style?: CSSProperties;
55
36
  apiRef?: Ref<LightingEditorApi | null>;
56
37
  onChange?: (cfg: LightingConfig) => void;
57
- onGenerate?: (cfg: LightingConfig) => void;
58
- /** Fires when the user clicks × or the Smart mode header toggle. */
59
- onSmartOpenChange?: (open: boolean) => void;
60
38
  }
39
+ /**
40
+ * React shell for the 3D lighting picker. Renders scene + controls;
41
+ * the host owns everything else (smart panel beside, action buttons
42
+ * in the controlsFooter slot, layout, theming the surrounding page).
43
+ */
61
44
  declare function LightingEditor(props: LightingEditorProps): react.JSX.Element;
62
45
 
63
46
  export { LightingEditor, type LightingEditorApi, type LightingEditorProps };
@@ -4,25 +4,17 @@ import { LightingConfig, LightingView, LightingEditorOptions } from '@aicut/core
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';
6
6
 
7
- /**
8
- * Imperative handle. Mirrors the core class's mutating surface, plus
9
- * a `requestGenerate()` shortcut so host buttons inside the smart slot
10
- * don't need to thread the api ref to fire onGenerate.
11
- */
12
7
  interface LightingEditorApi {
13
8
  setConfig(partial: Partial<LightingConfig>): void;
14
9
  getConfig(): LightingConfig;
15
10
  setSubjectImage(url: string): void;
16
11
  setView(v: LightingView): void;
17
12
  getView(): LightingView;
18
- setSmartEnabled(enabled: boolean): void;
19
- isSmartEnabled(): boolean;
20
- setSmartOpen(open: boolean): void;
21
- isSmartOpen(): boolean;
22
- requestGenerate(): void;
13
+ /** Convenience — restores config to safe defaults. */
14
+ reset(): void;
23
15
  }
24
16
  interface LightingEditorProps {
25
- /** Initial subject image (URL or data URI). */
17
+ /** Initial subject image (URL or data URI). Reactive. */
26
18
  subjectImageUrl?: string;
27
19
  /** Initial config. */
28
20
  defaultConfig?: Partial<LightingConfig>;
@@ -33,31 +25,22 @@ interface LightingEditorProps {
33
25
  /** Locale partial — reactive (calls editor.setLocale). */
34
26
  locale?: LightingEditorOptions["locale"];
35
27
  /**
36
- * Any React node — portaled into the editor's smart slot. Host uses
37
- * this for prompt textarea, preset grid, generate button, anything.
38
- * The library renders nothing into the slot until you populate it.
39
- */
40
- smartPanel?: ReactNode;
41
- /**
42
- * Whether the Smart mode feature is wired in at all. When false,
43
- * the column AND the controls-header toggle disappear — leaving a
44
- * clean 2-col scene + controls layout. Default `true`. Reactive.
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.
45
32
  */
46
- smartEnabled?: boolean;
47
- /**
48
- * When `smartEnabled`, whether the slot drawer starts open. Reactive
49
- * — flips re-fire the editor's `setSmartOpen` so the panel slides
50
- * in/out. Default `true`.
51
- */
52
- smartOpen?: boolean;
33
+ controlsFooter?: ReactNode;
53
34
  className?: string;
54
35
  style?: CSSProperties;
55
36
  apiRef?: Ref<LightingEditorApi | null>;
56
37
  onChange?: (cfg: LightingConfig) => void;
57
- onGenerate?: (cfg: LightingConfig) => void;
58
- /** Fires when the user clicks × or the Smart mode header toggle. */
59
- onSmartOpenChange?: (open: boolean) => void;
60
38
  }
39
+ /**
40
+ * React shell for the 3D lighting picker. Renders scene + controls;
41
+ * the host owns everything else (smart panel beside, action buttons
42
+ * in the controlsFooter slot, layout, theming the surrounding page).
43
+ */
61
44
  declare function LightingEditor(props: LightingEditorProps): react.JSX.Element;
62
45
 
63
46
  export { LightingEditor, type LightingEditorApi, type LightingEditorProps };
package/dist/lighting.js CHANGED
@@ -13,7 +13,7 @@ import { jsx } from "react/jsx-runtime";
13
13
  function LightingEditor(props) {
14
14
  const hostRef = useRef(null);
15
15
  const editorRef = useRef(null);
16
- const [slot, setSlot] = useState(null);
16
+ const [footerSlot, setFooterSlot] = useState(null);
17
17
  const cbRef = useRef(props);
18
18
  cbRef.current = props;
19
19
  useEffect(() => {
@@ -24,20 +24,16 @@ function LightingEditor(props) {
24
24
  subjectImageUrl: cbRef.current.subjectImageUrl,
25
25
  config: cbRef.current.defaultConfig,
26
26
  view: cbRef.current.defaultView,
27
- smartEnabled: cbRef.current.smartEnabled,
28
- smartOpen: cbRef.current.smartOpen,
29
27
  theme: cbRef.current.theme,
30
28
  locale: cbRef.current.locale,
31
- onChange: (cfg) => cbRef.current.onChange?.(cfg),
32
- onGenerate: (cfg) => cbRef.current.onGenerate?.(cfg),
33
- onSmartOpenChange: (open) => cbRef.current.onSmartOpenChange?.(open)
29
+ onChange: (cfg) => cbRef.current.onChange?.(cfg)
34
30
  });
35
31
  editorRef.current = editor;
36
- setSlot(editor.smartSlot);
32
+ setFooterSlot(editor.controlsFooter);
37
33
  return () => {
38
34
  editor.destroy();
39
35
  editorRef.current = null;
40
- setSlot(null);
36
+ setFooterSlot(null);
41
37
  };
42
38
  }, []);
43
39
  useEffect(() => {
@@ -50,14 +46,6 @@ function LightingEditor(props) {
50
46
  if (props.subjectImageUrl)
51
47
  editorRef.current?.setSubjectImage(props.subjectImageUrl);
52
48
  }, [props.subjectImageUrl]);
53
- useEffect(() => {
54
- if (props.smartEnabled !== void 0)
55
- editorRef.current?.setSmartEnabled(props.smartEnabled);
56
- }, [props.smartEnabled]);
57
- useEffect(() => {
58
- if (props.smartOpen !== void 0)
59
- editorRef.current?.setSmartOpen(props.smartOpen);
60
- }, [props.smartOpen]);
61
49
  useImperativeHandle(
62
50
  props.apiRef,
63
51
  () => {
@@ -69,14 +57,11 @@ function LightingEditor(props) {
69
57
  setSubjectImage: (url) => ed.setSubjectImage(url),
70
58
  setView: (v) => ed.setView(v),
71
59
  getView: () => ed.getView(),
72
- setSmartEnabled: (en) => ed.setSmartEnabled(en),
73
- isSmartEnabled: () => ed.isSmartEnabled(),
74
- setSmartOpen: (open) => ed.setSmartOpen(open),
75
- isSmartOpen: () => ed.isSmartOpen(),
76
- requestGenerate: () => ed.requestGenerate()
60
+ reset: () => ed.reset()
77
61
  };
78
62
  },
79
- [slot]
63
+ // Keyed on footerSlot — same null-lock fix the VideoEditor uses.
64
+ [footerSlot]
80
65
  );
81
66
  return /* @__PURE__ */ jsx(
82
67
  "div",
@@ -85,7 +70,7 @@ function LightingEditor(props) {
85
70
  className: props.className,
86
71
  style: props.style,
87
72
  "data-aicut-lighting-host": "",
88
- children: slot && props.smartPanel != null ? createPortal(props.smartPanel, slot) : null
73
+ children: footerSlot && props.controlsFooter != null ? createPortal(props.controlsFooter, footerSlot) : null
89
74
  }
90
75
  );
91
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 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\n/**\n * Imperative handle. Mirrors the core class's mutating surface, plus\n * a `requestGenerate()` shortcut so host buttons inside the smart slot\n * don't need to thread the api ref to fire onGenerate.\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 setSmartEnabled(enabled: boolean): void;\n isSmartEnabled(): boolean;\n setSmartOpen(open: boolean): void;\n isSmartOpen(): boolean;\n requestGenerate(): void;\n}\n\nexport interface LightingEditorProps {\n /** Initial subject image (URL or data URI). */\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 smart slot. Host uses\n * this for prompt textarea, preset grid, generate button, anything.\n * The library renders nothing into the slot until you populate it.\n */\n smartPanel?: ReactNode;\n /**\n * Whether the Smart mode feature is wired in at all. When false,\n * the column AND the controls-header toggle disappear leaving a\n * clean 2-col scene + controls layout. Default `true`. Reactive.\n */\n smartEnabled?: boolean;\n /**\n * When `smartEnabled`, whether the slot drawer starts open. Reactive\n * — flips re-fire the editor's `setSmartOpen` so the panel slides\n * in/out. Default `true`.\n */\n smartOpen?: boolean;\n\n className?: string;\n style?: CSSProperties;\n apiRef?: Ref<LightingEditorApi | null>;\n\n onChange?: (cfg: LightingConfig) => void;\n onGenerate?: (cfg: LightingConfig) => void;\n /** Fires when the user clicks × or the Smart mode header toggle. */\n onSmartOpenChange?: (open: boolean) => void;\n}\n\nexport function LightingEditor(props: LightingEditorProps) {\n const hostRef = useRef<HTMLDivElement | null>(null);\n const editorRef = useRef<CoreLightingEditor | null>(null);\n // smart slot DOM node set once the editor mounts; gates the portal.\n const [slot, setSlot] = useState<HTMLElement | null>(null);\n\n // Stable closure for callbacks the core only ever subscribes to once.\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 smartEnabled: cbRef.current.smartEnabled,\n smartOpen: cbRef.current.smartOpen,\n theme: cbRef.current.theme,\n locale: cbRef.current.locale,\n onChange: (cfg) => cbRef.current.onChange?.(cfg),\n onGenerate: (cfg) => cbRef.current.onGenerate?.(cfg),\n onSmartOpenChange: (open) =>\n cbRef.current.onSmartOpenChange?.(open),\n });\n editorRef.current = editor;\n setSlot(editor.smartSlot);\n return () => {\n editor.destroy();\n editorRef.current = null;\n setSlot(null);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Reactive prop mirror — theme + locale + subject only. Config /\n // view are owned by the editor instance after mount; host should\n // use `apiRef.current?.setConfig(…)` to push them.\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. Previously the `if (props.locale)` guard meant that\n // toggling ZH→EN in the host kept the ZH labels in place.\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 useEffect(() => {\n if (props.smartEnabled !== undefined)\n editorRef.current?.setSmartEnabled(props.smartEnabled);\n }, [props.smartEnabled]);\n useEffect(() => {\n if (props.smartOpen !== undefined)\n editorRef.current?.setSmartOpen(props.smartOpen);\n }, [props.smartOpen]);\n\n // Keyed on `slot` for the same reason VideoEditor's apiRef is —\n // useImperativeHandle's factory runs in the commit phase BEFORE\n // useEffect, so with `[]` deps the ref locks to null forever.\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 setSmartEnabled: (en) => ed.setSmartEnabled(en),\n isSmartEnabled: () => ed.isSmartEnabled(),\n setSmartOpen: (open) => ed.setSmartOpen(open),\n isSmartOpen: () => ed.isSmartOpen(),\n requestGenerate: () => ed.requestGenerate(),\n };\n },\n [slot],\n );\n\n return (\n <div\n ref={hostRef}\n className={props.className}\n style={props.style}\n data-aicut-lighting-host=\"\"\n >\n {slot && props.smartPanel != null\n ? createPortal(props.smartPanel, slot)\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":"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","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.2.0",
3
+ "version": "0.4.0",
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.2.0"
62
+ "@aicut/core": "0.4.0"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "react": "^18.0.0 || ^19.0.0",