@wipco/sdui 1.5.0 → 1.6.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.
Files changed (143) hide show
  1. package/dist/{DatePicker-C_TT_SOw.d.ts → DatePicker-DX9juElE.d.ts} +10 -1
  2. package/dist/{chunk-GZBY7I4Y.js → chunk-2KQL6DYG.js} +3 -3
  3. package/dist/{chunk-RYBBPFI7.js → chunk-2QXA52DG.js} +4 -4
  4. package/dist/{chunk-DMLDKW7K.js → chunk-37BNG4DT.js} +1 -1
  5. package/dist/{chunk-5WW4MBRO.js → chunk-3JTBOABA.js} +6 -2
  6. package/dist/{chunk-UV6HHFEW.js → chunk-4OABUPIU.js} +129 -68
  7. package/dist/{chunk-ZORQPB6B.js → chunk-53KS4E5N.js} +318 -359
  8. package/dist/{chunk-I5SD7XRA.js → chunk-6TI4WF5Y.js} +3 -3
  9. package/dist/{chunk-44TDRCKR.js → chunk-6XI2VF2O.js} +5 -5
  10. package/dist/{chunk-HBIIEYKW.js → chunk-7C54CKFZ.js} +4 -4
  11. package/dist/{chunk-IZ3JA2BR.js → chunk-7JP52QQO.js} +3 -3
  12. package/dist/{chunk-I45EUZC6.js → chunk-AY4GM6KQ.js} +4 -2
  13. package/dist/{chunk-SAPBDJJQ.js → chunk-AZSLSAJB.js} +2 -2
  14. package/dist/chunk-BITS7UJY.js +333 -0
  15. package/dist/{chunk-RNOUXUBO.js → chunk-BLFJMMBW.js} +306 -57
  16. package/dist/{chunk-YOZN35HD.js → chunk-CEVZAFPO.js} +3 -3
  17. package/dist/chunk-CI5OZ2QT.js +106 -0
  18. package/dist/{chunk-FNMS7OB7.js → chunk-CSE3PZOZ.js} +5 -5
  19. package/dist/chunk-CSZCWBOE.js +256 -0
  20. package/dist/{chunk-JIV4YHCZ.js → chunk-EJIWOD7W.js} +1 -1
  21. package/dist/{chunk-JZXU22PE.js → chunk-FEKKOBPX.js} +208 -117
  22. package/dist/chunk-GSM664TX.js +38 -0
  23. package/dist/{chunk-JYAR7D5U.js → chunk-HRHKQOOF.js} +2 -2
  24. package/dist/{chunk-CXW6KT6X.js → chunk-HXLQSO66.js} +3 -3
  25. package/dist/{chunk-O4IISNE7.js → chunk-IB2ZPUTF.js} +51 -12
  26. package/dist/{chunk-MPT7WJG2.js → chunk-IQXLNICY.js} +5 -5
  27. package/dist/{chunk-OQ52XMGG.js → chunk-K6RE23XX.js} +4 -4
  28. package/dist/{chunk-VY7MVBSE.js → chunk-KZ6U6FKB.js} +2 -2
  29. package/dist/{chunk-WPSK2CB5.js → chunk-LIZTK3HO.js} +16 -5
  30. package/dist/{chunk-Q4SWUINA.js → chunk-LKQJJSVJ.js} +1 -1
  31. package/dist/{chunk-E6ISNQK3.js → chunk-M6H3SHIS.js} +31 -8
  32. package/dist/chunk-MN4RZ2WP.js +380 -0
  33. package/dist/{chunk-GTGS2QRL.js → chunk-NA232CBX.js} +3 -3
  34. package/dist/{chunk-ZFNFPXPR.js → chunk-NAIWQQPW.js} +2 -2
  35. package/dist/{chunk-YVTV4ZV6.js → chunk-NWYYNE3Q.js} +1 -1
  36. package/dist/{chunk-DKVSM5HX.js → chunk-OUEJWC2Z.js} +6 -6
  37. package/dist/{chunk-DOL57OOP.js → chunk-OYDJHQNZ.js} +3 -3
  38. package/dist/{chunk-YWUDBLO6.js → chunk-QVS6K4FP.js} +2 -2
  39. package/dist/{chunk-N5RKM6IC.js → chunk-RU5U2YMT.js} +4 -4
  40. package/dist/{chunk-A6LAIGUS.js → chunk-V7BWMOHO.js} +7 -4
  41. package/dist/{chunk-2HLXCKK7.js → chunk-VPOOIEKT.js} +2 -2
  42. package/dist/{chunk-2YP5N4VV.js → chunk-VZOQRQAR.js} +1 -1
  43. package/dist/{chunk-GNA6SX2H.js → chunk-W4VA76QO.js} +2 -2
  44. package/dist/{chunk-BEM2YS7U.js → chunk-WEG3V7ZB.js} +3 -3
  45. package/dist/{chunk-XGFDY43I.js → chunk-WTIYILY3.js} +88 -18
  46. package/dist/{chunk-RWSH2MBU.js → chunk-X2NIRVLN.js} +73 -19
  47. package/dist/{chunk-AXGF4M57.js → chunk-XAXROQKB.js} +1 -1
  48. package/dist/{chunk-NIJUVTEW.js → chunk-XG22XKKD.js} +3 -3
  49. package/dist/{chunk-A5XQOEVH.js → chunk-YN7X7BEO.js} +4 -4
  50. package/dist/{chunk-KO6OEY3G.js → chunk-YNEEJCSZ.js} +3 -13
  51. package/dist/{chunk-7P7N43JV.js → chunk-YZTTFEL4.js} +2 -2
  52. package/dist/index.d.ts +418 -90
  53. package/dist/index.js +997 -123
  54. package/dist/{layout-C3nRXMwb.d.ts → layout-DmRikwZb.d.ts} +5 -4
  55. package/dist/meta.d.ts +1 -1
  56. package/dist/meta.js +236 -22
  57. package/dist/rich/ExcelUploadCard.d.ts +1 -1
  58. package/dist/rich/ExcelUploadCard.js +5 -4
  59. package/dist/rich.d.ts +21 -3
  60. package/dist/rich.js +277 -35
  61. package/dist/styles.css +102 -4
  62. package/dist/{types-COjGKgmd.d.ts → types-CYfIthux.d.ts} +57 -1
  63. package/dist/widgets/ApprovalLineEditor.d.ts +1 -1
  64. package/dist/widgets/ApprovalLineEditor.js +9 -8
  65. package/dist/widgets/ApprovalOverridesEditor.d.ts +1 -1
  66. package/dist/widgets/ApprovalOverridesEditor.js +11 -10
  67. package/dist/widgets/ApprovalStepper.d.ts +29 -4
  68. package/dist/widgets/ApprovalStepper.js +3 -3
  69. package/dist/widgets/AutocompleteField.d.ts +1 -1
  70. package/dist/widgets/AutocompleteField.js +9 -8
  71. package/dist/widgets/ColumnBoard.d.ts +1 -1
  72. package/dist/widgets/ColumnBoard.js +3 -3
  73. package/dist/widgets/DiffView.d.ts +1 -1
  74. package/dist/widgets/DiffView.js +3 -3
  75. package/dist/widgets/Disclosure.d.ts +1 -1
  76. package/dist/widgets/Disclosure.js +3 -3
  77. package/dist/widgets/EventCalendar.d.ts +5 -1
  78. package/dist/widgets/EventCalendar.js +5 -3
  79. package/dist/widgets/FileDropzone.d.ts +1 -1
  80. package/dist/widgets/FileDropzone.js +9 -7
  81. package/dist/widgets/ImageLightbox.d.ts +1 -1
  82. package/dist/widgets/ImageLightbox.js +7 -5
  83. package/dist/widgets/InlineEdit.d.ts +1 -1
  84. package/dist/widgets/InlineEdit.js +3 -3
  85. package/dist/widgets/JsonField.d.ts +1 -1
  86. package/dist/widgets/JsonField.js +3 -3
  87. package/dist/widgets/LiveClock.d.ts +1 -1
  88. package/dist/widgets/LiveClock.js +2 -2
  89. package/dist/widgets/OptionSchemaEditor.d.ts +1 -1
  90. package/dist/widgets/OptionSchemaEditor.js +4 -4
  91. package/dist/widgets/OptionValueFields.d.ts +1 -1
  92. package/dist/widgets/OptionValueFields.js +4 -4
  93. package/dist/widgets/OrgChart.d.ts +1 -1
  94. package/dist/widgets/OrgChart.js +4 -4
  95. package/dist/widgets/OrgChartBoard.d.ts +15 -33
  96. package/dist/widgets/OrgChartBoard.js +10 -8
  97. package/dist/widgets/PeriodFilter.d.ts +1 -1
  98. package/dist/widgets/PeriodFilter.js +6 -5
  99. package/dist/widgets/PermissionMatrixEditor.d.ts +34 -2
  100. package/dist/widgets/PermissionMatrixEditor.js +6 -4
  101. package/dist/widgets/PositionSelect.d.ts +1 -1
  102. package/dist/widgets/PositionSelect.js +5 -4
  103. package/dist/widgets/PrintSheet.d.ts +15 -2
  104. package/dist/widgets/PrintSheet.js +3 -3
  105. package/dist/widgets/ProfileEditDiffCard.d.ts +1 -1
  106. package/dist/widgets/ProfileEditDiffCard.js +5 -4
  107. package/dist/widgets/QRDisplay.d.ts +1 -1
  108. package/dist/widgets/QRDisplay.js +5 -4
  109. package/dist/widgets/RankBoard.d.ts +1 -1
  110. package/dist/widgets/RankBoard.js +8 -6
  111. package/dist/widgets/RecordSheet.d.ts +1 -1
  112. package/dist/widgets/RecordSheet.js +2 -2
  113. package/dist/widgets/RoleMemberPicker.d.ts +1 -1
  114. package/dist/widgets/RoleMemberPicker.js +6 -5
  115. package/dist/widgets/SearchableMultiPicker.d.ts +1 -1
  116. package/dist/widgets/SearchableMultiPicker.js +7 -6
  117. package/dist/widgets/SecureScreen.d.ts +1 -1
  118. package/dist/widgets/SecureScreen.js +3 -3
  119. package/dist/widgets/StackedMeter.d.ts +1 -1
  120. package/dist/widgets/StackedMeter.js +2 -2
  121. package/dist/widgets/StagePeriodEditor.d.ts +1 -1
  122. package/dist/widgets/StagePeriodEditor.js +6 -5
  123. package/dist/widgets/Strip.d.ts +1 -1
  124. package/dist/widgets/Strip.js +2 -2
  125. package/dist/widgets/SummaryTable.d.ts +1 -1
  126. package/dist/widgets/SummaryTable.js +2 -2
  127. package/dist/widgets/TagInput.d.ts +1 -1
  128. package/dist/widgets/TagInput.js +3 -3
  129. package/dist/widgets/TagMultiSelect.d.ts +1 -1
  130. package/dist/widgets/TagMultiSelect.js +5 -4
  131. package/dist/widgets/Timeline.d.ts +1 -1
  132. package/dist/widgets/Timeline.js +4 -4
  133. package/dist/widgets/TreePicker.d.ts +1 -1
  134. package/dist/widgets/TreePicker.js +8 -7
  135. package/dist/widgets/TreeView.d.ts +1 -1
  136. package/dist/widgets/TreeView.js +7 -6
  137. package/dist/widgets/index.d.ts +3 -3
  138. package/dist/widgets/index.js +84 -83
  139. package/package.json +1 -1
  140. package/dist/chunk-LFEJLOFN.js +0 -352
  141. package/dist/chunk-MMIGOPO5.js +0 -111
  142. package/dist/chunk-TFGNKFL2.js +0 -39
  143. package/dist/chunk-XQPTSEJC.js +0 -135
