underpost 2.8.878 → 2.8.882

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.
Files changed (61) hide show
  1. package/.env.development +35 -3
  2. package/.env.production +40 -3
  3. package/.env.test +35 -3
  4. package/.github/workflows/release.cd.yml +3 -3
  5. package/README.md +20 -2
  6. package/bin/deploy.js +40 -0
  7. package/cli.md +3 -1
  8. package/conf.js +29 -3
  9. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  10. package/manifests/deployment/dd-test-development/deployment.yaml +6 -6
  11. package/package.json +1 -2
  12. package/src/api/document/document.controller.js +66 -0
  13. package/src/api/document/document.model.js +51 -0
  14. package/src/api/document/document.router.js +24 -0
  15. package/src/api/document/document.service.js +133 -0
  16. package/src/cli/deploy.js +1 -1
  17. package/src/cli/index.js +2 -0
  18. package/src/cli/repository.js +2 -0
  19. package/src/cli/run.js +27 -1
  20. package/src/client/Default.index.js +46 -1
  21. package/src/client/components/core/Account.js +8 -1
  22. package/src/client/components/core/AgGrid.js +18 -9
  23. package/src/client/components/core/Auth.js +258 -89
  24. package/src/client/components/core/BtnIcon.js +13 -3
  25. package/src/client/components/core/Content.js +2 -1
  26. package/src/client/components/core/CssCore.js +40 -27
  27. package/src/client/components/core/Docs.js +189 -88
  28. package/src/client/components/core/Input.js +34 -19
  29. package/src/client/components/core/LoadingAnimation.js +5 -10
  30. package/src/client/components/core/Modal.js +280 -123
  31. package/src/client/components/core/ObjectLayerEngine.js +470 -104
  32. package/src/client/components/core/ObjectLayerEngineModal.js +1 -0
  33. package/src/client/components/core/Panel.js +9 -2
  34. package/src/client/components/core/PanelForm.js +234 -76
  35. package/src/client/components/core/Router.js +15 -15
  36. package/src/client/components/core/ToolTip.js +83 -19
  37. package/src/client/components/core/Translate.js +1 -1
  38. package/src/client/components/core/VanillaJs.js +7 -3
  39. package/src/client/components/core/windowGetDimensions.js +202 -0
  40. package/src/client/components/default/MenuDefault.js +105 -41
  41. package/src/client/components/default/RoutesDefault.js +2 -0
  42. package/src/client/services/default/default.management.js +1 -0
  43. package/src/client/services/document/document.service.js +97 -0
  44. package/src/client/services/file/file.service.js +2 -0
  45. package/src/client/ssr/Render.js +1 -1
  46. package/src/client/ssr/head/DefaultScripts.js +2 -0
  47. package/src/client/ssr/head/Seo.js +1 -0
  48. package/src/index.js +1 -1
  49. package/src/mailer/EmailRender.js +1 -1
  50. package/src/server/auth.js +68 -17
  51. package/src/server/client-build.js +2 -3
  52. package/src/server/client-formatted.js +40 -12
  53. package/src/server/conf.js +5 -1
  54. package/src/server/crypto.js +195 -76
  55. package/src/server/object-layer.js +196 -0
  56. package/src/server/peer.js +47 -5
  57. package/src/server/process.js +85 -1
  58. package/src/server/runtime.js +23 -23
  59. package/src/server/ssr.js +52 -10
  60. package/src/server/valkey.js +89 -1
  61. package/test/crypto.test.js +117 -0
@@ -1,113 +1,112 @@
1
1
  import { darkTheme, renderChessPattern } from './Css.js';
