labellife-design-tool 2.1.2 → 2.1.4

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
@@ -391,6 +391,7 @@ import { Workspace } from 'labellife-design-tool/canvas/workspace';
391
391
  |---|---|---|---|
392
392
  | `store` | Store | *required* | The MobX-State-Tree store instance |
393
393
  | `showInputFieldsPopup` | boolean | `true` | Show the input fields popup when a template with input fields is loaded. Set to `false` to disable. |
394
+ | `onInputFieldsComplete` | function | `undefined` | Callback fired after the user has completed (submitted or skipped) all input fields. Only called when the popup is enabled. |
394
395
  | `components` | object | `{}` | Override slots (e.g. `{ PageControls: MyComponent }`) |
395
396
 
396
397
  The workspace provides:
@@ -556,7 +557,7 @@ The input fields system consists of three parts:
556
557
  | `text` | Free-form text entry | Text input |
557
558
  | `date` | Date value with configurable format | Date picker |
558
559
  | `integer` | Whole number value | Number input (integers only) |
559
- | `image` | Image upload placeholder | **Not yet implemented** see [Roadmap](#roadmap) |
560
+ | `image` | Image upload placeholder | File picker with preview |
560
561
 
561
562
  ### Adding Input Fields (Admin Side)
562
563
 
@@ -614,6 +615,15 @@ The input fields popup is rendered inside the `<Workspace>` component and is **e
614
615
 
615
616
  // Popup disabled — input fields retain their template placeholder values
616
617
  <Workspace store={store} showInputFieldsPopup={false} />
618
+
619
+ // With completion callback
620
+ <Workspace
621
+ store={store}
622
+ onInputFieldsComplete={() => {
623
+ console.log('All input fields have been filled!');
624
+ // e.g. navigate, save, show a toast, etc.
625
+ }}
626
+ />
617
627
  ```
618
628
 
619
629
  When `showInputFieldsPopup` is set to `false`, all input fields are treated as if they were skipped — their original template values are preserved regardless of whether they are marked as required.
@@ -640,25 +650,83 @@ setInputFieldsConfig({
640
650
  });
641
651
  ```
642
652
 
643
- ### Providing a Custom Dialog Component
653
+ ### Providing Custom Dialog Components
654
+
655
+ You can replace the built-in popup with your own component — either globally for all types, or per input type.
644
656
 
645
- For complete control over the popup UI, provide a `CustomDialog` component:
657
+ **Per-type custom dialogs** (override specific types only):
646
658
 
647
659
  ```js
648
660
  import { setInputFieldsConfig } from 'labellife-design-tool';
649
661
 
650
662
  setInputFieldsConfig({
651
- CustomDialog: ({ fields, currentIndex, onSubmit, onSkip, onComplete }) => {
652
- // fields — array of input field elements
653
- // currentIndex index of the current field being displayed
654
- // onSubmit(elementId, value) call to set the value and advance
655
- // onSkip(elementId) — call to skip a field and advance
656
- // onComplete() — call when all fields are processed
657
- return <MyCustomDialogUI />;
658
- },
663
+ CustomTextDialog: MyTextPopup, // Used for text fields
664
+ CustomDateDialog: MyDatePopup, // Used for date fields
665
+ CustomIntegerDialog: MyNumberPopup, // Used for integer fields
666
+ CustomImageDialog: MyImagePopup, // Used for image fields
667
+ });
668
+ ```
669
+
670
+ **Global custom dialog** (catches any type without a per-type override):
671
+
672
+ ```js
673
+ setInputFieldsConfig({
674
+ CustomDialog: MyGenericPopup, // Fallback for all types
659
675
  });
660
676
  ```
661
677
 
678
+ **Mix and match** — per-type takes priority over global:
679
+
680
+ ```js
681
+ setInputFieldsConfig({
682
+ CustomImageDialog: MyImagePopup, // Only image fields use this
683
+ CustomDialog: MyGenericPopup, // Text, date, and integer use this
684
+ });
685
+ ```
686
+
687
+ **Resolution order:** `CustomTextDialog` / `CustomDateDialog` / `CustomIntegerDialog` / `CustomImageDialog` > `CustomDialog` > built-in dialog.
688
+
689
+ > **Note:** When `showInputFieldsPopup={false}` is set on `<Workspace>`, no popup is shown regardless of any custom dialog configuration.
690
+
691
+ **Props passed to every custom dialog component:**
692
+
693
+ | Prop | Type | Description |
694
+ |---|---|---|
695
+ | `field` | object | The current input field element (MST node with `id`, `custom`, etc.) |
696
+ | `fields` | array | All pending input field elements |
697
+ | `currentIndex` | number | Zero-based index of the current field |
698
+ | `totalCount` | number | Total number of fields to process |
699
+ | `onSubmit(elementId, value)` | function | Call to set the value and advance to the next field |
700
+ | `onSkip(elementId)` | function | Call to skip the field and advance |
701
+ | `onComplete()` | function | Call when all fields are processed |
702
+
703
+ **Example custom dialog:**
704
+
705
+ ```jsx
706
+ const MyImagePopup = ({ field, currentIndex, totalCount, onSubmit, onSkip }) => {
707
+ const promptText = field.custom?.promptText || 'Upload an image';
708
+
709
+ const handleFile = (e) => {
710
+ const file = e.target.files?.[0];
711
+ if (!file) return;
712
+ const reader = new FileReader();
713
+ reader.onload = (ev) => onSubmit(field.id, ev.target.result);
714
+ reader.readAsDataURL(file);
715
+ };
716
+
717
+ return (
718
+ <div className="my-dialog">
719
+ <h3>{promptText}</h3>
720
+ <p>Field {currentIndex + 1} of {totalCount}</p>
721
+ <input type="file" accept="image/*" onChange={handleFile} />
722
+ {!field.custom?.required && (
723
+ <button onClick={() => onSkip(field.id)}>Skip</button>
724
+ )}
725
+ </div>
726
+ );
727
+ };
728
+ ```
729
+
662
730
  ### Accessing Input Fields Programmatically
663
731
 
664
732
  ```js
@@ -864,7 +932,11 @@ setInputFieldsConfig({
864
932
  skipButtonStyle: {}, // MUI sx overrides for the Skip button
865
933
  submitButtonText: '', // Override Confirm button label
866
934
  skipButtonText: '', // Override Skip button label
867
- CustomDialog: null, // Provide a fully custom dialog component
935
+ CustomDialog: null, // Custom dialog for all types (global fallback)
936
+ CustomTextDialog: null, // Custom dialog for text fields only
937
+ CustomDateDialog: null, // Custom dialog for date fields only
938
+ CustomIntegerDialog: null,// Custom dialog for integer fields only
939
+ CustomImageDialog: null, // Custom dialog for image fields only
868
940
  });
869
941
  ```
870
942
 
@@ -969,11 +1041,42 @@ These are included in the package — no need to install separately:
969
1041
 
970
1042
  ---
971
1043
 
1044
+ ## WordPress Integration
1045
+
1046
+ A ready-to-use WordPress plugin starter is included at [`examples/wordpress/`](examples/wordpress/). It provides:
1047
+
1048
+ - A **pre-configured Vite + React app** that imports the library with all peer dependencies
1049
+ - A **WordPress plugin PHP file** with shortcode support (`[labellife_design_tool]`)
1050
+ - **Predictable build output** (no hashed file names) for easy `wp_enqueue_script`
1051
+ - A **step-by-step README** with setup, customization, and troubleshooting instructions
1052
+
1053
+ ### Quick Setup
1054
+
1055
+ ```bash
1056
+ # 1. Copy examples/wordpress/ into your WP plugins directory
1057
+ cp -r examples/wordpress/ /path/to/wp-content/plugins/labellife-design-tool/
1058
+
1059
+ # 2. Install dependencies and build
1060
+ cd /path/to/wp-content/plugins/labellife-design-tool/
1061
+ npm install
1062
+ npm run build
1063
+
1064
+ # 3. Activate the plugin in WP Admin → Plugins
1065
+ # 4. Add [labellife_design_tool] shortcode to any page
1066
+ ```
1067
+
1068
+ Customize the editor by editing `src/App.jsx` — same API as documented above. Rebuild with `npm run build` after changes.
1069
+
1070
+ See the full guide: [`examples/wordpress/README.md`](examples/wordpress/README.md)
1071
+
1072
+ ---
1073
+
972
1074
  ## Roadmap
973
1075
 
974
- The following features are planned but not yet implemented:
1076
+ All four input field types (Text, Date, Integer, Image) are fully implemented. Future enhancements under consideration:
975
1077
 
976
- - **Image Input Field Popup** The `image` input type can be added to templates and its metadata is preserved in JSON, but the popup dialog for uploading images at load time is not yet available. Image fields are skipped during the input collection flow.
1078
+ - **Drag-and-drop image upload** in the input fields popup dialog
1079
+ - **URL-based image input** as an alternative to file upload
977
1080
 
978
1081
  ---
979
1082
 
@@ -9,6 +9,7 @@ var material = require('@mui/material');
9
9
  var CloseIcon = require('@mui/icons-material/Close');
10
10
  var jsxRuntime = require('react/jsx-runtime');
11
11
  var Konva = require('konva');
12
+ var CloudUploadIcon = require('@mui/icons-material/CloudUpload');
12
13
 
13
14
  function _arrayLikeToArray(r, a) {
14
15
  (null == a || a > r.length) && (a = r.length);
@@ -1718,6 +1719,11 @@ var SingleFieldDialog = function SingleFieldDialog(_ref) {
1718
1719
  _useState2 = _slicedToArray(_useState, 2),
1719
1720
  value = _useState2[0],
1720
1721
  setValue = _useState2[1];
1722
+ var _useState3 = react.useState(null),
1723
+ _useState4 = _slicedToArray(_useState3, 2),
1724
+ imagePreview = _useState4[0],
1725
+ setImagePreview = _useState4[1];
1726
+ var fileInputRef = react.useRef(null);
1721
1727
  var config = getInputFieldsConfig();
1722
1728
  var custom = field.custom || {};
1723
1729
  var inputType = custom.inputType || 'text';
@@ -1726,7 +1732,21 @@ var SingleFieldDialog = function SingleFieldDialog(_ref) {
1726
1732
  var isRequired = custom.required === true;
1727
1733
  react.useEffect(function () {
1728
1734
  setValue('');
1735
+ setImagePreview(null);
1729
1736
  }, [field.id]);
1737
+ var handleImageSelect = function handleImageSelect(e) {
1738
+ var _e$target$files;
1739
+ var file = (_e$target$files = e.target.files) === null || _e$target$files === void 0 ? void 0 : _e$target$files[0];
1740
+ if (!file || !file.type.startsWith('image/')) return;
1741
+ var reader = new FileReader();
1742
+ reader.onload = function (ev) {
1743
+ var dataURL = ev.target.result;
1744
+ setValue(dataURL);
1745
+ setImagePreview(dataURL);
1746
+ };
1747
+ reader.readAsDataURL(file);
1748
+ if (fileInputRef.current) fileInputRef.current.value = '';
1749
+ };
1730
1750
  var handleSubmit = function handleSubmit() {
1731
1751
  if (inputType === 'date' && value) {
1732
1752
  var dateObj = parseDateFromFormat(value);
@@ -1783,6 +1803,93 @@ var SingleFieldDialog = function SingleFieldDialog(_ref) {
1783
1803
  mt: 1
1784
1804
  }, config.inputStyle)
1785
1805
  });
1806
+ case 'image':
1807
+ return /*#__PURE__*/jsxRuntime.jsxs(material.Box, {
1808
+ sx: {
1809
+ mt: 1
1810
+ },
1811
+ children: [/*#__PURE__*/jsxRuntime.jsx("input", {
1812
+ ref: fileInputRef,
1813
+ type: "file",
1814
+ accept: "image/*",
1815
+ onChange: handleImageSelect,
1816
+ style: {
1817
+ display: 'none'
1818
+ }
1819
+ }), imagePreview ? /*#__PURE__*/jsxRuntime.jsxs(material.Box, {
1820
+ sx: {
1821
+ display: 'flex',
1822
+ flexDirection: 'column',
1823
+ alignItems: 'center',
1824
+ gap: 1.5
1825
+ },
1826
+ children: [/*#__PURE__*/jsxRuntime.jsx(material.Box, {
1827
+ sx: {
1828
+ width: '100%',
1829
+ maxHeight: 220,
1830
+ borderRadius: 1,
1831
+ overflow: 'hidden',
1832
+ border: '1px solid #e0e0e0',
1833
+ display: 'flex',
1834
+ alignItems: 'center',
1835
+ justifyContent: 'center',
1836
+ backgroundColor: '#fafafa'
1837
+ },
1838
+ children: /*#__PURE__*/jsxRuntime.jsx("img", {
1839
+ src: imagePreview,
1840
+ alt: "Preview",
1841
+ style: {
1842
+ maxWidth: '100%',
1843
+ maxHeight: 220,
1844
+ objectFit: 'contain'
1845
+ }
1846
+ })
1847
+ }), /*#__PURE__*/jsxRuntime.jsx(material.Button, {
1848
+ variant: "outlined",
1849
+ size: "small",
1850
+ onClick: function onClick() {
1851
+ var _fileInputRef$current;
1852
+ return (_fileInputRef$current = fileInputRef.current) === null || _fileInputRef$current === void 0 ? void 0 : _fileInputRef$current.click();
1853
+ },
1854
+ sx: {
1855
+ textTransform: 'none',
1856
+ fontSize: 12
1857
+ },
1858
+ children: t('inputFieldsDialog.changeImage', 'Change Image')
1859
+ })]
1860
+ }) : /*#__PURE__*/jsxRuntime.jsxs(material.Box, {
1861
+ onClick: function onClick() {
1862
+ var _fileInputRef$current2;
1863
+ return (_fileInputRef$current2 = fileInputRef.current) === null || _fileInputRef$current2 === void 0 ? void 0 : _fileInputRef$current2.click();
1864
+ },
1865
+ sx: {
1866
+ border: '2px dashed #ccc',
1867
+ borderRadius: '8px',
1868
+ p: 3,
1869
+ textAlign: 'center',
1870
+ cursor: 'pointer',
1871
+ transition: 'all 0.2s ease',
1872
+ '&:hover': {
1873
+ borderColor: '#0d83cd',
1874
+ backgroundColor: 'rgba(13,131,205,0.04)'
1875
+ }
1876
+ },
1877
+ children: [/*#__PURE__*/jsxRuntime.jsx(CloudUploadIcon, {
1878
+ sx: {
1879
+ fontSize: 36,
1880
+ color: '#999',
1881
+ mb: 1
1882
+ }
1883
+ }), /*#__PURE__*/jsxRuntime.jsx(material.Typography, {
1884
+ variant: "body2",
1885
+ sx: {
1886
+ color: '#666',
1887
+ fontSize: 13
1888
+ },
1889
+ children: t('inputFieldsDialog.uploadImage', 'Click to select an image')
1890
+ })]
1891
+ })]
1892
+ });
1786
1893
  case 'text':
1787
1894
  default:
1788
1895
  return /*#__PURE__*/jsxRuntime.jsx(material.TextField, {
@@ -1875,10 +1982,10 @@ var InputFieldsDialog = mobxReactLite.observer(function (_ref2) {
1875
1982
  _onComplete = _ref2.onComplete,
1876
1983
  _ref2$enabled = _ref2.enabled,
1877
1984
  enabled = _ref2$enabled === void 0 ? true : _ref2$enabled;
1878
- var _useState3 = react.useState(0),
1879
- _useState4 = _slicedToArray(_useState3, 2),
1880
- currentIndex = _useState4[0],
1881
- setCurrentIndex = _useState4[1];
1985
+ var _useState5 = react.useState(0),
1986
+ _useState6 = _slicedToArray(_useState5, 2),
1987
+ currentIndex = _useState6[0],
1988
+ setCurrentIndex = _useState6[1];
1882
1989
  var config = getInputFieldsConfig();
1883
1990
  var pendingFields = store._pendingInputFields || [];
1884
1991
 
@@ -1888,10 +1995,8 @@ var InputFieldsDialog = mobxReactLite.observer(function (_ref2) {
1888
1995
  store.clearPendingInputFields();
1889
1996
  }
1890
1997
  }, [enabled, pendingFields.length, store]);
1891
-
1892
- // Filter out image types (not supported yet)
1893
1998
  var fields = pendingFields.filter(function (f) {
1894
- return f.custom && f.custom.inputType !== 'image';
1999
+ return f.custom && f.custom.inputField;
1895
2000
  });
1896
2001
  react.useEffect(function () {
1897
2002
  setCurrentIndex(0);
@@ -1900,47 +2005,20 @@ var InputFieldsDialog = mobxReactLite.observer(function (_ref2) {
1900
2005
  var currentField = fields[currentIndex];
1901
2006
  if (!currentField) return null;
1902
2007
 
1903
- // If the consuming project provided a CustomDialog, use that
1904
- var CustomDialog = config.CustomDialog;
1905
- if (CustomDialog) {
1906
- return /*#__PURE__*/jsxRuntime.jsx(CustomDialog, {
1907
- fields: fields,
1908
- currentIndex: currentIndex,
1909
- onSubmit: function onSubmit(elementId, value) {
1910
- var el = store.getElementById(elementId);
1911
- if (el && el.set) {
1912
- el.set({
1913
- text: String(value)
1914
- });
1915
- }
1916
- store._resolveInputField(elementId);
1917
- if (currentIndex + 1 >= fields.length) {
1918
- store.history.addUndoState();
1919
- if (_onComplete) _onComplete();
1920
- } else {
1921
- setCurrentIndex(currentIndex + 1);
1922
- }
1923
- },
1924
- onSkip: function onSkip(elementId) {
1925
- store._resolveInputField(elementId);
1926
- if (currentIndex + 1 >= fields.length) {
1927
- if (_onComplete) _onComplete();
1928
- } else {
1929
- setCurrentIndex(currentIndex + 1);
1930
- }
1931
- },
1932
- onComplete: function onComplete() {
1933
- store.history.addUndoState();
1934
- if (_onComplete) _onComplete();
1935
- }
1936
- });
1937
- }
2008
+ // ── Shared submit / skip helpers ────────────────────────────────────
1938
2009
  var handleSubmit = function handleSubmit(elementId, value) {
1939
2010
  var el = store.getElementById(elementId);
1940
2011
  if (el && el.set) {
1941
- el.set({
1942
- text: String(value)
1943
- });
2012
+ var isImage = el.custom && el.custom.inputType === 'image';
2013
+ if (isImage) {
2014
+ el.set({
2015
+ src: String(value)
2016
+ });
2017
+ } else {
2018
+ el.set({
2019
+ text: String(value)
2020
+ });
2021
+ }
1944
2022
  }
1945
2023
  store._resolveInputField(elementId);
1946
2024
  if (currentIndex + 1 >= fields.length) {
@@ -1962,6 +2040,33 @@ var InputFieldsDialog = mobxReactLite.observer(function (_ref2) {
1962
2040
  });
1963
2041
  }
1964
2042
  };
2043
+
2044
+ // ── Resolve which dialog component to use ───────────────────────────
2045
+ // Priority: per-type custom dialog > global CustomDialog > built-in
2046
+ var TYPE_DIALOG_MAP = {
2047
+ text: config.CustomTextDialog,
2048
+ date: config.CustomDateDialog,
2049
+ integer: config.CustomIntegerDialog,
2050
+ image: config.CustomImageDialog
2051
+ };
2052
+ var fieldType = currentField.custom && currentField.custom.inputType || 'text';
2053
+ var PerTypeDialog = TYPE_DIALOG_MAP[fieldType];
2054
+ var GlobalCustomDialog = config.CustomDialog;
2055
+ var ResolvedDialog = PerTypeDialog || GlobalCustomDialog || null;
2056
+ if (ResolvedDialog) {
2057
+ return /*#__PURE__*/jsxRuntime.jsx(ResolvedDialog, {
2058
+ field: currentField,
2059
+ fields: fields,
2060
+ currentIndex: currentIndex,
2061
+ totalCount: fields.length,
2062
+ onSubmit: handleSubmit,
2063
+ onSkip: handleSkip,
2064
+ onComplete: function onComplete() {
2065
+ store.history.addUndoState();
2066
+ if (_onComplete) _onComplete();
2067
+ }
2068
+ });
2069
+ }
1965
2070
  return /*#__PURE__*/jsxRuntime.jsx(SingleFieldDialog, {
1966
2071
  field: currentField,
1967
2072
  onSubmit: handleSubmit,
@@ -2284,7 +2389,8 @@ var Workspace = mobxReactLite.observer(function (_ref6) {
2284
2389
  _ref6$components = _ref6.components,
2285
2390
  components = _ref6$components === void 0 ? {} : _ref6$components,
2286
2391
  _ref6$showInputFields = _ref6.showInputFieldsPopup,
2287
- showInputFieldsPopup = _ref6$showInputFields === void 0 ? true : _ref6$showInputFields;
2392
+ showInputFieldsPopup = _ref6$showInputFields === void 0 ? true : _ref6$showInputFields,
2393
+ onInputFieldsComplete = _ref6.onInputFieldsComplete;
2288
2394
  var stageRef = react.useRef(null);
2289
2395
  var containerRef = react.useRef(null);
2290
2396
  var _useState5 = react.useState({
@@ -2569,7 +2675,8 @@ var Workspace = mobxReactLite.observer(function (_ref6) {
2569
2675
  store: store
2570
2676
  }) : null, /*#__PURE__*/jsxRuntime.jsx(InputFieldsDialog, {
2571
2677
  store: store,
2572
- enabled: showInputFieldsPopup
2678
+ enabled: showInputFieldsPopup,
2679
+ onComplete: onInputFieldsComplete
2573
2680
  })]
2574
2681
  });
2575
2682
  });