@bitstack/ng-boundary 14.0.6-alpha.2 → 14.0.6-alpha.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.
@@ -1,7 +1,7 @@
1
+ import { BreakpointObserver } from '@angular/cdk/layout';
1
2
  import * as i0 from '@angular/core';
2
3
  import { Injectable, EventEmitter, inject, ElementRef, Component, Input, Output } from '@angular/core';
3
- import { BreakpointObserver } from '@angular/cdk/layout';
4
- import { BehaviorSubject, Subject, takeUntil, fromEvent, debounceTime } from 'rxjs';
4
+ import { BehaviorSubject, Subject, takeUntil, fromEvent } from 'rxjs';
5
5
 
6
6
  class BsBoundaryContextService {
7
7
  constructor() {
@@ -21,115 +21,300 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImpor
21
21
  type: Injectable
22
22
  }] });
23
23
 
24
+ /**
25
+ * ============================================================
26
+ * BsBoundary Component - AWD 自適應邊界容器
27
+ * ============================================================
28
+ *
29
+ * 功能說明:
30
+ * 1. 提供響應式邊界容器,自動適應不同設備尺寸
31
+ * 2. 支援 5 種方向模式:auto, portrait, landscape, portraitLockRotation, landscapeLockRotation
32
+ * 3. 使用 CSS 變數實現動態計算,支援 px2vw() 函數
33
+ * 4. 相容 Chrome 74+,使用 JavaScript 計算邊界尺寸
34
+ *
35
+ * 使用範例:
36
+ * ```html
37
+ * <bs-boundary [orientation]="'auto'" [isFixedCenter]="true">
38
+ * <div class="content">...</div>
39
+ * </bs-boundary>
40
+ * ```
41
+ *
42
+ * CSS 變數設定:
43
+ * ```scss
44
+ * bs-boundary {
45
+ * --design-width: 375; // 設計稿寬度
46
+ * --design-height: 667; // 設計稿高度
47
+ * }
48
+ * ```
49
+ */
24
50
  class BsBoundary {
25
51
  constructor() {
52
+ // ============================================================
53
+ // Inputs & Outputs
54
+ // ============================================================
55
+ /**
56
+ * 是否將內容固定在視窗中央
57
+ * @default false
58
+ */
26
59
  this.isFixedCenter = false;
60
+ /**
61
+ * 是否禁用指標事件
62
+ * @default false
63
+ */
27
64
  this.pointerEventsNone = false;
28
- this.lockOrientation = 'auto';
65
+ /**
66
+ * 方向模式
67
+ * - auto: 自動檢測方向
68
+ * - portrait: 鎖定直向
69
+ * - landscape: 鎖定橫向
70
+ * - portraitLockRotation: 強制直向(橫向設備時旋轉 -90°)
71
+ * - landscapeLockRotation: 強制橫向(直向設備時旋轉 90°)
72
+ * @default 'auto'
73
+ */
74
+ this.orientation = 'auto';
75
+ /**
76
+ * 方向變化事件
77
+ * @emits true: landscape(橫屏)
78
+ * @emits false: portrait(直屏)
79
+ */
29
80
  this.orientationChange = new EventEmitter();
81
+ // ============================================================
82
+ // Private Dependencies
83
+ // ============================================================
30
84
  this.breakpointObserver = inject(BreakpointObserver);
31
85
  this.boundaryContext = inject(BsBoundaryContextService);
32
86
  this.elementRef = inject(ElementRef);
33
87
  this.destroy$ = new Subject();
88
+ // ============================================================
89
+ // Public Properties
90
+ // ============================================================
91
+ /**
92
+ * 實際設備方向(基於視口尺寸)
93
+ * true: 直向 (height > width)
94
+ * false: 橫向 (width >= height)
95
+ */
96
+ this.actualIsPortrait = false;
34
97
  }
98
+ // ============================================================
99
+ // Getters
100
+ // ============================================================
101
+ /**
102
+ * 判斷當前是否為橫向模式
103
+ * 根據 orientation 設定和實際設備方向判斷
104
+ */
35
105
  get isLandscape() {
106
+ // landscapeLockRotation: 強制橫屏模式
107
+ if (this.orientation === 'landscapeLockRotation') {
108
+ return true;
109
+ }
110
+ // portraitLockRotation: 強制直屏模式
111
+ if (this.orientation === 'portraitLockRotation') {
112
+ return false;
113
+ }
114
+ // landscape: 鎖定橫向
115
+ if (this.orientation === 'landscape') {
116
+ return true;
117
+ }
118
+ // portrait: 鎖定直向
119
+ if (this.orientation === 'portrait') {
120
+ return false;
121
+ }
122
+ // auto: 自動檢測
36
123
  return this.boundaryContext.getIsLandscape();
37
124
  }
125
+ // ============================================================
126
+ // Lifecycle Hooks
127
+ // ============================================================
38
128
  ngOnInit() {
129
+ this.updateActualOrientation();
39
130
  this.setupBreakpointObserver();
40
131
  this.setupResizeObserver();
41
- // 延遲執行以確保 CSS 變數已經設定
42
- setTimeout(() => this.updateBoundaryCalculations(), 0);
132
+ }
133
+ ngAfterViewInit() {
134
+ // 在 view 初始化後計算(確保 CSS 已完全應用)
135
+ this.updateBoundaryCalculations();
43
136
  }
44
137
  ngOnDestroy() {
45
138
  this.destroy$.next();
46
139
  this.destroy$.complete();
47
140
  }
141
+ // ============================================================
142
+ // Observers Setup - 監聽器設定
143
+ // ============================================================
48
144
  /**
49
- * 監控方向變化
145
+ * 設定方向變化監控
146
+ *
147
+ * 工作原理:
148
+ * - lock 模式(portrait/landscape):直接設定固定方向,不監聽變化
149
+ * - lockRotation 模式:設定固定方向,但需監聽實際設備方向以觸發旋轉
150
+ * - auto 模式:使用 BreakpointObserver 監聽設備方向變化
50
151
  */
51
152
  setupBreakpointObserver() {
52
- if (this.lockOrientation === 'landscape') {
153
+ // landscape landscapeLockRotation: 鎖定為橫向
154
+ if (this.orientation === 'landscape' || this.orientation === 'landscapeLockRotation') {
53
155
  this.boundaryContext.setIsLandscape(true);
54
156
  this.orientationChange.emit(true);
55
- // 鎖定方向後也需要更新計算
56
- setTimeout(() => this.updateBoundaryCalculations(), 0);
57
157
  return;
58
158
  }
59
- if (this.lockOrientation === 'portrait') {
159
+ // portrait portraitLockRotation: 鎖定為直向
160
+ if (this.orientation === 'portrait' || this.orientation === 'portraitLockRotation') {
60
161
  this.boundaryContext.setIsLandscape(false);
61
162
  this.orientationChange.emit(false);
62
- // 鎖定方向後也需要更新計算
63
- setTimeout(() => this.updateBoundaryCalculations(), 0);
64
163
  return;
65
164
  }
165
+ // auto 模式:監聽實際的設備方向(用於 orientationChange 事件)
66
166
  this.breakpointObserver
67
167
  .observe('(orientation: landscape)')
68
168
  .pipe(takeUntil(this.destroy$))
69
169
  .subscribe(result => {
70
170
  this.boundaryContext.setIsLandscape(result.matches);
71
171
  this.orientationChange.emit(result.matches);
172
+ // 方向變化時重新計算邊界
72
173
  this.updateBoundaryCalculations();
73
174
  });
74
175
  }
75
176
  /**
76
- * 監控視口大小變化
177
+ * 設定視窗大小變化監控
178
+ *
179
+ * 功能:
180
+ * - 監聽 window resize 事件
181
+ * - 更新實際設備方向
182
+ * - 重新計算邊界尺寸
77
183
  */
78
184
  setupResizeObserver() {
79
185
  fromEvent(window, 'resize')
80
- .pipe(debounceTime(100), takeUntil(this.destroy$))
186
+ .pipe(
187
+ // debounceTime(150), // 可選:防抖優化性能
188
+ takeUntil(this.destroy$))
81
189
  .subscribe(() => {
190
+ this.updateActualOrientation();
82
191
  this.updateBoundaryCalculations();
83
192
  });
84
193
  }
194
+ // ============================================================
195
+ // Calculation Methods - 計算方法
196
+ // ============================================================
197
+ /**
198
+ * 更新實際設備方向(基於視口尺寸)
199
+ *
200
+ * 工作原理:
201
+ * - 只在 lockRotation 模式下需要檢測實際方向
202
+ * - 通過比較 window.innerHeight 和 window.innerWidth 判斷
203
+ * - 結果用於觸發 CSS 旋轉變換
204
+ */
205
+ updateActualOrientation() {
206
+ // 只有在 lockRotation 模式下才需要檢測實際方向
207
+ if (this.orientation !== 'landscapeLockRotation' && this.orientation !== 'portraitLockRotation') {
208
+ this.actualIsPortrait = false;
209
+ return;
210
+ }
211
+ // 檢測實際視口方向:高度 > 寬度 = 直向
212
+ this.actualIsPortrait = window.innerHeight > window.innerWidth;
213
+ }
85
214
  /**
86
- * 更新邊界計算(支援 Chrome 74,不使用 CSS min/max 函數)
215
+ * 更新邊界計算(支援 Chrome 74,使用 JavaScript 模擬 min 函數)
216
+ *
217
+ * 工作原理:
218
+ * 1. 讀取基礎 CSS 變數(vw100, vh100, design-width, design-height)
219
+ * 2. 處理 lockRotation 模式的視口尺寸交換
220
+ * 3. 計算邊界最大值和實際邊界尺寸
221
+ * 4. 計算 px2vw 轉換比例
222
+ * 5. 設定 CSS 變數供樣式使用
223
+ *
224
+ * 設定的 CSS 變數:
225
+ * - --boundary-width: 實際邊界寬度(px)
226
+ * - --boundary-height: 實際邊界高度(px)
227
+ * - --px2vw-ratio: px 轉 vw 的比例係數
87
228
  */
88
229
  updateBoundaryCalculations() {
89
230
  const hostElement = this.elementRef.nativeElement;
90
231
  const computedStyle = getComputedStyle(hostElement);
91
- // 讀取 CSS 變數
92
- const designWidth = parseFloat(computedStyle.getPropertyValue('--design-width') || '1920');
93
- const designHeight = parseFloat(computedStyle.getPropertyValue('--design-height') || '1080');
94
- const vw100 = parseFloat(computedStyle.getPropertyValue('--vw100') || `${window.innerWidth}`);
95
- const vh100 = parseFloat(computedStyle.getPropertyValue('--vh100') || `${window.innerHeight}`);
96
- // 計算比例
232
+ // ========================================
233
+ // Step 1: 讀取基礎 CSS 變數
234
+ // ========================================
235
+ let vw100 = parseFloat(computedStyle.getPropertyValue('--vw100')) || window.innerWidth;
236
+ let vh100 = parseFloat(computedStyle.getPropertyValue('--vh100')) || window.innerHeight;
237
+ const designWidth = parseFloat(computedStyle.getPropertyValue('--design-width'));
238
+ const designHeight = parseFloat(computedStyle.getPropertyValue('--design-height'));
239
+ // 檢查基礎變數是否準備好
240
+ if (isNaN(designWidth)) {
241
+ console.error('[BsBoundary][BS001] CSS variable --design-width is not set or invalid');
242
+ return;
243
+ }
244
+ if (isNaN(designHeight)) {
245
+ console.error('[BsBoundary][BS002] CSS variable --design-height is not set or invalid');
246
+ return;
247
+ }
248
+ if (designWidth === 0) {
249
+ console.error('[BsBoundary][BS003] CSS variable --design-width cannot be zero');
250
+ return;
251
+ }
252
+ if (designHeight === 0) {
253
+ console.error('[BsBoundary][BS004] CSS variable --design-height cannot be zero');
254
+ return;
255
+ }
256
+ // ========================================
257
+ // Step 2: 處理 lockRotation 模式的視口尺寸交換
258
+ // ========================================
259
+ // landscapeLockRotation + 實際直向:交換視口尺寸
260
+ // 原因:旋轉後,內容的寬度對應設備的高度,內容的高度對應設備的寬度
261
+ if (this.orientation === 'landscapeLockRotation' && this.actualIsPortrait) {
262
+ [vw100, vh100] = [vh100, vw100];
263
+ }
264
+ // portraitLockRotation + 實際橫向:交換視口尺寸
265
+ if (this.orientation === 'portraitLockRotation' && !this.actualIsPortrait) {
266
+ [vw100, vh100] = [vh100, vw100];
267
+ }
268
+ // ========================================
269
+ // Step 3: 計算比例和邊界最大值
270
+ // ========================================
97
271
  const landscapeRatio = designWidth / designHeight;
98
272
  const portraitRatio = designHeight / designWidth;
99
- // 根據方向計算邊界最大值
273
+ // 根據方向模式計算邊界最大值
100
274
  const isLandscape = this.isLandscape;
101
275
  const boundaryMaxWidth = isLandscape
102
- ? vh100 * portraitRatio
103
- : vh100 * landscapeRatio;
276
+ ? vh100 * portraitRatio // 橫向:高度 * 高寬比
277
+ : vh100 * landscapeRatio; // 直向:高度 * 寬高比
104
278
  const boundaryMaxHeight = isLandscape
105
- ? vw100 * landscapeRatio
106
- : vw100 * portraitRatio;
107
- // 計算實際邊界寬高(模擬 CSS min 函數)
279
+ ? vw100 * landscapeRatio // 橫向:寬度 * 寬高比
280
+ : vw100 * portraitRatio; // 直向:寬度 * 高寬比
281
+ // ========================================
282
+ // Step 4: 計算實際邊界尺寸(使用 Math.min 模擬 CSS min())
283
+ // ========================================
284
+ // Chrome 79 才支援 CSS min() 函數,所以在 JavaScript 中計算
108
285
  const boundaryWidth = Math.min(vw100, boundaryMaxWidth);
109
286
  const boundaryHeight = Math.min(vh100, boundaryMaxHeight);
110
- // 計算設計基準
287
+ // ========================================
288
+ // Step 5: 計算 px2vw 轉換比例
289
+ // ========================================
290
+ // 橫向模式使用 design-height 作為基準,直向模式使用 design-width 作為基準
111
291
  const designBaseWidth = isLandscape ? designHeight : designWidth;
112
- const designBaseHeight = isLandscape ? designWidth : designHeight;
113
- // 計算 px2vw 比例
114
292
  const px2vwRatio = boundaryWidth / designBaseWidth;
115
- // 設定 CSS 變數到 host 元素
293
+ // ========================================
294
+ // Step 6: 設定 CSS 變數
295
+ // ========================================
116
296
  hostElement.style.setProperty('--boundary-width', `${boundaryWidth}px`);
117
297
  hostElement.style.setProperty('--boundary-height', `${boundaryHeight}px`);
118
298
  hostElement.style.setProperty('--px2vw-ratio', `${px2vwRatio}`);
119
299
  }
120
300
  }
121
301
  BsBoundary.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: BsBoundary, deps: [], target: i0.ɵɵFactoryTarget.Component });