@@ -0,0 +1,256 @@
1
+ import { useRef, useState, useCallback, useEffect } from 'react';
2
+
3
+ // src/widgets/usePanZoomCanvas.ts
4
+ var MIN_ZOOM = 0.35;
5
+ var MAX_ZOOM = 1.8;
6
+ var PINCH_CLICK_GRACE_MS = 400;
7
+ var PAN_SLOP_PX = 6;
8
+ var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
9
+ function applyZoom(current, nextScale, focal) {
10
+ const scale = clamp(nextScale, MIN_ZOOM, MAX_ZOOM);
11
+ return {
12
+ scale,
13
+ x: focal.x - (focal.x - current.x) / current.scale * scale,
14
+ y: focal.y - (focal.y - current.y) / current.scale * scale
15
+ };
16
+ }
17
+ function applyPinch(start, startDistance, startMid, distance, mid) {
18
+ const ratio = startDistance > 0 ? distance / startDistance : 1;
19
+ const zoomed = applyZoom(start, start.scale * ratio, startMid);
20
+ return { ...zoomed, x: zoomed.x + mid.x - startMid.x, y: zoomed.y + mid.y - startMid.y };
21
+ }
22
+ function usePanZoomCanvas(options) {
23
+ const { stopSelector, initialView } = options;
24
+ const viewportRef = useRef(null);
25
+ const dragRef = useRef(null);
26
+ const interactedRef = useRef(false);
27
+ const pointersRef = useRef(/* @__PURE__ */ new Map());
28
+ const pinchRef = useRef(null);
29
+ const pendingRef = useRef(null);
30
+ const gestureEndedAtRef = useRef(0);
31
+ const dragFromStopRef = useRef(false);
32
+ const viewRef = useRef(initialView);
33
+ const [view, setViewState] = useState(initialView);
34
+ const setView = useCallback((next) => {
35
+ setViewState((current) => {
36
+ const resolved = typeof next === "function" ? next(current) : next;
37
+ viewRef.current = resolved;
38
+ return resolved;
39
+ });
40
+ }, []);
41
+ const [dragging, setDragging] = useState(false);
42
+ const [nativeFullscreen, setNativeFullscreen] = useState(false);
43
+ const [pseudoFullscreen, setPseudoFullscreen] = useState(false);
44
+ const isFullscreen = nativeFullscreen || pseudoFullscreen;
45
+ useEffect(() => {
46
+ const handleFullscreenChange = () => {
47
+ setNativeFullscreen(document.fullscreenElement === viewportRef.current);
48
+ };
49
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
50
+ return () => document.removeEventListener("fullscreenchange", handleFullscreenChange);
51
+ }, []);
52
+ useEffect(() => {
53
+ if (!pseudoFullscreen) return;
54
+ const onKey = (event) => {
55
+ if (event.key === "Escape") setPseudoFullscreen(false);
56
+ };
57
+ document.addEventListener("keydown", onKey);
58
+ return () => document.removeEventListener("keydown", onKey);
59
+ }, [pseudoFullscreen]);
60
+ useEffect(() => {
61
+ const viewport = viewportRef.current;
62
+ if (!viewport) return;
63
+ const onTouchMove = (event) => {
64
+ if (event.touches.length >= 2 && event.cancelable) event.preventDefault();
65
+ };
66
+ viewport.addEventListener("touchmove", onTouchMove, { passive: false });
67
+ return () => viewport.removeEventListener("touchmove", onTouchMove);
68
+ }, []);
69
+ useEffect(() => {
70
+ const viewport = viewportRef.current;
71
+ if (!viewport) return;
72
+ const onWheel = (event) => {
73
+ event.preventDefault();
74
+ interactedRef.current = true;
75
+ const rect = viewport.getBoundingClientRect();
76
+ const focal = { x: event.clientX - rect.left, y: event.clientY - rect.top };
77
+ setView(
78
+ (current) => applyZoom(current, current.scale * (event.deltaY > 0 ? 0.9 : 1.1), focal)
79
+ );
80
+ };
81
+ viewport.addEventListener("wheel", onWheel, { passive: false });
82
+ return () => viewport.removeEventListener("wheel", onWheel);
83
+ }, []);
84
+ const viewportCenter = useCallback(() => {
85
+ const rect = viewportRef.current?.getBoundingClientRect();
86
+ if (!rect) return { x: 0, y: 0 };
87
+ return { x: rect.width / 2, y: rect.height / 2 };
88
+ }, []);
89
+ const zoomAt = useCallback((nextScale, focal) => {
90
+ interactedRef.current = true;
91
+ setView((current) => applyZoom(current, nextScale, focal));
92
+ }, []);
93
+ const localPoint = (event) => {
94
+ const rect = event.currentTarget.getBoundingClientRect();
95
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
96
+ };
97
+ const pinchGeometry = () => {
98
+ const [a, b] = [...pointersRef.current.values()];
99
+ return {
100
+ distance: Math.hypot(b.x - a.x, b.y - a.y),
101
+ mid: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }
102
+ };
103
+ };
104
+ const restartPan = (pointerId, point, event) => {
105
+ const rect = event.currentTarget.getBoundingClientRect();
106
+ dragRef.current = {
107
+ pointerId,
108
+ startX: point.x + rect.left,
109
+ startY: point.y + rect.top,
110
+ originX: viewRef.current.x,
111
+ originY: viewRef.current.y
112
+ };
113
+ dragFromStopRef.current = true;
114
+ };
115
+ const capturePointer = (event, pointerId) => {
116
+ try {
117
+ event.currentTarget.setPointerCapture(pointerId);
118
+ } catch {
119
+ }
120
+ };
121
+ const startPan = (event, pointerId, start, fromStop) => {
122
+ interactedRef.current = true;
123
+ dragRef.current = {
124
+ pointerId,
125
+ startX: start.x,
126
+ startY: start.y,
127
+ originX: viewRef.current.x,
128
+ originY: viewRef.current.y
129
+ };
130
+ dragFromStopRef.current = fromStop;
131
+ setDragging(true);
132
+ capturePointer(event, pointerId);
133
+ };
134
+ const handlePointerDown = (event) => {
135
+ const onStop = Boolean(event.target.closest(stopSelector));
136
+ if (onStop && event.pointerType !== "touch") return;
137
+ pointersRef.current.set(event.pointerId, localPoint(event));
138
+ if (pointersRef.current.size === 2) {
139
+ interactedRef.current = true;
140
+ dragRef.current = null;
141
+ pendingRef.current = null;
142
+ const { distance, mid } = pinchGeometry();
143
+ pinchRef.current = { start: viewRef.current, distance, mid };
144
+ for (const pointerId of pointersRef.current.keys()) capturePointer(event, pointerId);
145
+ setDragging(true);
146
+ return;
147
+ }
148
+ if (pointersRef.current.size > 2) return;
149
+ if (onStop || event.pointerType === "touch" && !isFullscreen) {
150
+ pendingRef.current = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, fromStop: onStop };
151
+ return;
152
+ }
153
+ startPan(event, event.pointerId, { x: event.clientX, y: event.clientY }, false);
154
+ };
155
+ const handleClickCapture = (event) => {
156
+ const dragFromStop = dragRef.current !== null && dragFromStopRef.current;
157
+ if (pinchRef.current || dragFromStop || Date.now() - gestureEndedAtRef.current < PINCH_CLICK_GRACE_MS) {
158
+ event.preventDefault();
159
+ event.stopPropagation();
160
+ }
161
+ };
162
+ const handlePointerMove = (event) => {
163
+ if (!pointersRef.current.has(event.pointerId)) return;
164
+ pointersRef.current.set(event.pointerId, localPoint(event));
165
+ const pinch = pinchRef.current;
166
+ if (pinch && pointersRef.current.size >= 2) {
167
+ const { distance, mid } = pinchGeometry();
168
+ setView(applyPinch(pinch.start, pinch.distance, pinch.mid, distance, mid));
169
+ return;
170
+ }
171
+ const pending = pendingRef.current;
172
+ if (pending && pending.pointerId === event.pointerId) {
173
+ const dx = event.clientX - pending.startX;
174
+ const dy = event.clientY - pending.startY;
175
+ if (Math.hypot(dx, dy) < PAN_SLOP_PX) return;
176
+ pendingRef.current = null;
177
+ if (!isFullscreen && Math.abs(dy) > Math.abs(dx)) return;
178
+ startPan(event, event.pointerId, { x: pending.startX, y: pending.startY }, pending.fromStop);
179
+ }
180
+ const drag = dragRef.current;
181
+ if (!drag || drag.pointerId !== event.pointerId) return;
182
+ setView((current) => ({
183
+ ...current,
184
+ x: drag.originX + event.clientX - drag.startX,
185
+ y: drag.originY + event.clientY - drag.startY
186
+ }));
187
+ };
188
+ const handlePointerUp = (event) => {
189
+ if (!pointersRef.current.has(event.pointerId)) return;
190
+ pointersRef.current.delete(event.pointerId);
191
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
192
+ event.currentTarget.releasePointerCapture(event.pointerId);
193
+ }
194
+ if (pendingRef.current?.pointerId === event.pointerId) pendingRef.current = null;
195
+ if (pinchRef.current && pointersRef.current.size < 2) {
196
+ pinchRef.current = null;
197
+ gestureEndedAtRef.current = Date.now();
198
+ const rest = [...pointersRef.current.entries()][0];
199
+ if (rest) restartPan(rest[0], rest[1], event);
200
+ }
201
+ if (dragRef.current?.pointerId === event.pointerId) {
202
+ if (dragFromStopRef.current) gestureEndedAtRef.current = Date.now();
203
+ dragRef.current = null;
204
+ dragFromStopRef.current = false;
205
+ }
206
+ if (pointersRef.current.size === 0) {
207
+ dragRef.current = null;
208
+ dragFromStopRef.current = false;
209
+ setDragging(false);
210
+ }
211
+ };
212
+ const toggleFullscreen = async () => {
213
+ const viewport = viewportRef.current;
214
+ if (!viewport) return;
215
+ if (pseudoFullscreen) {
216
+ setPseudoFullscreen(false);
217
+ return;
218
+ }
219
+ if (document.fullscreenElement === viewport) {
220
+ try {
221
+ await document.exitFullscreen();
222
+ } catch {
223
+ }
224
+ return;
225
+ }
226
+ try {
227
+ if (typeof viewport.requestFullscreen !== "function") throw new Error("unsupported");
228
+ await viewport.requestFullscreen();
229
+ } catch {
230
+ setPseudoFullscreen(true);
231
+ }
232
+ };
233
+ return {
234
+ viewportRef,
235
+ view,
236
+ setView,
237
+ dragging,
238
+ isFullscreen,
239
+ /** 브라우저 전체화면 대신 화면을 덮는 고정 배치로 들어가 있는가(위젯이 배치 클래스를 붙인다). */
240
+ pseudoFullscreen,
241
+ interactedRef,
242
+ viewportCenter,
243
+ zoomAt,
244
+ toggleFullscreen,
245
+ /** 뷰포트 div에 그대로 spread — 패닝 4종 + 핀치 직후 click 차단. */
246
+ pointerHandlers: {
247
+ onPointerDown: handlePointerDown,
248
+ onPointerMove: handlePointerMove,
249
+ onPointerUp: handlePointerUp,
250
+ onPointerCancel: handlePointerUp,
251
+ onClickCapture: handleClickCapture
252
+ }
253
+ };
254
+ }
255
+
256
+ export { MAX_ZOOM, MIN_ZOOM, usePanZoomCanvas };
@@ -1,4 +1,4 @@
1
- import { boxed } from './chunk-5WW4MBRO.js';
1
+ import { boxed } from './chunk-3JTBOABA.js';
2
2
  import { jsxs, jsx } from 'react/jsx-runtime';