2
-
3
- const templateHTML = html`
4
- <style>
5
- :host {
6
- --border: 1px solid #bbb;
7
- --gap: 8px;
8
- display: inline-block;
9
- font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
10
- }
11
- .wrap {
12
- display: flex;
13
- flex-direction: column;
14
- gap: var(--gap);
15
- align-items: flex-start;
16
- }
17
- .canvas-frame {
18
- border: var(--border);
19
- display: inline-block;
20
- line-height: 0;
21
- position: relative;
22
- background: transparent;
23
- }
24
- canvas.canvas-layer {
25
- display: block;
26
- image-rendering: pixelated;
27
- touch-action: none;
28
- cursor: crosshair;
29
- }
30
- canvas.grid-layer {
31
- position: absolute;
32
- left: 0;
33
- top: 0;
34
- pointer-events: none;
35
- }
36
- .toolbar {
37
- display: flex;
38
- gap: 8px;
39
- flex-wrap: wrap;
40
- align-items: center;
41
- }
42
- .toolbar label {
43
- display: inline-flex;
44
- gap: 6px;
45
- align-items: center;
46
- }
47
- .group {
48
- display: inline-flex;
49
- gap: 6px;
50
- align-items: center;
51
- }
52
- </style>
53
-
54
- <div class="wrap">
55
- <div class="toolbar">
56
- <input type="color" part="color" title="Brush color" value="#000000" />
57
- <select part="tool">
58
- <option value="pencil">pencil</option>
59
- <option value="eraser">eraser</option>
60
- <option value="fill">fill</option>
61
- <option value="eyedropper">eyedropper</option>
62
- </select>
63
-
64
- <label>brush <input type="number" part="brush-size" min="1" value="1" /></label>
65
- <label>pixel-size <input type="number" part="pixel-size" min="1" value="16" /></label>
66
-
67
- <!-- New: cell dimensions (width x height) -->
68
- <label
69
- >cells <input type="number" part="cell-width" min="1" value="16" style="width:6ch" /> x
70
- <input type="number" part="cell-height" min="1" value="16" style="width:6ch"
71
- /></label>
72
-
73
- <label class="switch"> <input type="checkbox" part="toggle-grid" /> grid </label>
74
-
75
- <!-- New: transform tools -->
76
- <div class="group">
77
- <button part="flip-h" title="Flip horizontally">Flip H</button>
78
- <button part="flip-v" title="Flip vertically">Flip V</button>
79
- <button part="rot-ccw" title="Rotate -90°">⟲</button>
80
- <button part="rot-cw" title="Rotate +90°">⟳</button>
81
- </div>
82
-
83
- <label
84
- >opacity <input type="range" part="opacity" min="0" max="255" value="255" style="width:10rem" /><input
85
- type="number"
86
- part="opacity-num"
87
- min="0"
88
- max="255"
89
- value="255"
90
- style="width:5ch;margin-left:4px"
91
- /></label>
92
-
93
- <button part="clear" title="Clear (make fully transparent)">Clear</button>
94
-
95
- <button part="export">Export PNG</button>
96
- <button part="export-json">Export JSON</button>
97
- <button part="import-json">Import JSON</button>
98
- </div>
99
- <div class="canvas-frame" style="${renderChessPattern()}">
100
- <canvas part="canvas" class="canvas-layer"></canvas>
101
- <canvas part="grid" class="grid-layer"></canvas>
102
- </div>
103
- </div>
104
- `;
2
+ import { append, htmls } from './VanillaJs.js';
105
3
 