122
- BsBoundary.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: BsBoundary, isStandalone: true, selector: "bs-boundary", inputs: { isFixedCenter: "isFixedCenter", pointerEventsNone: "pointerEventsNone", lockOrientation: "lockOrientation", forwardStyle: "forwardStyle" }, outputs: { orientationChange: "orientationChange" }, host: { properties: { "class.landscape": "isLandscape" } }, providers: [BsBoundaryContextService], ngImport: i0, template: "<div\n class=\"boundary-wrapper\"\n [class.fixed-center]=\"isFixedCenter\"\n [class.pointerEventsNone]=\"pointerEventsNone\"\n [style]=\"forwardStyle\"\n>\n <ng-content></ng-content>\n</div>\n", styles: [":host{--landscape-ratio: calc(var(--design-width) / var(--design-height));--portrait-ratio: calc(var(--design-height) / var(--design-width))}.boundary-wrapper{width:var(--vw100);height:var(--vh100);max-width:calc(var(--vh100) * var(--landscape-ratio));max-height:calc(var(--vw100) * var(--portrait-ratio))}.boundary-wrapper.pointerEventsNone{pointer-events:none}:host.landscape .boundary-wrapper{max-width:calc(var(--vh100) * var(--portrait-ratio));max-height:calc(var(--vw100) * var(--landscape-ratio))}.fixed-center{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);transform-origin:center;z-index:99}\n"] });
302
+ BsBoundary.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: BsBoundary, isStandalone: true, selector: "bs-boundary", inputs: { isFixedCenter: "isFixedCenter", pointerEventsNone: "pointerEventsNone", orientation: "orientation", forwardStyle: "forwardStyle" }, outputs: { orientationChange: "orientationChange" }, host: { properties: { "class.landscape": "isLandscape && orientation === \"landscape\"", "class.portrait": "!isLandscape && orientation === \"portrait\"", "class.landscape-lock-rotation": "orientation === \"landscapeLockRotation\"", "class.portrait-lock-rotation": "orientation === \"portraitLockRotation\"", "class.actual-portrait": "actualIsPortrait", "class.actual-landscape": "!actualIsPortrait" } }, providers: [BsBoundaryContextService], ngImport: i0, template: "<div\n class=\"boundary-wrapper\"\n [class.fixed-center]=\"isFixedCenter\"\n [class.pointerEventsNone]=\"pointerEventsNone\"\n [style]=\"forwardStyle\"\n>\n <ng-content></ng-content>\n</div>\n", styles: [":host{--landscape-ratio: calc(var(--design-width) / var(--design-height));--portrait-ratio: calc(var(--design-height) / var(--design-width));--boundary-max-width: calc(var(--vh100) * var(--landscape-ratio));--boundary-max-height: calc(var(--vw100) * var(--portrait-ratio));--design-base-width: var(--design-width);--design-base-height: var(--design-height);--px2vw-ratio: calc(var(--vw100) / var(--design-base-width))}.boundary-wrapper{width:var(--vw100);height:var(--vh100);max-width:var(--boundary-max-width);max-height:var(--boundary-max-height)}.boundary-wrapper.pointerEventsNone{pointer-events:none}:host.portrait{--boundary-max-width: calc(var(--vh100) * var(--landscape-ratio));--boundary-max-height: calc(var(--vw100) * var(--portrait-ratio));--design-base-width: var(--design-width);--design-base-height: var(--design-height);--px2vw-ratio: calc(var(--vw100) / var(--design-base-width))}:host.landscape{--boundary-max-width: calc(var(--vh100) * var(--portrait-ratio));--boundary-max-height: calc(var(--vw100) * var(--landscape-ratio));--design-base-width: var(--design-height);--design-base-height: var(--design-width);--px2vw-ratio: calc(var(--vh100) * var(--portrait-ratio) / var(--design-base-width))}@media (orientation: landscape){:host:not(.landscape):not(.portrait):not(.landscape-lock-rotation):not(.portrait-lock-rotation){--boundary-max-width: calc(var(--vh100) * var(--portrait-ratio));--boundary-max-height: calc(var(--vw100) * var(--landscape-ratio));--design-base-width: var(--design-height);--design-base-height: var(--design-width);--px2vw-ratio: calc(var(--vh100) * var(--portrait-ratio) / var(--design-base-width))}}.fixed-center{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);transform-origin:center;z-index:99}:host.landscape-lock-rotation{--boundary-max-width: calc(var(--vh100) * var(--portrait-ratio));--boundary-max-height: calc(var(--vw100) * var(--landscape-ratio));--design-base-width: var(--design-height);--design-base-height: var(--design-width);--px2vw-ratio: calc(var(--vh100) * var(--portrait-ratio) / var(--design-base-width))}:host.landscape-lock-rotation.actual-portrait .boundary-wrapper{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) rotate(90deg);transform-origin:center;width:var(--boundary-width);height:var(--boundary-height);max-width:none;max-height:none}:host.portrait-lock-rotation{--boundary-max-width: calc(var(--vh100) * var(--landscape-ratio));--boundary-max-height: calc(var(--vw100) * var(--portrait-ratio));--design-base-width: var(--design-width);--design-base-height: var(--design-height);--px2vw-ratio: calc(var(--vw100) / var(--design-base-width))}:host.portrait-lock-rotation.actual-landscape .boundary-wrapper{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) rotate(-90deg);transform-origin:center;width:var(--boundary-width);height:var(--boundary-height);max-width:none;max-height:none}\n"] });
123
303
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: BsBoundary, decorators: [{
124
304
  type: Component,
125
305
  args: [{ selector: 'bs-boundary', standalone: true, providers: [BsBoundaryContextService], host: {
126
- '[class.landscape]': 'isLandscape'
127
- }, template: "<div\n class=\"boundary-wrapper\"\n [class.fixed-center]=\"isFixedCenter\"\n [class.pointerEventsNone]=\"pointerEventsNone\"\n [style]=\"forwardStyle\"\n>\n <ng-content></ng-content>\n</div>\n", styles: [":host{--landscape-ratio: calc(var(--design-width) / var(--design-height));--portrait-ratio: calc(var(--design-height) / var(--design-width))}.boundary-wrapper{width:var(--vw100);height:var(--vh100);max-width:calc(var(--vh100) * var(--landscape-ratio));max-height:calc(var(--vw100) * var(--portrait-ratio))}.boundary-wrapper.pointerEventsNone{pointer-events:none}:host.landscape .boundary-wrapper{max-width:calc(var(--vh100) * var(--portrait-ratio));max-height:calc(var(--vw100) * var(--landscape-ratio))}.fixed-center{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);transform-origin:center;z-index:99}\n"] }]
306
+ '[class.landscape]': 'isLandscape && orientation === "landscape"',
307
+ '[class.portrait]': '!isLandscape && orientation === "portrait"',
308
+ '[class.landscape-lock-rotation]': 'orientation === "landscapeLockRotation"',
309
+ '[class.portrait-lock-rotation]': 'orientation === "portraitLockRotation"',
310
+ '[class.actual-portrait]': 'actualIsPortrait',
311
+ '[class.actual-landscape]': '!actualIsPortrait'
312
+ }, template: "<div\n class=\"boundary-wrapper\"\n [class.fixed-center]=\"isFixedCenter\"\n [class.pointerEventsNone]=\"pointerEventsNone\"\n [style]=\"forwardStyle\"\n>\n <ng-content></ng-content>\n</div>\n", styles: [":host{--landscape-ratio: calc(var(--design-width) / var(--design-height));--portrait-ratio: calc(var(--design-height) / var(--design-width));--boundary-max-width: calc(var(--vh100) * var(--landscape-ratio));--boundary-max-height: calc(var(--vw100) * var(--portrait-ratio));--design-base-width: var(--design-width);--design-base-height: var(--design-height);--px2vw-ratio: calc(var(--vw100) / var(--design-base-width))}.boundary-wrapper{width:var(--vw100);height:var(--vh100);max-width:var(--boundary-max-width);max-height:var(--boundary-max-height)}.boundary-wrapper.pointerEventsNone{pointer-events:none}:host.portrait{--boundary-max-width: calc(var(--vh100) * var(--landscape-ratio));--boundary-max-height: calc(var(--vw100) * var(--portrait-ratio));--design-base-width: var(--design-width);--design-base-height: var(--design-height);--px2vw-ratio: calc(var(--vw100) / var(--design-base-width))}:host.landscape{--boundary-max-width: calc(var(--vh100) * var(--portrait-ratio));--boundary-max-height: calc(var(--vw100) * var(--landscape-ratio));--design-base-width: var(--design-height);--design-base-height: var(--design-width);--px2vw-ratio: calc(var(--vh100) * var(--portrait-ratio) / var(--design-base-width))}@media (orientation: landscape){:host:not(.landscape):not(.portrait):not(.landscape-lock-rotation):not(.portrait-lock-rotation){--boundary-max-width: calc(var(--vh100) * var(--portrait-ratio));--boundary-max-height: calc(var(--vw100) * var(--landscape-ratio));--design-base-width: var(--design-height);--design-base-height: var(--design-width);--px2vw-ratio: calc(var(--vh100) * var(--portrait-ratio) / var(--design-base-width))}}.fixed-center{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);transform-origin:center;z-index:99}:host.landscape-lock-rotation{--boundary-max-width: calc(var(--vh100) * var(--portrait-ratio));--boundary-max-height: calc(var(--vw100) * var(--landscape-ratio));--design-base-width: var(--design-height);--design-base-height: var(--design-width);--px2vw-ratio: calc(var(--vh100) * var(--portrait-ratio) / var(--design-base-width))}:host.landscape-lock-rotation.actual-portrait .boundary-wrapper{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) rotate(90deg);transform-origin:center;width:var(--boundary-width);height:var(--boundary-height);max-width:none;max-height:none}:host.portrait-lock-rotation{--boundary-max-width: calc(var(--vh100) * var(--landscape-ratio));--boundary-max-height: calc(var(--vw100) * var(--portrait-ratio));--design-base-width: var(--design-width);--design-base-height: var(--design-height);--px2vw-ratio: calc(var(--vw100) / var(--design-base-width))}:host.portrait-lock-rotation.actual-landscape .boundary-wrapper{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) rotate(-90deg);transform-origin:center;width:var(--boundary-width);height:var(--boundary-height);max-width:none;max-height:none}\n"] }]
128
313
  }], propDecorators: { isFixedCenter: [{
129
314
  type: Input
130
315
  }], pointerEventsNone: [{
131
316
  type: Input
132
- }], lockOrientation: [{
317
+ }], orientation: [{
133
318
  type: Input
134
319
  }], forwardStyle: [{
135
320
  type: Input
@@ -1 +1 @@
1
- {"version":3,"file":"bitstack-ng-boundary.mjs","sources":["../../../projects/bitstack-ng-boundary/src/lib/bs-boundary-context.service.ts","../../../projects/bitstack-ng-boundary/src/lib/bs-boundary.ts","../../../projects/bitstack-ng-boundary/src/lib/template.html","../../../projects/bitstack-ng-boundary/src/public-api.ts","../../../projects/bitstack-ng-boundary/src/bitstack-ng-boundary.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\nimport { BehaviorSubject } from 'rxjs';\n\n@Injectable()\nexport class BsBoundaryContextService {\n private _isLandscape = new BehaviorSubject<boolean>(false);\n\n readonly isLandscape$ = this._isLandscape.asObservable();\n\n setIsLandscape(value: boolean): void {\n this._isLandscape.next(value);\n }\n\n getIsLandscape(): boolean {\n return this._isLandscape.value;\n }\n}\n","import {\n Component,\n ElementRef,\n EventEmitter,\n inject,\n Input, OnDestroy,\n OnInit,\n Output,\n} from '@angular/core';\nimport {TOrientationMode} from './model';\nimport {BreakpointObserver} from '@angular/cdk/layout';\nimport {debounceTime, fromEvent, Subject, takeUntil} from 'rxjs';\nimport {BsBoundaryContextService} from './bs-boundary-context.service';\n\n@Component({\n selector: 'bs-boundary',\n templateUrl: 'template.html',\n standalone: true,\n styleUrls: ['./styles.scss'],\n providers: [BsBoundaryContextService],\n host: {\n '[class.landscape]': 'isLandscape'\n }\n})\nexport class BsBoundary implements OnInit, OnDestroy {\n\n @Input() isFixedCenter?: boolean = false;\n @Input() pointerEventsNone?: boolean = false;\n @Input() lockOrientation?: TOrientationMode = 'auto';\n @Input() forwardStyle?: Record<string, string>;\n\n @Output() orientationChange = new EventEmitter<boolean>();\n\n private breakpointObserver = inject(BreakpointObserver);\n private boundaryContext = inject(BsBoundaryContextService);\n private elementRef = inject(ElementRef);\n private destroy$ = new Subject<void>();\n\n get isLandscape(): boolean {\n return this.boundaryContext.getIsLandscape();\n }\n\n ngOnInit(): void {\n this.setupBreakpointObserver();\n this.setupResizeObserver();\n // 延遲執行以確保 CSS 變數已經設定\n setTimeout(() => this.updateBoundaryCalculations(), 0);\n }\n\n ngOnDestroy(): void {\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n /**\n * 監控方向變化\n */\n setupBreakpointObserver() {\n if (this.lockOrientation === 'landscape') {\n this.boundaryContext.setIsLandscape(true);\n this.orientationChange.emit(true);\n // 鎖定方向後也需要更新計算\n setTimeout(() => this.updateBoundaryCalculations(), 0);\n return;\n }\n\n if (this.lockOrientation === 'portrait') {\n this.boundaryContext.setIsLandscape(false);\n this.orientationChange.emit(false);\n // 鎖定方向後也需要更新計算\n setTimeout(() => this.updateBoundaryCalculations(), 0);\n return;\n }\n\n this.breakpointObserver\n .observe('(orientation: landscape)')\n .pipe(takeUntil(this.destroy$))\n .subscribe(result => {\n this.boundaryContext.setIsLandscape(result.matches);\n this.orientationChange.emit(result.matches);\n this.updateBoundaryCalculations();\n });\n }\n\n /**\n * 監控視口大小變化\n */\n setupResizeObserver() {\n fromEvent(window, 'resize')\n .pipe(\n debounceTime(100),\n takeUntil(this.destroy$)\n )\n .subscribe(() => {\n this.updateBoundaryCalculations();\n });\n }\n\n /**\n * 更新邊界計算(支援 Chrome 74,不使用 CSS min/max 函數)\n */\n updateBoundaryCalculations() {\n const hostElement = this.elementRef.nativeElement as HTMLElement;\n const computedStyle = getComputedStyle(hostElement);\n\n // 讀取 CSS 變數\n const designWidth = parseFloat(computedStyle.getPropertyValue('--design-width') || '1920');\n const designHeight = parseFloat(computedStyle.getPropertyValue('--design-height') || '1080');\n const vw100 = parseFloat(computedStyle.getPropertyValue('--vw100') || `${window.innerWidth}`);\n const vh100 = parseFloat(computedStyle.getPropertyValue('--vh100') || `${window.innerHeight}`);\n\n // 計算比例\n const landscapeRatio = designWidth / designHeight;\n const portraitRatio = designHeight / designWidth;\n\n // 根據方向計算邊界最大值\n const isLandscape = this.isLandscape;\n const boundaryMaxWidth = isLandscape\n ? vh100 * portraitRatio\n : vh100 * landscapeRatio;\n const boundaryMaxHeight = isLandscape\n ? vw100 * landscapeRatio\n : vw100 * portraitRatio;\n\n // 計算實際邊界寬高(模擬 CSS min 函數)\n const boundaryWidth = Math.min(vw100, boundaryMaxWidth);\n const boundaryHeight = Math.min(vh100, boundaryMaxHeight);\n\n // 計算設計基準\n const designBaseWidth = isLandscape ? designHeight : designWidth;\n const designBaseHeight = isLandscape ? designWidth : designHeight;\n\n // 計算 px2vw 比例\n const px2vwRatio = boundaryWidth / designBaseWidth;\n\n // 設定 CSS 變數到 host 元素\n hostElement.style.setProperty('--boundary-width', `${boundaryWidth}px`);\n hostElement.style.setProperty('--boundary-height', `${boundaryHeight}px`);\n hostElement.style.setProperty('--px2vw-ratio', `${px2vwRatio}`);\n }\n}\n","<div\n class=\"boundary-wrapper\"\n [class.fixed-center]=\"isFixedCenter\"\n [class.pointerEventsNone]=\"pointerEventsNone\"\n [style]=\"forwardStyle\"\n>\n <ng-content></ng-content>\n</div>\n","/*\n * Public API Surface of @bitstack/ng-boundary\n */\n\nexport * from './lib/bs-boundary';\nexport * from './lib/bs-boundary-context.service';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bitstack-ng-boundary.mjs","sources":["../../../projects/bitstack-ng-boundary/src/lib/bs-boundary-context.service.ts","../../../projects/bitstack-ng-boundary/src/lib/bs-boundary.ts","../../../projects/bitstack-ng-boundary/src/lib/template.html","../../../projects/bitstack-ng-boundary/src/public-api.ts","../../../projects/bitstack-ng-boundary/src/bitstack-ng-boundary.ts"],"sourcesContent":["import {Injectable} from '@angular/core';\nimport {BehaviorSubject} from 'rxjs';\n\n@Injectable()\nexport class BsBoundaryContextService {\n private _isLandscape = new BehaviorSubject<boolean>(false);\n\n readonly isLandscape$ = this._isLandscape.asObservable();\n\n setIsLandscape(value: boolean): void {\n this._isLandscape.next(value);\n }\n\n getIsLandscape(): boolean {\n return this._isLandscape.value;\n }\n}\n","import {BreakpointObserver} from '@angular/cdk/layout';\nimport {\n AfterViewInit,\n Component,\n ElementRef,\n EventEmitter,\n inject,\n Input, OnDestroy,\n OnInit,\n Output,\n} from '@angular/core';\nimport {debounceTime, fromEvent, Subject, takeUntil} from 'rxjs';\n\nimport {BsBoundaryContextService} from './bs-boundary-context.service';\nimport {TOrientationMode} from './model';\n\n/**\n * ============================================================\n * BsBoundary Component - AWD 自適應邊界容器\n * ============================================================\n *\n * 功能說明:\n * 1. 提供響應式邊界容器,自動適應不同設備尺寸\n * 2. 支援 5 種方向模式:auto, portrait, landscape, portraitLockRotation, landscapeLockRotation\n * 3. 使用 CSS 變數實現動態計算,支援 px2vw() 函數\n * 4. 相容 Chrome 74+,使用 JavaScript 計算邊界尺寸\n *\n * 使用範例:\n * ```html\n * <bs-boundary [orientation]=\"'auto'\" [isFixedCenter]=\"true\">\n * <div class=\"content\">...</div>\n * </bs-boundary>\n * ```\n *\n * CSS 變數設定:\n * ```scss\n * bs-boundary {\n * --design-width: 375; // 設計稿寬度\n * --design-height: 667; // 設計稿高度\n * }\n * ```\n */\n@Component({\n selector: 'bs-boundary',\n templateUrl: './template.html',\n standalone: true,\n styleUrls: ['./styles.scss'],\n providers: [BsBoundaryContextService],\n host: {\n '[class.landscape]': 'isLandscape && orientation === \"landscape\"',\n '[class.portrait]': '!isLandscape && orientation === \"portrait\"',\n '[class.landscape-lock-rotation]': 'orientation === \"landscapeLockRotation\"',\n '[class.portrait-lock-rotation]': 'orientation === \"portraitLockRotation\"',\n '[class.actual-portrait]': 'actualIsPortrait',\n '[class.actual-landscape]': '!actualIsPortrait'\n }\n})\nexport class BsBoundary implements OnInit, AfterViewInit, OnDestroy {\n\n // ============================================================\n // Inputs & Outputs\n // ============================================================\n\n /**\n * 是否將內容固定在視窗中央\n * @default false\n */\n @Input() isFixedCenter?: boolean = false;\n\n /**\n * 是否禁用指標事件\n * @default false\n */\n @Input() pointerEventsNone?: boolean = false;\n\n /**\n * 方向模式\n * - auto: 自動檢測方向\n * - portrait: 鎖定直向\n * - landscape: 鎖定橫向\n * - portraitLockRotation: 強制直向(橫向設備時旋轉 -90°)\n * - landscapeLockRotation: 強制橫向(直向設備時旋轉 90°)\n * @default 'auto'\n */\n @Input() orientation?: TOrientationMode = 'auto';\n\n /**\n * 向內層容器轉發的自訂樣式\n */\n @Input() forwardStyle?: Record<string, string>;\n\n /**\n * 方向變化事件\n * @emits true: landscape(橫屏)\n * @emits false: portrait(直屏)\n */\n @Output() orientationChange = new EventEmitter<boolean>();\n\n // ============================================================\n // Private Dependencies\n // ============================================================\n\n private breakpointObserver = inject(BreakpointObserver);\n private boundaryContext = inject(BsBoundaryContextService);\n private elementRef = inject(ElementRef);\n private destroy$ = new Subject<void>();\n\n // ============================================================\n // Public Properties\n // ============================================================\n\n /**\n * 實際設備方向(基於視口尺寸)\n * true: 直向 (height > width)\n * false: 橫向 (width >= height)\n */\n actualIsPortrait = false;\n\n // ============================================================\n // Getters\n // ============================================================\n\n /**\n * 判斷當前是否為橫向模式\n * 根據 orientation 設定和實際設備方向判斷\n */\n get isLandscape(): boolean {\n // landscapeLockRotation: 強制橫屏模式\n if (this.orientation === 'landscapeLockRotation') {\n return true;\n }\n // portraitLockRotation: 強制直屏模式\n if (this.orientation === 'portraitLockRotation') {\n return false;\n }\n // landscape: 鎖定橫向\n if (this.orientation === 'landscape') {\n return true;\n }\n // portrait: 鎖定直向\n if (this.orientation === 'portrait') {\n return false;\n }\n // auto: 自動檢測\n return this.boundaryContext.getIsLandscape();\n }\n\n // ============================================================\n // Lifecycle Hooks\n // ============================================================\n\n ngOnInit(): void {\n this.updateActualOrientation();\n this.setupBreakpointObserver();\n this.setupResizeObserver();\n }\n\n ngAfterViewInit(): void {\n // 在 view 初始化後計算(確保 CSS 已完全應用)\n this.updateBoundaryCalculations();\n }\n\n ngOnDestroy(): void {\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n // ============================================================\n // Observers Setup - 監聽器設定\n // ============================================================\n\n /**\n * 設定方向變化監控\n *\n * 工作原理:\n * - lock 模式(portrait/landscape):直接設定固定方向,不監聽變化\n * - lockRotation 模式:設定固定方向,但需監聽實際設備方向以觸發旋轉\n * - auto 模式:使用 BreakpointObserver 監聽設備方向變化\n */\n private setupBreakpointObserver() {\n // landscape 或 landscapeLockRotation: 鎖定為橫向\n if (this.orientation === 'landscape' || this.orientation === 'landscapeLockRotation') {\n this.boundaryContext.setIsLandscape(true);\n this.orientationChange.emit(true);\n return;\n }\n\n // portrait 或 portraitLockRotation: 鎖定為直向\n if (this.orientation === 'portrait' || this.orientation === 'portraitLockRotation') {\n this.boundaryContext.setIsLandscape(false);\n this.orientationChange.emit(false);\n return;\n }\n\n // auto 模式:監聽實際的設備方向(用於 orientationChange 事件)\n this.breakpointObserver\n .observe('(orientation: landscape)')\n .pipe(takeUntil(this.destroy$))\n .subscribe(result => {\n this.boundaryContext.setIsLandscape(result.matches);\n this.orientationChange.emit(result.matches);\n // 方向變化時重新計算邊界\n this.updateBoundaryCalculations();\n });\n }\n\n /**\n * 設定視窗大小變化監控\n *\n * 功能:\n * - 監聽 window resize 事件\n * - 更新實際設備方向\n * - 重新計算邊界尺寸\n */\n private setupResizeObserver() {\n fromEvent(window, 'resize')\n .pipe(\n // debounceTime(150), // 可選:防抖優化性能\n takeUntil(this.destroy$)\n )\n .subscribe(() => {\n this.updateActualOrientation();\n this.updateBoundaryCalculations();\n });\n }\n\n // ============================================================\n // Calculation Methods - 計算方法\n // ============================================================\n\n /**\n * 更新實際設備方向(基於視口尺寸)\n *\n * 工作原理:\n * - 只在 lockRotation 模式下需要檢測實際方向\n * - 通過比較 window.innerHeight 和 window.innerWidth 判斷\n * - 結果用於觸發 CSS 旋轉變換\n */\n private updateActualOrientation() {\n // 只有在 lockRotation 模式下才需要檢測實際方向\n if (this.orientation !== 'landscapeLockRotation' && this.orientation !== 'portraitLockRotation') {\n this.actualIsPortrait = false;\n return;\n }\n\n // 檢測實際視口方向:高度 > 寬度 = 直向\n this.actualIsPortrait = window.innerHeight > window.innerWidth;\n }\n\n /**\n * 更新邊界計算(支援 Chrome 74,使用 JavaScript 模擬 min 函數)\n *\n * 工作原理:\n * 1. 讀取基礎 CSS 變數(vw100, vh100, design-width, design-height)\n * 2. 處理 lockRotation 模式的視口尺寸交換\n * 3. 計算邊界最大值和實際邊界尺寸\n * 4. 計算 px2vw 轉換比例\n * 5. 設定 CSS 變數供樣式使用\n *\n * 設定的 CSS 變數:\n * - --boundary-width: 實際邊界寬度(px)\n * - --boundary-height: 實際邊界高度(px)\n * - --px2vw-ratio: px 轉 vw 的比例係數\n */\n private updateBoundaryCalculations() {\n const hostElement = this.elementRef.nativeElement as HTMLElement;\n const computedStyle = getComputedStyle(hostElement);\n\n // ========================================\n // Step 1: 讀取基礎 CSS 變數\n // ========================================\n let vw100 = parseFloat(computedStyle.getPropertyValue('--vw100')) || window.innerWidth;\n let vh100 = parseFloat(computedStyle.getPropertyValue('--vh100')) || window.innerHeight;\n const designWidth = parseFloat(computedStyle.getPropertyValue('--design-width'));\n const designHeight = parseFloat(computedStyle.getPropertyValue('--design-height'));\n\n // 檢查基礎變數是否準備好\n if (isNaN(designWidth)) {\n console.error('[BsBoundary][BS001] CSS variable --design-width is not set or invalid');\n return;\n }\n if (isNaN(designHeight)) {\n console.error('[BsBoundary][BS002] CSS variable --design-height is not set or invalid');\n return;\n }\n if (designWidth === 0) {\n console.error('[BsBoundary][BS003] CSS variable --design-width cannot be zero');\n return;\n }\n if (designHeight === 0) {\n console.error('[BsBoundary][BS004] CSS variable --design-height cannot be zero');\n return;\n }\n\n // ========================================\n // Step 2: 處理 lockRotation 模式的視口尺寸交換\n // ========================================\n // landscapeLockRotation + 實際直向:交換視口尺寸\n // 原因:旋轉後,內容的寬度對應設備的高度,內容的高度對應設備的寬度\n if (this.orientation === 'landscapeLockRotation' && this.actualIsPortrait) {\n [vw100, vh100] = [vh100, vw100];\n }\n\n // portraitLockRotation + 實際橫向:交換視口尺寸\n if (this.orientation === 'portraitLockRotation' && !this.actualIsPortrait) {\n [vw100, vh100] = [vh100, vw100];\n }\n\n // ========================================\n // Step 3: 計算比例和邊界最大值\n // ========================================\n const landscapeRatio = designWidth / designHeight;\n const portraitRatio = designHeight / designWidth;\n\n // 根據方向模式計算邊界最大值\n const isLandscape = this.isLandscape;\n const boundaryMaxWidth = isLandscape\n ? vh100 * portraitRatio // 橫向:高度 * 高寬比\n : vh100 * landscapeRatio; // 直向:高度 * 寬高比\n const boundaryMaxHeight = isLandscape\n ? vw100 * landscapeRatio // 橫向:寬度 * 寬高比\n : vw100 * portraitRatio; // 直向:寬度 * 高寬比\n\n // ========================================\n // Step 4: 計算實際邊界尺寸(使用 Math.min 模擬 CSS min())\n // ========================================\n // Chrome 79 才支援 CSS min() 函數,所以在 JavaScript 中計算\n const boundaryWidth = Math.min(vw100, boundaryMaxWidth);\n const boundaryHeight = Math.min(vh100, boundaryMaxHeight);\n\n // ========================================\n // Step 5: 計算 px2vw 轉換比例\n // ========================================\n // 橫向模式使用 design-height 作為基準,直向模式使用 design-width 作為基準\n const designBaseWidth = isLandscape ? designHeight : designWidth;\n const px2vwRatio = boundaryWidth / designBaseWidth;\n\n // ========================================\n // Step 6: 設定 CSS 變數\n // ========================================\n hostElement.style.setProperty('--boundary-width', `${boundaryWidth}px`);\n hostElement.style.setProperty('--boundary-height', `${boundaryHeight}px`);\n hostElement.style.setProperty('--px2vw-ratio', `${px2vwRatio}`);\n }\n}\n","<div\n class=\"boundary-wrapper\"\n [class.fixed-center]=\"isFixedCenter\"\n [class.pointerEventsNone]=\"pointerEventsNone\"\n [style]=\"forwardStyle\"\n>\n <ng-content></ng-content>\n</div>\n","/*\n * Public API Surface of @bitstack/ng-boundary\n */\n\nexport * from './lib/bs-boundary';\nexport * from './lib/bs-boundary-context.service';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}