3
3
 
4
4
  var resolveColor = (color, index) => {
@@ -1,10 +1,10 @@
1
1
  import { parseJobPositionRow } from './chunk-XRHR452N.js';
2
- import { usePanZoomCanvas } from './chunk-MMIGOPO5.js';
3
- import { ImageLightbox } from './chunk-VY7MVBSE.js';
4
- import { useSduiAdapter, hostFetchOf, defaultAdapter } from './chunk-KO6OEY3G.js';
5
- import { warnOnce, Badge, Button, IconButton, Icon } from './chunk-RNOUXUBO.js';
6
- import { boxed } from './chunk-5WW4MBRO.js';
7
- import { useState, useMemo, useRef, useCallback, useEffect } from 'react';
2
+ import { usePanZoomCanvas, MIN_ZOOM } from './chunk-CSZCWBOE.js';
3
+ import { ImageLightbox } from './chunk-KZ6U6FKB.js';
4
+ import { useSduiAdapter, hostFetchOf, defaultAdapter } from './chunk-YNEEJCSZ.js';
5
+ import { warnOnce, Badge, Button, IconButton, Icon } from './chunk-BLFJMMBW.js';
6
+ import { boxed } from './chunk-3JTBOABA.js';
7
+ import { useState, useMemo, useRef, useCallback, useEffect, useLayoutEffect } from 'react';
8
8
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
9
 
10
10
  var PAGE_SIZE = 500;
@@ -42,6 +42,25 @@ var LEADER_HEIGHT = 62;
42
42
  var TOP_GAP = 58;
43
43
  var ROOT_ROW_GAP = 70;
44
44
  var DEFAULT_VIEW = { scale: 0.82, x: 48, y: 48 };
45
+ var FIT_PAD = 16;
46
+ function fitCanvasView(viewportWidth, viewportHeight, layoutWidth, layoutHeight) {
47
+ if (!(viewportWidth > 0) || !(viewportHeight > 0) || !(layoutWidth > 0) || !(layoutHeight > 0)) {
48
+ return DEFAULT_VIEW;
49
+ }
50
+ const raw = Math.min(
51
+ (viewportWidth - FIT_PAD * 2) / layoutWidth,
52
+ (viewportHeight - FIT_PAD * 2) / layoutHeight,
53
+ 1
54
+ );
55
+ const scale = Math.max(MIN_ZOOM, raw);
56
+ const width = layoutWidth * scale;
57
+ const height = layoutHeight * scale;
58
+ return {
59
+ scale,
60
+ x: (viewportWidth - width) / 2,
61
+ y: height <= viewportHeight - FIT_PAD * 2 ? (viewportHeight - height) / 2 : FIT_PAD
62
+ };
63
+ }
45
64
  var DUTY_ORDER = [
46
65
  "\uD68C\uC7A5",
47
66
  "\uBD80\uD68C\uC7A5",
@@ -108,6 +127,14 @@ var OCB_CSS = `
108
127
  .erp-ocb-member:hover { border-color: var(--wp-border-brand); background: var(--wp-surface-hover); }
109
128
  .erp-ocb-group { transition: opacity .15s ease; }
110
129
  .erp-ocb-group:hover { opacity: .9; }
130
+ .erp-ocb-viewport { position: relative; height: var(--erp-ocb-h, 72vh); min-height: 620px; overflow: hidden; border-top: var(--wp-border-w) solid var(--wp-border-default); background: var(--wp-surface-page); user-select: none; cursor: grab; touch-action: pan-y; }
131
+ .erp-ocb-viewport--dragging { cursor: grabbing; }
132
+ .erp-ocb-viewport.erp-ocb-viewport--pseudo-fs { position: fixed; inset: 0; z-index: var(--wp-z-dialog); height: 100dvh; min-height: 0; border-top: 0; }
133
+ /* \uC804\uCCB4\uD654\uBA74(\uBE0C\uB77C\uC6B0\uC800\xB7\uB300\uCCB4)\uC5D0\uC11C\uB294 \uC591\uBCF4\uD560 \uD398\uC774\uC9C0\uAC00 \uC5C6\uB2E4 \u2014 \uD55C \uC190\uAC00\uB77D\uB3C4 \uC5B4\uB290 \uBC29\uD5A5\uC774\uB4E0 \uCE94\uBC84\uC2A4\uAC00 \uAC16\uB294\uB2E4. */
134
+ .erp-ocb-viewport.erp-ocb-viewport--pseudo-fs, .erp-ocb-viewport:fullscreen { touch-action: none; }
135
+ @media screen and (max-width: 767.98px) {
136
+ .erp-ocb-viewport { min-height: 320px; height: min(var(--erp-ocb-h, 72vh), 68dvh); }
137
+ }
111
138
  `;
112
139
  async function fetchAllPages(api, endpoint, pick) {
113
140
  const all = [];
@@ -915,6 +942,8 @@ function OrganizationCanvas({
915
942
  setView,
916
943
  dragging,
917
944
  isFullscreen,
945
+ pseudoFullscreen,
946
+ interactedRef,
918
947
  viewportCenter,
919
948
  zoomAt,
920
949
  toggleFullscreen,
@@ -924,118 +953,180 @@ function OrganizationCanvas({
924
953
  () => buildCanvasLayout(model, employees, leadership),
925
954
  [model, employees, leadership]
926
955
  );
956
+ const fitView = useCallback(() => {
957
+ const el = viewportRef.current;
958
+ if (!el) return;
959
+ interactedRef.current = false;
960
+ setView(fitCanvasView(el.clientWidth, el.clientHeight, layout.width, layout.height));
961
+ }, [layout.width, layout.height, setView, viewportRef, interactedRef]);
962
+ useLayoutEffect(() => {
963
+ fitView();
964
+ }, [fitView]);
927
965
  useEffect(() => {
928
- setView(DEFAULT_VIEW);
929
- }, [layout.width, layout.height, setView]);
930
- const zoomStep = (delta) => zoomAt(view.scale + delta, viewportCenter());
931
- return /* @__PURE__ */ jsxs(
932
- "div",
933
- {
934
- ref: viewportRef,
935
- style: {
936
- position: "relative",
937
- height,
938
- minHeight: 620,
939
- overflow: "hidden",
940
- borderTop: "var(--wp-border-w) solid var(--wp-border-default)",
941
- background: "var(--wp-surface-page)",
942
- userSelect: "none",
943
- cursor: dragging ? "grabbing" : "grab",
944
- touchAction: "none"
945
- },
946
- ...pointerHandlers,
947
- children: [
948
- /* @__PURE__ */ jsxs(
949
- "div",
950
- {
951
- "data-org-card": true,
952
- style: {
953
- position: "absolute",
954
- right: 12,
955
- top: 12,
956
- zIndex: 30,
957
- display: "flex",
958
- alignItems: "center",
959
- gap: 4,
960
- borderRadius: "var(--wp-radius-md)",
961
- border: "var(--wp-border-w) solid var(--wp-border-default)",
962
- background: "var(--wp-surface-card)",
963
- padding: 4,
964
- boxShadow: "var(--wp-shadow-sm)"
965
- },
966
- children: [
967
- /* @__PURE__ */ jsx(IconButton, { icon: "minus", label: "\uCD95\uC18C", size: "sm", onClick: () => zoomStep(-0.1) }),
968
- /* @__PURE__ */ jsxs("div", { style: { width: 48, textAlign: "center", fontSize: 12, fontWeight: 500 }, children: [
969
- Math.round(view.scale * 100),
970
- "%"
971
- ] }),
972
- /* @__PURE__ */ jsx(IconButton, { icon: "plus", label: "\uD655\uB300", size: "sm", onClick: () => zoomStep(0.1) }),
973
- /* @__PURE__ */ jsx(
974
- IconButton,
975
- {
976
- icon: isFullscreen ? "minimize" : "maximize",
977
- label: isFullscreen ? "\uC804\uCCB4\uD654\uBA74 \uD574\uC81C" : "\uC804\uCCB4\uD654\uBA74",
978
- size: "sm",
979
- onClick: () => void toggleFullscreen()
980
- }
981
- )
982
- ]
983
- }
984
- ),
985
- /* @__PURE__ */ jsxs(
986
- "div",
987
- {
988
- style: {
989
- position: "absolute",
990
- left: 0,
991
- top: 0,
992
- width: layout.width,
993
- height: layout.height,
994
- transform: `translate(${view.x}px, ${view.y}px) scale(${view.scale})`,
995
- transformOrigin: "0 0"
996
- },
997
- children: [
998
- /* @__PURE__ */ jsx(
999
- "svg",
1000
- {
1001
- style: { position: "absolute", left: 0, top: 0, pointerEvents: "none" },
1002
- width: layout.width,
1003
- height: layout.height,
1004
- children: layout.connectors.map((connector, index) => /* @__PURE__ */ jsx(
1005
- "path",
1006
- {
1007
- d: connectorPath(connector),
1008
- fill: "none",
1009
- stroke: "var(--wp-border-default)",
1010
- strokeWidth: 2
1011
- },
1012
- index
1013
- ))
1014
- }
1015
- ),
1016
- layout.groups.map((item) => /* @__PURE__ */ jsx(
1017
- CanvasGroupNode,
1018
- {
1019
- item,
1020
- selected: selectedGroupId === item.group.id,
1021
- onSelect
1022
- },
1023
- item.group.id
1024
- )),
1025
- layout.employees.map((item) => /* @__PURE__ */ jsx(
1026
- CanvasEmployeeNode,
1027
- {
1028
- item,
1029
- onOpenCard
1030
- },
1031
- `${item.featured ? "leader" : "employee"}-${item.employee.id}`
1032
- ))
1033
- ]
1034
- }
1035
- )
1036
- ]
966
+ const el = viewportRef.current;
967
+ if (!el || typeof ResizeObserver === "undefined") return;
968
+ let last = `${el.clientWidth}x${el.clientHeight}`;
969
+ const observer = new ResizeObserver(() => {
970
+ const size = `${el.clientWidth}x${el.clientHeight}`;
971
+ if (size === last) return;
972
+ last = size;
973
+ if (!interactedRef.current) fitView();
974
+ });
975
+ observer.observe(el);
976
+ return () => observer.disconnect();
977
+ }, [fitView, viewportRef, interactedRef]);
978
+ const savedScrollRef = useRef([]);
979
+ const rememberScroll = () => {
980
+ const saved = [];
981
+ let node = viewportRef.current?.parentElement ?? null;
982
+ while (node) {
983
+ if (node.scrollTop > 0) saved.push([node, node.scrollTop]);
984
+ node = node.parentElement;
1037
985
  }
1038
- );
986
+ if (typeof window !== "undefined" && window.scrollY > 0) {
987
+ saved.push([document.scrollingElement ?? document.documentElement, window.scrollY]);
988
+ }
989
+ savedScrollRef.current = saved;
990
+ };
991
+ const fullscreenSeen = useRef(isFullscreen);
992
+ useEffect(() => {
993
+ if (fullscreenSeen.current === isFullscreen) return;
994
+ fullscreenSeen.current = isFullscreen;
995
+ const restore = () => {
996
+ for (const [el, top] of savedScrollRef.current) el.scrollTop = top;
997
+ };
998
+ const frame = window.requestAnimationFrame(() => {
999
+ fitView();
1000
+ if (!isFullscreen) restore();
1001
+ });
1002
+ const timer = isFullscreen ? 0 : window.setTimeout(() => {
1003
+ restore();
1004
+ savedScrollRef.current = [];
1005
+ }, 200);
1006
+ return () => {
1007
+ window.cancelAnimationFrame(frame);
1008
+ if (timer) window.clearTimeout(timer);
1009
+ };
1010
+ }, [isFullscreen, fitView]);
1011
+ const zoomStep = (delta) => zoomAt(view.scale + delta, viewportCenter());
1012
+ const flowHeightRef = useRef(0);
1013
+ const viewportClass = [
1014
+ "erp-ocb-viewport",
1015
+ dragging ? "erp-ocb-viewport--dragging" : "",
1016
+ pseudoFullscreen ? "erp-ocb-viewport--pseudo-fs" : ""
1017
+ ].filter(Boolean).join(" ");
1018
+ const viewportStyle = {
1019
+ "--erp-ocb-h": typeof height === "number" ? `${height}px` : height
1020
+ };
1021
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1022
+ pseudoFullscreen ? /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "erp-ocb-placeholder", style: { height: flowHeightRef.current } }) : null,
1023
+ /* @__PURE__ */ jsxs(
1024
+ "div",
1025
+ {
1026
+ ref: viewportRef,
1027
+ className: viewportClass,
1028
+ style: viewportStyle,
1029
+ ...pointerHandlers,
1030
+ children: [
1031
+ /* @__PURE__ */ jsxs(
1032
+ "div",
1033
+ {
1034
+ "data-org-card": true,
1035
+ style: {
1036
+ position: "absolute",
1037
+ right: 12,
1038
+ top: 12,
1039
+ zIndex: 30,
1040
+ display: "flex",
1041
+ alignItems: "center",
1042
+ gap: 4,
1043
+ borderRadius: "var(--wp-radius-md)",
1044
+ border: "var(--wp-border-w) solid var(--wp-border-default)",
1045
+ background: "var(--wp-surface-card)",
1046
+ padding: 4,
1047
+ boxShadow: "var(--wp-shadow-sm)"
1048
+ },
1049
+ children: [
1050
+ /* @__PURE__ */ jsx(IconButton, { icon: "minus", label: "\uCD95\uC18C", size: "sm", onClick: () => zoomStep(-0.1) }),
1051
+ /* @__PURE__ */ jsxs("div", { style: { width: 48, textAlign: "center", fontSize: 12, fontWeight: 500 }, children: [
1052
+ Math.round(view.scale * 100),
1053
+ "%"
1054
+ ] }),
1055
+ /* @__PURE__ */ jsx(IconButton, { icon: "plus", label: "\uD655\uB300", size: "sm", onClick: () => zoomStep(0.1) }),
1056
+ /* @__PURE__ */ jsx(IconButton, { icon: "locate-fixed", label: "\uD654\uBA74\uC5D0 \uB9DE\uCDA4", size: "sm", onClick: fitView }),
1057
+ /* @__PURE__ */ jsx(
1058
+ IconButton,
1059
+ {
1060
+ icon: isFullscreen ? "minimize" : "maximize",
1061
+ label: isFullscreen ? "\uC804\uCCB4\uD654\uBA74 \uD574\uC81C" : "\uC804\uCCB4\uD654\uBA74",
1062
+ size: "sm",
1063
+ onClick: () => {
1064
+ if (!isFullscreen && viewportRef.current) {
1065
+ flowHeightRef.current = viewportRef.current.offsetHeight;
1066
+ rememberScroll();
1067
+ }
1068
+ void toggleFullscreen();
1069
+ }
1070
+ }
1071
+ )
1072
+ ]
1073
+ }
1074
+ ),
1075
+ /* @__PURE__ */ jsxs(
1076
+ "div",
1077
+ {
1078
+ style: {
1079
+ position: "absolute",
1080
+ left: 0,
1081
+ top: 0,
1082
+ width: layout.width,
1083
+ height: layout.height,
1084
+ transform: `translate(${view.x}px, ${view.y}px) scale(${view.scale})`,
1085
+ transformOrigin: "0 0"
1086
+ },
1087
+ children: [
1088
+ /* @__PURE__ */ jsx(
1089
+ "svg",
1090
+ {
1091
+ style: { position: "absolute", left: 0, top: 0, pointerEvents: "none" },
1092
+ width: layout.width,
1093
+ height: layout.height,
1094
+ children: layout.connectors.map((connector, index) => /* @__PURE__ */ jsx(
1095
+ "path",
1096
+ {
1097
+ d: connectorPath(connector),
1098
+ fill: "none",
1099
+ stroke: "var(--wp-border-default)",
1100
+ strokeWidth: 2
1101
+ },
1102
+ index
1103
+ ))
1104
+ }
1105
+ ),
1106
+ layout.groups.map((item) => /* @__PURE__ */ jsx(
1107
+ CanvasGroupNode,
1108
+ {
1109
+ item,
1110
+ selected: selectedGroupId === item.group.id,
1111
+ onSelect
1112
+ },
1113
+ item.group.id
1114
+ )),
1115
+ layout.employees.map((item) => /* @__PURE__ */ jsx(
1116
+ CanvasEmployeeNode,
1117
+ {
1118
+ item,
1119
+ onOpenCard
1120
+ },
1121
+ `${item.featured ? "leader" : "employee"}-${item.employee.id}`
1122
+ ))
1123
+ ]
1124
+ }
1125
+ )
1126
+ ]
1127
+ }
1128
+ )
1129
+ ] });
1039
1130
  }