106
4
  class ObjectLayerEngineElement extends HTMLElement {
107
5
  constructor() {
108
6
  super();
109
7
  this.attachShadow({ mode: 'open' });
110
- this.shadowRoot.innerHTML = templateHTML;
8
+ this.shadowRoot.innerHTML = html`
9
+ <style>
10
+ :host {
11
+ --border: 1px solid #bbb;
12
+ --gap: 8px;
13
+ display: inline-block;
14
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
15
+ }
16
+ .wrap {
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: var(--gap);
20
+ align-items: flex-start;
21
+ }
22
+ .canvas-frame {
23
+ border: var(--border);
24
+ display: inline-block;
25
+ line-height: 0;
26
+ position: relative;
27
+ background: transparent;
28
+ }
29
+ canvas.canvas-layer {
30
+ display: block;
31
+ image-rendering: pixelated;
32
+ touch-action: none;
33
+ cursor: crosshair;
34
+ }
35
+ canvas.grid-layer {
36
+ position: absolute;
37
+ left: 0;
38
+ top: 0;
39
+ pointer-events: none;
40
+ }
41
+ .toolbar {
42
+ display: flex;
43
+ gap: 8px;
44
+ flex-wrap: wrap;
45
+ align-items: center;
46
+ }
47
+ .toolbar label {
48
+ display: inline-flex;
49
+ gap: 6px;
50
+ align-items: center;
51
+ }
52
+ .group {
53
+ display: inline-flex;
54
+ gap: 6px;
55
+ align-items: center;
56
+ }
57
+ </style>
58
+
59
+ <div class="wrap">
60
+ <div class="toolbar">
61
+ <input type="color" part="color" title="Brush color" value="#000000" />
62
+ <select part="tool">
63
+ <option value="pencil">pencil</option>
64
+ <option value="eraser">eraser</option>
65
+ <option value="fill">fill</option>
66
+ <option value="eyedropper">eyedropper</option>
67
+ </select>
68
+
69
+ <label>brush <input type="number" part="brush-size" min="1" value="1" /></label>
70
+ <label>pixel-size <input type="number" part="pixel-size" min="1" value="16" /></label>
71
+
72
+ <!-- New: cell dimensions (width x height) -->
73
+ <label
74
+ >cells <input type="number" part="cell-width" min="1" value="16" style="width:6ch" /> x
75
+ <input type="number" part="cell-height" min="1" value="16" style="width:6ch"
76
+ /></label>
77
+
78
+ <label class="switch"> <input type="checkbox" part="toggle-grid" /> grid </label>
79
+
80
+ <!-- New: transform tools -->
81
+ <div class="group">
82
+ <button part="flip-h" title="Flip horizontally">Flip H</button>
83
+ <button part="flip-v" title="Flip vertically">Flip V</button>
84
+ <button part="rot-ccw" title="Rotate -90°">⟲</button>
85
+ <button part="rot-cw" title="Rotate +90°">⟳</button>
86
+ </div>
87
+
88
+ <label
89
+ >opacity <input type="range" part="opacity" min="0" max="255" value="255" style="width:10rem" /><input
90
+ type="number"
91
+ part="opacity-num"
92
+ min="0"
93
+ max="255"
94
+ value="255"
95
+ style="width:5ch;margin-left:4px"
96
+ /></label>
97
+
98
+ <button part="clear" title="Clear (make fully transparent)">Clear</button>
99
+
100
+ <button part="export">Export PNG</button>
101
+ <button part="export-json">Export JSON</button>
102
+ <button part="import-json">Import JSON</button>
103
+ </div>
104
+ <div class="canvas-frame" style="${renderChessPattern()}">
105
+ <canvas part="canvas" class="canvas-layer"></canvas>
106
+ <canvas part="grid" class="grid-layer"></canvas>
107
+ </div>
108
+ </div>
109
+ `;
111
110
 
112
111
  // DOM
113
112
  this._pixelCanvas = this.shadowRoot.querySelector('canvas[part="canvas"]');
@@ -861,3 +860,370 @@ customElements.define('object-layer-engine', ObjectLayerEngineElement);
861
860
  Example usage:
862
861
  <object-layer-engine id="ole" width="20" height="12" pixel-size="20"></object-layer-engine>
863
862
  */
863
+
864
+ class ObjectLayerPngLoader extends HTMLElement {
865
+ constructor() {
866
+ super();
867
+ this.attachShadow({ mode: 'open' });
868
+ this.shadowRoot.innerHTML = html`
869
+ <style>
870
+ :host {
871
+ display: block;
872
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Arial;
873
+ }
874
+ .wrap {
875
+ display: flex;
876
+ flex-direction: column;
877
+ gap: 8px;
878
+ }
879
+ .controls {
880
+ display: flex;
881
+ gap: 8px;
882
+ align-items: center;
883
+ flex-wrap: wrap;
884
+ }
885
+ .drop-area {
886
+ border: 2px dashed #999;
887
+ padding: 12px;
888
+ border-radius: 8px;
889
+ text-align: center;
890
+ color: #555;
891
+ user-select: none;
892
+ }
893
+ .drop-area.dragover {
894
+ border-color: #4a90e2;
895
+ color: #1a73e8;
896
+ background: rgba(74, 144, 226, 0.04);
897
+ }
898
+ input[type='file'] {
899
+ display: inline-block;
900
+ }
901
+ .hint {
902
+ font-size: 0.9rem;
903
+ color: #666;
904
+ }
905
+ </style>
906
+
907
+ <div class="wrap">
908
+ <div class="controls">
909
+ <label title="Load PNG file">
910
+ <input type="file" accept="image/png" part="file-input" />
911
+ <span class="btn">Choose PNG</span>
912
+ </label>
913
+ <div class="hint">Only PNG images accepted. Drop PNG onto the box below.</div>
914
+ </div>
915
+
916
+ <div class="drop-area" part="drop-area">Drop PNG here or click "Choose PNG"</div>
917
+ </div>
918
+ `;
919
+
920
+ this._fileInput = this.shadowRoot.querySelector('input[type="file"]');
921
+ this._dropArea = this.shadowRoot.querySelector('.drop-area');
922
+
923
+ this._editor = null; // will hold external editor instance
924
+ this._options = { fitMode: 'contain' };
925
+
926
+ // Bind handlers
927
+ this._onFileChange = this._onFileChange.bind(this);
928
+ this._onDrop = this._onDrop.bind(this);
929
+ this._onDragOver = this._onDragOver.bind(this);
930
+ this._onDragLeave = this._onDragLeave.bind(this);
931
+ }
932
+
933
+ static get observedAttributes() {
934
+ return ['editor-selector', 'fit-mode', 'target-cells-x', 'target-cells-y'];
935
+ }
936
+
937
+ attributeChangedCallback(name, oldVal, newVal) {
938
+ if (name === 'editor-selector' && newVal) {
939
+ const el = document.querySelector(newVal);
940
+ if (el) this.setEditor(el);
941
+ }
942
+ if (name === 'fit-mode') {
943
+ this._options.fitMode = newVal || 'contain';
944
+ }
945
+ }
946
+
947
+ connectedCallback() {
948
+ this._fileInput.addEventListener('change', this._onFileChange);
949
+ this._dropArea.addEventListener('dragover', this._onDragOver);
950
+ this._dropArea.addEventListener('dragleave', this._onDragLeave);
951
+ this._dropArea.addEventListener('drop', this._onDrop);
952
+ this.addEventListener('dragover', this._onDragOver);
953
+ this.addEventListener('dragleave', this._onDragLeave);
954
+ this.addEventListener('drop', this._onDrop);
955
+
956
+ // If editor-selector attribute was present at creation, try to resolve
957
+ const sel = this.getAttribute('editor-selector');
958
+ if (sel) {
959
+ const target = document.querySelector(sel);
960
+ if (target) this.setEditor(target);
961
+ }
962
+
963
+ // read fit-mode
964
+ const fit = this.getAttribute('fit-mode');
965
+ if (fit) this._options.fitMode = fit;
966
+ }
967
+
968
+ disconnectedCallback() {
969
+ this._fileInput.removeEventListener('change', this._onFileChange);
970
+ this._dropArea.removeEventListener('dragover', this._onDragOver);
971
+ this._dropArea.removeEventListener('dragleave', this._onDragLeave);
972
+ this._dropArea.removeEventListener('drop', this._onDrop);
973
+ this.removeEventListener('dragover', this._onDragOver);
974
+ this.removeEventListener('dragleave', this._onDragLeave);
975
+ this.removeEventListener('drop', this._onDrop);
976
+ }
977
+
978
+ // ----------------- Public API -----------------
979
+ setEditor(editor) {
980
+ if (!editor) throw new Error('Editor cannot be null/undefined');
981
+ if (typeof editor.loadMatrix !== 'function') {
982
+ throw new Error('Provided editor does not expose loadMatrix(matrix)');
983
+ }
984
+ this._editor = editor;
985
+ this.dispatchEvent(new CustomEvent('editorconnected', { detail: { editor } }));
986
+ }
987
+
988
+ setOptions(options = {}) {
989
+ if (options.fitMode) this._options.fitMode = options.fitMode;
990
+ if (options.targetCellsX) this.setAttribute('target-cells-x', String(options.targetCellsX));
991
+ if (options.targetCellsY) this.setAttribute('target-cells-y', String(options.targetCellsY));
992
+ }
993
+
994
+ get editor() {
995
+ return this._editor;
996
+ }
997
+
998
+ // ----------------- Events -----------------
999
+ _onFileChange(e) {
1000
+ const file = e.target.files && e.target.files[0] ? e.target.files[0] : null;
1001
+ if (!file) return;
1002
+ this._handleFile(file);
1003
+ this._fileInput.value = '';
1004
+ }
1005
+
1006
+ _onDragOver(e) {
1007
+ e.preventDefault();
1008
+ e.dataTransfer.dropEffect = 'copy';
1009
+ this._dropArea.classList.add('dragover');
1010
+ }
1011
+ _onDragLeave(e) {
1012
+ e.preventDefault();
1013
+ this._dropArea.classList.remove('dragover');
1014
+ }
1015
+
1016
+ _onDrop(e) {
1017
+ e.preventDefault();
1018
+ this._dropArea.classList.remove('dragover');
1019
+ const file = e.dataTransfer.files && e.dataTransfer.files[0] ? e.dataTransfer.files[0] : null;
1020
+ if (!file) return;
1021
+ this._handleFile(file);
1022
+ }
1023
+
1024
+ // ----------------- File handling -----------------
1025
+ async _handleFile(file) {
1026
+ const isPngByType = file.type === 'image/png';
1027
+ const isPngByName = file.name && file.name.toLowerCase().endsWith('.png');
1028
+ if (!isPngByType && !isPngByName) {
1029
+ this._showError('Only PNG files are supported.');
1030
+ return;
1031
+ }
1032
+
1033
+ if (!this._editor) {
1034
+ this._showError('No editor connected. Use setEditor(editor) or provide editor-selector attribute.');
1035
+ return;
1036
+ }
1037
+
1038
+ try {
1039
+ await this._loadPngToEditorAdaptive(file);
1040
+ this._dispatchLoadedEvent(file.name);
1041
+ } catch (err) {
1042
+ console.error('Failed to load PNG', err);
1043
+ this._showError('Failed to load PNG (see console).');
1044
+ }
1045
+ }
1046
+
1047
+ _showError(msg) {
1048
+ alert(msg);
1049
+ }
1050
+ _dispatchLoadedEvent(filename) {
1051
+ this.dispatchEvent(new CustomEvent('pngloaded', { detail: { filename } }));
1052
+ }
1053
+
1054
+ // ----------------- Adaptive load -----------------
1055
+ _readEditorConfig() {
1056
+ const ed = this._editor;
1057
+ const cfg = { pixelSize: null, cellsX: null, cellsY: null };
1058
+
1059
+ if (!ed) return cfg;
1060
+
1061
+ // pixel size detection (try multiple forms)
1062
+ cfg.pixelSize = ed.pixelSize || ed.pixel_size || null;
1063
+ if (!cfg.pixelSize) {
1064
+ const attr = ed.getAttribute && (ed.getAttribute('pixel-size') || ed.getAttribute('pixelSize'));
1065
+ if (attr) cfg.pixelSize = parseInt(attr, 10);
1066
+ }
1067
+ if (typeof cfg.pixelSize === 'string') cfg.pixelSize = parseInt(cfg.pixelSize, 10);
1068
+
1069
+ // cells detection (common attribute names: width/height on engine represent cells)
1070
+ const widthAttr = ed.getAttribute && ed.getAttribute('width');
1071
+ const heightAttr = ed.getAttribute && ed.getAttribute('height');
1072
+ if (widthAttr && heightAttr) {
1073
+ cfg.cellsX = parseInt(widthAttr, 10);
1074
+ cfg.cellsY = parseInt(heightAttr, 10);
1075
+ }
1076
+
1077
+ // alternative property names
1078
+ cfg.cellsX = cfg.cellsX || ed.cellsX || ed.cellCountX || (ed.cells && ed.cells.x) || null;
1079
+ cfg.cellsY = cfg.cellsY || ed.cellsY || ed.cellCountY || (ed.cells && ed.cells.y) || null;
1080
+
1081
+ // if editor exposes getCells() prefer that
1082
+ try {
1083
+ if ((!cfg.cellsX || !cfg.cellsY) && typeof ed.getCells === 'function') {
1084
+ const c = ed.getCells();
1085
+ if (c && c.x && c.y) {
1086
+ cfg.cellsX = cfg.cellsX || c.x;
1087
+ cfg.cellsY = cfg.cellsY || c.y;
1088
+ }
1089
+ }
1090
+ } catch (e) {
1091
+ /* ignore */
1092
+ }
1093
+
1094
+ return cfg;
1095
+ }
1096
+
1097
+ // core adaptive loader: scales image to editor cells (or computes fallback)
1098
+ async _loadPngToEditorAdaptive(blobOrFile) {
1099
+ const imgBitmap = await createImageBitmap(blobOrFile);
1100
+ const srcW = imgBitmap.width;
1101
+ const srcH = imgBitmap.height;
1102
+
1103
+ // read editor config and loader explicit overrides
1104
+ const editorCfg = this._readEditorConfig();
1105
+ const overrideX = this.getAttribute('target-cells-x');
1106
+ const overrideY = this.getAttribute('target-cells-y');
1107
+
1108
+ let targetCellsX = overrideX ? parseInt(overrideX, 10) : editorCfg.cellsX || null;
1109
+ let targetCellsY = overrideY ? parseInt(overrideY, 10) : editorCfg.cellsY || null;
1110
+
1111
+ // if cells unknown but pixelSize known, compute approximate cells from image dimensions
1112
+ if ((!targetCellsX || !targetCellsY) && editorCfg.pixelSize) {
1113
+ const px = parseInt(editorCfg.pixelSize, 10);
1114
+ if (px > 0) {
1115
+ if (!targetCellsX) targetCellsX = Math.max(1, Math.round(srcW / px));
1116
+ if (!targetCellsY) targetCellsY = Math.max(1, Math.round(srcH / px));
1117
+ }
1118
+ }
1119
+
1120
+ // if still missing, fallback to native image pixels
1121
+ if (!targetCellsX) targetCellsX = srcW;
1122
+ if (!targetCellsY) targetCellsY = srcH;
1123
+
1124
+ // Decide fit mode
1125
+ const fitMode = this._options.fitMode || this.getAttribute('fit-mode') || 'contain';
1126
+
1127
+ // Create a small canvas sized to the target cells (we will render the image into this canvas
1128
+ // with smoothing disabled to preserve blocky/pixel look). Then read each pixel as a cell.
1129
+ const small = document.createElement('canvas');
1130
+ small.width = targetCellsX;
1131
+ small.height = targetCellsY;
1132
+ const sctx = small.getContext('2d');
1133
+
1134
+ // nearest-neighbour / crisp scaling
1135
+ sctx.imageSmoothingEnabled = false;
1136
+ sctx.clearRect(0, 0, small.width, small.height);
1137
+
1138
+ if (fitMode === 'stretch') {
1139
+ // non-uniform scale to fill exactly
1140
+ sctx.drawImage(imgBitmap, 0, 0, srcW, srcH, 0, 0, small.width, small.height);
1141
+ } else {
1142
+ // compute uniform scale to contain or cover
1143
+ let scaleX = small.width / srcW;
1144
+ let scaleY = small.height / srcH;
1145
+ let scale = fitMode === 'cover' ? Math.max(scaleX, scaleY) : Math.min(scaleX, scaleY);
1146
+ // compute destination size in small-canvas pixels
1147
+ const destW = Math.max(1, Math.round(srcW * scale));
1148
+ const destH = Math.max(1, Math.round(srcH * scale));
1149
+ const dx = Math.floor((small.width - destW) / 2);
1150
+ const dy = Math.floor((small.height - destH) / 2);
1151
+ sctx.drawImage(imgBitmap, 0, 0, srcW, srcH, dx, dy, destW, destH);
1152
+ }
1153
+
1154
+ // read pixel data from the small canvas
1155
+ const imageData = sctx.getImageData(0, 0, small.width, small.height).data;
1156
+
1157
+ // build matrix[y][x] = [r,g,b,a]
1158
+ const matrix = new Array(small.height);
1159
+ let p = 0;
1160
+ for (let y = 0; y < small.height; y++) {
1161
+ const row = new Array(small.width);
1162
+ for (let x = 0; x < small.width; x++) {
1163
+ const r = imageData[p++];
1164
+ const g = imageData[p++];
1165
+ const b = imageData[p++];
1166
+ const a = imageData[p++];
1167
+ row[x] = [r, g, b, a];
1168
+ }
1169
+ matrix[y] = row;
1170
+ }
1171
+
1172
+ // attempt to optionally align editor settings (best-effort)
1173
+ try {
1174
+ // if editor has setCells(x,y) or setCellCount, call it
1175
+ if (typeof this._editor.setCells === 'function') {
1176
+ this._editor.setCells(small.width, small.height);
1177
+ } else if (typeof this._editor.setCellCount === 'function') {
1178
+ this._editor.setCellCount(small.width, small.height);
1179
+ } else {
1180
+ // try common attribute setter
1181
+ if (this._editor.setAttribute) {
1182
+ this._editor.setAttribute('width', String(small.width));
1183
+ this._editor.setAttribute('height', String(small.height));
1184
+ }
1185
+ }
1186
+
1187
+ // if editor has setPixelSize and editorCfg.pixelSize exists, keep it
1188
+ if (editorCfg.pixelSize && typeof this._editor.setPixelSize === 'function') {
1189
+ this._editor.setPixelSize(parseInt(editorCfg.pixelSize, 10));
1190
+ }
1191
+ } catch (e) {
1192
+ // non-critical; continue
1193
+ console.warn('Failed to align editor config:', e);
1194
+ }
1195
+
1196
+ // finally, hand matrix to editor
1197
+ if (!this._editor || typeof this._editor.loadMatrix !== 'function') {
1198
+ throw new Error('Editor disconnected or does not expose loadMatrix');
1199
+ }
1200
+
1201
+ this._editor.loadMatrix(matrix);
1202
+ }
1203
+
1204
+ // Public helpers
1205
+ async loadPngBlob(blob) {
1206
+ return this._handleFile(blob);
1207
+ }
1208
+ async loadPngUrl(url) {
1209
+ const resp = await fetch(url);
1210
+ const blob = await resp.blob();
1211
+ return this._handleFile(blob);
1212
+ }
1213
+ }
1214
+
1215
+ customElements.define('object-layer-png-loader', ObjectLayerPngLoader);
1216
+
1217
+ /* Example wiring (NOT code repeated in canvas):
1218
+
1219
+ // HTML
1220
+ <object-layer-engine id="editor"></object-layer-engine>
1221
+ <object-layer-png-loader id="loader" editor-selector="#editor"></object-layer-png-loader>
1222
+
1223
+ // JS (programmatic)
1224
+ const editor = document.getElementById('editor');
1225
+ const loader = document.getElementById('loader');
1226
+ // Alternatively: loader.setEditor(editor);
1227
+ loader.addEventListener('pngloaded', (e) => console.log('Loaded', e.detail.filename));
1228
+
1229
+ */
@@ -272,6 +272,7 @@ const ObjectLayerEngineModal = {
272
272
 
273
273
  <object-layer-engine id="ole" width="${cells}" height="${cells}" pixel-size="${pixelSize}">
274
274
  </object-layer-engine>
275
+ <object-layer-png-loader id="loader" editor-selector="#ole"></object-layer-png-loader>
275
276
  </div>
276
277
 
277
278
  <div class="in section-mp section-mp-border">
@@ -114,6 +114,7 @@ const Panel = {
114
114
  options.originData().find((d) => d._id === obj._id || d.id === obj.id),
115
115
  options.filesData().find((d) => d._id === obj._id || d.id === obj.id),
116
116
  );
117
+ if (options.on.initEdit) await options.on.initEdit({ data: obj });
117
118
  });
118
119
  s(`.a-${payload._id}`).onclick = async (e) => {
119
120
  e.preventDefault();
@@ -130,6 +131,7 @@ const Panel = {
130
131
  ${await BtnIcon.Render({
131
132
  class: `in flr main-btn-menu action-bar-box ${idPanel}-btn-tool ${idPanel}-btn-edit-${id}`,
132
133
  label: html`<div class="abs center"><i class="fas fa-edit"></i></div>`,
134
+ useVisibilityHover: true,
133
135
  tooltipHtml: await Badge.Render({
134
136
  id: `tooltip-${idPanel}-${id}`,
135
137
  text: `${Translate.Render(`edit`)}`,
@@ -140,6 +142,7 @@ const Panel = {
140
142
  ${await BtnIcon.Render({
141
143
  class: `in flr main-btn-menu action-bar-box ${idPanel}-btn-tool ${idPanel}-btn-delete-${id}`,
142
144
  label: html`<div class="abs center"><i class="fas fa-trash"></i></div>`,
145
+ useVisibilityHover: true,
143
146
  tooltipHtml: await Badge.Render({
144
147
  id: `tooltip-${idPanel}-${id}`,
145
148
  text: `${Translate.Render(`delete`)}`,
@@ -469,7 +472,7 @@ const Panel = {
469
472
  }
470
473
  s(`.${idPanel}-form-body`).classList.add('hide');
471
474
  };
472
- s(`.btn-${idPanel}-add`).onclick = (e) => {
475
+ s(`.btn-${idPanel}-add`).onclick = async (e) => {
473
476
  e.preventDefault();
474
477
  // s(`.btn-${idPanel}-clean`).click();
475
478
  Panel.Tokens[idPanel].editId = undefined;
@@ -478,16 +481,20 @@ const Panel = {
478
481
  s(`.${scrollClassContainer}`).scrollTop = 0;
479
482
 
480
483
  openPanelForm();
484
+ if (options.on.initAdd) await options.on.initAdd();
481
485
  };
482
486
  if (s(`.${scrollClassContainer}`)) s(`.${scrollClassContainer}`).style.overflow = 'auto';
483
487
  });
484
488
 
485
489
  if (data.length > 0) for (const obj of data) render += await renderPanel(obj);
486
- else
490
+ else {
487
491
  render += html`<div class="in" style="min-height: 200px">
488
492
  <div class="abs center"><i class="fas fa-exclamation-circle"></i> ${Translate.Render(`no-result-found`)}</div>
489
493
  </div>`;
490
494
 
495
+ if (options.on.noResultFound) setTimeout(options.on.noResultFound);
496
+ }
497
+
491
498
  this.Tokens[idPanel] = { idPanel, scrollClassContainer, formData, data, titleKey, subTitleKey, renderPanel };
492
499
 
493
500
  let customButtonsRender = '';