1040
1131
  function OrgChartBoardImpl({
1041
1132
  onMemberClick,
@@ -1237,4 +1328,4 @@ function OrgChartBoardImpl({
1237
1328
  }
1238
1329
  var OrgChartBoard = boxed(OrgChartBoardImpl, "OrgChartBoard");
1239
1330
 
1240
- export { OrgChartBoard, buildModel, compareMembers, isUnassignedGroup, leadershipEmployees, orgVocabOf, pickEmployees, pickGroups, pickPositions };
1331
+ export { OrgChartBoard, buildModel, compareMembers, fitCanvasView, isUnassignedGroup, leadershipEmployees, orgVocabOf, pickEmployees, pickGroups, pickPositions };
@@ -0,0 +1,38 @@
1
+ import { useSyncExternalStore } from 'react';
2
+
3
+ // src/env.ts
4
+ var DEV = (() => {
5
+ try {
6
+ const meta = import.meta;
7
+ return Boolean(meta.env?.DEV);
8
+ } catch {
9
+ return false;
10
+ }
11
+ })();
12
+ var MOBILE_MEDIA_QUERY = "screen and (max-width: 767.98px)";
13
+ function mobileQueryList() {
14
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return null;
15
+ return window.matchMedia(MOBILE_MEDIA_QUERY);
16
+ }
17
+ function subscribeMobile(onChange) {
18
+ const list = mobileQueryList();
19
+ if (!list) return () => {
20
+ };
21
+ if (typeof list.addEventListener === "function") {
22
+ list.addEventListener("change", onChange);
23
+ return () => list.removeEventListener("change", onChange);
24
+ }
25
+ list.addListener(onChange);
26
+ return () => list.removeListener(onChange);
27
+ }
28
+ function mobileSnapshot() {
29
+ return mobileQueryList()?.matches ?? false;
30
+ }
31
+ function mobileServerSnapshot() {
32
+ return false;
33
+ }
34
+ function useIsMobile() {
35
+ return useSyncExternalStore(subscribeMobile, mobileSnapshot, mobileServerSnapshot);
36
+ }
37
+
38
+ export { DEV, MOBILE_MEDIA_QUERY, useIsMobile };
@@ -1,5 +1,5 @@
1
- import { Icon, Badge } from './chunk-RNOUXUBO.js';
2
- import { boxed } from './chunk-5WW4MBRO.js';
1
+ import { Icon, Badge } from './chunk-BLFJMMBW.js';
2
+ import { boxed } from './chunk-3JTBOABA.js';
3
3
  import React from 'react';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
 
@@ -1,6 +1,6 @@
1
- import { Text } from './chunk-WPSK2CB5.js';
2
- import { Field, Input, Select, IconButton, Button } from './chunk-RNOUXUBO.js';
3
- import { boxed } from './chunk-5WW4MBRO.js';
1
+ import { Text } from './chunk-LIZTK3HO.js';
2
+ import { Field, Input, Select, IconButton, Button } from './chunk-BLFJMMBW.js';
3
+ import { boxed } from './chunk-3JTBOABA.js';
4
4
  import { useMemo } from 'react';
5
5
  import { jsxs, jsx } from 'react/jsx-runtime';
6
6